From b1ce1927022d374198441afafeb27199fc8a052a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 24 Sep 2026 17:14:54 +0100 Subject: Establish native micro runtime checkpoint --- .github/workflows/native-macos-arm64.yml | 55 + .gitignore | 7 + CHECKPOINT.md | 235 + README.md | 342 +- docs/design/001-operation-semantics.md | 126 + docs/design/002-memory-lifetime.md | 135 + docs/design/003-module-contract.md | 119 + docs/design/004-prepared-connections.md | 39 + docs/design/005-field-connections.md | 28 + docs/design/006-native-protocol.md | 139 + docs/design/007-execution-domains.md | 222 + docs/design/abi-v0.h | 76 + docs/design/check-abi-layout.py | 46 + .../validation/2026-09-22-record-layout.json | 80 + docs/research/000-working-method.md | 163 + docs/research/001-research-brief.md | 166 + docs/research/002-checkpoint-results.md | 174 + docs/research/003-live-rules.md | 139 + docs/research/004-authoring.md | 176 + docs/research/005-foundation-contract.md | 139 + docs/research/006-problem-landscape.md | 263 + docs/research/007-buffer-retention-results.md | 195 + docs/research/008-json-fanout-results.md | 202 + docs/research/009-bounded-display-integration.md | 141 + docs/research/010-roadmap.md | 288 + docs/research/011-direct-presentation.md | 102 + docs/research/012-refresh-ownership.md | 100 + docs/research/013-authored-pointer-input.md | 109 + docs/research/014-layout-persistence.md | 93 + docs/research/015-shared-rules.md | 115 + docs/research/016-pointer-routing.md | 98 + docs/research/017-web-layout-boundary.md | 116 + docs/research/018-native-connections.md | 81 + docs/research/019-byte-storage.md | 84 + docs/research/020-prepared-connections.md | 87 + docs/research/021-field-connections.md | 89 + docs/research/022-execution-core.md | 72 + docs/research/023-document-edits.md | 102 + docs/research/024-native-app-instances.md | 109 + docs/research/025-native-authoring-shell.md | 103 + docs/research/026-secure-peer-link.md | 105 + docs/research/027-fixed-native-capacity.md | 90 + docs/research/028-shared-fixed-program.md | 79 + docs/research/029-native-document-window.md | 62 + docs/research/030-authenticated-document-peer.md | 85 + docs/research/031-relocatable-document-preview.md | 52 + .../032-micro-native-authoring-boundary.md | 82 + docs/research/033-paged-document-storage.md | 92 + docs/research/034-hybrid-document-storage.md | 77 + docs/research/035-native-multi-document-window.md | 66 + docs/research/036-gtk-memory-boundary.md | 89 + docs/research/037-build-time-prepared-runner.md | 59 + docs/research/038-fixed-http-document-host.md | 51 + docs/research/039-idle-http-wakeups.md | 50 + docs/research/040-document-service-capacity.md | 48 + docs/research/041-posix-idle-signal-wakeup.md | 37 + docs/research/042-micro-native-terminal-editor.md | 44 + docs/research/043-micro-package-boundary.md | 29 + docs/research/044-terminal-viewport-cost.md | 49 + docs/research/045-owner-thread-async-effects.md | 51 + docs/research/046-optional-async-document-http.md | 55 + .../047-bounded-native-document-browser.md | 46 + docs/research/048-immutable-read-write-overlap.md | 56 + docs/research/049-idle-app-work-slots.md | 101 + docs/research/050-bounded-document-prefix-find.md | 33 + docs/research/051-optional-runtime-metrics.md | 59 + docs/research/052-clean-native-source-preview.md | 27 + .../053-source-loaded-native-terminal-editor.md | 40 + docs/research/054-direct-document-path.md | 53 + .../055-authenticated-peer-transport-cost.md | 70 + docs/research/056-server-only-document-package.md | 54 + .../research/057-portable-native-terminal-check.md | 45 + .../058-portable-authenticated-peer-check.md | 38 + docs/research/059-immutable-file-copy-path.md | 57 + docs/research/060-host-native-checkpoint.md | 24 + docs/research/061-durable-document-edit-latency.md | 54 + docs/research/062-paged-store-rollback.md | 45 + docs/research/063-real-in-process-routing.md | 47 + docs/research/064-release-callback-reentrancy.md | 29 + docs/research/065-native-save-latency.md | 57 + docs/research/066-application-boundary.md | 65 + docs/research/067-work-slot-layout.md | 46 + docs/research/068-opaque-app-core.md | 74 + docs/research/069-atomic-authored-size-policy.md | 72 + docs/research/070-micro-default-build.md | 31 + prototypes/common/tls-link.c | 217 + prototypes/common/tls-link.h | 32 + prototypes/connections/.gitignore | 1 + prototypes/connections/Makefile | 251 + prototypes/connections/README.md | 545 + prototypes/connections/SOURCE_PREVIEW.md | 119 + prototypes/connections/async-core-budget.json | 16 + prototypes/connections/async-server-budget.json | 12 + prototypes/connections/check.py | 552 + prototypes/connections/check_async_server.py | 181 + prototypes/connections/check_copy_path.py | 100 + prototypes/connections/check_core.py | 74 + prototypes/connections/check_document_peer.py | 273 + .../connections/check_document_server_package.py | 100 + prototypes/connections/check_edits.py | 108 + prototypes/connections/check_fixed_program.py | 159 + prototypes/connections/check_fixed_server.py | 180 + prototypes/connections/check_flat_export.py | 134 + prototypes/connections/check_idle_wakes.py | 95 + prototypes/connections/check_micro_memory.py | 106 + prototypes/connections/check_mirror.py | 137 + prototypes/connections/check_package.py | 209 + prototypes/connections/check_policy.py | 188 + prototypes/connections/check_profile.py | 82 + prototypes/connections/check_service_scale.py | 131 + prototypes/connections/check_small_scan.py | 54 + prototypes/connections/check_source_package.py | 145 + prototypes/connections/check_tui.py | 352 + prototypes/connections/check_window.py | 553 + prototypes/connections/check_write_cost.py | 93 + prototypes/connections/compile-program.c | 116 + prototypes/connections/core-budget.json | 15 + prototypes/connections/core-layout.c | 14 + prototypes/connections/document-peer.c | 330 + prototypes/connections/edit-tui.c | 945 ++ prototypes/connections/edit-window.c | 638 + prototypes/connections/edit.c | 177 + prototypes/connections/evaluate.c | 53 + .../connections/examples/bounded-editor.pure | 19 + prototypes/connections/examples/editor.pure | 19 + prototypes/connections/examples/excerpt.pure | 13 + prototypes/connections/examples/select-text.py | 72 + prototypes/connections/examples/selection.pure | 19 + prototypes/connections/examples/storage.pure | 11 + prototypes/connections/export_flat_store.py | 143 + prototypes/connections/fixed-budget.json | 18 + prototypes/connections/fixed-package-budget.json | 10 + prototypes/connections/flow.c | 41 + prototypes/connections/flow.h | 25 + prototypes/connections/hybrid-budget.json | 18 + prototypes/connections/hybrid-tui-budget.json | 12 + prototypes/connections/measure-peak-rss.c | 43 + prototypes/connections/measure_tui_latency.py | 101 + prototypes/connections/memory-budget.json | 7 + prototypes/connections/mirror.c | 145 + prototypes/connections/package_native.py | 361 + prototypes/connections/package_source.py | 67 + prototypes/connections/paged-budget.json | 18 + prototypes/connections/peer-budget.json | 12 + prototypes/connections/peer-package-budget.json | 8 + prototypes/connections/peer-server-budget.json | 12 + prototypes/connections/profile-budget.json | 18 + prototypes/connections/program.c | 374 + prototypes/connections/program.h | 50 + prototypes/connections/result.h | 8 + .../results/2026-09-23-byte-storage/README.md | 31 + .../2026-09-23-byte-storage/initial/native.json | 116 + .../2026-09-23-byte-storage/initial/sanitized.json | 116 + .../2026-09-23-byte-storage/initial/test-flow.c | 178 + .../results/2026-09-23-byte-storage/native.json | 116 + .../results/2026-09-23-byte-storage/sanitized.json | 116 + .../sources/prototypes/connections/Makefile | 20 + .../sources/prototypes/connections/check.py | 317 + .../sources/prototypes/connections/flow.c | 41 + .../sources/prototypes/connections/flow.h | 25 + .../sources/prototypes/connections/server.c | 347 + .../sources/prototypes/connections/store.c | 225 + .../sources/prototypes/connections/store.h | 34 + .../sources/prototypes/connections/test-flow.c | 183 + .../sources/prototypes/workbench/cli.mjs | 127 + .../prototypes/workbench/examples/direct.pure | 47 + .../sources/prototypes/workbench/remote.mjs | 89 + .../sources/prototypes/workbench/web/compiler.mjs | 496 + .../sources/prototypes/workbench/web/core.wasm | Bin 0 -> 61072 bytes .../sources/prototypes/workbench/web/project.mjs | 47 + .../sources/prototypes/workbench/web/runtime.mjs | 350 + .../2026-09-23-byte-storage/verification.json | 52 + .../results/2026-09-23-document-edits/README.md | 27 + .../2026-09-23-document-edits/boundary-checks.json | 34 + .../results/2026-09-23-document-edits/core.json | 53 + .../results/2026-09-23-document-edits/native.json | 635 + .../2026-09-23-document-edits/sanitized.json | 635 + .../sources/prototypes/connections/Makefile | 59 + .../sources/prototypes/connections/check.py | 551 + .../sources/prototypes/connections/check_core.py | 74 + .../sources/prototypes/connections/check_edits.py | 108 + .../prototypes/connections/core-budget.json | 15 + .../sources/prototypes/connections/core-layout.c | 11 + .../sources/prototypes/connections/edit.c | 153 + .../sources/prototypes/connections/evaluate.c | 53 + .../prototypes/connections/examples/editor.pure | 19 + .../prototypes/connections/examples/excerpt.pure | 13 + .../prototypes/connections/examples/select-text.py | 72 + .../prototypes/connections/examples/selection.pure | 19 + .../prototypes/connections/examples/storage.pure | 11 + .../sources/prototypes/connections/flow.c | 41 + .../sources/prototypes/connections/flow.h | 25 + .../sources/prototypes/connections/program.c | 366 + .../sources/prototypes/connections/program.h | 44 + .../sources/prototypes/connections/result.h | 8 + .../sources/prototypes/connections/runtime.c | 273 + .../sources/prototypes/connections/runtime.h | 88 + .../sources/prototypes/connections/server.c | 372 + .../sources/prototypes/connections/store-port.c | 44 + .../sources/prototypes/connections/store-port.h | 6 + .../sources/prototypes/connections/store.c | 258 + .../sources/prototypes/connections/store.h | 44 + .../sources/prototypes/connections/test-fields.c | 153 + .../sources/prototypes/connections/test-flow.c | 183 + .../sources/prototypes/connections/test-program.c | 111 + .../sources/prototypes/connections/test-runtime.c | 285 + .../sources/prototypes/connections/test-splice.c | 172 + .../sources/prototypes/workbench/cli.mjs | 127 + .../prototypes/workbench/examples/direct.pure | 47 + .../sources/prototypes/workbench/remote.mjs | 89 + .../sources/prototypes/workbench/web/compiler.mjs | 496 + .../sources/prototypes/workbench/web/core.wasm | Bin 0 -> 61072 bytes .../sources/prototypes/workbench/web/project.mjs | 47 + .../sources/prototypes/workbench/web/runtime.mjs | 350 + .../2026-09-23-document-edits/verification.json | 32 + .../results/2026-09-23-execution-core/README.md | 28 + .../results/2026-09-23-execution-core/native.json | 505 + .../2026-09-23-execution-core/sanitized.json | 505 + .../sources/prototypes/connections/Makefile | 43 + .../sources/prototypes/connections/check.py | 536 + .../sources/prototypes/connections/evaluate.c | 48 + .../prototypes/connections/examples/excerpt.pure | 13 + .../prototypes/connections/examples/select-text.py | 72 + .../prototypes/connections/examples/selection.pure | 19 + .../prototypes/connections/examples/storage.pure | 11 + .../sources/prototypes/connections/flow.c | 41 + .../sources/prototypes/connections/flow.h | 25 + .../sources/prototypes/connections/program.c | 357 + .../sources/prototypes/connections/program.h | 39 + .../sources/prototypes/connections/result.h | 8 + .../sources/prototypes/connections/runtime.c | 265 + .../sources/prototypes/connections/runtime.h | 86 + .../sources/prototypes/connections/server.c | 368 + .../sources/prototypes/connections/store-port.c | 39 + .../sources/prototypes/connections/store-port.h | 6 + .../sources/prototypes/connections/store.c | 228 + .../sources/prototypes/connections/store.h | 37 + .../sources/prototypes/connections/test-fields.c | 153 + .../sources/prototypes/connections/test-flow.c | 183 + .../sources/prototypes/connections/test-program.c | 111 + .../sources/prototypes/connections/test-runtime.c | 285 + .../sources/prototypes/workbench/cli.mjs | 127 + .../prototypes/workbench/examples/direct.pure | 47 + .../sources/prototypes/workbench/remote.mjs | 89 + .../sources/prototypes/workbench/web/compiler.mjs | 496 + .../sources/prototypes/workbench/web/core.wasm | Bin 0 -> 61072 bytes .../sources/prototypes/workbench/web/project.mjs | 47 + .../sources/prototypes/workbench/web/runtime.mjs | 350 + .../2026-09-23-execution-core/verification.json | 30 + .../results/2026-09-23-field-connections/README.md | 31 + .../2026-09-23-field-connections/native.json | 488 + .../2026-09-23-field-connections/sanitized.json | 488 + .../sources/prototypes/connections/Makefile | 28 + .../sources/prototypes/connections/check.py | 528 + .../prototypes/connections/examples/excerpt.pure | 13 + .../prototypes/connections/examples/select-text.py | 72 + .../prototypes/connections/examples/selection.pure | 19 + .../prototypes/connections/examples/storage.pure | 11 + .../sources/prototypes/connections/flow.c | 41 + .../sources/prototypes/connections/flow.h | 25 + .../sources/prototypes/connections/program.c | 402 + .../sources/prototypes/connections/program.h | 39 + .../sources/prototypes/connections/server.c | 393 + .../sources/prototypes/connections/store.c | 228 + .../sources/prototypes/connections/store.h | 40 + .../sources/prototypes/connections/test-fields.c | 153 + .../sources/prototypes/connections/test-flow.c | 183 + .../sources/prototypes/connections/test-program.c | 111 + .../sources/prototypes/workbench/cli.mjs | 127 + .../prototypes/workbench/examples/direct.pure | 47 + .../sources/prototypes/workbench/remote.mjs | 89 + .../sources/prototypes/workbench/web/compiler.mjs | 496 + .../sources/prototypes/workbench/web/core.wasm | Bin 0 -> 61072 bytes .../sources/prototypes/workbench/web/project.mjs | 47 + .../sources/prototypes/workbench/web/runtime.mjs | 350 + .../2026-09-23-field-connections/verification.json | 62 + .../2026-09-23-native-connections/README.md | 26 + .../2026-09-23-native-connections/native.json | 98 + .../2026-09-23-native-connections/sanitized.json | 98 + .../sources/prototypes/connections/Makefile | 22 + .../sources/prototypes/connections/check.py | 306 + .../sources/prototypes/connections/flow.c | 41 + .../sources/prototypes/connections/flow.h | 25 + .../sources/prototypes/connections/server.c | 346 + .../sources/prototypes/connections/store.c | 132 + .../sources/prototypes/connections/store.h | 25 + .../sources/prototypes/connections/test-flow.c | 110 + .../sources/prototypes/workbench/cli.mjs | 127 + .../sources/prototypes/workbench/cli.test.mjs | 121 + .../prototypes/workbench/examples/direct.pure | 47 + .../sources/prototypes/workbench/remote.mjs | 89 + .../sources/prototypes/workbench/web/compiler.mjs | 496 + .../sources/prototypes/workbench/web/core.wasm | Bin 0 -> 61072 bytes .../sources/prototypes/workbench/web/project.mjs | 47 + .../sources/prototypes/workbench/web/runtime.mjs | 350 + .../verification.json | 29 + .../2026-09-23-prepared-connections/README.md | 31 + .../2026-09-23-prepared-connections/native.json | 269 + .../2026-09-23-prepared-connections/sanitized.json | 269 + .../sources/prototypes/connections/Makefile | 24 + .../sources/prototypes/connections/check.py | 419 + .../prototypes/connections/examples/excerpt.pure | 13 + .../prototypes/connections/examples/storage.pure | 11 + .../sources/prototypes/connections/flow.c | 41 + .../sources/prototypes/connections/flow.h | 25 + .../sources/prototypes/connections/program.c | 233 + .../sources/prototypes/connections/program.h | 26 + .../sources/prototypes/connections/server.c | 385 + .../sources/prototypes/connections/store.c | 227 + .../sources/prototypes/connections/store.h | 39 + .../sources/prototypes/connections/test-flow.c | 183 + .../sources/prototypes/connections/test-program.c | 111 + .../sources/prototypes/workbench/cli.mjs | 127 + .../prototypes/workbench/examples/direct.pure | 47 + .../sources/prototypes/workbench/remote.mjs | 89 + .../sources/prototypes/workbench/web/compiler.mjs | 496 + .../sources/prototypes/workbench/web/core.wasm | Bin 0 -> 61072 bytes .../sources/prototypes/workbench/web/project.mjs | 47 + .../sources/prototypes/workbench/web/runtime.mjs | 350 + .../verification.json | 51 + .../2026-09-24-async-effects/async-core.json | 55 + .../2026-09-24-async-effects/default-core.json | 53 + .../results/2026-09-24-async-effects/flow.json | 1 + .../2026-09-24-async-effects/sanitized.json | 1 + .../results/2026-09-24-async-http/full-check.json | 614 + .../results/2026-09-24-async-http/gated.json | 38 + .../results/2026-09-24-async-http/profile.json | 21 + .../results/2026-09-24-async-http/sanitized.json | 38 + .../service-scale-memory.json | 100 + .../2026-09-24-async-read-write/full-check.json | 614 + .../results/2026-09-24-async-read-write/gated.json | 53 + .../2026-09-24-async-read-write/profile.json | 21 + .../2026-09-24-async-read-write/sanitized.json | 53 + .../2026-09-24-async-read-write/service-scale.json | 100 + .../2026-09-24-atomic-policy/policy-peer.json | 12 + .../results/2026-09-24-atomic-policy/policy.json | 11 + .../pure-document-fixed-cli-linux-aarch64.tar.gz | Bin 0 -> 12966 bytes ...ument-fixed-cli-linux-aarch64.tar.gz.check.json | 12 + ...ure-document-fixed-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 32144 bytes ...t-fixed-cli-tui-linux-aarch64.tar.gz.check.json | 14 + .../pure-document-source-cli-linux-aarch64.tar.gz | Bin 0 -> 22862 bytes ...ment-source-cli-linux-aarch64.tar.gz.check.json | 12 + ...re-document-source-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 51398 bytes ...-source-cli-tui-linux-aarch64.tar.gz.check.json | 14 + .../pure-source-preview.tar.gz | Bin 0 -> 337937 bytes .../source-package-check.json | 16 + .../2026-09-24-compact-runtime/cli-check.json | 12 + .../2026-09-24-compact-runtime/cli-tui-check.json | 14 + .../2026-09-24-compact-runtime/cli-tui.tar.gz | Bin 0 -> 31971 bytes .../results/2026-09-24-compact-runtime/cli.tar.gz | Bin 0 -> 12885 bytes .../results/2026-09-24-compact-runtime/core.json | 53 + .../default-profile.json | 32 + .../2026-09-24-compact-runtime/fixed-memory.json | 15 + .../2026-09-24-compact-runtime/fixed-profile.json | 32 + .../source-cli-check.json | 12 + .../2026-09-24-compact-runtime/source-cli.tar.gz | Bin 0 -> 22454 bytes .../2026-09-24-compact-runtime/tui-profile.json | 21 + .../results/2026-09-24-compact-runtime/tui.json | 22 + .../pure-source-preview.tar.gz | Bin 0 -> 324261 bytes .../source-check-linux-aarch64.json | 16 + .../results/2026-09-24-copy-range/copy-path.json | 27 + .../2026-09-24-copy-range/default-core.json | 53 + .../default-fixed-memory.json | 15 + .../default-fixed-profile.json | 32 + .../2026-09-24-copy-range/default-functional.json | 636 + .../2026-09-24-copy-range/default-memory.json | 15 + .../2026-09-24-copy-range/default-profile.json | 32 + .../results/2026-09-24-copy-range/functional.json | 636 + .../results/2026-09-24-copy-range/memory.json | 15 + .../results/2026-09-24-copy-range/profile.json | 32 + .../2026-09-24-copy-range/service-scale.json | 70 + .../2026-09-24-document-peer/clean-peer.log | 43 + .../2026-09-24-document-peer/clean-release.log | 737 ++ .../results/2026-09-24-document-peer/dynamic.txt | 31 + .../2026-09-24-document-peer/file-sizes.txt | 2 + .../results/2026-09-24-document-peer/hashes.txt | 4 + .../2026-09-24-document-peer/package-check.json | 19 + .../2026-09-24-document-peer/package-hashes.txt | 3 + .../2026-09-24-document-peer/peer-budget.json | 23 + .../2026-09-24-document-peer/peer-flow.json | 16 + .../2026-09-24-document-peer/peer-sanitized.json | 16 + .../pure-document-linux-aarch64.tar.gz | Bin 0 -> 316927 bytes .../2026-09-24-document-peer/scalar-refactor.json | 1 + .../results/2026-09-24-document-peer/size.txt | 2 + .../2026-09-24-fixed-http/backpressure.json | 61 + .../results/2026-09-24-fixed-http/final.json | 65 + .../2026-09-24-fixed-http/fixed-profile.json | 32 + .../results/2026-09-24-fixed-http/package.json | 14 + .../results/2026-09-24-fixed-http/parity.json | 43 + .../results/2026-09-24-fixed-http/peak-memory.json | 67 + .../pure-document-fixed-http-linux-aarch64.tar.gz | Bin 0 -> 31733 bytes .../2026-09-24-fixed-http/source-profile.json | 32 + .../results/2026-09-24-fixed-http/verified.json | 68 + .../2026-09-24-fixed-program/memory-final.json | 15 + .../2026-09-24-fixed-program/memory-verified.json | 15 + .../results/2026-09-24-fixed-program/memory.json | 15 + .../2026-09-24-fixed-program/package-final.json | 12 + .../results/2026-09-24-fixed-program/package.json | 12 + .../2026-09-24-fixed-program/parity-final-v2.json | 12 + .../2026-09-24-fixed-program/parity-final.json | 11 + .../parity-reconfigured.json | 13 + .../2026-09-24-fixed-program/parity-release.json | 14 + .../2026-09-24-fixed-program/parity-verified.json | 12 + .../results/2026-09-24-fixed-program/parity.json | 10 + .../2026-09-24-fixed-program/profile-final.json | 21 + .../results/2026-09-24-fixed-program/profile.json | 21 + .../pure-document-fixed-final-linux-aarch64.tar.gz | Bin 0 -> 14385 bytes .../pure-document-fixed-linux-aarch64.tar.gz | Bin 0 -> 14386 bytes .../window-regression.json | 1 + .../2026-09-24-fixed-tui-final/package.json | 14 + .../2026-09-24-fixed-tui-final/profile.json | 21 + .../2026-09-24-fixed-tui-final/pty-flow.json | 16 + .../2026-09-24-fixed-tui-final/pty-sanitized.json | 16 + .../pure-document-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 33654 bytes .../2026-09-24-fixed-tui/package-cursor.json | 14 + .../results/2026-09-24-fixed-tui/package.json | 14 + .../results/2026-09-24-fixed-tui/profile.json | 21 + .../2026-09-24-fixed-tui/pty-cursor-flow.json | 16 + .../2026-09-24-fixed-tui/pty-cursor-sanitized.json | 16 + .../results/2026-09-24-fixed-tui/pty-flow.json | 16 + .../2026-09-24-fixed-tui/pty-sanitized.json | 16 + .../pure-document-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 33625 bytes .../pure-source-preview.tar.gz | Bin 0 -> 329118 bytes .../source-check-linux-aarch64.json | 14 + .../2026-09-24-flat-export/export-check.json | 27 + .../2026-09-24-flat-export/export-ext4.json | 28 + .../2026-09-24-flat-export/export-scale-1024.json | 27 + .../2026-09-24-hybrid-storage-v2/app-asan.json | 635 + .../2026-09-24-hybrid-storage-v2/app-release.json | 635 + .../2026-09-24-hybrid-storage-v2/gtk-flow.json | 1 + .../2026-09-24-hybrid-storage-v2/memory.json | 14 + .../2026-09-24-hybrid-storage-v2/package-full.json | 21 + .../package-micro.json | 13 + .../results/2026-09-24-hybrid-storage-v2/peer.json | 16 + .../2026-09-24-hybrid-storage-v2/profile.json | 32 + ...-document-hybrid-authoring-linux-aarch64.tar.gz | Bin 0 -> 114025 bytes ...pure-document-hybrid-micro-linux-aarch64.tar.gz | Bin 0 -> 26045 bytes .../2026-09-24-hybrid-storage-v2/small-scan.json | 5 + .../2026-09-24-hybrid-storage-v2/unit-asan.json | 1 + .../results/2026-09-24-hybrid-storage-v2/unit.json | 1 + .../2026-09-24-hybrid-storage-v2/write-cost.json | 49 + .../2026-09-24-hybrid-storage/app-asan.json | 635 + .../2026-09-24-hybrid-storage/app-release.json | 635 + .../2026-09-24-hybrid-storage/gtk-flow.json | 1 + .../results/2026-09-24-hybrid-storage/memory.json | 14 + .../2026-09-24-hybrid-storage/package-full.json | 21 + .../2026-09-24-hybrid-storage/package-micro.json | 13 + .../results/2026-09-24-hybrid-storage/peer.json | 16 + .../results/2026-09-24-hybrid-storage/profile.json | 32 + ...-document-hybrid-authoring-linux-aarch64.tar.gz | Bin 0 -> 114009 bytes ...pure-document-hybrid-micro-linux-aarch64.tar.gz | Bin 0 -> 26040 bytes .../2026-09-24-hybrid-storage/unit-asan.json | 1 + .../results/2026-09-24-hybrid-storage/unit.json | 1 + .../2026-09-24-hybrid-storage/write-cost.json | 49 + .../2026-09-24-idle-wake/archived-comparison.json | 21 + .../2026-09-24-idle-wake/fixed-profile.json | 32 + .../fixed-server-regression.json | 68 + .../results/2026-09-24-idle-wake/idle.json | 21 + .../results/2026-09-24-idle-wake/package.json | 14 + ...e-document-fixed-http-idle-linux-aarch64.tar.gz | Bin 0 -> 31773 bytes .../reproducible-comparison.json | 21 + .../2026-09-24-idle-wake/source-profile.json | 32 + .../2026-09-24-idle-work-binding/async-core.json | 55 + .../2026-09-24-idle-work-binding/async-gated.json | 53 + .../async-server-profile.json | 21 + .../2026-09-24-idle-work-binding/cli-check.json | 12 + .../cli-http-check.json | 14 + .../cli-http-tui-check.json | 16 + .../cli-http-tui.tar.gz | Bin 0 -> 47419 bytes .../2026-09-24-idle-work-binding/cli-http.tar.gz | Bin 0 -> 28719 bytes .../cli-tui-check.json | 14 + .../2026-09-24-idle-work-binding/cli-tui.tar.gz | Bin 0 -> 31726 bytes .../2026-09-24-idle-work-binding/cli.tar.gz | Bin 0 -> 12987 bytes .../results/2026-09-24-idle-work-binding/core.json | 53 + .../default-profile.json | 32 + .../document-peer-profile.json | 23 + .../document-peer.json | 16 + .../2026-09-24-idle-work-binding/fixed-memory.json | 15 + .../fixed-profile.json | 32 + .../2026-09-24-idle-work-binding/full-check.json | 636 + .../package-reproducibility.json | 31 + .../precompact-budget-failure.txt | 9 + .../2026-09-24-idle-work-binding/release.log | 800 ++ .../runtime-sanitized.json | 1 + .../service-scale.json | 100 + .../source-cli-check.json | 12 + .../2026-09-24-idle-work-binding/source-cli.tar.gz | Bin 0 -> 22595 bytes .../source-memory.json | 15 + .../2026-09-24-idle-work-binding/tui-profile.json | 21 + .../results/2026-09-24-idle-work-binding/tui.json | 22 + .../2026-09-24-in-process-routing/mirror-ext4.json | 56 + .../mirror-sanitized.json | 46 + .../2026-09-24-in-process-routing/mirror-tmp.json | 56 + .../pure-source-preview.tar.gz | Bin 0 -> 338097 bytes .../source-package-check.json | 14 + .../cli-check.json | 12 + .../2026-09-24-micro-package-boundary/cli.tar.gz | Bin 0 -> 14692 bytes .../cli_http-check.json | 14 + .../cli_http.tar.gz | Bin 0 -> 32548 bytes .../cli_http_tui-check.json | 16 + .../cli_http_tui.tar.gz | Bin 0 -> 51424 bytes .../cli_tui-check.json | 14 + .../cli_tui.tar.gz | Bin 0 -> 33761 bytes .../default-host-budget.json | 32 + .../pure-source-preview.tar.gz | Bin 0 -> 332792 bytes .../source-check-linux-aarch64.json | 15 + .../gtk-asan-final.json | 1 + .../gtk-asan-repeated.json | 1 + .../2026-09-24-multi-document-window/gtk-asan.json | 1 + .../gtk-flow-final.json | 1 + .../gtk-flow-repeated.json | 1 + .../2026-09-24-multi-document-window/gtk-flow.json | 1 + .../hybrid-gtk-flow.json | 1 + .../legacy-package-recheck.json | 14 + .../legacy-package-repeated.json | 14 + .../package-final.json | 14 + .../package-repeated.json | 14 + .../2026-09-24-multi-document-window/package.json | 14 + ...pure-document-window-final-linux-aarch64.tar.gz | Bin 0 -> 49746 bytes .../pure-document-window-linux-aarch64.tar.gz | Bin 0 -> 49722 bytes .../switch-memory-fresh-buffer.json | 1 + .../switch-memory-initial.json | 1 + .../window-profile.json | 25 + .../2026-09-24-native-checkpoint/fixed-memory.json | 43 + .../2026-09-24-native-checkpoint/fixed-tui.json | 24 + .../source-memory.json | 43 + .../2026-09-24-native-checkpoint/source-tui.json | 27 + .../full-package-check.json | 20 + .../gtk-flow-final.json | 1 + .../gtk-flow.json | 1 + .../gtk-sanitized-final.json | 1 + .../gtk-sanitized.json | 1 + .../http-only-package-check.json | 14 + .../micro-memory.json | 14 + .../micro-package-check.json | 12 + .../peer-only-package-check.json | 16 + .../pure-document-authoring-linux-aarch64.tar.gz | Bin 0 -> 100900 bytes .../pure-document-http-only-linux-aarch64.tar.gz | Bin 0 -> 47527 bytes .../pure-document-micro-linux-aarch64.tar.gz | Bin 0 -> 22482 bytes .../pure-document-peer-only-linux-aarch64.tar.gz | Bin 0 -> 49186 bytes .../pure-document-window-only-linux-aarch64.tar.gz | Bin 0 -> 48934 bytes .../window-authoring.png | Bin 0 -> 76178 bytes .../window-budget.json | 25 + .../window-only-package-check.json | 14 + .../2026-09-24-native-document-window/build.json | 16 + .../connection-check.json | 635 + .../core-budget.json | 53 + .../dependencies.txt | 10 + .../gtk-flow.json | 1 + .../gtk-sanitized.json | 1 + .../2026-09-24-native-document-window/size.txt | 6 + .../2026-09-24-native-document-window/stripped.txt | 3 + .../2026-09-24-native-document-window/window.png | Bin 0 -> 23918 bytes .../fixed-cli-package-check.json | 12 + .../fixed-package-check.json | 14 + .../2026-09-24-native-save/flat-fixed-tui.json | 24 + .../2026-09-24-native-save/flat-package-check.json | 14 + .../2026-09-24-native-save/flat-source-tui.json | 27 + .../2026-09-24-native-save/hybrid-fixed-tui.json | 24 + .../2026-09-24-native-save/hybrid-source-tui.json | 27 + .../2026-09-24-native-save/package-check.json | 15 + .../pure-document-fixed-cli-linux-aarch64.tar.gz | Bin 0 -> 12966 bytes ...ure-document-fixed-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 32072 bytes ...pure-document-flat-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 51117 bytes ...re-document-hybrid-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 57788 bytes .../pure-source-preview.tar.gz | Bin 0 -> 333925 bytes .../source-package-check.json | 16 + .../pure-source-preview.tar.gz | Bin 0 -> 325436 bytes .../source-check-linux-aarch64.json | 13 + .../pure-document-fixed-cli-linux-aarch64.tar.gz | Bin 0 -> 12886 bytes ...ument-fixed-cli-linux-aarch64.tar.gz.check.json | 12 + ...ure-document-fixed-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 31965 bytes ...t-fixed-cli-tui-linux-aarch64.tar.gz.check.json | 14 + .../pure-document-source-cli-linux-aarch64.tar.gz | Bin 0 -> 22655 bytes ...ment-source-cli-linux-aarch64.tar.gz.check.json | 12 + ...re-document-source-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 51005 bytes ...-source-cli-tui-linux-aarch64.tar.gz.check.json | 14 + .../pure-source-preview.tar.gz | Bin 0 -> 334682 bytes .../source-package-check.json | 15 + .../results/2026-09-24-paged-storage/app-asan.json | 635 + .../2026-09-24-paged-storage/app-release.json | 635 + .../results/2026-09-24-paged-storage/gtk-flow.json | 1 + .../results/2026-09-24-paged-storage/memory.json | 14 + .../2026-09-24-paged-storage/package-full.json | 20 + .../2026-09-24-paged-storage/package-micro.json | 12 + .../2026-09-24-paged-storage/paged-profile.json | 32 + .../results/2026-09-24-paged-storage/peer.json | 16 + ...e-document-paged-authoring-linux-aarch64.tar.gz | Bin 0 -> 111765 bytes .../pure-document-paged-micro-linux-aarch64.tar.gz | Bin 0 -> 25456 bytes .../2026-09-24-paged-storage/unit-asan.json | 1 + .../results/2026-09-24-paged-storage/unit.json | 1 + .../2026-09-24-paged-storage/write-cost.json | 49 + .../pure-source-preview.tar.gz | Bin 0 -> 321958 bytes .../source-check-linux-aarch64.json | 15 + .../client-server-linux-aarch64.json | 17 + .../client-server-linux-rss-regression.json | 17 + .../client-server-profile.json | 23 + .../server-only-linux-aarch64.json | 17 + .../server-only-linux-rss-regression.json | 17 + .../server-only-profile.json | 23 + .../pure-source-preview.tar.gz | Bin 0 -> 321399 bytes .../source-check-linux-aarch64.json | 14 + .../fixed-linux-aarch64.json | 24 + .../fixed-linux-rss-regression.json | 24 + .../source-linux-aarch64.json | 27 + .../source-linux-rss-regression.json | 27 + .../2026-09-24-posix-idle/fixed-profile.json | 32 + .../fixed-server-regression.json | 68 + .../results/2026-09-24-posix-idle/package.json | 14 + .../pure-document-fixed-http-linux-aarch64.tar.gz | Bin 0 -> 32346 bytes .../reproducible-comparison.json | 21 + .../2026-09-24-posix-idle/service-scale.json | 62 + .../2026-09-24-posix-idle/source-profile.json | 32 + .../results/2026-09-24-prefix-find/cli-tui.tar.gz | Bin 0 -> 32233 bytes .../2026-09-24-prefix-find/package-check.json | 14 + .../2026-09-24-prefix-find/tui-profile.json | 21 + .../results/2026-09-24-prefix-find/tui.json | 22 + .../2026-09-24-release-callback/async-core.json | 55 + .../results/2026-09-24-release-callback/core.json | 53 + .../micro-package-check.json | 12 + .../pure-document-micro-linux-aarch64.tar.gz | Bin 0 -> 22758 bytes .../pure-source-preview.tar.gz | Bin 0 -> 333037 bytes .../2026-09-24-release-callback/runtime.json | 1 + .../source-package-check.json | 15 + .../combined-peer-package-check.json | 27 + ...ure-document-combined-peer-linux-aarch64.tar.gz | Bin 0 -> 49382 bytes .../pure-document-server-only-linux-aarch64.tar.gz | Bin 0 -> 47686 bytes .../server-only-package-check.json | 23 + .../2026-09-24-signal-safe-idle/fixed-profile.json | 32 + .../fixed-server-regression.json | 68 + .../2026-09-24-signal-safe-idle/package.json | 14 + .../pure-document-fixed-http-linux-aarch64.tar.gz | Bin 0 -> 32023 bytes .../reproducible-comparison.json | 21 + .../service-scale-restored.json | 62 + .../2026-09-24-signal-safe-idle/service-scale.json | 60 + .../source-profile.json | 32 + .../pure-source-preview.tar.gz | Bin 0 -> 316748 bytes .../2026-09-24-source-preview/source-check.json | 12 + .../results/2026-09-24-source-tui/core.json | 53 + .../2026-09-24-source-tui/fixed-cli-tui-check.json | 14 + .../2026-09-24-source-tui/fixed-cli-tui.tar.gz | Bin 0 -> 31929 bytes .../2026-09-24-source-tui/fixed-tui-profile.json | 21 + .../pure-source-preview.tar.gz | Bin 0 -> 318619 bytes .../2026-09-24-source-tui/source-check.json | 13 + .../source-cli-tui-check.json | 14 + .../2026-09-24-source-tui/source-cli-tui.tar.gz | Bin 0 -> 50942 bytes .../results/2026-09-24-source-tui/tui-profile.json | 21 + .../results/2026-09-24-source-tui/tui.json | 25 + .../cli-check.json | 12 + .../cli-http-tui-check.json | 16 + .../cli-http-tui.tar.gz | Bin 0 -> 53385 bytes .../cli-tui-check.json | 14 + .../cli-tui.tar.gz | Bin 0 -> 35642 bytes .../2026-09-24-terminal-document-list/cli.tar.gz | Bin 0 -> 14706 bytes .../2026-09-24-terminal-document-list/profile.json | 21 + .../sanitized.json | 22 + .../2026-09-24-terminal-document-list/tui.json | 22 + .../2026-09-24-tui-viewport/latency-baseline.json | 176 + .../2026-09-24-tui-viewport/latency-current.json | 176 + .../2026-09-24-tui-viewport/package-http-tui.json | 16 + .../results/2026-09-24-tui-viewport/package.json | 14 + .../results/2026-09-24-tui-viewport/profile.json | 21 + .../results/2026-09-24-tui-viewport/pty-flow.json | 19 + .../2026-09-24-tui-viewport/pty-sanitized.json | 19 + ...pure-document-cli-http-tui-linux-aarch64.tar.gz | Bin 0 -> 51844 bytes .../pure-document-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 34216 bytes .../final-native-window.json | 1 + .../line-width-4096.json | 1 + .../mallinfo.json | 1 + .../no-document-undo.json | 1 + .../no-key-undo.json | 1 + .../2026-09-24-window-memory-analysis/no-wrap.json | 1 + .../replace-view.json | 1 + .../same-body.json | 1 + .../same-key.json | 1 + .../settled-final.json | 1 + .../skip-text.json | 1 + .../2026-09-24-window-memory-analysis/smaps.json | 1 + .../pure-document-fixed-cli-linux-aarch64.tar.gz | Bin 0 -> 12972 bytes ...ure-document-fixed-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 32119 bytes .../pure-document-source-cli-linux-aarch64.tar.gz | Bin 0 -> 22784 bytes ...re-document-source-cli-tui-linux-aarch64.tar.gz | Bin 0 -> 51167 bytes .../pure-source-preview.tar.gz | Bin 0 -> 334267 bytes .../2026-09-24-work-slot/source-package-check.json | 16 + prototypes/connections/runtime.c | 393 + prototypes/connections/runtime.h | 147 + prototypes/connections/server.c | 645 + prototypes/connections/source-tui-budget.json | 12 + prototypes/connections/store-paged.c | 529 + prototypes/connections/store-port.c | 51 + prototypes/connections/store-port.h | 7 + prototypes/connections/store.c | 309 + prototypes/connections/store.h | 48 + prototypes/connections/test-async-server-gate.c | 21 + prototypes/connections/test-async.c | 330 + prototypes/connections/test-fields.c | 153 + prototypes/connections/test-flow.c | 183 + prototypes/connections/test-paged-store.c | 270 + prototypes/connections/test-program.c | 111 + prototypes/connections/test-runtime.c | 356 + prototypes/connections/test-splice.c | 190 + prototypes/connections/tui-budget.json | 12 + prototypes/connections/window-budget.json | 12 + prototypes/forgetful-core/README.md | 116 + prototypes/forgetful-core/check.py | 69 + prototypes/forgetful-core/core.c | 163 + prototypes/forgetful-core/core.h | 58 + prototypes/forgetful-core/test.c | 256 + prototypes/forgetful-core/verification.json | 67 + prototypes/live-rules/.gitignore | 1 + prototypes/live-rules/README.md | 126 + prototypes/live-rules/adapter.test.mjs | 96 + prototypes/live-rules/cli.mjs | 64 + prototypes/live-rules/core.zig | 504 + prototypes/live-rules/manage.py | 88 + prototypes/live-rules/measure.mjs | 48 + prototypes/live-rules/qa-browser.js | 131 + .../2026-09-22-linux-aarch64/browser-checks.json | 215 + .../results/2026-09-22-linux-aarch64/desktop.png | Bin 0 -> 150506 bytes .../results/2026-09-22-linux-aarch64/mobile.png | Bin 0 -> 123520 bytes .../storage-and-replay.json | 503 + prototypes/live-rules/runtime.mjs | 100 + prototypes/live-rules/web/app.js | 294 + prototypes/live-rules/web/core.wasm | Bin 0 -> 32544 bytes prototypes/live-rules/web/index.html | 101 + prototypes/live-rules/web/style.css | 1 + prototypes/workbench/.gitignore | 2 + prototypes/workbench/CONTRACT.md | 575 + prototypes/workbench/README.md | 354 + prototypes/workbench/baseline.test.mjs | 78 + prototypes/workbench/cli.mjs | 127 + prototypes/workbench/cli.test.mjs | 121 + prototypes/workbench/compare.mjs | 231 + prototypes/workbench/compiler.test.mjs | 240 + prototypes/workbench/core.zig | 999 ++ prototypes/workbench/engine.zig | 700 ++ prototypes/workbench/evaluation-check.mjs | 76 + prototypes/workbench/evaluation.test.mjs | 133 + prototypes/workbench/evaluation.zig | 164 + prototypes/workbench/examples/charge.pure | 34 + prototypes/workbench/examples/counter.pure | 8 + prototypes/workbench/examples/direct.pure | 47 + prototypes/workbench/examples/document.pure | 6 + prototypes/workbench/examples/layout.pure | 92 + prototypes/workbench/examples/routed-layout.pure | 70 + prototypes/workbench/examples/shared-layout.pure | 75 + prototypes/workbench/examples/toggle.pure | 17 + prototypes/workbench/manage.py | 161 + prototypes/workbench/native-fixed.zig | 11 + prototypes/workbench/native.zig | 146 + prototypes/workbench/native/README.md | 136 + prototypes/workbench/native/app-memory.c | 87 + prototypes/workbench/native/app.c | 260 + prototypes/workbench/native/app.h | 32 + prototypes/workbench/native/check.mjs | 178 + prototypes/workbench/native/check_app_memory.py | 103 + prototypes/workbench/native/desktop.c | 503 + prototypes/workbench/native/instance-memory.c | 49 + prototypes/workbench/native/main.c | 135 + prototypes/workbench/native/peer-check.py | 228 + prototypes/workbench/native/peer-wire-test.c | 85 + prototypes/workbench/native/peer-wire.c | 152 + prototypes/workbench/native/peer-wire.h | 32 + prototypes/workbench/native/peer.c | 297 + prototypes/workbench/native/plan.c | 45 + prototypes/workbench/native/plan.h | 39 + prototypes/workbench/native/prepare.mjs | 67 + prototypes/workbench/native/scalar.h | 51 + prototypes/workbench/native/test-fixed.c | 68 + prototypes/workbench/native/test.c | 152 + prototypes/workbench/pointer.test.mjs | 118 + prototypes/workbench/presentation.test.mjs | 132 + prototypes/workbench/profile-refresh.js | 66 + prototypes/workbench/project.test.mjs | 79 + prototypes/workbench/qa-browser.js | 228 + prototypes/workbench/qa-document-guards.js | 30 + prototypes/workbench/qa-document.js | 103 + prototypes/workbench/qa-evaluation.js | 89 + prototypes/workbench/qa-layout.js | 103 + prototypes/workbench/qa-pointer.js | 122 + prototypes/workbench/qa-presentation.js | 141 + prototypes/workbench/qa-presentation.py | 54 + prototypes/workbench/qa-refresh.js | 141 + prototypes/workbench/qa-refresh.py | 53 + prototypes/workbench/qa-release.js | 59 + prototypes/workbench/qa-routed-input.js | 164 + prototypes/workbench/qa-shared-rules.js | 137 + prototypes/workbench/remote.mjs | 89 + .../results/2026-09-22-evaluation/browser.json | 231 + .../results/2026-09-22-evaluation/comparison.json | 309 + .../sources/baseline.test.mjs | 78 + .../results/2026-09-22-evaluation/sources/cli.mjs | 69 + .../2026-09-22-evaluation/sources/cli.test.mjs | 83 + .../sources/compiler.test.mjs | 240 + .../results/2026-09-22-evaluation/sources/core.zig | 813 ++ .../sources/evaluation-check.mjs | 76 + .../sources/evaluation.test.mjs | 133 + .../2026-09-22-evaluation/sources/evaluation.zig | 156 + .../sources/examples/charge.pure | 34 + .../sources/examples/counter.pure | 8 + .../sources/examples/toggle.pure | 17 + .../2026-09-22-evaluation/sources/manage.py | 87 + .../2026-09-22-evaluation/sources/manifest.json | 21 + .../2026-09-22-evaluation/sources/qa-evaluation.js | 86 + .../2026-09-22-evaluation/sources/runtime.test.mjs | 197 + .../2026-09-22-evaluation/sources/web/app.js | 427 + .../2026-09-22-evaluation/sources/web/baseline.mjs | 68 + .../2026-09-22-evaluation/sources/web/compiler.mjs | 303 + .../2026-09-22-evaluation/sources/web/core.wasm | Bin 0 -> 53125 bytes .../2026-09-22-evaluation/sources/web/runtime.mjs | 197 + .../2026-09-22-evaluation/verification.json | 68 + .../results/2026-09-22-linux-aarch64/authored.png | Bin 0 -> 176600 bytes .../results/2026-09-22-linux-aarch64/blank.png | Bin 0 -> 148863 bytes .../2026-09-22-linux-aarch64/browser-checks.json | 159 + .../2026-09-22-linux-aarch64/comparison.json | 298 + .../2026-09-22-linux-aarch64/exported-draft.json | 5 + .../results/2026-09-22-linux-aarch64/mobile.png | Bin 0 -> 151662 bytes .../2026-09-22-linux-aarch64/verification.json | 239 + .../workbench/results/2026-09-23-layout/README.md | 21 + .../2026-09-23-layout/authoring-browser.json | 128 + .../workbench/results/2026-09-23-layout/build.json | 23 + .../results/2026-09-23-layout/layout-browser.json | 97 + .../results/2026-09-23-layout/manifest.json | 21 + .../2026-09-23-layout/pure-layout-desktop.png | Bin 0 -> 206894 bytes .../2026-09-23-layout/pure-layout-mobile.png | Bin 0 -> 162877 bytes .../2026-09-23-layout/pure-layout-saved.json | 96 + .../results/2026-09-23-layout/qa-browser.js | 228 + .../results/2026-09-23-layout/sources/CONTRACT.md | 371 + .../results/2026-09-23-layout/sources/cli.mjs | 93 + .../results/2026-09-23-layout/sources/cli.test.mjs | 121 + .../results/2026-09-23-layout/sources/core.zig | 1136 ++ .../2026-09-23-layout/sources/evaluation.zig | 164 + .../2026-09-23-layout/sources/examples/layout.pure | 92 + .../2026-09-23-layout/sources/project.test.mjs | 79 + .../results/2026-09-23-layout/sources/qa-layout.js | 103 + .../results/2026-09-23-layout/sources/web/app.js | 403 + .../2026-09-23-layout/sources/web/core.wasm | Bin 0 -> 56203 bytes .../2026-09-23-layout/sources/web/index.html | 94 + .../2026-09-23-layout/sources/web/project.mjs | 45 + .../2026-09-23-layout/sources/web/runtime.mjs | 324 + .../2026-09-23-layout/sources/web/scene-input.mjs | 83 + .../2026-09-23-layout/sources/workspace.test.mjs | 89 + .../results/2026-09-23-layout/verification.json | 56 + .../results/2026-09-23-native-instances/README.md | 36 + .../application-build.json | 40 + .../auxiliary-sources.json | 28 + .../2026-09-23-native-instances/browser-build.json | 24 + .../2026-09-23-native-instances/browser-tests.log | 95 + .../default-linkage.json | 12 + .../2026-09-23-native-instances/native.json | 159 + .../2026-09-23-native-instances/sanitized.json | 159 + .../sources/prototypes/connections/evaluate.c | 53 + .../sources/prototypes/connections/flow.h | 25 + .../sources/prototypes/connections/program.h | 44 + .../sources/prototypes/connections/result.h | 8 + .../sources/prototypes/connections/runtime.c | 273 + .../sources/prototypes/connections/runtime.h | 88 + .../sources/prototypes/connections/store-port.c | 44 + .../sources/prototypes/connections/store-port.h | 6 + .../sources/prototypes/connections/store.c | 258 + .../sources/prototypes/connections/store.h | 44 + .../sources/prototypes/workbench/baseline.test.mjs | 78 + .../sources/prototypes/workbench/cli.mjs | 127 + .../sources/prototypes/workbench/cli.test.mjs | 121 + .../sources/prototypes/workbench/compare.mjs | 231 + .../sources/prototypes/workbench/compiler.test.mjs | 240 + .../sources/prototypes/workbench/core.zig | 999 ++ .../sources/prototypes/workbench/engine.zig | 601 + .../prototypes/workbench/evaluation-check.mjs | 76 + .../prototypes/workbench/evaluation.test.mjs | 133 + .../sources/prototypes/workbench/evaluation.zig | 164 + .../prototypes/workbench/examples/charge.pure | 34 + .../prototypes/workbench/examples/counter.pure | 8 + .../prototypes/workbench/examples/direct.pure | 47 + .../prototypes/workbench/examples/document.pure | 6 + .../prototypes/workbench/examples/layout.pure | 92 + .../workbench/examples/routed-layout.pure | 70 + .../workbench/examples/shared-layout.pure | 75 + .../prototypes/workbench/examples/toggle.pure | 17 + .../sources/prototypes/workbench/manage.py | 127 + .../sources/prototypes/workbench/native.zig | 127 + .../sources/prototypes/workbench/native/app.c | 208 + .../sources/prototypes/workbench/native/app.h | 27 + .../sources/prototypes/workbench/native/check.mjs | 129 + .../sources/prototypes/workbench/native/main.c | 135 + .../sources/prototypes/workbench/native/plan.c | 38 + .../sources/prototypes/workbench/native/plan.h | 36 + .../prototypes/workbench/native/prepare.mjs | 35 + .../sources/prototypes/workbench/native/scalar.h | 46 + .../sources/prototypes/workbench/native/test.c | 120 + .../sources/prototypes/workbench/pointer.test.mjs | 118 + .../prototypes/workbench/presentation.test.mjs | 132 + .../sources/prototypes/workbench/project.test.mjs | 79 + .../sources/prototypes/workbench/remote.mjs | 89 + .../sources/prototypes/workbench/routing.test.mjs | 114 + .../sources/prototypes/workbench/rules.test.mjs | 94 + .../sources/prototypes/workbench/runtime.test.mjs | 197 + .../prototypes/workbench/value-input.test.mjs | 50 + .../sources/prototypes/workbench/web/baseline.mjs | 68 + .../sources/prototypes/workbench/web/compiler.mjs | 496 + .../sources/prototypes/workbench/web/core.wasm | Bin 0 -> 61065 bytes .../sources/prototypes/workbench/web/project.mjs | 47 + .../sources/prototypes/workbench/web/runtime.mjs | 350 + .../prototypes/workbench/workspace-check.mjs | 62 + .../prototypes/workbench/workspace.test.mjs | 91 + .../2026-09-23-native-instances/verification.json | 39 + .../results/2026-09-23-pointer-routing/README.md | 51 + .../authoring-result.log | 13 + .../2026-09-23-pointer-routing/authoring-run.log | 237 + .../2026-09-23-pointer-routing/authoring.json | 130 + .../2026-09-23-pointer-routing/before/compiler.mjs | 447 + .../2026-09-23-pointer-routing/before/core.wasm | Bin 0 -> 58440 bytes .../2026-09-23-pointer-routing/before/core.zig | 1230 ++ .../2026-09-23-pointer-routing/before/runtime.mjs | 328 + .../browser-initial-failure.json | 32 + .../2026-09-23-pointer-routing/browser-result.log | 12 + .../2026-09-23-pointer-routing/browser-run.log | 172 + .../2026-09-23-pointer-routing/browser.json | 113 + .../results/2026-09-23-pointer-routing/build.json | 23 + .../2026-09-23-pointer-routing/legacy-browser.json | 106 + .../2026-09-23-pointer-routing/legacy-result.log | 12 + .../2026-09-23-pointer-routing/legacy-run.log | 146 + .../2026-09-23-pointer-routing/manifest.json | 47 + .../2026-09-23-pointer-routing/node-tests.log | 91 + .../pure-routed-layout-desktop.png | Bin 0 -> 211960 bytes .../pure-routed-layout-mobile.png | Bin 0 -> 165704 bytes .../pure-routed-layout-saved.json | 108 + .../2026-09-23-pointer-routing/qa-authoring.js | 228 + .../qa-legacy-pointer.js | 138 + .../2026-09-23-pointer-routing/representation.json | 73 + .../2026-09-23-pointer-routing/sources/CONTRACT.md | 496 + .../2026-09-23-pointer-routing/sources/cli.mjs | 98 + .../2026-09-23-pointer-routing/sources/core.zig | 1398 +++ .../sources/evaluation.zig | 164 + .../sources/examples/routed-layout.pure | 70 + .../sources/presentation.test.mjs | 132 + .../sources/qa-routed-input.js | 164 + .../sources/routing.test.mjs | 114 + .../2026-09-23-pointer-routing/sources/web/app.js | 403 + .../sources/web/compiler.mjs | 484 + .../sources/web/core.wasm | Bin 0 -> 60774 bytes .../sources/web/index.html | 94 + .../sources/web/output.mjs | 103 + .../sources/web/runtime.mjs | 339 + .../sources/web/scene-input.mjs | 84 + .../sources/workspace.test.mjs | 91 + .../2026-09-23-pointer-routing/touch-evidence.json | 246 + .../2026-09-23-pointer-routing/touch-evidence.log | 12 + .../touch-reproduction.js | 21 + .../touch-reproduction.log | 28 + .../2026-09-23-pointer-routing/touch-run.log | 29 + .../2026-09-23-pointer-routing/verification.json | 13 + .../workbench/results/2026-09-23-pointer/README.md | 23 + .../results/2026-09-23-pointer/after/CONTRACT.md | 308 + .../results/2026-09-23-pointer/after/cli.mjs | 74 + .../results/2026-09-23-pointer/after/cli.test.mjs | 97 + .../results/2026-09-23-pointer/after/core.zig | 1027 ++ .../2026-09-23-pointer/after/evaluation.zig | 164 + .../2026-09-23-pointer/after/examples/direct.pure | 48 + .../2026-09-23-pointer/after/pointer.test.mjs | 110 + .../results/2026-09-23-pointer/after/qa-pointer.js | 120 + .../results/2026-09-23-pointer/after/web/app.js | 378 + .../2026-09-23-pointer/after/web/compiler.mjs | 326 + .../results/2026-09-23-pointer/after/web/core.wasm | Bin 0 -> 54511 bytes .../2026-09-23-pointer/after/web/index.html | 93 + .../2026-09-23-pointer/after/web/output.mjs | 95 + .../2026-09-23-pointer/after/web/runtime.mjs | 298 + .../2026-09-23-pointer/after/web/scene-input.mjs | 68 + .../results/2026-09-23-pointer/after/web/style.css | 10 + .../2026-09-23-pointer/authoring-browser.json | 128 + .../results/2026-09-23-pointer/before/cli.mjs | 69 + .../results/2026-09-23-pointer/before/core.zig | 942 ++ .../2026-09-23-pointer/before/evaluation.zig | 164 + .../2026-09-23-pointer/before/manifest.json | 12 + .../results/2026-09-23-pointer/before/web/app.js | 368 + .../2026-09-23-pointer/before/web/compiler.mjs | 303 + .../2026-09-23-pointer/before/web/core.wasm | Bin 0 -> 53952 bytes .../2026-09-23-pointer/before/web/index.html | 93 + .../2026-09-23-pointer/before/web/output.mjs | 92 + .../2026-09-23-pointer/before/web/runtime.mjs | 267 + .../2026-09-23-pointer/before/web/style.css | 8 + .../results/2026-09-23-pointer/build.json | 23 + .../results/2026-09-23-pointer/manifest.json | 23 + .../2026-09-23-pointer/pointer-browser.json | 32 + .../2026-09-23-pointer/pure-pointer-desktop.png | Bin 0 -> 197508 bytes .../2026-09-23-pointer/pure-pointer-mobile.png | Bin 0 -> 159460 bytes .../results/2026-09-23-pointer/qa-browser.js | 228 + .../results/2026-09-23-pointer/qa-pointer.js | 120 + .../results/2026-09-23-pointer/verification.json | 50 + .../2026-09-23-presentation/before/core.zig | 942 ++ .../2026-09-23-presentation/before/evaluation.zig | 164 + .../2026-09-23-presentation/before/manifest.json | 10 + .../2026-09-23-presentation/before/web/app.js | 427 + .../before/web/compiler.mjs | 303 + .../2026-09-23-presentation/before/web/core.wasm | Bin 0 -> 53952 bytes .../2026-09-23-presentation/before/web/index.html | 93 + .../2026-09-23-presentation/before/web/runtime.mjs | 205 + .../2026-09-23-presentation/before/web/style.css | 8 + .../2026-09-23-presentation/browser-authoring.json | 131 + .../2026-09-23-presentation/browser-limit.json | 242 + .../results/2026-09-23-presentation/browser.json | 1536 +++ .../2026-09-23-presentation/verification.json | 58 + .../results/2026-09-23-refresh/before/core.zig | 942 ++ .../2026-09-23-refresh/before/evaluation.zig | 164 + .../2026-09-23-refresh/before/manifest.json | 14 + .../before/presentation.test.mjs | 123 + .../2026-09-23-refresh/before/qa-presentation.js | 141 + .../2026-09-23-refresh/before/qa-presentation.py | 54 + .../results/2026-09-23-refresh/before/web/app.js | 349 + .../2026-09-23-refresh/before/web/compiler.mjs | 303 + .../2026-09-23-refresh/before/web/core.wasm | Bin 0 -> 53952 bytes .../2026-09-23-refresh/before/web/index.html | 93 + .../2026-09-23-refresh/before/web/output.mjs | 92 + .../2026-09-23-refresh/before/web/runtime.mjs | 267 + .../2026-09-23-refresh/before/web/style.css | 8 + .../2026-09-23-refresh/browser-authoring.json | 131 + .../results/2026-09-23-refresh/browser-limit.json | 242 + .../results/2026-09-23-refresh/browser.json | 1487 +++ .../results/2026-09-23-refresh/profile-before.json | 34 + .../results/2026-09-23-refresh/verification.json | 41 + .../workbench/results/2026-09-23-release/README.md | 19 + .../results/2026-09-23-release/after-browser.json | 62 + .../results/2026-09-23-release/after/cli.test.mjs | 97 + .../2026-09-23-release/after/examples/direct.pure | 47 + .../2026-09-23-release/after/pointer.test.mjs | 118 + .../results/2026-09-23-release/after/qa-pointer.js | 122 + .../results/2026-09-23-release/after/qa-release.js | 59 + .../after/web/examples/direct.pure | 47 + .../2026-09-23-release/after/web/scene-input.mjs | 83 + .../results/2026-09-23-release/before-browser.json | 21 + .../results/2026-09-23-release/manifest.json | 6 + .../2026-09-23-release/pointer-browser.json | 33 + .../results/2026-09-23-shared-rules/README.md | 24 + .../2026-09-23-shared-rules/authoring-browser.json | 128 + .../2026-09-23-shared-rules/before/core.zig | 1136 ++ .../before/examples/layout.pure | 92 + .../before/web/compiler.mjs | 326 + .../2026-09-23-shared-rules/before/web/core.wasm | Bin 0 -> 56203 bytes .../2026-09-23-shared-rules/before/web/runtime.mjs | 324 + .../results/2026-09-23-shared-rules/build.json | 23 + .../results/2026-09-23-shared-rules/manifest.json | 21 + .../pure-shared-layout-desktop.png | Bin 0 -> 228262 bytes .../pure-shared-layout-mobile.png | Bin 0 -> 167873 bytes .../pure-shared-layout-saved.json | 108 + .../results/2026-09-23-shared-rules/qa-browser.js | 228 + .../2026-09-23-shared-rules/representation.json | 403 + .../2026-09-23-shared-rules/shared-browser.json | 106 + .../2026-09-23-shared-rules/sources/CONTRACT.md | 437 + .../2026-09-23-shared-rules/sources/core.zig | 1230 ++ .../2026-09-23-shared-rules/sources/evaluation.zig | 164 + .../sources/examples/shared-layout.pure | 75 + .../sources/qa-shared-rules.js | 137 + .../2026-09-23-shared-rules/sources/rules.test.mjs | 94 + .../sources/runtime.test.mjs | 197 + .../2026-09-23-shared-rules/sources/web/app.js | 403 + .../sources/web/compiler.mjs | 447 + .../2026-09-23-shared-rules/sources/web/core.wasm | Bin 0 -> 58440 bytes .../2026-09-23-shared-rules/sources/web/index.html | 94 + .../sources/web/project.mjs | 47 + .../sources/web/runtime.mjs | 328 + .../sources/web/scene-input.mjs | 83 + .../sources/workspace.test.mjs | 89 + .../2026-09-23-shared-rules/verification.json | 62 + .../results/2026-09-23-web-layout/README.md | 47 + .../2026-09-23-web-layout/authoring-result.log | 13 + .../2026-09-23-web-layout/authoring-run.log | 237 + .../results/2026-09-23-web-layout/authoring.json | 130 + .../2026-09-23-web-layout/before/compiler.mjs | 484 + .../results/2026-09-23-web-layout/before/core.wasm | Bin 0 -> 60774 bytes .../results/2026-09-23-web-layout/before/core.zig | 1398 +++ .../2026-09-23-web-layout/before/runtime.mjs | 339 + .../2026-09-23-web-layout/browser-result.log | 12 + .../results/2026-09-23-web-layout/browser-run.log | 111 + .../results/2026-09-23-web-layout/browser.json | 89 + .../results/2026-09-23-web-layout/build.json | 23 + .../2026-09-23-web-layout/compiler-check.log | 21 + .../2026-09-23-web-layout/guards-result.log | 9 + .../results/2026-09-23-web-layout/guards-run.log | 38 + .../results/2026-09-23-web-layout/guards.json | 10 + .../results/2026-09-23-web-layout/manifest.json | 45 + .../results/2026-09-23-web-layout/native-build.log | 39 + .../results/2026-09-23-web-layout/node-tests.log | 95 + .../pure-document-desktop.png | Bin 0 -> 184975 bytes .../2026-09-23-web-layout/pure-document-mobile.png | Bin 0 -> 106808 bytes .../2026-09-23-web-layout/pure-document-saved.txt | 3 + .../results/2026-09-23-web-layout/qa-authoring.js | 229 + .../2026-09-23-web-layout/qa-value-control.js | 12 + .../2026-09-23-web-layout/sources/CONTRACT.md | 568 + .../results/2026-09-23-web-layout/sources/cli.mjs | 103 + .../results/2026-09-23-web-layout/sources/core.zig | 1443 +++ .../2026-09-23-web-layout/sources/evaluation.zig | 164 + .../sources/examples/document.pure | 6 + .../sources/qa-document-guards.js | 30 + .../2026-09-23-web-layout/sources/qa-document.js | 103 + .../sources/value-input.test.mjs | 50 + .../2026-09-23-web-layout/sources/web/app.js | 411 + .../2026-09-23-web-layout/sources/web/compiler.mjs | 496 + .../2026-09-23-web-layout/sources/web/core.wasm | Bin 0 -> 61072 bytes .../2026-09-23-web-layout/sources/web/document.css | 3 + .../sources/web/document.html | 32 + .../2026-09-23-web-layout/sources/web/document.mjs | 132 + .../2026-09-23-web-layout/sources/web/index.html | 95 + .../2026-09-23-web-layout/sources/web/runtime.mjs | 350 + .../sources/web/split-input.mjs | 57 + .../2026-09-23-web-layout/value-control.json | 7 + .../2026-09-23-web-layout/value-control.log | 18 + .../2026-09-23-web-layout/verification.json | 14 + .../results/2026-09-23-workspace/browser.json | 240 + .../results/2026-09-23-workspace/comparison.json | 142 + .../results/2026-09-23-workspace/verification.json | 75 + .../app-memory-three-way-final.json | 160 + .../2026-09-24-fixed-capacity/fixed-cli-build.json | 48 + .../fixed-parity-final.json | 209 + .../fixed-peer-build.json | 52 + .../general-parity-final.json | 209 + .../2026-09-24-fixed-capacity/instance-memory.json | 65 + .../2026-09-24-fixed-capacity/peer-check.json | 1 + .../results/2026-09-24-fixed-capacity/size.txt | 6 + .../results/2026-09-24-fixed-capacity/stripped.txt | 3 + .../2026-09-24-idle-work-binding/app-memory.json | 108 + .../native-sanitized.json | 1 + .../2026-09-24-idle-work-binding/native-unit.json | 1 + .../2026-09-24-idle-work-binding/scalar-peer.json | 1 + .../2026-09-24-idle-work-binding/stack-baseline.su | 9 + .../2026-09-24-idle-work-binding/stack-elastic.su | 9 + .../results/2026-09-24-native-desktop/README.md | 38 + .../2026-09-24-native-desktop/authored-after.pure | 72 + .../2026-09-24-native-desktop/capture-x11.c | 43 + .../2026-09-24-native-desktop/core-budget.json | 53 + .../2026-09-24-native-desktop/drag-and-save.png | Bin 0 -> 134122 bytes .../2026-09-24-native-desktop/manifest.json | 141 + .../native-parity-sanitized.json | 159 + .../2026-09-24-native-desktop/native-parity.json | 159 + .../2026-09-24-native-desktop/numeric-input.png | Bin 0 -> 45958 bytes .../2026-09-24-native-desktop/recovered-edit.png | Bin 0 -> 139858 bytes .../2026-09-24-native-desktop/rejected-edit.png | Bin 0 -> 142502 bytes .../2026-09-24-native-desktop/reopened-state.png | Bin 0 -> 134717 bytes .../2026-09-24-native-desktop/resized-input.png | Bin 0 -> 108314 bytes .../results/2026-09-24-native-desktop/resized.png | Bin 0 -> 108735 bytes .../2026-09-24-native-desktop/sources/manage.py | 137 + .../2026-09-24-native-desktop/sources/native/app.c | 239 + .../2026-09-24-native-desktop/sources/native/app.h | 32 + .../sources/native/check.mjs | 132 + .../sources/native/desktop.c | 503 + .../sources/native/test.c | 151 + .../results/2026-09-24-secure-peer/build.json | 48 + .../results/2026-09-24-secure-peer/peer-check.json | 1 + .../results/2026-09-24-secure-peer/size.txt | 4 + .../2026-09-24-shared-program/cli-build.json | 49 + .../2026-09-24-shared-program/dependencies.txt | 6 + .../2026-09-24-shared-program/desktop-build.json | 84 + .../2026-09-24-shared-program/fixed-parity.json | 205 + .../2026-09-24-shared-program/general-parity.json | 209 + .../2026-09-24-shared-program/instance-memory.json | 71 + .../2026-09-24-shared-program/peer-build.json | 53 + .../2026-09-24-shared-program/peer-check.json | 1 + .../results/2026-09-24-shared-program/size.txt | 5 + .../results/2026-09-24-shared-program/stripped.txt | 2 + .../results/2026-09-24-shared-program/symbols.txt | 4 + prototypes/workbench/routing.test.mjs | 114 + prototypes/workbench/rules.test.mjs | 94 + prototypes/workbench/runtime.test.mjs | 197 + prototypes/workbench/value-input.test.mjs | 50 + prototypes/workbench/web/app.js | 411 + prototypes/workbench/web/baseline.html | 108 + prototypes/workbench/web/baseline.mjs | 68 + prototypes/workbench/web/compiler.mjs | 496 + prototypes/workbench/web/core.wasm | Bin 0 -> 61073 bytes prototypes/workbench/web/document.css | 3 + prototypes/workbench/web/document.html | 32 + prototypes/workbench/web/document.mjs | 132 + prototypes/workbench/web/index.html | 95 + prototypes/workbench/web/output.mjs | 103 + prototypes/workbench/web/project.mjs | 47 + prototypes/workbench/web/runtime.mjs | 350 + prototypes/workbench/web/scene-input.mjs | 84 + prototypes/workbench/web/split-input.mjs | 57 + prototypes/workbench/web/style.css | 10 + prototypes/workbench/workspace-check.mjs | 62 + prototypes/workbench/workspace.test.mjs | 91 + .../001-checkpoint-granularity/README.md | 110 + .../001-checkpoint-granularity/benchmark.c | 276 + .../2026-09-22-linux-aarch64/environment.json | 330 + .../results/2026-09-22-linux-aarch64/raw.csv | 281 + .../results/2026-09-22-linux-aarch64/summary.csv | 41 + .../experiments/001-checkpoint-granularity/run.py | 148 + .../experiments/002-buffer-retention/README.md | 165 + .../experiments/002-buffer-retention/benchmark.c | 338 + .../experiments/002-buffer-retention/plan.json | 35 + .../2026-09-22-linux-aarch64-v2/complete.json | 5 + .../2026-09-22-linux-aarch64-v2/environment.json | 57 + .../2026-09-22-linux-aarch64-v2/fixtures.json | 68 + .../results/2026-09-22-linux-aarch64-v2/plan.json | 113 + .../results/2026-09-22-linux-aarch64-v2/raw.csv | 241 + .../results/2026-09-22-linux-aarch64-v2/raw.json | 9122 ++++++++++++++ .../sources/benchmark.c | 338 + .../2026-09-22-linux-aarch64-v2/sources/plan.json | 35 + .../2026-09-22-linux-aarch64-v2/sources/run.py | 300 + .../2026-09-22-linux-aarch64-v2/summary.json | 3122 +++++ .../2026-09-22-linux-aarch64-v2/verification.json | 94 + .../results/2026-09-22-linux-aarch64/audit.json | 6 + .../results/2026-09-22-linux-aarch64/complete.json | 5 + .../2026-09-22-linux-aarch64/environment.json | 57 + .../results/2026-09-22-linux-aarch64/fixtures.json | 57 + .../results/2026-09-22-linux-aarch64/plan.json | 103 + .../results/2026-09-22-linux-aarch64/raw.csv | 201 + .../results/2026-09-22-linux-aarch64/raw.json | 7202 +++++++++++ .../2026-09-22-linux-aarch64/sources/benchmark.c | 320 + .../2026-09-22-linux-aarch64/sources/plan.json | 33 + .../2026-09-22-linux-aarch64/sources/run.py | 281 + .../results/2026-09-22-linux-aarch64/summary.json | 2602 ++++ .../2026-09-22-linux-aarch64/verification.json | 92 + research/experiments/002-buffer-retention/run.py | 300 + research/experiments/003-json-fanout/.gitignore | 1 + research/experiments/003-json-fanout/README.md | 164 + research/experiments/003-json-fanout/benchmark.c | 391 + research/experiments/003-json-fanout/plan.json | 32 + .../results/2026-09-22-linux-aarch64/complete.json | 5 + .../2026-09-22-linux-aarch64/environment.json | 96 + .../results/2026-09-22-linux-aarch64/fixtures.json | 128 + .../results/2026-09-22-linux-aarch64/plan.json | 177 + .../results/2026-09-22-linux-aarch64/raw.csv | 211 + .../results/2026-09-22-linux-aarch64/raw.json | 12532 +++++++++++++++++++ .../2026-09-22-linux-aarch64/sources/benchmark.c | 391 + .../2026-09-22-linux-aarch64/sources/plan.json | 32 + .../2026-09-22-linux-aarch64/sources/run.py | 351 + .../sources/vendor/yyjson/LICENSE | 21 + .../sources/vendor/yyjson/UPSTREAM.json | 23 + .../sources/vendor/yyjson/yyjson.c | 11065 ++++++++++++++++ .../sources/vendor/yyjson/yyjson.h | 8230 ++++++++++++ .../results/2026-09-22-linux-aarch64/summary.json | 4006 ++++++ .../2026-09-22-linux-aarch64/verification.json | 114 + research/experiments/003-json-fanout/run.py | 351 + .../003-json-fanout/vendor/yyjson/LICENSE | 21 + .../003-json-fanout/vendor/yyjson/UPSTREAM.json | 23 + .../003-json-fanout/vendor/yyjson/yyjson.c | 11065 ++++++++++++++++ .../003-json-fanout/vendor/yyjson/yyjson.h | 8230 ++++++++++++ .../experiments/004-direct-document-path/README.md | 50 + .../004-direct-document-path/document-path.c | 207 + .../004-direct-document-path/peak-launcher.c | 26 + .../results/2026-09-24-ext4-1m-linux-aarch64.json | 251 + .../results/2026-09-24-ext4-8k-linux-aarch64.json | 251 + ...6-09-24-tmpfs-1m-preliminary-linux-aarch64.json | 238 + ...6-09-24-tmpfs-8k-preliminary-linux-aarch64.json | 322 + .../experiments/004-direct-document-path/run.py | 160 + .../experiments/005-direct-document-peer/README.md | 60 + .../direct-peer-parity-linux-aarch64.json | 16 + ...direct-peer-sanitized-parity-linux-aarch64.json | 16 + .../005-direct-document-peer/direct-peer.c | 207 + ...re-peer-after-nodelay-parity-linux-aarch64.json | 16 + .../pure-peer-parity-linux-aarch64.json | 16 + ...-server-after-nodelay-parity-linux-aarch64.json | 16 + ...server-after-nodelay-profile-linux-aarch64.json | 23 + .../pure-server-parity-linux-aarch64.json | 16 + .../pure-server-profile-linux-aarch64.json | 23 + ...pure-server-sanitized-parity-linux-aarch64.json | 16 + .../2026-09-24-after-nodelay-8k-linux-aarch64.json | 109 + ...2026-09-24-before-nodelay-8k-linux-aarch64.json | 109 + .../2026-09-24-nodelay-1m-linux-aarch64.json | 157 + .../2026-09-24-nodelay-8k-linux-aarch64.json | 205 + .../experiments/005-direct-document-peer/run.py | 145 + ...ar-peer-after-nodelay-parity-linux-aarch64.json | 1 + .../2026-09-24-check-hybrid.log.gz | Bin 0 -> 6376 bytes .../2026-09-24-ext4-linux-aarch64.json | 463 + .../2026-09-24-sequence-ext4-linux-aarch64.json | 862 ++ .../005-storage-edit-latency/copy-memory.json | 43 + .../005-storage-edit-latency/flat-memory.json | 43 + .../005-storage-edit-latency/measure.py | 99 + .../005-storage-edit-latency/paged-memory.json | 43 + .../005-storage-edit-latency/sequence.py | 85 + .../2026-09-24-ext4-linux-aarch64.json | 221 + .../experiments/006-native-save-latency/measure.py | 139 + 1272 files changed, 222212 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/native-macos-arm64.yml create mode 100644 .gitignore create mode 100644 CHECKPOINT.md create mode 100644 docs/design/001-operation-semantics.md create mode 100644 docs/design/002-memory-lifetime.md create mode 100644 docs/design/003-module-contract.md create mode 100644 docs/design/004-prepared-connections.md create mode 100644 docs/design/005-field-connections.md create mode 100644 docs/design/006-native-protocol.md create mode 100644 docs/design/007-execution-domains.md create mode 100644 docs/design/abi-v0.h create mode 100644 docs/design/check-abi-layout.py create mode 100644 docs/design/validation/2026-09-22-record-layout.json create mode 100644 docs/research/000-working-method.md create mode 100644 docs/research/001-research-brief.md create mode 100644 docs/research/002-checkpoint-results.md create mode 100644 docs/research/003-live-rules.md create mode 100644 docs/research/004-authoring.md create mode 100644 docs/research/005-foundation-contract.md create mode 100644 docs/research/006-problem-landscape.md create mode 100644 docs/research/007-buffer-retention-results.md create mode 100644 docs/research/008-json-fanout-results.md create mode 100644 docs/research/009-bounded-display-integration.md create mode 100644 docs/research/010-roadmap.md create mode 100644 docs/research/011-direct-presentation.md create mode 100644 docs/research/012-refresh-ownership.md create mode 100644 docs/research/013-authored-pointer-input.md create mode 100644 docs/research/014-layout-persistence.md create mode 100644 docs/research/015-shared-rules.md create mode 100644 docs/research/016-pointer-routing.md create mode 100644 docs/research/017-web-layout-boundary.md create mode 100644 docs/research/018-native-connections.md create mode 100644 docs/research/019-byte-storage.md create mode 100644 docs/research/020-prepared-connections.md create mode 100644 docs/research/021-field-connections.md create mode 100644 docs/research/022-execution-core.md create mode 100644 docs/research/023-document-edits.md create mode 100644 docs/research/024-native-app-instances.md create mode 100644 docs/research/025-native-authoring-shell.md create mode 100644 docs/research/026-secure-peer-link.md create mode 100644 docs/research/027-fixed-native-capacity.md create mode 100644 docs/research/028-shared-fixed-program.md create mode 100644 docs/research/029-native-document-window.md create mode 100644 docs/research/030-authenticated-document-peer.md create mode 100644 docs/research/031-relocatable-document-preview.md create mode 100644 docs/research/032-micro-native-authoring-boundary.md create mode 100644 docs/research/033-paged-document-storage.md create mode 100644 docs/research/034-hybrid-document-storage.md create mode 100644 docs/research/035-native-multi-document-window.md create mode 100644 docs/research/036-gtk-memory-boundary.md create mode 100644 docs/research/037-build-time-prepared-runner.md create mode 100644 docs/research/038-fixed-http-document-host.md create mode 100644 docs/research/039-idle-http-wakeups.md create mode 100644 docs/research/040-document-service-capacity.md create mode 100644 docs/research/041-posix-idle-signal-wakeup.md create mode 100644 docs/research/042-micro-native-terminal-editor.md create mode 100644 docs/research/043-micro-package-boundary.md create mode 100644 docs/research/044-terminal-viewport-cost.md create mode 100644 docs/research/045-owner-thread-async-effects.md create mode 100644 docs/research/046-optional-async-document-http.md create mode 100644 docs/research/047-bounded-native-document-browser.md create mode 100644 docs/research/048-immutable-read-write-overlap.md create mode 100644 docs/research/049-idle-app-work-slots.md create mode 100644 docs/research/050-bounded-document-prefix-find.md create mode 100644 docs/research/051-optional-runtime-metrics.md create mode 100644 docs/research/052-clean-native-source-preview.md create mode 100644 docs/research/053-source-loaded-native-terminal-editor.md create mode 100644 docs/research/054-direct-document-path.md create mode 100644 docs/research/055-authenticated-peer-transport-cost.md create mode 100644 docs/research/056-server-only-document-package.md create mode 100644 docs/research/057-portable-native-terminal-check.md create mode 100644 docs/research/058-portable-authenticated-peer-check.md create mode 100644 docs/research/059-immutable-file-copy-path.md create mode 100644 docs/research/060-host-native-checkpoint.md create mode 100644 docs/research/061-durable-document-edit-latency.md create mode 100644 docs/research/062-paged-store-rollback.md create mode 100644 docs/research/063-real-in-process-routing.md create mode 100644 docs/research/064-release-callback-reentrancy.md create mode 100644 docs/research/065-native-save-latency.md create mode 100644 docs/research/066-application-boundary.md create mode 100644 docs/research/067-work-slot-layout.md create mode 100644 docs/research/068-opaque-app-core.md create mode 100644 docs/research/069-atomic-authored-size-policy.md create mode 100644 docs/research/070-micro-default-build.md create mode 100644 prototypes/common/tls-link.c create mode 100644 prototypes/common/tls-link.h create mode 100644 prototypes/connections/.gitignore create mode 100644 prototypes/connections/Makefile create mode 100644 prototypes/connections/README.md create mode 100644 prototypes/connections/SOURCE_PREVIEW.md create mode 100644 prototypes/connections/async-core-budget.json create mode 100644 prototypes/connections/async-server-budget.json create mode 100644 prototypes/connections/check.py create mode 100644 prototypes/connections/check_async_server.py create mode 100644 prototypes/connections/check_copy_path.py create mode 100644 prototypes/connections/check_core.py create mode 100644 prototypes/connections/check_document_peer.py create mode 100644 prototypes/connections/check_document_server_package.py create mode 100644 prototypes/connections/check_edits.py create mode 100644 prototypes/connections/check_fixed_program.py create mode 100644 prototypes/connections/check_fixed_server.py create mode 100644 prototypes/connections/check_flat_export.py create mode 100644 prototypes/connections/check_idle_wakes.py create mode 100644 prototypes/connections/check_micro_memory.py create mode 100644 prototypes/connections/check_mirror.py create mode 100644 prototypes/connections/check_package.py create mode 100644 prototypes/connections/check_policy.py create mode 100644 prototypes/connections/check_profile.py create mode 100644 prototypes/connections/check_service_scale.py create mode 100644 prototypes/connections/check_small_scan.py create mode 100644 prototypes/connections/check_source_package.py create mode 100644 prototypes/connections/check_tui.py create mode 100644 prototypes/connections/check_window.py create mode 100644 prototypes/connections/check_write_cost.py create mode 100644 prototypes/connections/compile-program.c create mode 100644 prototypes/connections/core-budget.json create mode 100644 prototypes/connections/core-layout.c create mode 100644 prototypes/connections/document-peer.c create mode 100644 prototypes/connections/edit-tui.c create mode 100644 prototypes/connections/edit-window.c create mode 100644 prototypes/connections/edit.c create mode 100644 prototypes/connections/evaluate.c create mode 100644 prototypes/connections/examples/bounded-editor.pure create mode 100644 prototypes/connections/examples/editor.pure create mode 100644 prototypes/connections/examples/excerpt.pure create mode 100644 prototypes/connections/examples/select-text.py create mode 100644 prototypes/connections/examples/selection.pure create mode 100644 prototypes/connections/examples/storage.pure create mode 100644 prototypes/connections/export_flat_store.py create mode 100644 prototypes/connections/fixed-budget.json create mode 100644 prototypes/connections/fixed-package-budget.json create mode 100644 prototypes/connections/flow.c create mode 100644 prototypes/connections/flow.h create mode 100644 prototypes/connections/hybrid-budget.json create mode 100644 prototypes/connections/hybrid-tui-budget.json create mode 100644 prototypes/connections/measure-peak-rss.c create mode 100644 prototypes/connections/measure_tui_latency.py create mode 100644 prototypes/connections/memory-budget.json create mode 100644 prototypes/connections/mirror.c create mode 100644 prototypes/connections/package_native.py create mode 100644 prototypes/connections/package_source.py create mode 100644 prototypes/connections/paged-budget.json create mode 100644 prototypes/connections/peer-budget.json create mode 100644 prototypes/connections/peer-package-budget.json create mode 100644 prototypes/connections/peer-server-budget.json create mode 100644 prototypes/connections/profile-budget.json create mode 100644 prototypes/connections/program.c create mode 100644 prototypes/connections/program.h create mode 100644 prototypes/connections/result.h create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/README.md create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/initial/native.json create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/initial/sanitized.json create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/initial/test-flow.c create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/native.json create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sanitized.json create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/Makefile create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/check.py create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/flow.c create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/flow.h create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/server.c create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.c create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.h create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/test-flow.c create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/cli.mjs create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/examples/direct.pure create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/remote.mjs create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/compiler.mjs create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/core.wasm create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/project.mjs create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/connections/results/2026-09-23-byte-storage/verification.json create mode 100644 prototypes/connections/results/2026-09-23-document-edits/README.md create mode 100644 prototypes/connections/results/2026-09-23-document-edits/boundary-checks.json create mode 100644 prototypes/connections/results/2026-09-23-document-edits/core.json create mode 100644 prototypes/connections/results/2026-09-23-document-edits/native.json create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sanitized.json create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/Makefile create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check.py create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_core.py create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_edits.py create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-budget.json create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-layout.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/edit.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/evaluate.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/editor.pure create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/excerpt.pure create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/select-text.py create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/selection.pure create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/storage.pure create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.h create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.h create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/result.h create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.h create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/server.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.h create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.h create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-fields.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-flow.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-program.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-runtime.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-splice.c create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/cli.mjs create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/examples/direct.pure create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/remote.mjs create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/compiler.mjs create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/core.wasm create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/project.mjs create mode 100644 prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/connections/results/2026-09-23-document-edits/verification.json create mode 100644 prototypes/connections/results/2026-09-23-execution-core/README.md create mode 100644 prototypes/connections/results/2026-09-23-execution-core/native.json create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sanitized.json create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/Makefile create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/check.py create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/evaluate.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/excerpt.pure create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/select-text.py create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/selection.pure create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/storage.pure create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/flow.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/flow.h create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/program.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/program.h create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/result.h create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/runtime.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/runtime.h create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/server.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store-port.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store-port.h create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store.h create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-fields.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-flow.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-program.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-runtime.c create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/cli.mjs create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/examples/direct.pure create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/remote.mjs create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/compiler.mjs create mode 100755 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/core.wasm create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/project.mjs create mode 100644 prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/connections/results/2026-09-23-execution-core/verification.json create mode 100644 prototypes/connections/results/2026-09-23-field-connections/README.md create mode 100644 prototypes/connections/results/2026-09-23-field-connections/native.json create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sanitized.json create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/Makefile create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/check.py create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/examples/excerpt.pure create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/examples/select-text.py create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/examples/selection.pure create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/examples/storage.pure create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/flow.c create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/flow.h create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/program.c create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/program.h create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/server.c create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/store.c create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/store.h create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-fields.c create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-flow.c create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-program.c create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/cli.mjs create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/examples/direct.pure create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/remote.mjs create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/compiler.mjs create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/core.wasm create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/project.mjs create mode 100644 prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/connections/results/2026-09-23-field-connections/verification.json create mode 100644 prototypes/connections/results/2026-09-23-native-connections/README.md create mode 100644 prototypes/connections/results/2026-09-23-native-connections/native.json create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sanitized.json create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/Makefile create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.c create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.h create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/server.c create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.mjs create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.test.mjs create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/examples/direct.pure create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/remote.mjs create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/compiler.mjs create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/core.wasm create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/project.mjs create mode 100644 prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/connections/results/2026-09-23-native-connections/verification.json create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/README.md create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/native.json create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sanitized.json create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/Makefile create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/check.py create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/examples/excerpt.pure create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/examples/storage.pure create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/flow.c create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/flow.h create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/program.c create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/program.h create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/server.c create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/store.c create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/store.h create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/test-flow.c create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/test-program.c create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/cli.mjs create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/examples/direct.pure create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/remote.mjs create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/compiler.mjs create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/core.wasm create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/project.mjs create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/connections/results/2026-09-23-prepared-connections/verification.json create mode 100644 prototypes/connections/results/2026-09-24-async-effects/async-core.json create mode 100644 prototypes/connections/results/2026-09-24-async-effects/default-core.json create mode 100644 prototypes/connections/results/2026-09-24-async-effects/flow.json create mode 100644 prototypes/connections/results/2026-09-24-async-effects/sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-async-http/full-check.json create mode 100644 prototypes/connections/results/2026-09-24-async-http/gated.json create mode 100644 prototypes/connections/results/2026-09-24-async-http/profile.json create mode 100644 prototypes/connections/results/2026-09-24-async-http/sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-async-http/service-scale-memory.json create mode 100644 prototypes/connections/results/2026-09-24-async-read-write/full-check.json create mode 100644 prototypes/connections/results/2026-09-24-async-read-write/gated.json create mode 100644 prototypes/connections/results/2026-09-24-async-read-write/profile.json create mode 100644 prototypes/connections/results/2026-09-24-async-read-write/sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-async-read-write/service-scale.json create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/policy-peer.json create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/policy.json create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-atomic-policy/source-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/cli-check.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/cli-tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/cli-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/cli.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/core.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/default-profile.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/fixed-memory.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/fixed-profile.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/source-cli-check.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/source-cli.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/tui-profile.json create mode 100644 prototypes/connections/results/2026-09-24-compact-runtime/tui.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range-source/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-copy-range-source/source-check-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/copy-path.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/default-core.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/default-fixed-memory.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/default-fixed-profile.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/default-functional.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/default-memory.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/default-profile.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/functional.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/memory.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/profile.json create mode 100644 prototypes/connections/results/2026-09-24-copy-range/service-scale.json create mode 100644 prototypes/connections/results/2026-09-24-document-peer/clean-peer.log create mode 100644 prototypes/connections/results/2026-09-24-document-peer/clean-release.log create mode 100644 prototypes/connections/results/2026-09-24-document-peer/dynamic.txt create mode 100644 prototypes/connections/results/2026-09-24-document-peer/file-sizes.txt create mode 100644 prototypes/connections/results/2026-09-24-document-peer/hashes.txt create mode 100644 prototypes/connections/results/2026-09-24-document-peer/package-check.json create mode 100644 prototypes/connections/results/2026-09-24-document-peer/package-hashes.txt create mode 100644 prototypes/connections/results/2026-09-24-document-peer/peer-budget.json create mode 100644 prototypes/connections/results/2026-09-24-document-peer/peer-flow.json create mode 100644 prototypes/connections/results/2026-09-24-document-peer/peer-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-document-peer/pure-document-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-document-peer/scalar-refactor.json create mode 100644 prototypes/connections/results/2026-09-24-document-peer/size.txt create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/backpressure.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/final.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/fixed-profile.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/package.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/parity.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/peak-memory.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/pure-document-fixed-http-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/source-profile.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-http/verified.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/memory-final.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/memory-verified.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/memory.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/package-final.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/package.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/parity-final-v2.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/parity-final.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/parity-reconfigured.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/parity-release.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/parity-verified.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/parity.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/profile-final.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/profile.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-final-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-fixed-program/window-regression.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui-final/package.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui-final/profile.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui-final/pty-flow.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui-final/pty-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui-final/pure-document-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/package-cursor.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/package.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/profile.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-flow.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/pty-flow.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/pty-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-fixed-tui/pure-document-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-flat-export-source/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-flat-export-source/source-check-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-flat-export/export-check.json create mode 100644 prototypes/connections/results/2026-09-24-flat-export/export-ext4.json create mode 100644 prototypes/connections/results/2026-09-24-flat-export/export-scale-1024.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-asan.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-release.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/gtk-flow.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/memory.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-full.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-micro.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/peer.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/profile.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-authoring-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-micro-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/small-scan.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit-asan.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage-v2/write-cost.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/app-asan.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/app-release.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/gtk-flow.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/memory.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/package-full.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/package-micro.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/peer.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/profile.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-authoring-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-micro-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/unit-asan.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/unit.json create mode 100644 prototypes/connections/results/2026-09-24-hybrid-storage/write-cost.json create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/archived-comparison.json create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/fixed-profile.json create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/fixed-server-regression.json create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/idle.json create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/package.json create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/pure-document-fixed-http-idle-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/reproducible-comparison.json create mode 100644 prototypes/connections/results/2026-09-24-idle-wake/source-profile.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/async-core.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/async-gated.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/async-server-profile.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli-check.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-check.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli-http.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/cli.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/core.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/default-profile.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/document-peer-profile.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/document-peer.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/fixed-memory.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/fixed-profile.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/full-check.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/package-reproducibility.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/precompact-budget-failure.txt create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/release.log create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/runtime-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/service-scale.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/source-cli-check.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/source-cli.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/source-memory.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/tui-profile.json create mode 100644 prototypes/connections/results/2026-09-24-idle-work-binding/tui.json create mode 100644 prototypes/connections/results/2026-09-24-in-process-routing/mirror-ext4.json create mode 100644 prototypes/connections/results/2026-09-24-in-process-routing/mirror-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-in-process-routing/mirror-tmp.json create mode 100644 prototypes/connections/results/2026-09-24-micro-default/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-micro-default/source-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli-check.json create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http-check.json create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-micro-profile/default-host-budget.json create mode 100644 prototypes/connections/results/2026-09-24-mirror-source/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-mirror-source/source-check-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-final.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-repeated.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-final.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-repeated.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/hybrid-gtk-flow.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-recheck.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-repeated.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/package-final.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/package-repeated.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/package.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-final-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-fresh-buffer.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-initial.json create mode 100644 prototypes/connections/results/2026-09-24-multi-document-window/window-profile.json create mode 100644 prototypes/connections/results/2026-09-24-native-checkpoint/fixed-memory.json create mode 100644 prototypes/connections/results/2026-09-24-native-checkpoint/fixed-tui.json create mode 100644 prototypes/connections/results/2026-09-24-native-checkpoint/source-memory.json create mode 100644 prototypes/connections/results/2026-09-24-native-checkpoint/source-tui.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/full-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow-final.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized-final.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/http-only-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/micro-memory.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/micro-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/peer-only-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-authoring-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-http-only-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-micro-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-peer-only-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-window-only-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/window-authoring.png create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/window-budget.json create mode 100644 prototypes/connections/results/2026-09-24-native-definition-authoring/window-only-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/build.json create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/connection-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/core-budget.json create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/dependencies.txt create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/gtk-flow.json create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/gtk-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/size.txt create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/stripped.txt create mode 100644 prototypes/connections/results/2026-09-24-native-document-window/window.png create mode 100644 prototypes/connections/results/2026-09-24-native-save/fixed-cli-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/fixed-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/flat-fixed-tui.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/flat-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/flat-source-tui.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/hybrid-fixed-tui.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/hybrid-source-tui.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-save/pure-document-flat-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-save/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-save/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-save/source-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-native-source/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-native-source/source-check-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-opaque-core/source-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/app-asan.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/app-release.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/gtk-flow.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/memory.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/package-full.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/package-micro.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/paged-profile.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/peer.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-authoring-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-micro-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/unit-asan.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/unit.json create mode 100644 prototypes/connections/results/2026-09-24-paged-storage/write-cost.json create mode 100644 prototypes/connections/results/2026-09-24-portable-peer-source/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-portable-peer-source/source-check-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-rss-regression.json create mode 100644 prototypes/connections/results/2026-09-24-portable-peer/client-server-profile.json create mode 100644 prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-rss-regression.json create mode 100644 prototypes/connections/results/2026-09-24-portable-peer/server-only-profile.json create mode 100644 prototypes/connections/results/2026-09-24-portable-source/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-portable-source/source-check-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-rss-regression.json create mode 100644 prototypes/connections/results/2026-09-24-portable-tui/source-linux-aarch64.json create mode 100644 prototypes/connections/results/2026-09-24-portable-tui/source-linux-rss-regression.json create mode 100644 prototypes/connections/results/2026-09-24-posix-idle/fixed-profile.json create mode 100644 prototypes/connections/results/2026-09-24-posix-idle/fixed-server-regression.json create mode 100644 prototypes/connections/results/2026-09-24-posix-idle/package.json create mode 100644 prototypes/connections/results/2026-09-24-posix-idle/pure-document-fixed-http-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-posix-idle/reproducible-comparison.json create mode 100644 prototypes/connections/results/2026-09-24-posix-idle/service-scale.json create mode 100644 prototypes/connections/results/2026-09-24-posix-idle/source-profile.json create mode 100644 prototypes/connections/results/2026-09-24-prefix-find/cli-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-prefix-find/package-check.json create mode 100644 prototypes/connections/results/2026-09-24-prefix-find/tui-profile.json create mode 100644 prototypes/connections/results/2026-09-24-prefix-find/tui.json create mode 100644 prototypes/connections/results/2026-09-24-release-callback/async-core.json create mode 100644 prototypes/connections/results/2026-09-24-release-callback/core.json create mode 100644 prototypes/connections/results/2026-09-24-release-callback/micro-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-release-callback/pure-document-micro-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-release-callback/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-release-callback/runtime.json create mode 100644 prototypes/connections/results/2026-09-24-release-callback/source-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-server-only-package/combined-peer-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-server-only-package/pure-document-combined-peer-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-server-only-package/pure-document-server-only-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-server-only-package/server-only-package-check.json create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-profile.json create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-server-regression.json create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/package.json create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/pure-document-fixed-http-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/reproducible-comparison.json create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale-restored.json create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale.json create mode 100644 prototypes/connections/results/2026-09-24-signal-safe-idle/source-profile.json create mode 100644 prototypes/connections/results/2026-09-24-source-preview/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-source-preview/source-check.json create mode 100644 prototypes/connections/results/2026-09-24-source-tui/core.json create mode 100644 prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-source-tui/fixed-tui-profile.json create mode 100644 prototypes/connections/results/2026-09-24-source-tui/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-source-tui/source-check.json create mode 100644 prototypes/connections/results/2026-09-24-source-tui/source-cli-tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-source-tui/source-cli-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-source-tui/tui-profile.json create mode 100644 prototypes/connections/results/2026-09-24-source-tui/tui.json create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/cli-check.json create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui-check.json create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/cli.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/profile.json create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-terminal-document-list/tui.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/latency-baseline.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/latency-current.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/package-http-tui.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/package.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/profile.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/pty-flow.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/pty-sanitized.json create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-http-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/final-native-window.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/line-width-4096.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/mallinfo.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/no-document-undo.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/no-key-undo.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/no-wrap.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/replace-view.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/same-body.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/same-key.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/settled-final.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/skip-text.json create mode 100644 prototypes/connections/results/2026-09-24-window-memory-analysis/smaps.json create mode 100644 prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-tui-linux-aarch64.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-work-slot/pure-source-preview.tar.gz create mode 100644 prototypes/connections/results/2026-09-24-work-slot/source-package-check.json create mode 100644 prototypes/connections/runtime.c create mode 100644 prototypes/connections/runtime.h create mode 100644 prototypes/connections/server.c create mode 100644 prototypes/connections/source-tui-budget.json create mode 100644 prototypes/connections/store-paged.c create mode 100644 prototypes/connections/store-port.c create mode 100644 prototypes/connections/store-port.h create mode 100644 prototypes/connections/store.c create mode 100644 prototypes/connections/store.h create mode 100644 prototypes/connections/test-async-server-gate.c create mode 100644 prototypes/connections/test-async.c create mode 100644 prototypes/connections/test-fields.c create mode 100644 prototypes/connections/test-flow.c create mode 100644 prototypes/connections/test-paged-store.c create mode 100644 prototypes/connections/test-program.c create mode 100644 prototypes/connections/test-runtime.c create mode 100644 prototypes/connections/test-splice.c create mode 100644 prototypes/connections/tui-budget.json create mode 100644 prototypes/connections/window-budget.json create mode 100644 prototypes/forgetful-core/README.md create mode 100644 prototypes/forgetful-core/check.py create mode 100644 prototypes/forgetful-core/core.c create mode 100644 prototypes/forgetful-core/core.h create mode 100644 prototypes/forgetful-core/test.c create mode 100644 prototypes/forgetful-core/verification.json create mode 100644 prototypes/live-rules/.gitignore create mode 100644 prototypes/live-rules/README.md create mode 100644 prototypes/live-rules/adapter.test.mjs create mode 100644 prototypes/live-rules/cli.mjs create mode 100644 prototypes/live-rules/core.zig create mode 100644 prototypes/live-rules/manage.py create mode 100644 prototypes/live-rules/measure.mjs create mode 100644 prototypes/live-rules/qa-browser.js create mode 100644 prototypes/live-rules/results/2026-09-22-linux-aarch64/browser-checks.json create mode 100644 prototypes/live-rules/results/2026-09-22-linux-aarch64/desktop.png create mode 100644 prototypes/live-rules/results/2026-09-22-linux-aarch64/mobile.png create mode 100644 prototypes/live-rules/results/2026-09-22-linux-aarch64/storage-and-replay.json create mode 100644 prototypes/live-rules/runtime.mjs create mode 100644 prototypes/live-rules/web/app.js create mode 100755 prototypes/live-rules/web/core.wasm create mode 100644 prototypes/live-rules/web/index.html create mode 100644 prototypes/live-rules/web/style.css create mode 100644 prototypes/workbench/.gitignore create mode 100644 prototypes/workbench/CONTRACT.md create mode 100644 prototypes/workbench/README.md create mode 100644 prototypes/workbench/baseline.test.mjs create mode 100644 prototypes/workbench/cli.mjs create mode 100644 prototypes/workbench/cli.test.mjs create mode 100644 prototypes/workbench/compare.mjs create mode 100644 prototypes/workbench/compiler.test.mjs create mode 100644 prototypes/workbench/core.zig create mode 100644 prototypes/workbench/engine.zig create mode 100644 prototypes/workbench/evaluation-check.mjs create mode 100644 prototypes/workbench/evaluation.test.mjs create mode 100644 prototypes/workbench/evaluation.zig create mode 100644 prototypes/workbench/examples/charge.pure create mode 100644 prototypes/workbench/examples/counter.pure create mode 100644 prototypes/workbench/examples/direct.pure create mode 100644 prototypes/workbench/examples/document.pure create mode 100644 prototypes/workbench/examples/layout.pure create mode 100644 prototypes/workbench/examples/routed-layout.pure create mode 100644 prototypes/workbench/examples/shared-layout.pure create mode 100644 prototypes/workbench/examples/toggle.pure create mode 100644 prototypes/workbench/manage.py create mode 100644 prototypes/workbench/native-fixed.zig create mode 100644 prototypes/workbench/native.zig create mode 100644 prototypes/workbench/native/README.md create mode 100644 prototypes/workbench/native/app-memory.c create mode 100644 prototypes/workbench/native/app.c create mode 100644 prototypes/workbench/native/app.h create mode 100644 prototypes/workbench/native/check.mjs create mode 100644 prototypes/workbench/native/check_app_memory.py create mode 100644 prototypes/workbench/native/desktop.c create mode 100644 prototypes/workbench/native/instance-memory.c create mode 100644 prototypes/workbench/native/main.c create mode 100644 prototypes/workbench/native/peer-check.py create mode 100644 prototypes/workbench/native/peer-wire-test.c create mode 100644 prototypes/workbench/native/peer-wire.c create mode 100644 prototypes/workbench/native/peer-wire.h create mode 100644 prototypes/workbench/native/peer.c create mode 100644 prototypes/workbench/native/plan.c create mode 100644 prototypes/workbench/native/plan.h create mode 100644 prototypes/workbench/native/prepare.mjs create mode 100644 prototypes/workbench/native/scalar.h create mode 100644 prototypes/workbench/native/test-fixed.c create mode 100644 prototypes/workbench/native/test.c create mode 100644 prototypes/workbench/pointer.test.mjs create mode 100644 prototypes/workbench/presentation.test.mjs create mode 100644 prototypes/workbench/profile-refresh.js create mode 100644 prototypes/workbench/project.test.mjs create mode 100644 prototypes/workbench/qa-browser.js create mode 100644 prototypes/workbench/qa-document-guards.js create mode 100644 prototypes/workbench/qa-document.js create mode 100644 prototypes/workbench/qa-evaluation.js create mode 100644 prototypes/workbench/qa-layout.js create mode 100644 prototypes/workbench/qa-pointer.js create mode 100644 prototypes/workbench/qa-presentation.js create mode 100644 prototypes/workbench/qa-presentation.py create mode 100644 prototypes/workbench/qa-refresh.js create mode 100644 prototypes/workbench/qa-refresh.py create mode 100644 prototypes/workbench/qa-release.js create mode 100644 prototypes/workbench/qa-routed-input.js create mode 100644 prototypes/workbench/qa-shared-rules.js create mode 100644 prototypes/workbench/remote.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/browser.json create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/comparison.json create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/baseline.test.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/cli.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/cli.test.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/compiler.test.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/core.zig create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation-check.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.test.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/examples/charge.pure create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/examples/counter.pure create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/examples/toggle.pure create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/manage.py create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/manifest.json create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/qa-evaluation.js create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/runtime.test.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/web/app.js create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/web/baseline.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/web/compiler.mjs create mode 100755 prototypes/workbench/results/2026-09-22-evaluation/sources/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/sources/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-22-evaluation/verification.json create mode 100644 prototypes/workbench/results/2026-09-22-linux-aarch64/authored.png create mode 100644 prototypes/workbench/results/2026-09-22-linux-aarch64/blank.png create mode 100644 prototypes/workbench/results/2026-09-22-linux-aarch64/browser-checks.json create mode 100644 prototypes/workbench/results/2026-09-22-linux-aarch64/comparison.json create mode 100644 prototypes/workbench/results/2026-09-22-linux-aarch64/exported-draft.json create mode 100644 prototypes/workbench/results/2026-09-22-linux-aarch64/mobile.png create mode 100644 prototypes/workbench/results/2026-09-22-linux-aarch64/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-layout/README.md create mode 100644 prototypes/workbench/results/2026-09-23-layout/authoring-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-layout/build.json create mode 100644 prototypes/workbench/results/2026-09-23-layout/layout-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-layout/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-layout/pure-layout-desktop.png create mode 100644 prototypes/workbench/results/2026-09-23-layout/pure-layout-mobile.png create mode 100644 prototypes/workbench/results/2026-09-23-layout/pure-layout-saved.json create mode 100644 prototypes/workbench/results/2026-09-23-layout/qa-browser.js create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/CONTRACT.md create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/cli.mjs create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/cli.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/examples/layout.pure create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/project.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/qa-layout.js create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/web/project.mjs create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/web/scene-input.mjs create mode 100644 prototypes/workbench/results/2026-09-23-layout/sources/workspace.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-layout/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/README.md create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/application-build.json create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/auxiliary-sources.json create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/browser-build.json create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/browser-tests.log create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/default-linkage.json create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/native.json create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sanitized.json create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/evaluate.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/flow.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/program.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/result.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store-port.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store-port.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/baseline.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/cli.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/cli.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compare.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compiler.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/engine.zig create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation-check.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/charge.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/counter.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/direct.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/document.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/layout.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/routed-layout.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/shared-layout.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/toggle.pure create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/manage.py create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native.zig create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/check.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/main.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/prepare.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/scalar.h create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/test.c create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/pointer.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/presentation.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/project.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/remote.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/routing.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/rules.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/runtime.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/value-input.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/baseline.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/project.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace-check.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-native-instances/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/README.md create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/authoring-result.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/authoring-run.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/authoring.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/before/compiler.mjs create mode 100755 prototypes/workbench/results/2026-09-23-pointer-routing/before/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/before/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/before/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/browser-initial-failure.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/browser-result.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/browser-run.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/browser.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/build.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/legacy-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/legacy-result.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/legacy-run.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/node-tests.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-desktop.png create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-mobile.png create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-saved.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/qa-authoring.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/qa-legacy-pointer.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/representation.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/CONTRACT.md create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/cli.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/examples/routed-layout.pure create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/presentation.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/qa-routed-input.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/routing.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/output.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/scene-input.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/sources/workspace.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/touch-run.log create mode 100644 prototypes/workbench/results/2026-09-23-pointer-routing/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer/README.md create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/CONTRACT.md create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/cli.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/cli.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/examples/direct.pure create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/pointer.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/qa-pointer.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/output.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/scene-input.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/after/web/style.css create mode 100644 prototypes/workbench/results/2026-09-23-pointer/authoring-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/cli.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/web/output.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-pointer/before/web/style.css create mode 100644 prototypes/workbench/results/2026-09-23-pointer/build.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer/pointer-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-pointer/pure-pointer-desktop.png create mode 100644 prototypes/workbench/results/2026-09-23-pointer/pure-pointer-mobile.png create mode 100644 prototypes/workbench/results/2026-09-23-pointer/qa-browser.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer/qa-pointer.js create mode 100644 prototypes/workbench/results/2026-09-23-pointer/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-presentation/before/web/style.css create mode 100644 prototypes/workbench/results/2026-09-23-presentation/browser-authoring.json create mode 100644 prototypes/workbench/results/2026-09-23-presentation/browser-limit.json create mode 100644 prototypes/workbench/results/2026-09-23-presentation/browser.json create mode 100644 prototypes/workbench/results/2026-09-23-presentation/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/presentation.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.js create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.py create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/web/output.mjs create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-refresh/before/web/style.css create mode 100644 prototypes/workbench/results/2026-09-23-refresh/browser-authoring.json create mode 100644 prototypes/workbench/results/2026-09-23-refresh/browser-limit.json create mode 100644 prototypes/workbench/results/2026-09-23-refresh/browser.json create mode 100644 prototypes/workbench/results/2026-09-23-refresh/profile-before.json create mode 100644 prototypes/workbench/results/2026-09-23-refresh/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-release/README.md create mode 100644 prototypes/workbench/results/2026-09-23-release/after-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-release/after/cli.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-release/after/examples/direct.pure create mode 100644 prototypes/workbench/results/2026-09-23-release/after/pointer.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-release/after/qa-pointer.js create mode 100644 prototypes/workbench/results/2026-09-23-release/after/qa-release.js create mode 100644 prototypes/workbench/results/2026-09-23-release/after/web/examples/direct.pure create mode 100644 prototypes/workbench/results/2026-09-23-release/after/web/scene-input.mjs create mode 100644 prototypes/workbench/results/2026-09-23-release/before-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-release/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-release/pointer-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/README.md create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/authoring-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/before/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/before/examples/layout.pure create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/before/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/before/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/before/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/build.json create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-desktop.png create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-mobile.png create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-saved.json create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/qa-browser.js create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/representation.json create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/shared-browser.json create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/CONTRACT.md create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/examples/shared-layout.pure create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/qa-shared-rules.js create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/rules.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/runtime.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/web/project.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/web/scene-input.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/sources/workspace.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-shared-rules/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/README.md create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/authoring-result.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/authoring-run.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/authoring.json create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/before/compiler.mjs create mode 100755 prototypes/workbench/results/2026-09-23-web-layout/before/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/before/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/before/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/browser-result.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/browser-run.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/browser.json create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/build.json create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/compiler-check.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/guards-result.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/guards-run.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/guards.json create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/manifest.json create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/native-build.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/node-tests.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/pure-document-desktop.png create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/pure-document-mobile.png create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/pure-document-saved.txt create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/qa-authoring.js create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/qa-value-control.js create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/CONTRACT.md create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/cli.mjs create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/core.zig create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/evaluation.zig create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/examples/document.pure create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document-guards.js create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document.js create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/value-input.test.mjs create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/app.js create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/compiler.mjs create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/core.wasm create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.css create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.html create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.mjs create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/index.html create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/runtime.mjs create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/sources/web/split-input.mjs create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/value-control.json create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/value-control.log create mode 100644 prototypes/workbench/results/2026-09-23-web-layout/verification.json create mode 100644 prototypes/workbench/results/2026-09-23-workspace/browser.json create mode 100644 prototypes/workbench/results/2026-09-23-workspace/comparison.json create mode 100644 prototypes/workbench/results/2026-09-23-workspace/verification.json create mode 100644 prototypes/workbench/results/2026-09-24-compact-runtime/app-memory-three-way-final.json create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-cli-build.json create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-parity-final.json create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-peer-build.json create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/general-parity-final.json create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/instance-memory.json create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/peer-check.json create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/size.txt create mode 100644 prototypes/workbench/results/2026-09-24-fixed-capacity/stripped.txt create mode 100644 prototypes/workbench/results/2026-09-24-idle-work-binding/app-memory.json create mode 100644 prototypes/workbench/results/2026-09-24-idle-work-binding/native-sanitized.json create mode 100644 prototypes/workbench/results/2026-09-24-idle-work-binding/native-unit.json create mode 100644 prototypes/workbench/results/2026-09-24-idle-work-binding/scalar-peer.json create mode 100644 prototypes/workbench/results/2026-09-24-idle-work-binding/stack-baseline.su create mode 100644 prototypes/workbench/results/2026-09-24-idle-work-binding/stack-elastic.su create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/README.md create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/authored-after.pure create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/capture-x11.c create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/core-budget.json create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/drag-and-save.png create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/manifest.json create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/native-parity-sanitized.json create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/native-parity.json create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/numeric-input.png create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/recovered-edit.png create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/rejected-edit.png create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/reopened-state.png create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/resized-input.png create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/resized.png create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/sources/manage.py create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.h create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/sources/native/check.mjs create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/sources/native/desktop.c create mode 100644 prototypes/workbench/results/2026-09-24-native-desktop/sources/native/test.c create mode 100644 prototypes/workbench/results/2026-09-24-secure-peer/build.json create mode 100644 prototypes/workbench/results/2026-09-24-secure-peer/peer-check.json create mode 100644 prototypes/workbench/results/2026-09-24-secure-peer/size.txt create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/cli-build.json create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/dependencies.txt create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/desktop-build.json create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/fixed-parity.json create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/general-parity.json create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/instance-memory.json create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/peer-build.json create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/peer-check.json create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/size.txt create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/stripped.txt create mode 100644 prototypes/workbench/results/2026-09-24-shared-program/symbols.txt create mode 100644 prototypes/workbench/routing.test.mjs create mode 100644 prototypes/workbench/rules.test.mjs create mode 100644 prototypes/workbench/runtime.test.mjs create mode 100644 prototypes/workbench/value-input.test.mjs create mode 100644 prototypes/workbench/web/app.js create mode 100644 prototypes/workbench/web/baseline.html create mode 100644 prototypes/workbench/web/baseline.mjs create mode 100644 prototypes/workbench/web/compiler.mjs create mode 100755 prototypes/workbench/web/core.wasm create mode 100644 prototypes/workbench/web/document.css create mode 100644 prototypes/workbench/web/document.html create mode 100644 prototypes/workbench/web/document.mjs create mode 100644 prototypes/workbench/web/index.html create mode 100644 prototypes/workbench/web/output.mjs create mode 100644 prototypes/workbench/web/project.mjs create mode 100644 prototypes/workbench/web/runtime.mjs create mode 100644 prototypes/workbench/web/scene-input.mjs create mode 100644 prototypes/workbench/web/split-input.mjs create mode 100644 prototypes/workbench/web/style.css create mode 100644 prototypes/workbench/workspace-check.mjs create mode 100644 prototypes/workbench/workspace.test.mjs create mode 100644 research/experiments/001-checkpoint-granularity/README.md create mode 100644 research/experiments/001-checkpoint-granularity/benchmark.c create mode 100644 research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/environment.json create mode 100644 research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/raw.csv create mode 100644 research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/summary.csv create mode 100644 research/experiments/001-checkpoint-granularity/run.py create mode 100644 research/experiments/002-buffer-retention/README.md create mode 100644 research/experiments/002-buffer-retention/benchmark.c create mode 100644 research/experiments/002-buffer-retention/plan.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/complete.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/environment.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/fixtures.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/plan.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.csv create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/benchmark.c create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/plan.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/run.py create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/summary.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/verification.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/audit.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/complete.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/environment.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/fixtures.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/plan.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.csv create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/benchmark.c create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/plan.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/run.py create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/summary.json create mode 100644 research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/verification.json create mode 100644 research/experiments/002-buffer-retention/run.py create mode 100644 research/experiments/003-json-fanout/.gitignore create mode 100644 research/experiments/003-json-fanout/README.md create mode 100644 research/experiments/003-json-fanout/benchmark.c create mode 100644 research/experiments/003-json-fanout/plan.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/complete.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/environment.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/fixtures.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/plan.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.csv create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/benchmark.c create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/plan.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/run.py create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/LICENSE create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/UPSTREAM.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.c create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.h create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/summary.json create mode 100644 research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/verification.json create mode 100644 research/experiments/003-json-fanout/run.py create mode 100644 research/experiments/003-json-fanout/vendor/yyjson/LICENSE create mode 100644 research/experiments/003-json-fanout/vendor/yyjson/UPSTREAM.json create mode 100644 research/experiments/003-json-fanout/vendor/yyjson/yyjson.c create mode 100644 research/experiments/003-json-fanout/vendor/yyjson/yyjson.h create mode 100644 research/experiments/004-direct-document-path/README.md create mode 100644 research/experiments/004-direct-document-path/document-path.c create mode 100644 research/experiments/004-direct-document-path/peak-launcher.c create mode 100644 research/experiments/004-direct-document-path/results/2026-09-24-ext4-1m-linux-aarch64.json create mode 100644 research/experiments/004-direct-document-path/results/2026-09-24-ext4-8k-linux-aarch64.json create mode 100644 research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-1m-preliminary-linux-aarch64.json create mode 100644 research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-8k-preliminary-linux-aarch64.json create mode 100644 research/experiments/004-direct-document-path/run.py create mode 100644 research/experiments/005-direct-document-peer/README.md create mode 100644 research/experiments/005-direct-document-peer/direct-peer-parity-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/direct-peer-sanitized-parity-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/direct-peer.c create mode 100644 research/experiments/005-direct-document-peer/pure-peer-after-nodelay-parity-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/pure-peer-parity-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/pure-server-after-nodelay-parity-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/pure-server-after-nodelay-profile-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/pure-server-parity-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/pure-server-profile-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/pure-server-sanitized-parity-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/results/2026-09-24-after-nodelay-8k-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/results/2026-09-24-before-nodelay-8k-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-1m-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-8k-linux-aarch64.json create mode 100644 research/experiments/005-direct-document-peer/run.py create mode 100644 research/experiments/005-direct-document-peer/scalar-peer-after-nodelay-parity-linux-aarch64.json create mode 100644 research/experiments/005-storage-edit-latency/2026-09-24-check-hybrid.log.gz create mode 100644 research/experiments/005-storage-edit-latency/2026-09-24-ext4-linux-aarch64.json create mode 100644 research/experiments/005-storage-edit-latency/2026-09-24-sequence-ext4-linux-aarch64.json create mode 100644 research/experiments/005-storage-edit-latency/copy-memory.json create mode 100644 research/experiments/005-storage-edit-latency/flat-memory.json create mode 100644 research/experiments/005-storage-edit-latency/measure.py create mode 100644 research/experiments/005-storage-edit-latency/paged-memory.json create mode 100644 research/experiments/005-storage-edit-latency/sequence.py create mode 100644 research/experiments/006-native-save-latency/2026-09-24-ext4-linux-aarch64.json create mode 100644 research/experiments/006-native-save-latency/measure.py diff --git a/.github/workflows/native-macos-arm64.yml b/.github/workflows/native-macos-arm64.yml new file mode 100644 index 0000000..7d46d6a --- /dev/null +++ b/.github/workflows/native-macos-arm64.yml @@ -0,0 +1,55 @@ +name: Native macOS arm64 checkpoint + +"on": + push: + pull_request: + workflow_dispatch: + +jobs: + native: + runs-on: macos-15 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - name: Confirm native Apple Silicon runner + run: | + test "$(uname -s)" = Darwin + test "$(uname -m)" = arm64 + cc --version + - name: Keep the default build small + run: | + make -C prototypes/connections BUILD=build/mac-default all + test -f prototypes/connections/build/mac-default/libpure-execution.a + test -x prototypes/connections/build/mac-default/pure-edit + test ! -e prototypes/connections/build/mac-default/pure-connections + test ! -e prototypes/connections/build/mac-default/pure-document-peer + test ! -e prototypes/connections/build/mac-default/pure-edit-window + - name: Build and exercise native document app + run: | + make -C prototypes/connections BUILD=build/mac-native check-native-checkpoint \ + PORTABLE_SOURCE_MEMORY_EVIDENCE="$RUNNER_TEMP/source-memory.json" \ + PORTABLE_FIXED_MEMORY_EVIDENCE="$RUNNER_TEMP/fixed-memory.json" + - name: Exercise in-process document routing + run: | + make -C prototypes/connections BUILD=build/mac-native check-mirror \ + MIRROR_EVIDENCE="$RUNNER_TEMP/mirror.json" + - name: Exercise authored document policy + run: | + make -C prototypes/connections BUILD=build/mac-native check-policy \ + POLICY_EVIDENCE="$RUNNER_TEMP/policy.json" + - name: Show whole-process results + if: always() + run: | + for result in "$RUNNER_TEMP"/{source-memory,fixed-memory,mirror,policy}.json; do + if test -f "$result"; then cat "$result"; fi + done + - uses: actions/upload-artifact@v4 + if: always() + with: + name: native-macos-arm64-evidence + path: | + ${{ runner.temp }}/source-memory.json + ${{ runner.temp }}/fixed-memory.json + ${{ runner.temp }}/mirror.json + ${{ runner.temp }}/policy.json + if-no-files-found: warn diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edce92d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.playwright-cli/ +/output/playwright/ +__pycache__/ +*.pyc +/dist/ +/prototypes/*/build/ +/prototypes/*/node_modules/ diff --git a/CHECKPOINT.md b/CHECKPOINT.md new file mode 100644 index 0000000..1968c89 --- /dev/null +++ b/CHECKPOINT.md @@ -0,0 +1,235 @@ +# 첫 공개 체크포인트 — 진행 중 + +목표는 작은 OS의 운영 모델을 가진 네이티브 마이크로 런타임을 실제 앱으로 보여주는 것이다. +작은 코어, 새 문법, 단일 버퍼 실험만으로 완료하지 않는다. 사용자가 직접 작성·변경할 수 있는 +앱과 재현 가능한 실행·비용·실패 검증을 함께 공개할 수 있어야 한다. + +## 공개할 작업물과 완료 증거 + +기존 작업물 편집·저장 흐름을 발전시킨 **네이티브 작업물 편집기**를 하나의 검증 앱으로 삼는다. +앱 정의와 현재 상태를 구분하고, 선택한 항목·내용을 변경·저장·복원하며, 다른 실행기와 +변경을 주고받는다. CLI의 자동화와 네이티브 UI가 같은 앱 로직을 사용해야 한다. +현재 브라우저 UI나 Python 요청 클라이언트를 네이티브 앱 완성으로 대체하지 않는다. + +| 요구 | 완료에 필요한 증거 | 현재 상태 | +|---|---|---| +| Pure가 실행·자원·연결을 소유 | 호스트 타입 없는 실행 코어, 실제 입출력 어댑터, 종료·완료·회수 검증 | 저장과 선택한 scalar 앱 입력·상태·복원을 같은 코어에 연결. [045](docs/research/045-owner-thread-async-effects.md)의 선택형 완료 원시 기능과 [048](docs/research/048-immutable-read-write-overlap.md)의 실제 HTTP 호스트에서 수명·기한·종료·쓰기 중 읽기를 검증했다. 인증 피어 통합과 일반 앱 표현은 미완료 | +| 마이크로 기본 비용 | 실행 전용 구성과 선택 기능의 분리, 코드·메모리·작업별 비용과 전체 앱 비용 공개 | [069](docs/research/069-atomic-authored-size-policy.md)에서 기본 코어 `text` 4,632 B, 유휴 영역 120 B, 작업 슬롯 216 B와 준비된 문서 정의 680 B를 유지하며 실제 문서 크기 정책을 연결했다. 정책을 선택한 소스 CLI/HTTP `text`는 36,135/44,342 B, 고정 CLI/HTTP는 19,268/26,689 B다. 현재 소스·고정 CLI 압축 묶음은 22,862/12,966 B, 고정·소스 CLI+TUI는 32,144/51,398 B이고 모두 이동 후 검증했다. Linux 1 MiB 편집 피크 RSS는 소스 CLI 2,080 KiB, 고정 CLI 2,112 KiB다. 혼합 저장소 HTTP는 456 B 커져 선택형 상한만 검토해 조정했다. HTTP·TLS·GTK는 선택 기능이며 큰 앱의 전체 피크·작업당 비용은 미완료 | +| 작성 가능한 실용 앱 | 사용자가 입력·연산·연결을 수정해 동작을 바꾸고, 새 실행기에서 상태 복원 | [069](docs/research/069-atomic-authored-size-policy.md)의 작성 가능한 문서 크기 정책이 실제 CLI·HTTP·TUI·TLS 효과를 제어하고 새 실행에서 바뀜. 기존 .pure의 네이티브 빌드·실시간 소스 적용·새 프로세스 복원 연결; JSON 프로젝트 변환·큰 앱 표현은 미완료 | +| 네이티브 사용 경험 | 실제 편집·입력·크기 변경·저장·오류 복구를 네이티브 UI에서 실행 | [035](docs/research/035-native-multi-document-window.md)의 Linux GTK 창은 여러 문서·정의 초안과 충돌을 다루지만 [036](docs/research/036-gtk-memory-boundary.md)에서 확인한 약 44 MiB RSS와 전환 중 증가 때문에 저메모리 프로필은 아니다. [044](docs/research/044-terminal-viewport-cost.md)의 선택형 네이티브 TUI는 같은 준비된 문서 앱에서 한국어·여러 행·탭 입력, 키 전환·저장·충돌·크기 변경·1 MiB 작업을 처리한다. [050](docs/research/050-bounded-document-prefix-find.md)은 1,024개 실제 문서의 목록·전환·접두어 탐색을 검사했다. 고정 TUI의 이전 Linux PTY 검사에서 1 MiB 피크 RSS는 2,832~3,168 KiB였다. 최신 고정 CLI+TUI 묶음은 32,144 B다. [053](docs/research/053-source-loaded-native-terminal-editor.md)의 선택형 소스 TUI는 같은 문서 앱을 `.pure`에서 읽고 다른 필드 배치·1 MiB 편집·1,024개 문서 탐색을 실제 PTY에서 확인했다. 현재 소스 CLI+TUI 묶음은 51,398 B다. [065](docs/research/065-native-save-latency.md)는 선택형 페이지 저장소 TUI의 실제 저장 완료 시간·코드·RSS를 검증했다. 본문 검색, 저메모리 GUI, macOS·IME·복합 문자소 처리는 남음. 정의 작성은 별도 작성 호스트가 맡는다 | +| 내부 연결과 Pure 간 RPC | 같은 앱 의미의 내부 연결·원격 연결, 수명·형식·충돌·기한·종료 처리 | [063](docs/research/063-real-in-process-routing.md)에서 실제 1 MiB 문서를 한 프로세스의 두 저장 영역에 추가 입력 버퍼 없이 전달하고 독립 충돌·회수를 검증했다. [030](docs/research/030-authenticated-document-peer.md)의 선택형 TLS 1.3 문서 피어가 CLI·HTTP·창의 같은 `editor.pure` 읽기·변경·충돌·복원을 처리. [055](docs/research/055-authenticated-peer-transport-cost.md)에서 서버 전용 선택 빌드와 같은 보안 전송의 직접 기준선을 추가하고 공통 TCP 지연을 줄였다. [058](docs/research/058-portable-authenticated-peer-check.md)은 인증·1 MiB 문서·실패 시나리오를 Linux 전용 RSS 수집 없이 실행했다. 장기 실행 영역 관리자·macOS·실패망·대규모 부하는 미완료 | +| Pure 외부와의 연결 | 일반 클라이언트와 양방향 입출력, 프로토콜 오류·호환성 검증 | 문서 창·CLI·HTTP·C TLS 피어가 같은 저장 버전과 편집 효과를 공유. 별도 Python TLS 클라이언트가 같은 문서 형식으로 읽고 쓰며 1 MiB 본문을 교환. 실서비스 상호운용성은 미완료 | +| 성능·크기 주장에 대한 근거 | 실제 앱 작업으로 동등한 직접 구현·해당 RPC 기준과 비교, 기능·보안 조건 공개 | [034](docs/research/034-hybrid-document-storage.md)에서 같은 문서 앱의 파일 쓰기 요청량은 큰 문서 약 61.9배 감소, 1 B 문서는 기본과 동일함을 확인했다. 코드 크기는 증가했다. [049](docs/research/049-idle-app-work-slots.md)에서는 실제 네이티브 앱 1,000개의 입력·저장·복원 후 메모리 비용을 비교했다. [040](docs/research/040-document-service-capacity.md)에서는 같은 문서 앱으로 호스트당 1,024개 객체·4,096 HTTP 작업·8동시 연결의 정확성·RSS·회수를 확인했다. [054](docs/research/054-direct-document-path.md)는 같은 저장 작업의 직접 호출과 Pure 경로에서 결과·최종 파일이 같음을 검사하고, Pure 비교 실행물의 `text`가 8,276 B 더 큼을 확인했다. [055](docs/research/055-authenticated-peer-transport-cost.md)는 인증 TLS 문서 읽기에서 직접 서버와 Pure 서버가 같은 결과를 내며, 공통 TCP 지연을 제거하자 짧은 루프백 배치가 크게 단축됨을 보였다. 서버 전용 Pure 빌드의 `text`는 클라이언트 겸용보다 3,472 B 작고, 동일 실험 빌드에서는 직접 서버보다 5,592 B 크다. 직접 기준선은 런타임 전체 계약과 동등하지 않고 속도 우열도 아직 미판정이다. 물리 쓰기량·장시간 서비스 부하·동등 서버 지연·Kitex 수준 성능은 미입증 | +| 공개 가능한 패키지 | 깨끗한 환경의 빌드·실행, 예제 수정 안내, 실패·한도·플랫폼별 검증 자료 | [069](docs/research/069-atomic-authored-size-policy.md)의 현재 소스 묶음을 다른 디렉터리에서 빌드해 C·TUI·실제 문서 RSS·수정한 `.pure` 동작·프로세스 내 실제 문서 전달·선택형 저장 형식 내보내기·Linux 릴리스 검사를 통과했다. 앞선 [059](docs/research/059-immutable-file-copy-path.md) 묶음은 선택형 TLS 피어·Linux 파일 간 복사도 검사했다. [056](docs/research/056-server-only-document-package.md)의 Linux 서버 전용 TLS 묶음은 클라이언트 겸용보다 압축 전체가 1,696 B 작고 두 구성 모두 검토한 상한 안에서 이동 후 인증·1 MiB 문서·복원 작업을 통과했다. 시스템 공유 라이브러리, macOS 빌드·검증, 서명·라이선스와 플랫폼별 설치 절차는 미완료 | + +현재 검증하지 못한 성능·격리·자동 최적화 능력을 소개 문구로 대신하지 않는다. +문서 앱과 scalar 앱은 수명·예산 코어를 공유하지만 아직 하나의 작성 가능한 +앱 모델이 아니다. [069](docs/research/069-atomic-authored-size-policy.md)의 숫자 +상한은 실제 문서 효과를 제어하는 첫 좁은 정책이지 범용 산술 엔진은 아니다. 저장 전용 해석은 [068](docs/research/068-opaque-app-core.md)에서 선택형 어댑터로 +옮겼다. 실제 문서 수정 정책으로 scalar 규칙을 통합할 합격 조건은 +[066 경계 조사](docs/research/066-application-boundary.md)에 기록했다. +코어 수치만으로 범용 앱 런타임을 완성했다고 주장하지 않는다. +macOS 저장 어댑터의 `O_NOFOLLOW` 헤더 노출 문제를 고쳤고, Linux 전용 크기·RSS +도구 없이 C 동작을 실행하는 `make -C prototypes/connections check-portable` 경로를 +추가했다. Linux AArch64에서 통과했으며 Apple Silicon 빌드·실행은 아직 확인하지 않았다. +실제 터미널 작업을 검사하는 `check-portable-tui`도 Linux AArch64에서 통과했다. +이 경로는 메모리 수치를 수집하지 않으므로 기존 Linux RSS 상한 검사를 대신하지 않는다. +[Apple Silicon CI 작업](.github/workflows/native-macos-arm64.yml)은 네이티브 앱· +터미널·1 MiB RSS·프로세스 내 전달을 실제 macOS arm64에서 검사하도록 작성했다. +현재 Git 원격은 로컬 저장소라 작업 실행 결과가 없으며, macOS 통과로 표기하지 않는다. +일반 OS 전체·모든 미디어 코덱·클러스터 운영기를 한꺼번에 만드는 것도 이 체크포인트의 +완료 조건으로 추가하지 않는다. 위 실제 앱에 필요한 연결을 깊게 완성한다. +코어의 4,864 B 검사를 통과하더라도 필수 실행물이나 선택한 앱의 실제 메모리가 커지면 +마이크로 목표를 달성한 것이 아니다. 새 프로토콜·UI는 선택 빌드로 분리하고, +전체 앱 비용을 확인한 뒤에만 작은 앱이라는 표현을 쓴다. [043](docs/research/043-micro-package-boundary.md)부터 +고정 정의 CLI·HTTP·TUI의 선택별 압축 묶음 크기도 별도 상한으로 검증한다. 작성 기능을 +고정 실행 TUI에 중복해서 넣지 않고, 현재의 소스 CLI·선택형 창 경로에서 다룬다. +새 기능은 기본 코어의 `text`·인스턴스 상태, 선택 실행물의 링크 크기, +실제 작업의 전체 RSS와 배포 묶음 증가를 각각 기록한 뒤 받아들인다. +첫 두 항목만 작거나 소스 배포 파일이 작다는 이유로 마이크로라고 부르지 않는다. +기본 `make all`은 코어와 파서 없는 고정 정의 CLI만 빌드한다. 실행 중 정의를 +바꾸는 소스 CLI는 `make editor`, HTTP 서버는 `make server`를 선택해야 한다. +프로세스 관리자나 VM 계층도 첫 체크포인트의 필수 비용으로 추가하지 않는다. +[059](docs/research/059-immutable-file-copy-path.md)의 Linux 선택형 파일 복사는 +기존 불변 형식의 큰 편집에서 사용자 공간 호출을 줄였지만, 실제 디스크 쓰기량이나 +지연 개선을 증명하지 않았으므로 기본 저장 경로로 승격하지 않는다. +[061](docs/research/061-durable-document-edit-latency.md)의 실제 1 MiB 문서 +리비전 수정 비교에서는 페이지 저장이 이 Linux/ext4 호스트에서 훨씬 짧은 +중앙값을 보였다. 파일 간 복사의 지연 이득은 구분되지 않았다. 페이지 형식은 +추가 코드·꼬리 지연·macOS 검증이 남아 선택형으로 유지한다. +[065](docs/research/065-native-save-latency.md)는 같은 효과를 실제 네이티브 +TUI의 Ctrl+S부터 저장 완료 표시까지 확인했다. 1 MiB 마지막 1 B 수정의 +중앙값은 기본 93.17 ms, 선택형 페이지 22.04 ms였고, 256 B 문서는 +8.54·9.23 ms였다. 선택형 소스 TUI는 코드가 4,958 B 크고 큰 문서의 저장 +형식도 달라 기본으로 올리지 않았다. 기본 코어와 기본 TUI의 실행 비용은 +이 선택형 실험 때문에 늘리지 않는다. +[062](docs/research/062-paged-store-rollback.md)에서 별도 디렉터리로 본문과 +리비전을 내보내 기본 저장기로 다시 읽는 경로를 검증했다. 운영 중 투명한 +형식 전환을 뜻하지 않는다. + +## 구현 순서와 현재 근거 + +1. **실행 코어와 호스트 책임 분리.** 기존 저장·응답 경로를 그대로 사용해 영역의 작업 접수, + 준비·실행·완료·회수와 내부 결과 전달을 구현한다. 현재 [022](docs/research/022-execution-core.md)에 + 기록한 코드와 검증이 있다. 첫 체크포인트 전체 완료가 아니다. +2. **앱 작성 모델 통합.** get/put 전용 프로필을 실제 작업물의 입력·계산·상태·효과에 필요한 + 범위로 확장하고, CLI·UI·원격 입력이 같은 준비된 앱을 실행하도록 만든다. [023](docs/research/023-document-edits.md)은 + 네이티브 CLI와 HTTP의 같은 부분 변경 효과, 코어 비용 경계까지 구현한 중간 결과다. + [024](docs/research/024-native-app-instances.md)에서는 기존 Workbench 작성물을 독립된 네이티브 + 계산 인스턴스로 실행하고 같은 코어에서 입력·상태·조건부 저장·복원을 연결했다. + [025](docs/research/025-native-authoring-shell.md)에서는 선택형 Linux 네이티브 작성 창에서 + 정의 교체까지 같은 코어의 작업으로 연결하고 CLI·창 비용을 각각 확인했다. + [026](docs/research/026-secure-peer-link.md)에서는 선택형 인증 피어에서 같은 앱 작업을 + 원격으로 실행하고 CLI·피어 비용을 분리했다. + [027](docs/research/027-fixed-native-capacity.md)에서는 고정 정의의 실제 용량만 예약해 + 반복 메모리를 줄이고 11개 작성물의 동작 일치를 확인했다. + [028](docs/research/028-shared-fixed-program.md)에서는 CLI·피어의 정의를 한 번 링크하고 + 인스턴스에는 가변 상태만 남겨 반복 비용을 더 줄였다. + [029](docs/research/029-native-document-window.md)에서는 기존 문서 편집 효과를 Linux + GTK 창에 붙여 CLI·HTTP와 같은 문서 변경·충돌·복원을 검증했다. UI 비용은 별도다. + [030](docs/research/030-authenticated-document-peer.md)에서는 같은 문서 편집 효과를 + 인증된 Pure 피어와 일반 Python TLS 클라이언트에 연결하고, 소비하지 않는 응답 본문을 + 선택적으로 전송하지 않는 경로와 선택 빌드 전체 비용을 확인했다. + [031](docs/research/031-relocatable-document-preview.md)에서는 같은 앱을 선택형 Linux + 묶음으로 만들어 다른 디렉터리에서 전체 경로를 재검증하고, 1바이트 편집도 저장 파일 + 전체를 다시 쓰는 현재 한계를 계산했다. + [032](docs/research/032-micro-native-authoring-boundary.md)에서는 문서 창과 정의 적용을 + 통합한 뒤, 기본 배포를 CLI만 선택하도록 줄이고 실제 CLI RSS 회귀 검사를 추가했다. + [033](docs/research/033-paged-document-storage.md)에서는 같은 문서 앱에 선택형 + 페이지 저장 경로를 연결해 큰 문서의 쓰기량 감소와 작은 문서의 손해를 함께 확인했다. + [034](docs/research/034-hybrid-document-storage.md)에서는 작은 값에 기존 파일 형식을 + 적용해 그 손해를 제거하고, 늘어난 코드 크기와 역호환 경계를 검증했다. + [035](docs/research/035-native-multi-document-window.md)에서는 선택형 네이티브 창에서 + 여러 문서를 열고 초안·정의·읽기 실패를 보호해 실제 앱 조작 범위를 넓혔다. + [036](docs/research/036-gtk-memory-boundary.md)에서는 반복 문서 전환의 힙 증가를 + GTK 텍스트 갱신 경계까지 분리했다. 작은 기본 실행물의 비용과 선택형 GUI의 + 비용을 혼동하지 않으며, 이 창을 저메모리 목표 달성으로 취급하지 않는다. + [037](docs/research/037-build-time-prepared-runner.md)에서는 기존 문서 앱의 검증된 + 연결을 빌드 시 준비해 파서 없는 실행기를 만들었다. 기존 문서 작업과 세 가지 + 정의의 결과 일치를 확인하고, 링크 코드 크기·실제 RSS·이동 패키지 비용을 + 별도로 기록했다. 실행 중 작성 기능은 기존 소스 경로에 남아 있다. + [038](docs/research/038-fixed-http-document-host.md)에서는 같은 준비된 문서 + 앱을 파서 없는 HTTP 호스트에도 연결했다. CLI·소스 HTTP와 저장 버전·1 MiB + 본문·충돌을 교차 검증하고, 느린 수신자의 응답 대여가 예산을 차지하다가 + 전송 후 회수됨을 확인했다. CLI+HTTP 묶음을 다른 디렉터리에서도 실행했다. + [039](docs/research/039-idle-http-wakeups.md)에서는 두 HTTP 호스트가 유휴 중 + 50ms마다 깨어나던 경로를 연결 기한에 따른 대기로 바꿨다. 신호가 대기 직전에 + 도착해 종료가 멈출 수 있는 경합도 Linux `ppoll`의 원자적 마스크로 막았다. 보관된 실행물과 + 현재 실행물의 시스템 호출 추적으로 차이를 확인하고 기한·종료·패키지 동작을 + 재검증했다. + [040](docs/research/040-document-service-capacity.md)에서는 같은 문서 앱을 + 1,024개 저장 객체와 8개 동시 클라이언트에 적용했다. 소스 정의·고정 정의 + HTTP 호스트에서 각 4,096개 작업의 응답과 최종 문서 해시를 검증하고, + 프로세스 메모리·버퍼 회수·남은 입력 복사량을 기록했다. + [041](docs/research/041-posix-idle-signal-wakeup.md)에서는 Linux 전용 + `ppoll` 대기를 POSIX 신호 알림 파이프와 `poll`로 교체하고, 추가된 코드· + 파일 디스크립터 비용과 실제 종료·문서 작업을 검증했다. macOS 빌드는 남는다. + [042](docs/research/042-micro-native-terminal-editor.md)에서는 같은 준비된 + 문서 앱을 선택형 저메모리 터미널 편집기로 연결했다. 실제 PTY의 한국어 + 입력·문서 전환·저장·충돌·크기 변경·1 MiB 수정과 터미널 복구를 검사하고, + 실행물·프로세스·이동 가능한 CLI+TUI 묶음의 비용을 따로 기록했다. + [043](docs/research/043-micro-package-boundary.md)에서는 고정 정의 CLI·HTTP·TUI의 + 네 가지 선택 묶음 전체 크기에 상한을 적용하고, 모두 다른 위치에서 다시 실행했다. + [044](docs/research/044-terminal-viewport-cost.md)에서는 실제 TUI의 1 MiB 화면 갱신 + 비용을 줄이고, 여러 행·탭 문서를 다시 열지 못하던 결함을 고쳤다. 동일 PTY 화면 + 바이트와 편집·복원 결과를 확인하고, 추가 코드·RSS·전체 묶음 비용을 기록했다. + [045](docs/research/045-owner-thread-async-effects.md)에서는 기본 코어를 키우지 않고 + 선택형 비동기 효과 완료 경로를 만들었다. 실제 문서 저장소와 기존 `editor.pure`로 + 요청 해제 후 완료, 다른 작업의 선행 완료, 영역 간 본문 전달, 예산·종료를 확인했다. + [046](docs/research/046-optional-async-document-http.md)에서는 선택형 HTTP 호스트의 + 단일 저장 작업자와 완료 파이프를 실제 문서 앱에 붙였다. 지연된 저장 효과 중 + 다른 연결의 응답·기한·종료와 1,024개 문서 작업의 결과·메모리 비용을 확인했다. + [047](docs/research/047-bounded-native-document-browser.md)에서는 저메모리 TUI가 + 실제 저장 문서를 목록에서 다시 찾도록 했다. 1,024개 문서의 탐색·전환·회수와 + 증가한 TUI 코드·이동 묶음 비용을 검증했고 기본 CLI·실행 코어는 유지했다. + [048](docs/research/048-immutable-read-write-overlap.md)에서는 기존 문서의 쓰기를 + 파일 동기화 직전에 멈춘 동안 이전 버전 읽기와 이후 새 버전 읽기를 검증했다. + 선택형 호스트만 작업자 둘로 늘리고 기본 코어·HTTP 크기를 유지했으며, + 1,024개 문서 작업에서 선택형 호스트의 전체 프로세스 메모리 비용을 기록했다. + [049](docs/research/049-idle-app-work-slots.md)에서는 동기 네이티브 앱의 작업 슬롯을 + 호출 중에만 연결해 유휴 앱 1,000개의 반복 메모리를 줄였다. 코어·스택·실행물 + 증가 비용과 저장·복원·피어·TUI의 기존 동작을 함께 확인했다. + [050](docs/research/050-bounded-document-prefix-find.md)에서는 선택형 TUI의 정렬된 + 실제 문서 1,024개에서 추가 인덱스 없이 이름 접두어를 찾아 열었다. 코어를 + 늘리지 않고 TUI 코드·RSS·이동 묶음 비용을 확인했으며, macOS TUI 헤더 + 노출을 수정했다. + [051](docs/research/051-optional-runtime-metrics.md)에서는 기본 영역의 누적 계측을 + 선택형으로 분리했다. 같은 실제 앱 1,000개의 입력·저장·복원에서 영역 크기, + 전체 RSS와 이동 가능한 CLI·TUI 묶음 비용을 확인했다. + [052](docs/research/052-clean-native-source-preview.md)에서는 C 실행 경로와 작성 + 소스의 재현 가능한 묶음을 만들고, 깨끗한 디렉터리에서 빌드·테스트·실제 + 문서 편집과 `.pure` 규칙 수정을 검증했다. macOS 실행과 라이선스는 남는다. + [053](docs/research/053-source-loaded-native-terminal-editor.md)에서는 같은 TUI가 + 선택형 `.pure` 소스 정의를 실행하도록 연결했다. 필드 배치를 바꾼 정의의 + 실제 편집·저장, 이동 묶음의 PTY 작업과 선택 비용을 확인했다. + [054](docs/research/054-direct-document-path.md)에서는 기존 문서 앱의 생성·읽기· + 부분 편집·재읽기를 직접 저장 호출과 Pure 경로로 각각 실행했다. 같은 결과와 + 저장 파일을 확인했으며, 실행물 코드 비용과 ext4 지연의 변동을 공개했다. + [055](docs/research/055-authenticated-peer-transport-cost.md)에서는 같은 정의와 + 인증 TLS 경계의 직접 서버를 비교했다. 양쪽 공통 TCP 지연을 제거하고 + 서버 전용 선택 빌드의 크기·RSS·보안 시나리오를 검사했다. + [056](docs/research/056-server-only-document-package.md)에서는 서버 전용 TLS + 피어와 CLI를 선택한 압축 묶음으로 만들고 전체 크기 상한을 적용했다. + 다른 위치에 풀어 외부 클라이언트의 인증·1 MiB 편집·복원 및 CLI 공유를 확인했다. + [057](docs/research/057-portable-native-terminal-check.md)에서는 실제 PTY 편집 + 검사의 Linux 전용 RSS 수집을 분리하고 고정·소스 정의 TUI를 휴대형 경로에서 + 실행했다. 소스 묶음의 이동 후 검사와 기존 Linux RSS 상한도 다시 통과했다. + 새 검사는 개발용 Python·Make 경로에만 있으며 C 런타임 코드는 키우지 않았다. + [058](docs/research/058-portable-authenticated-peer-check.md)에서는 선택형 + TLS 피어 검사에서도 Linux 전용 RSS 수집을 분리했다. 범용·서버 전용 + C 피어와 외부 클라이언트의 인증·문서·실패 작업을 실제 소켓과 이동한 + 소스 묶음에서 실행하고 기존 Linux 코드·RSS 비용을 별도로 확인했다. + [059](docs/research/059-immutable-file-copy-path.md)에서는 기존 1 MiB + 불변 문서의 1 B 수정에서 원본 읽기·새 파일 쓰기 호출을 추적했다. + Linux 파일 간 복사를 선택하면 같은 결과·프로세스 RSS로 호출 수를 + 줄였으며, 기본 코어는 그대로 두고 선택형 코드 증가를 기록했다. + [060](docs/research/060-host-native-checkpoint.md)에서는 기존 C·실제 PTY 검사와 + 고정/소스 CLI의 1 MiB 문서 생성·읽기·1 B 수정을 한 명령으로 재현하도록 했다. + Linux AArch64에서는 통과했고 기존 RSS 상한도 유지했다. 측정 도우미는 macOS + `wait4` 원시 단위를 KiB로 정규화하지만 Apple Silicon 실측은 아직 없다. + [061](docs/research/061-durable-document-edit-latency.md)에서는 같은 앱의 + 1 MiB 내구성 문서 편집을 기본·파일 간 복사·페이지 저장으로 직접 실행했다. + 각 방식 18회와 연속 70회 수정에서 결과·리비전·저장 공간·완료 시간· + 페이지 재구성을 확인하고 선택형 저장 전략의 이득과 제한을 분리했다. + [062](docs/research/062-paged-store-rollback.md)에서는 선택형 페이지 저장소의 + 문서 본문·리비전을 기존 기본 형식의 새 저장 디렉터리로 내보냈다. + 실제 다섯 문서와 1,024개 추가 문서의 재읽기, 변환 중 쓰기 차단·실패 시 + 미공개·이후 기본 저장기 수정까지 확인했다. 런타임 코드는 그대로다. + [063](docs/research/063-real-in-process-routing.md)에서는 실제 1 MiB 문서를 + 한 프로세스의 두 독립된 Pure 영역으로 전달했다. 대상 입력 할당기를 + 비활성화해도 결과가 같고, 원본 티켓 해제 후 수명·부분 충돌·예산 거절·복원·회수를 + 확인했다. 코어 변경 없이 선택형 네이티브 명령만 추가했다. + [064](docs/research/064-release-callback-reentrancy.md)에서는 자원 해제 콜백이 + 같은 영역에 재진입해 슬롯 회수 상태를 바꿀 수 있던 경로를 막았다. 기본 + 코드는 72 B 늘었으나 6 KiB 상한과 영역 128 B를 유지했고, 실제 앱· + 새 소스 묶음의 검증을 통과했다. + [065](docs/research/065-native-save-latency.md)에서는 기존 선택형 페이지 + 저장소를 실제 네이티브 TUI에 연결해 Ctrl+S 완료 시간·코드·RSS를 비교했다. + 기본·선택형 CLI+TUI를 각각 이동해 검증하고, 선택형 경로도 깨끗한 소스 + 빌드에서 다시 실행했다. 기본 코어와 기본 저장 경로는 유지했다. + [066](docs/research/066-application-boundary.md)에서는 문서의 저장 전용 + 연산과 scalar 앱의 이벤트 우회 연결이 아직 하나의 작성 모델이 아님을 + 코드에서 확인하고, 기존 문서 수정에 계산 정책을 결합할 합격 조건을 정했다. + [067](docs/research/067-work-slot-layout.md)에서는 활성 작업 슬롯을 + 224→216 B로 줄이면서 기본 코어 코드를 5,980 B로 유지했다. 비동기· + 네이티브 C 앱·TUI·실제 문서 전달을 검증하고 당시 소스 묶음과 네 가지 + 실행 묶음을 다른 위치에서 확인했다. macOS 실행은 남아 있다. + [068](docs/research/068-opaque-app-core.md)에서는 문서 해석을 기본 코어 밖의 + 선택형 어댑터로 옮겼다. 기본 코어 `text` 4,632 B, 유휴 영역 120 B를 + 검증하고, 선택형 HTTP의 증가 비용까지 공개했다. 동기·비동기 서비스와 + 인증 피어의 실제 문서 작업, 이동한 새 소스·실행 묶음을 검사했다. + [069](docs/research/069-atomic-authored-size-policy.md)에서는 작성한 최종 문서 + 크기 상한을 같은 저장 버전의 쓰기 잠금 안에서 검사했다. 기본 코어와 + 준비된 정의 크기를 늘리지 않고 CLI·HTTP·TUI·인증 피어의 결과와 + 선택형 호스트 코드 비용을 함께 검증했다. 일반 계산 모델은 남아 있다. + [070](docs/research/070-micro-default-build.md)에서는 기본 빌드를 코어와 + 파서 없는 고정 정의 CLI로 제한하고 깨끗한 소스 추출에서 선택 기능이 + 기본 출력에 섞이지 않는지 확인했다. 링크 코드가 줄었지만 RSS 개선은 + 입증하지 않았다. +3. **실제 네이티브 앱과 통신.** 작성·편집·저장·복원·충돌 처리를 연결하고 자체 통신 형식과 + 검증된 보안 전송을 붙인다. 외부 호환 입력·출력도 유지한다. +4. **규모·실패·사용성 확인과 공개 자료.** 큰 작업물·느린 소비자·혼합 작업에서 구조가 + 유효한지 확인한다. 기능·정확성·필요한 보안을 유지한 비교만 수행하고 약한 부분을 수정한다. + +각 단계는 앞 단계의 실제 작업물을 확장한다. 새로운 빈 데모나 별개의 미니 언어를 반복해서 +만들지 않는다. 목표를 한 번의 작업 분량이나 이미 통과한 테스트 범위로 축소하지 않는다. diff --git a/README.md b/README.md index a57d2e5..f213a9a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,343 @@ # Pure -micro runtime +A standalone native micro runtime being designed as a small operating +system. Pure owns the application model for execution, memory, protection, +resource lifetimes, storage, and communication. Efficient connections between +inputs, computation, state, and outputs guide that design, with the aim of +reducing unnecessary representations, copies, retained data, and waiting. + +Micro size is a primary constraint. The always-present core covers only the +required execution, ownership, resource limits, and completion rules. Protocols, +storage implementations, UI, and codecs are selected for the application; +supporting them does not mean bundling or starting them all. Prefer build-time +composition without requiring a dynamic plugin loader. Track core size, the +complete selected build, idle memory, and per-active-work overhead separately; +moving costs into dependencies does not count as a reduction. + +The current [12,966-byte Linux AArch64 fixed document preview](prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz) +contains a parser-free native CLI. The [22,862-byte source-editable preview](prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz) +also includes an editable `editor.pure`. HTTP, authenticated +peer, and GTK authoring are [explicit selections](docs/research/032-micro-native-authoring-boundary.md). +The archive was extracted elsewhere and passed an edit and restore check. +The [clean default-build check](docs/research/070-micro-default-build.md) +keeps the parser, HTTP, TUI and TLS out of `make all` runtime outputs. +An [authored final-size policy](docs/research/069-atomic-authored-size-policy.md) now +guards the real document write under its storage lock. Source and fixed CLI, +HTTP and terminal hosts share the policy; an optional authenticated peer was +checked separately. The default core remains 4,632 B of object `text`. +The selected GTK process still uses about 43 MiB RSS; compressed archive size +is not a memory claim. The public checkpoint remains in progress. + +The [optional native document window](docs/research/035-native-multi-document-window.md) +can now switch among stored documents in one session while preserving unsaved +document and definition drafts. It still uses GTK and remains outside the micro +default. In one Linux test, switching two 1 MiB documents 64 times raised the +window's RSS from 43,920 to 58,752 KiB. A controlled follow-up isolated the +growth to repeated GTK text-view replacement; the optional window is not a +low-memory runtime profile. See [the memory analysis](docs/research/036-gtk-memory-boundary.md). +The GTK window has no document list or multi-object update operation. + +The [optional native terminal editor](docs/research/050-bounded-document-prefix-find.md) +uses the same build-time prepared document app as the CLI and HTTP host. It +handles UTF-8 input, a bounded list of stored keys with prefix lookup, resize, +revision conflicts, and a 1 MiB edit without GTK. Its current linked `text` is +32,023 B and its measured 1 MiB peak RSS was 3,168 KiB on the recorded Linux host. The +current [CLI+TUI artifact](prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz) +is 32,144 B compressed. It does not edit +the `.pure` definition or establish a low-memory GUI result. + +The [source-loaded terminal variant](docs/research/053-source-loaded-native-terminal-editor.md) +reads `editor.pure` at startup and shares the CLI's document store. It is an +optional 48,972 B linked-`text` host; the [CLI+TUI archive](prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz) +is 51,398 B compressed. A changed definition takes effect in a new session. +The default core and parser-free TUI do not link the `.pure` parser. + +The [selected 16 KiB-page native terminal app](docs/research/065-native-save-latency.md) +uses the same authored document program. On the recorded Linux/ext4 host, +Ctrl+S for a one-byte edit in a 1 MiB document reached the completed screen +in 93.17 ms with the default store and 22.04 ms with the selected page store +(24 saves per profile). The 256 B case was 8.54 versus 9.23 ms. The selected +source TUI adds 4,958 B linked `text`; its [CLI+TUI archive](prototypes/connections/results/2026-09-24-native-save/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz) +is 57,788 B compressed and passed a relocated app check. Large documents use +an incompatible on-disk page format, so this remains an opt-in Linux/16 KiB-page +research profile; the default core and storage path are unchanged. + +The current execution profile has separate `core`, `fixed-editor`, `editor`, and +`server` build targets. `make all` produces the core and the parser-free CLI +from the build-time prepared `editor.pure`; it also builds a compiler tool that +is not linked into the CLI. Source-loaded rule editing requires +an explicit `make editor`, and HTTP requires `make server`. `(cd prototypes/connections && make check-release)` runs behavior +tests and checks both the core and default linked hosts against reviewed release +size and dependency ceilings. The default core is 4,632 B of object `text` and +120 B per idle domain and 216 B per active work slot on Linux AArch64. +It contains no document interpreter; that code is linked with a selected host. +The optional metrics profile uses 192 B per domain on this toolchain. +[The earlier 1,000-app comparison](docs/research/051-optional-runtime-metrics.md) +measured 80 KiB lower RSS for the default profile. These figures exclude the +parser, selected backends, stacks, payloads and process memory. The default +document CLI and HTTP host are separately checked at 36,135 B and 44,342 B of +linked `text`, with libc as their only direct shared +library. These are regression boundaries for the recorded toolchain, not total +memory targets. [The current authored policy and total cost](docs/research/069-atomic-authored-size-policy.md). +`make -C prototypes/connections check-portable` runs the C behavior checks +without Linux-only size/RSS tools; it is the first check to run on Apple Silicon +macOS with Apple Clang. `make -C prototypes/connections check-portable-tui` +runs both terminal editors through real PTYs without Linux `/proc`; it does not +measure their RSS. `make -C prototypes/connections BUILD=build/native-checkpoint +check-native-checkpoint` combines those checks with real CLI document RSS samples +for 1 MiB creation, read and 1 B revision edit. It adds no runtime code or +macOS memory ceiling. The storage adapter now exposes the Darwin open flags it uses. +These checks have passed on Linux AArch64, but no macOS SDK build or +native run has been completed here, so this is not a macOS support claim. +The [Apple Silicon CI job](.github/workflows/native-macos-arm64.yml) runs those +native app, PTY, authored policy, RSS and in-process document-routing checks on a macOS arm64 +runner when this source is hosted on GitHub; a passing run is still required. +The optional `make -C prototypes/connections check-portable-peer` runs real +authenticated TLS document work for both native peer profiles without Linux +`/proc`; it requires OpenSSL and has [separate Linux cost evidence](docs/research/058-portable-authenticated-peer-check.md). +The current [source-editable CLI archive](prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz) +includes `editor.pure` and is 22,862 B compressed. +[A separate native source preview](prototypes/connections/results/2026-09-24-atomic-policy/pure-source-preview.tar.gz) +contains the C execution path and Workbench sources without prior build/results +trees. It was rebuilt and tested after extraction, including both PTY editors, +native CLI RSS samples, in-process document fan-out, selected page-to-flat +export and Linux release gates; see the +[clean-build evidence](prototypes/connections/results/2026-09-24-atomic-policy/source-package-check.json). +Its 337,937 B compressed size is a source-distribution cost, not resident runtime memory. +[The CLI whole-process RSS gate](prototypes/connections/results/2026-09-24-native-definition-authoring/micro-memory.json) +checks a 1 B edit and 1 MiB edit/read on the recorded Linux AArch64 system. +[Document editing and the complete measured scope](docs/research/023-document-edits.md). + +A [build-time prepared runner](docs/research/037-build-time-prepared-runner.md) +executes the same document definition without linking the `.pure` parser: its +linked `text` is now 19,268 B versus 36,135 B for the source-loading CLI. Its +1 MiB edit peak RSS was about 2.1 MiB in these runs, so this is a code-size gain, not a RAM +gain. The [parser-free HTTP host](docs/research/038-fixed-http-document-host.md) +uses the same prepared app and now 26,689 B of linked `text`; the source-loading +HTTP host uses 44,342 B. Source editing still uses the separate source-loading +path. The [previous CLI+HTTP artifact](prototypes/connections/results/2026-09-24-idle-work-binding/cli-http.tar.gz) +is 28,719 B compressed and was +exercised after extraction; it has no dynamic definition editing. Both HTTP +hosts now [sleep until an event or connection deadline](docs/research/041-posix-idle-signal-wakeup.md) +instead of waking every 50 ms while idle. A small POSIX signal pipe also wakes +the host if shutdown arrives just before sleeping; macOS execution is untested. + +The [document service capacity check](docs/research/040-document-service-capacity.md) +ran the same app with 1,024 stored 8 KiB documents and eight simultaneous +clients: 4,096 checked HTTP operations per host, matching final documents, +and 1,680 KiB peak RSS for the fixed-definition server in that run. This is +a bounded correctness and resource result, not a throughput claim or a +large-service comparison. + +The [optional asynchronous HTTP host](docs/research/048-immutable-read-write-overlap.md) +keeps one read worker and one write worker. A deterministic test pauses a write +before publication: the same document remains readable at its old revision, +then exposes the new revision after the write completes. This adds 4,711 B of +linked `text` over the default source HTTP host. The latest 1,024-document run +observed 1,776 KiB peak RSS for this selected host versus 1,728 KiB for the +default source host. The core also now +[binds work slots only while an app needs them](docs/research/049-idle-app-work-slots.md): +1,000 real native apps retained 448 B less per controller and used 432 KiB less +process RSS in the recorded comparison, while the core grew by 144 B. + +The current host OS supplies resources and imposes execution constraints. +Its processes, threads, file descriptors, and sockets are backend mechanisms, +not the organizing model for Pure applications. Development starts hosted on +existing systems; direct hardware execution remains a separate implementation +target, not a prerequisite for defining Pure's own operating model. + +Networking, RPC, databases, and UI are domains where this model applies. The +research concerns how their data and work connect, including representation, +ownership, lifetime, and execution timing. Servers and CLI tools must be able to +use the runtime without loading UI. Web interoperability and standard formats +remain compatibility requirements. End-to-end efficiency is a research goal, +not an established result of the current prototypes. + +Communication with non-Pure services, runtimes, and programs is a core +requirement in both directions. Boundary adapters support the peer's protocols +and data formats without requiring the peer to run Pure or making those formats +the mandatory internal representation. + +SQL and relational schemas are not Pure's internal data model. Memory and +storage representations follow the data's access, update, and lifetime needs; +the runtime must not require data to pass through a SQL engine. +An [optional inline/page storage experiment](docs/research/034-hybrid-document-storage.md) +preserves the same contiguous read interface while reducing a 1 MiB document's +one-byte replacement from 1,048,600 to 16,944 user-space file-write bytes. +Unlike the earlier [page-only experiment](docs/research/033-paged-document-storage.md), +a one-byte document still writes 25 bytes. In the current build the selected CLI +grows from 35,783 to 41,040 linked text bytes. Large objects become unreadable +by the default store after conversion. A separate +[offline export](docs/research/062-paged-store-rollback.md) now preserves bodies +and revisions in a new default-format directory, but this backend is not the +micro default. +A separate [Linux file-copy experiment](docs/research/059-immutable-file-copy-path.md) +keeps the existing immutable file format and reduces user-space read/write calls +for a one-byte edit of a 1 MiB document. It adds 269 B of linked CLI `text` +when explicitly selected; the default core and build stay unchanged. This is +syscall evidence, not a claim of less physical I/O or lower latency. +[A durable edit comparison](docs/research/061-durable-document-edit-latency.md) +now times the same 1 MiB native document action on ext4: the page profile has +a much shorter median edit, while the file-copy path is not distinguishable +from the default on this host. A 70-edit run exposes the page rebuild and +occasional long delays, so the page format remains optional. + +HTTP is a compatibility boundary, not the required model for Pure-to-Pure +communication. Selected scalar and document apps now use bounded binary peer +protocols over mutually authenticated TLS 1.3/TCP. The document peer shares +`editor.pure` work and storage with the CLI, HTTP adapter, and GTK window; +its request can omit an output body that the caller will not consume. The +plaintext HTTP/1.1 loopback adapter remains a separate compatibility path; +neither peer is a QUIC implementation. See the [native protocol design](docs/design/006-native-protocol.md), +[scalar peer evidence](docs/research/026-secure-peer-link.md), and +[document peer evidence and full cost](docs/research/030-authenticated-document-peer.md). + +Execution domains are internal units of Pure's operating model. The core +defines their scheduling, ownership, authority, connections, and lifetimes; +platform backends provide the mechanisms available to enforce those contracts. +This is a [design direction](docs/design/007-execution-domains.md), not an +implemented complete OS or sandbox. A limited [execution core](docs/research/022-execution-core.md) +now owns admission, input budgets, ready work, completion, and borrowed results +for the storage profile. The HTTP adapter uses the same core as local domain +connections. Current host limits and native-code fault boundaries still apply. +The optional [in-process document mirror](docs/research/063-real-in-process-routing.md) +now forwards a real 1 MiB stored value to two domains without allocating target +input buffers. Each target writes independently and reports its own revision +conflict; the source mapping is released after both finish. This is one-shot +domain composition, not yet a long-running app supervisor. + +The [first public checkpoint](CHECKPOINT.md) remains in progress. Its requirements +include a real authored native app, internal and external communication, +reproducible cost evidence, and a usable build and distribution path. +An [actual document-path comparison](docs/research/054-direct-document-path.md) +now checks direct store calls against the prepared Pure app on equal final files. +The Pure benchmark executable links 8,276 B more `text`; the ext4 timing samples +are too variable to establish a speed advantage. This comparison covers the +valid document workflow, not a feature-equivalent direct runtime or RPC service. +An [authenticated document-peer comparison](docs/research/055-authenticated-peer-transport-cost.md) +found a shared TCP stall in both direct and Pure servers; setting TCP_NODELAY +removed it in the local repeated-read test. The optional server-only build +links 3,472 B less `text` than the combined native peer. Its selected TLS +process still occupies roughly 8–10 MiB here, so it is not the micro default. +The [server-only TLS archive](docs/research/056-server-only-document-package.md) +is 47,686 B compressed, includes the source-editable CLI, and passed extracted +external-client and document recovery checks. The corresponding combined +client/server archive is 49,382 B; neither bundles OpenSSL or libc. + +The browser, terminal, and optional Linux authoring window validate parts of the execution and +authoring model. They do not yet implement the full application runtime or its +network, RPC, database, and general native UI subsystems. See the +[native runtime roadmap](docs/research/010-roadmap.md) for the current direction. + +Current experiment: [author a work from a blank document](prototypes/workbench/README.md). + +The same authored scalar definitions can now be built as a +[standalone native CLI, optional Linux GTK authoring window, or optional authenticated peer](prototypes/workbench/native/README.md). +Independent calculation instances, inputs, snapshots, plan replacement, and conditional +storage use the execution core. The CLI runs without Node, GTK, or Wasm. The authoring +window uses Node and a C compiler to apply edits, and its complete memory cost is much +larger than the execution-only CLI. [Native window evidence and separate costs](docs/research/025-native-authoring-shell.md). +Fixed-definition CLI and peer builds now share one immutable program across +independent scalar instances. For the routed layout, each instance occupies 280 B +and the compiled program occupies 2,480 B once; [the measured 1,000-instance +comparison and complete build costs](docs/research/028-shared-fixed-program.md) +keep the authoring window and Wasm at full capacity. +The native app controller now occupies 408 B per idle instance, down from +928 B after [idle work-slot binding](docs/research/049-idle-app-work-slots.md) +and [optional metrics](docs/research/051-optional-runtime-metrics.md). +The peer host uses the same app operations without adding OpenSSL to the CLI or core; +its [complete process cost](docs/research/026-secure-peer-link.md) is measured separately. + +The native path is runnable: [author storage and response connections](prototypes/connections/README.md). +It resolves named boundary fields, constant byte ranges and aliases before +serving local TCP requests, evaluates ranges that depend on input values, +preserves bounds failures and storage effects, and holds source data through +transfer completion. A text-selection client exercises this path with UTF-8 files. +This is a limited native authoring profile, not yet a +general-purpose I/O execution model or an end-to-end performance result. + +The authored document splice program now also runs through an [optional Linux +GTK document window](docs/research/029-native-document-window.md). The same +document is edited by the native CLI and HTTP boundary with revision conflicts; +GTK and its measured 44 MiB process cost stay outside the micro core. This is +a hosted native app experiment, not the final low-memory UI backend. +The window also edits and safely applies its `.pure` definition in place; +[the selected app cost and recovery checks](docs/research/032-micro-native-authoring-boundary.md) +are reported separately from the default runtime. +An [optional authenticated document peer](docs/research/030-authenticated-document-peer.md) +now performs the same edit and read over TLS without adding TLS to the default +CLI, HTTP host, or execution core. + +```sh +python3 prototypes/workbench/manage.py serve --port 8766 +``` + +Open . Declare your own states, inputs, shapes, and event +handlers. Apply edits while preserving states by name. Browser and terminal use +the same Zig execution core, with separate instances. Optional examples never run +automatically. The prototype is an authoring experiment, not a claim of superior +developer experience or performance. + +The earlier [Dino demo](prototypes/live-rules/README.md) tests changing predefined +rules while keeping state; it does not establish development experience. + +Research in progress: + +- [Bounded prefix lookup across 1,024 documents without a second index](docs/research/050-bounded-document-prefix-find.md) +- [Idle work-slot binding across 1,000 complete native apps](docs/research/049-idle-app-work-slots.md) +- [Optional read/write overlap and snapshot correctness in the document HTTP host](docs/research/048-immutable-read-write-overlap.md) +- [Bounded native document browser over 1,024 real documents](docs/research/047-bounded-native-document-browser.md) +- [Optional asynchronous document HTTP host and measured process cost](docs/research/046-optional-async-document-http.md) +- [Low-memory native terminal editor for the same document app](docs/research/042-micro-native-terminal-editor.md) +- [POSIX idle HTTP shutdown wakeup with two host-only file descriptors](docs/research/041-posix-idle-signal-wakeup.md) +- [One document app across 1,024 objects, 4,096 operations and eight clients](docs/research/040-document-service-capacity.md) +- [Idle HTTP wakeups: 25 timed polls become one blocking wait](docs/research/039-idle-http-wakeups.md) +- [One prepared app in native CLI and external HTTP, with bounded slow-client output](docs/research/038-fixed-http-document-host.md) +- [Build-time prepared runner: same document operations with 46% less linked code](docs/research/037-build-time-prepared-runner.md) +- [GTK document-switch memory boundary and micro runtime cost separation](docs/research/036-gtk-memory-boundary.md) +- [Selected page storage: 1 MiB write reduction, small-value regression, and real app checks](docs/research/033-paged-document-storage.md) +- [Selected inline/page storage: small-value cost fixed, code cost and format boundary measured](docs/research/034-hybrid-document-storage.md) +- [Native multi-document window: switch stored objects without losing drafts](docs/research/035-native-multi-document-window.md) +- [Micro default package, whole-process memory gate, and optional native authoring](docs/research/032-micro-native-authoring-boundary.md) +- [Relocatable Linux document preview, modified source behavior, and remaining storage rewrite cost](docs/research/031-relocatable-document-preview.md) +- [Authenticated document peer: same authored edit, 1 MiB ingress and output demand](docs/research/030-authenticated-document-peer.md) +- [Native document window: one authored edit path through GTK, CLI, and HTTP](docs/research/029-native-document-window.md) +- [Shared immutable native program, 280-byte scalar instances, and full selected build costs](docs/research/028-shared-fixed-program.md) +- [Fixed-definition native capacity, 11-app parity and 1,000-instance memory cost](docs/research/027-fixed-native-capacity.md) +- [Optional authenticated binary Pure-peer connection and its complete cost](docs/research/026-secure-peer-link.md) +- [Optional native authoring window, core-owned plan replacement, and full process cost](docs/research/025-native-authoring-shell.md) +- [Native application instances, input and storage parity](docs/research/024-native-app-instances.md) +- [Execution core: owned work, bounded admission, and shared results between domains](docs/research/022-execution-core.md) + +- [Named fields and input-dependent selection: document → computation → storage and response](docs/research/021-field-connections.md) + +- [Authored native connections: prepare range access while preserving effects and lifetimes](docs/research/020-prepared-connections.md) + +- [SQL-free byte storage: mapped ranges, publication, and persistence boundaries](docs/research/019-byte-storage.md) + +- [Native request → database → response: historical SQLite implementation, replaced by 019](docs/research/018-native-connections.md) + +- [Browser layout and text editing experiment; architecture conclusion superseded](docs/research/017-web-layout-boundary.md) + +- [One pointer input, independently owned targets: shared event routing](docs/research/016-pointer-routing.md) + +- [One rule body, separate object fields: shared execution without code expansion](docs/research/015-shared-rules.md) + +- [Editable multi-object layouts and atomic document persistence](docs/research/014-layout-persistence.md) + +- [Authored direct manipulation: pointer coordinates, cancellation, and shared execution](docs/research/013-authored-pointer-input.md) + +- [Separate execution refresh from editor work: measured input-path improvement](docs/research/012-refresh-ownership.md) +- [Direct input-to-output publication: removed scene reconstruction, with measured limits](docs/research/011-direct-presentation.md) +- [Next roadmap: efficient interfaces with fewer representation and execution layers](docs/research/010-roadmap.md) +- [Bounded display evaluation integrated with authored work](docs/research/009-bounded-display-integration.md) +- [Forgetful execution core: bounded records and work, with verified semantics](prototypes/forgetful-core/README.md) +- [Research brief](docs/research/001-research-brief.md) +- [First experiment and results](docs/research/002-checkpoint-results.md) +- [Live rules prototype and storage report](docs/research/003-live-rules.md) +- [Blank authoring experiment and comparison](docs/research/004-authoring.md) +- [Operation, memory, and module contract draft](docs/research/005-foundation-contract.md) +- [Problem landscape and research selection strategy](docs/research/006-problem-landscape.md) +- [Buffer retention comparison and measured limits](docs/research/007-buffer-retention-results.md) +- [JSON parser and shared-buffer fan-out results](docs/research/008-json-fanout-results.md) +- [Working method](docs/research/000-working-method.md) diff --git a/docs/design/001-operation-semantics.md b/docs/design/001-operation-semantics.md new file mode 100644 index 0000000..ae2ed5d --- /dev/null +++ b/docs/design/001-operation-semantics.md @@ -0,0 +1,126 @@ +# 연산 의미 초안 0.1 + +상태: **다음 구현을 위한 명세**. 현재 제작 도구의 문법·실행기를 변경하지 않는다. +아래에서 `현재`는 구현을 확인한 동작, `제안`은 후속 구현의 요구조건이다. +현재 동작의 기준은 [Workbench 계약](../../prototypes/workbench/CONTRACT.md)이다. + +## 1. 연산의 구분 + +| 종류 | 읽고 바꾸는 것 | 실패의 의미 | +|---|---|---| +| 순수 계산 | 명시적인 값·읽기 전용 데이터 | 결과 없음; 외부 효과 없음 | +| 상태 전이 | 한 실행 영역의 관리 상태 | 확정 전 실패는 이전 관리 상태 유지 | +| 버퍼 변환 | 입력 구간, 독점 출력 구간, 변환 문맥 | 이번 호출의 유효 출력과 문맥의 재사용 가능 여부를 명시 | +| 외부 작업 | 파일·네트워크·장치·시계 등 | 이미 발생한 효과가 남을 수 있음; 실제 완료로 보고 | + +이 구분은 제안이다. 현재 코어에는 앞의 두 종류와 순수한 표시 계산만 있다. +비용이나 실패 의미가 다른 작업을 모두 숫자 함수처럼 처리하지 않는다. +재계산 가능한 연산은 첫 종류로 제한한다. 파일 쓰기·난수 획득·작업 제출은 재계산으로 반복하지 않는다. + +## 2. 현재 숫자 규칙과 확장 경계 + +현재 상태는 유한한 `f64`다. 대입과 같은 이벤트의 핸들러는 소스 순서로 실행한다. +`if` 조건은 한 번 계산하며 `&&`·`||`는 양쪽을 모두 평가한다. +0 나눗셈, 유한하지 않은 계산 결과, 스택·분기 오류는 이벤트 전체의 숫자 상태 변경을 취소한다. +표시 표현식은 상태·이벤트 진단을 변경하지 않는다. + +현재 시간 입력은 프레임당 1/60초, 일반 입력에서 0이다. +이는 작은 화면 실험의 규칙이다. 서버의 시계나 미디어 타임스탬프에 60Hz를 강제하는 계약이 아니다. +후속 시간·난수·외부 완료는 명시적인 입력 기록으로 전달한다. + +제안하는 데이터 경계 규칙: + +- 바이트·버퍼 길이·오프셋·식별자는 정수로 다룬다. `f64`를 경유해 큰 정수의 정확성을 잃지 않는다. +- 길이·메모리 계산은 검사된 정수 연산을 사용한다. 넘치면 오류다. 축약·순환 연산은 별도 명시가 있어야 한다. +- 정수 폭, 바이트 단위, 정렬 요구와 변환 가능 범위를 각 연산에 기록한다. +- 부동소수점 확장은 연산 순서, 반올림·서브노멀·부호 있는 0 처리와 허용 오차를 명시한 뒤 추가한다. + 현재 테스트는 모든 네이티브 타깃의 비트 단위 일치를 증명하지 않는다. +- 기존 논리 연산을 단락 평가로 바꾸는 일은 의미 변경이다. 최적화라는 이름으로 조용히 바꾸지 않는다. + +Wasm의 수치 형식은 IEEE 754의 이진 형식과 부호 있는 0·NaN 등을 정의한다. +Pure가 허용하는 값·오류와 재생 비교 규칙은 그 위에 별도로 정해야 한다. +[WebAssembly 값 명세](https://webassembly.github.io/spec/core/syntax/values.html#floating-point) + +## 3. 관리 상태 확정과 외부 작업 제출 — 제안 + +한 이벤트에서 다음 순서를 지킨다. + +1. 관리 상태의 다음 값을 계산하고, 필요한 버퍼 대여·요청 슬롯·완료 슬롯을 예약한다. + 준비 중에는 외부 작업을 시작하거나 공유 출력 버퍼를 변경하지 않는다. +2. 검사·예약이 실패하면 준비한 자원을 반납하고 이전 관리 상태를 유지한다. +3. 성공하면 관리 상태와 준비된 작업 목록을 함께 확정한다. 이 단계는 외부 작업의 성공을 의미하지 않는다. +4. 어댑터가 확정된 작업을 실제로 제출한다. 제출 실패도 완료 사건으로 전달한다. +5. 완료 사건을 후속 상태 전이의 입력으로 처리한다. 재시도·보상·사용자 알림은 앱의 명시적 정책이다. + +이미 확정·제출된 파일 쓰기를 이후 이벤트의 실패로 되돌렸다고 보고하지 않는다. +처리 중 프로세스가 종료되는 경우의 내구성·중복 실행 방지는 이 메모리 내 계약에 포함하지 않는다. +영속 작업에는 이후 별도 로그·식별자·복구 계약이 필요하다. + +## 4. 바이트 스트림 연산 — 첫 모듈 프로파일 제안 + +첫 프로파일은 입력 구간 하나, 출력 구간 하나, 유지되는 문맥 하나를 받는다. +각 입력 구간은 유효 데이터 길이, 출력 구간은 쓸 수 있는 용량을 나타낸다. +v0 출력은 유효 길이가 0인 독점 버퍼이며 출력 오프셋은 0이다. 접두부의 유효 길이는 +완료 회수 시 `produced`로 교체한다. 기존 데이터에 덧붙이기·덮어쓰기는 이 프로파일에 없다. +한 번에 최대 32비트 길이의 구간을 다루며 전체 스트림의 누적 길이는 검사된 64비트 정수로 센다. +파일 전체를 한 버퍼에 넣어야 한다는 뜻은 아니다. + +반환값은 `consumed`, `produced`, `status`, `error`다. + +- `consumed`는 이번 입력 구간에서 다시 제공하지 않아도 되는 접두부 길이다. +- `produced`는 출력 구간 시작에서 후속 작업이 사용할 수 있도록 확정된 접두부 길이다. + 임시로 쓴 바이트·내부 복사량과 다르다. 나머지 출력 용량은 유효 데이터가 아니다. +- 각 길이는 제공된 구간 길이 이하여야 한다. 이전 호출의 양을 다시 포함하지 않는다. +- 입력 소비와 출력 생성은 서로 독립적이다. 입력을 다 소비해도 출력·종료 처리가 남을 수 있다. +- v0은 입력·출력에 같은 버퍼를 사용하는 것을 거절한다. 제자리 변환은 별도 협상할 확장이다. +- 요청 하나의 완료와 스트림 전체의 완료는 다르다. `NEED_INPUT/NEED_OUTPUT/MORE`를 + 반환한 요청도 대여를 종료하지만, 해당 스트림 문맥은 다음 요청을 위해 열린 상태로 남는다. + +| 상태 | 의미와 다음 행동 | +|---|---| +| `MORE` | 진전이 있었으며 다시 호출할 수 있음. 양쪽 길이가 0이면 사용할 수 없음 | +| `NEED_INPUT` | 아직 끝나지 않음. 새 입력 또는 입력 종료 통지를 기다림 | +| `NEED_OUTPUT` | 미처리 입력이나 내부 출력이 남음. 새 출력 용량을 마련하고 남은 입력부터 재개 | +| `DONE` | 이 문맥의 스트림 완료. 입력 종료를 단순히 소비했다는 뜻과 다름 | +| `FAILED` | 문맥 종료. 오류 코드 필수, 부분 소비·검증된 출력이 있으면 정확히 보고 | +| `CANCELLED` | 최종 취소 완료. 이미 검증·공개된 부분이 있으면 정확히 보고 | + +종료 통지 `END_INPUT`은 마지막 입력 뒤에 더 이상 입력이 없다는 약속이다. +부분 소비 시 남은 입력과 이 플래그를 함께 재제공한다. 마지막 입력을 모두 소비한 뒤에도 +`NEED_OUTPUT`이면 빈 입력·같은 플래그·새 출력으로 종료 처리를 계속한다. +`END_INPUT`이 설정된 호출에서는 `NEED_INPUT`을 반환하지 않는다. 더 읽어야 하는 잘린 데이터면 `FAILED`다. +`DONE`에 미소비 입력이 있으면 호출자가 다음 스트림·후행 데이터 정책을 결정한다. +`DONE/FAILED/CANCELLED` 문맥은 후속 `step`을 받지 않으며 닫기만 허용한다. + +Zstd는 입력·출력 위치와 종료 진행을 따로 보고하며 내부 윈도 메모리도 필요로 한다. +이 자료는 위 프로파일을 설계하는 근거이며 Zstd의 API를 Pure의 보편 규칙으로 간주하지 않는다. +참조한 매뉴얼은 1.5.1이고 채택할 라이브러리 버전은 아직 결정하지 않았다. +[Zstandard 스트리밍 매뉴얼](https://facebook.github.io/zstd/zstd_manual.html#Chapter7) + +## 5. 부분 성공과 인증 + +위 스트림 프로파일은 바이트 변환의 결과다. 파일·네트워크 출력은 별도 효과 결과로 +실제로 전달한 바이트 수를 보고해야 한다. 변환의 `produced`를 파일에 저장된 양으로 재사용하지 않는다. + +인증 복호화 어댑터는 성공한 인증 단위의 평문만 유효 출력으로 공개한다. +현재 단위의 인증에 실패하면 그 단위의 `produced`는 0이고 임시 평문은 공개하지 않는다. +이전 호출에서 인증·공개된 단위까지 되돌렸다고 주장하지 않는다. +단위 크기와 보류 버퍼에 예산을 두고 검증된 라이브러리의 계약을 따른다. +암호 알고리즘을 이 명세에서 새로 정의하지 않는다. +[libsodium 인증 암호화 API](https://doc.libsodium.org/secret-key_cryptography/secretbox) + +## 6. 컴파일러가 감당하는 작업 + +현재 한도는 소스 32,768바이트, 상태 32개, 입력 8개, 출력 24개, 핸들러 64개, +중간 명령 512개, 스택 64칸이다. JS 파서·AST·맵의 실제 최대 힙은 아직 측정되지 않았다. + +제안: 코어 연산 집합은 버전으로 관리하고 코덱·문서·미디어 라이브러리는 모듈로 연결한다. +라이브러리마다 새 문법·타입별 특수화·컴파일타임 실행을 추가하지 않는다. +모듈 호출 한 개가 적은 작업이라는 가정도 하지 않는다. 데이터 구간·문맥 메모리·한 호출의 +최대 작업량을 별도로 제한한다. 중단할 수 없는 라이브러리 호출에는 엄격한 CPU 시간 보장을 표기하지 않는다. + +추가 기능마다 컴파일러 산출물 크기, 빌드 시간, 피크 메모리, 생성 코드 크기와 실행 비용을 분리 측정한다. +초기 확장에는 전역 타입 추론·사용자 컴파일타임 실행·제한 없는 특수화를 도입하지 않는다. +특수화가 필요해지면 적용 대상을 제한한 후보와 공통 코드 후보를 비교한다. +이는 Pure의 설계 선택이다. Rust는 타입별 코드 생성의 이득과 빌드 시간·바이너리 크기 비용을 문서화한다. +[Rust 단형화 문서](https://rustc-dev-guide.rust-lang.org/backend/monomorph.html) diff --git a/docs/design/002-memory-lifetime.md b/docs/design/002-memory-lifetime.md new file mode 100644 index 0000000..a706a36 --- /dev/null +++ b/docs/design/002-memory-lifetime.md @@ -0,0 +1,135 @@ +# 메모리와 데이터 수명 초안 0.1 + +상태: **제안, 실행기 미구현**. 현재 Workbench에는 바이트 버퍼·비동기 완료 큐가 없다. +이 문서는 [연산 의미](001-operation-semantics.md)를 구현할 때의 불변조건과 회수 규칙을 정한다. + +## 1. 예산의 소유자 + +각 실행 영역에 바이트 예산과 객체·요청 개수 예산을 부여한다. 영역의 소유자는 런타임이다. +첫 구현 후보는 고정 크기 청크 풀과 세대 번호를 가진 핸들 테이블이다. +장기 상태·스트림 문맥·작업 임시 공간은 수명이 달라 별도 항목으로 센다. +이 정책이 최적이라고 가정하지 않으며 일반 할당 방식과 같은 작업으로 비교한다. + +| 계측할 공간 | 반드시 포함할 것 | +|---|---| +| 확보한 backing 용량 | 사용 중인 청크와 비어 있지만 보유 중인 청크, 정렬·단편화 | +| 메타데이터 | 핸들·대여·문맥·작업·완료·대기열 슬롯 | +| 살아 있는 데이터 | 앱·소비자·진행 중 작업이 여전히 참조하는 실제 버퍼 | +| 모듈 내부 공간 | 코덱 윈도·사전·파싱 상태·작업 공간·라이브러리의 자체 할당 | +| 교체 중 공간 | 이전 코드·문맥·진행 중 작업과 다음 프로그램의 준비 공간 | +| 외부 자원 | 별도 호스트 힙·OS·GPU 자원과 추적할 수 없는 할당 | + +같은 물리 버퍼를 여러 독자가 대여해도 물리 용량은 한 번만 센다. +물리 용량과 사용량은 포함 관계이므로 합산하지 않는다. 모듈이 예산 밖에서 할당하면 +그 공간을 보고서에서 제외하지 않고 외부·미계측으로 표시하며 전체 상한 보장을 중단한다. +요청별 예산을 만들었다고 라이브러리의 숨은 할당이 자동 통제되는 것은 아니다. +풀에서 할당한 모듈 작업 공간·메타데이터는 그 풀 확보 용량의 세부 귀속 항목이다. +풀 용량에 다시 더하지 않는다. 확보 용량은 할당한 backing의 크기이며 OS의 상주 RAM 측정값과 다르다. + +현재 64 KiB는 Wasm 선형 메모리의 초기·최대 용량이다. JS 호스트·컴파일러·렌더러까지 +포함한 전체 예산이 아니다. 새 버퍼 풀을 그 공간 밖에 만들면 별도로 포함해야 한다. + +## 2. 버퍼 식별과 범위 + +`BufferHandle = (slot:u32, generation:u32)`를 첫 후보로 정한다. 포인터·파일 오프셋이 아니다. +세대 0은 무효, 처음 발급은 1이다. 슬롯 재사용마다 세대를 증가시키고 최댓값에 도달한 슬롯은 +회수 후 영구 퇴역한다. 순환으로 오래된 핸들이 다시 유효해지지 않게 한다. +핸들은 발급한 실행 영역 안에서만 의미가 있다. 다른 영역으로 숫자만 복사해 전달할 수 없다. + +구간은 핸들과 `offset:u32, length:u32`다. 런타임은 다음을 검사한다. + +- 현재 세대·소유자·요청자 권한·접근 모드가 일치한다. +- 읽기는 현재 유효 데이터 길이, 쓰기는 예약 용량 안에 있다. +- `offset <= bound`를 먼저 확인하고 `length <= bound - offset`을 검사해 덧셈 오버플로를 피한다. +- 정렬 요구를 충족한다. v0 바이트 프로파일의 기본 정렬은 1이며 더 큰 정렬은 모듈 설명에 명시한다. +- v0 출력 버퍼는 현재 유효 길이 0, 출력 구간 오프셋 0이어야 한다. 접수 때 암묵적으로 + 이전 데이터를 버리지 않는다. 소비가 끝난 뒤 대여가 없는 소유자가 명시적으로 유효 길이를 0으로 + 줄여 재사용한다. 유효 길이를 늘리는 일은 검증된 쓰기 완료·유효 출력 공개로만 허용한다. + +같은 프로세스의 임의 네이티브 코드로부터 메모리를 격리하는 기능은 아니다. +이 계약의 첫 대상은 계약을 지키는 내부 모듈이다. 불신 모듈의 격리는 별도 실행 경계가 필요하다. + +## 3. 소유권과 대여 + +한 버퍼에는 소유자 하나가 있으며 런타임이 대여 기록을 관리한다. +복사 없는 전달의 후보는 버퍼 내용 대신 이 소유권 또는 읽기 대여를 넘기는 것이다. + +| 상태 | 허용하는 일 | 금지하는 일 | +|---|---|---| +| 소유, 대여 없음 | 읽기·독점 쓰기·소유권 이전·반납 | 범위 밖 접근 | +| 읽기 대여 1개 이상 | 대여받은 독자의 읽기, 추가 읽기 대여 | 쓰기·소유권 이전·재사용·반납 | +| 독점 쓰기 대여 1개 | 지정된 작성자의 범위 내 쓰기 | 다른 읽기·쓰기·이전·재사용·반납 | +| 반납 완료 | 같은 슬롯의 새로운 세대로 재할당 가능 | 이전 핸들 접근 | + +첫 구현은 **서로 다른 구간이라도 버퍼 전체 단위로 대여**한다. 구간별 동시 쓰기나 +동적 겹침 증명은 후속 연구다. 읽기·쓰기 대여가 동시에 존재하지 않는다는 조건을 단순하게 유지한다. + +모듈 호출을 위해 해석한 실제 포인터는 허용된 접근 기간에만 유효하다. 동기 백엔드는 함수 반환 후 +포인터를 사용하지 않는다. 비동기 백엔드는 최종 완료 게시 전에 접근을 끝낸다. +런타임에 접수된 요청의 대여는 백엔드의 동기·비동기 여부와 관계없이 **완료 회수까지 유지**한다. +모듈이 계속 데이터를 보유해야 한다면 +자체 문맥에 예산을 잡아 복사하거나, 별도의 추적 가능한 장기 대여가 필요하다. +첫 프로파일은 호출 종료 뒤 입력·출력 포인터를 숨겨 보유하는 것을 금지한다. + +`반납`은 대여가 있으면 `BUSY`로 실패하고 소유권을 유지한다. 암묵적 지연 반납을 만들지 않는다. +비밀 데이터 버퍼의 지우기는 마지막 접근이 끝난 뒤 수행하며 사용한 라이브러리의 보장·한계와 +복사본의 수명까지 별도로 확인한다. 단순히 슬롯 번호를 지우는 것으로 데이터 삭제를 주장하지 않는다. + +## 4. 비동기 요청의 수명 + +첫 계약은 한 실행 영역의 직렬 제어 흐름을 사용한다. 모듈의 worker는 완료를 게시하고 +상태·핸들 테이블의 회수는 영역 소유자가 수행한다. 제어 API의 재진입을 허용하지 않는다. +스트림 문맥 하나에는 진행 중 호출을 최대 하나만 허용한다. + +```text +준비 → 런타임 접수 거절 : 예약 해제, 티켓 없음 +준비 → 런타임 접수/확정 → 백엔드 제출 → 완료 게시 → 완료 회수 + → 취소 요청 → 완료 게시 → 완료 회수 +``` + +- 접수 전 요청 슬롯·대여 슬롯·완료 보관 슬롯을 함께 확보한다. 실패하면 `WOULD_BLOCK` 또는 `LIMIT`이다. +- 런타임 접수 전 거절은 입력·출력·문맥을 바꾸지 않는다. 접수된 요청에는 세대가 붙은 티켓 하나를 발급한다. + 이벤트 안의 접수는 준비된 관리 상태와 함께 확정한다. 실제 백엔드 제출은 그 이후다. + 백엔드 제출 거절은 기존 티켓의 실패 완료로 보고하며 티켓 없는 거절로 되돌리지 않는다. +- 모든 접수된 요청은 성공·실패·취소 중 하나의 최종 완료로 정리해야 한다. + 실제 백엔드가 응답하지 않을 수 있으므로 유한 시간 안에 완료된다는 보장은 별도다. +- 즉시 완료해도 제출 호출 안에서 앱 콜백을 재진입하지 않고 완료 큐로 전달한다. +- 취소 호출은 **요청**이다. 거절되거나 정상 완료와 경합할 수 있다. 버퍼를 반납할 권한을 주지 않는다. +- 완료 게시 뒤에도 앱이 완료를 회수하기 전에는 해당 요청의 대여·슬롯을 보유한다. +- 완료 회수는 티켓·모듈 세대·길이·상태를 검증하고 유효 출력 길이와 문맥 상태를 반영한 뒤 + 대여·요청 슬롯을 한 번만 해제한다. 중복·오래된 완료는 진단하고 두 번 반납하지 않는다. + v0은 출력 버퍼의 유효 길이를 해당 요청의 `produced`로 설정한다. 실패·취소도 동일하며 + 물리적으로 썼지만 검증되지 않은 꼬리 부분은 공개하지 않는다. +- 중복 완료를 무시하는 것만으로 잘못된 백엔드의 추가 메모리 쓰기가 안전해지는 것은 아니다. + 모듈은 완료 게시 전에 작업·worker·장치의 접근 종료와 필요한 동기화를 보장해야 한다. +- 제한 시간 초과만으로 메모리를 재사용하지 않는다. 실제 접근이 종료됐다는 백엔드의 보장까지 보유한다. + +libuv도 취소 요청 후 완료 콜백 전에는 요청 관련 메모리를 해제하면 안 된다고 명시한다. +이 원칙을 Pure의 대여·티켓 상태에 반영했다. 취소에 외부 효과의 롤백을 포함하지 않는다. +[libuv 요청·취소 계약](https://docs.libuv.org/en/v1.x/request.html#c.uv_cancel) + +## 5. 포화·느린 소비자·교체 + +요청·완료·버퍼 슬롯이 없으면 생산자에게 역압력을 반환한다. 완료를 조용히 버리거나 +대기 요청을 무제한 늘리지 않는다. 앱은 재시도 시점·거절·명시적 버리기 정책을 선택한다. +대여를 오래 유지하는 소비자는 메모리를 붙잡는다. 복사량뿐 아니라 유지 용량과 바이트×시간을 센다. +소유권 전달을 했다는 사실만으로 GPU·드라이버·프로세스 경계의 복사가 없어졌다고 주장하지 않는다. + +프로그램·모듈 교체의 첫 정책은 **관련 진행 작업·대여·열린 문맥이 있으면 `BUSY`로 거절**하는 것이다. +작업을 배출하고 명시적으로 문맥을 닫은 뒤 교체한다. 시간 초과로 강제 언로드하지 않는다. +다중 버전을 동시에 유지하는 무중단 교체는 첫 계약 밖이다. +현재 이름 기반 숫자 상태 보존은 파일 핸들·코덱 문맥·키·GPU 자원의 자동 이전을 뜻하지 않는다. + +## 6. 구현 시 필수 확인 + +아직 통과한 검사가 아니라 다음 구현의 합격 조건이다. + +1. 버퍼 2개와 요청 1개로 여러 번 제출·완료·반납해 슬롯 수와 예산이 보존된다. +2. 포화된 제출은 상태·유효 길이·소유권을 변경하지 않는다. +3. 취소 요청만 받은 출력 버퍼는 다른 읽기·쓰기·반납이 거절된다. 입력 버퍼의 공유 읽기는 + 가능하나 쓰기·반납은 거절된다. 관련 문맥·모듈 교체도 거절되고 최종 완료 회수 전에는 해제하지 않는다. +4. 늦은 완료·중복 완료·해제된 세대의 핸들이 새 버퍼를 회수하거나 수정하지 않는다. +5. 실패 출력은 명시한 유효 접두부만 공개한다. 인증 실패 단위에서는 유효 길이가 0이다. +6. 입력 소비 완료 후 남는 출력·종료 처리가 정확히 진행된다. 느린 소비자에서 사용량이 예산을 넘지 않는다. +7. 길이·누적 크기 넘침, 세대 소진, 할당 실패를 명시적으로 처리한다. +8. 모듈 내부 공간과 대기 작업까지 포함한 피크를 계산하고 외부 할당과 전체 RSS를 따로 보고한다. diff --git a/docs/design/003-module-contract.md b/docs/design/003-module-contract.md new file mode 100644 index 0000000..1fcdb76 --- /dev/null +++ b/docs/design/003-module-contract.md @@ -0,0 +1,119 @@ +# 모듈 계약 초안 0.1 + +상태: **제안**. 이 문서는 논리적 호출·수명·호환 조건과 데이터 레코드를 정한다. +동적 로더·네이티브 함수 테이블·Wasm 모듈 연결은 아직 구현하지 않았다. +[C17 레코드 헤더](abi-v0.h)는 데이터 표현을 검토·컴파일하기 위한 초안이며 실행 가능한 모듈 SDK가 아니다. + +## 1. 세 가지 경계 + +| 경계 | 첫 방침 | +|---|---| +| Pure 내부 연산·모듈 | 값 종류, 버퍼 접근, 길이, 예산, 진행·오류·완료 의미를 명시 | +| 플랫폼의 네이티브 함수 호출 | 해당 타깃의 기존 C 호출 규약을 사용. 레지스터·스택 규약을 새로 발명하지 않음 | +| 저장·전송 형식 | 별도 버전·필드 폭·바이트 순서·검증·호환 규칙을 설계한 뒤 도입 | + +구조체·포인터를 파일로 덤프하는 것을 영속 형식으로 삼지 않는다. 반대로 같은 프로세스에서 +모듈을 호출할 때마다 데이터 전체를 직렬화할 필요도 없다. +Arm의 호출 규약도 데이터 모델·포인터·정렬·패딩을 다룬다. 이것이 Apple을 포함한 모든 +플랫폼의 호출 차이를 없애주는 것은 아니므로 타깃별 경계를 검증해야 한다. +[AAPCS64 고정 원문](https://github.com/ARM-software/abi-aa/blob/0948ccc42ed2ea6f1ae88a3cbd5438d904709112/aapcs64/aapcs64.rst#L635-L695) + +Wasm 선형 메모리의 숫자 저장은 little-endian이다. Wasm 어댑터는 그 규칙을 따르며, +네이티브 레코드를 포인터 그대로 다른 인스턴스나 프로세스에 넘기지 않는다. +[WebAssembly 숫자 저장](https://webassembly.github.io/spec/core/exec/numerics.html#storage) + +## 2. 프로파일과 버전 + +모듈마다 다음 정보를 로드 전에 확인한다. + +- 연산 계약 버전과 프로파일 ID. 첫 제안은 `0.1 / byte_stream=1`이다. +- 모듈 구현 식별자·빌드 해시·설정, 상태 스키마 식별자. 실행·재생 기록에 보관한다. +- 입력·출력 종류와 개수, 구간 최대 크기, 정렬, 문맥·임시 공간 예산. +- 동기/비동기 지원, 종료·취소 동작, 모듈 내부 외부 할당의 계측 가능 여부. + +`0.x` 동안 호환성은 정확한 버전 일치로만 허용한다. 임의로 더 높은 minor를 받아들이지 않는다. +기능 필드·상태·플래그를 알 수 없으면 작업 시작 전에 `UNSUPPORTED`로 거절한다. +이후 호환 확장을 도입하려면 필수·선택 기능의 구분과 구버전 검사를 별도로 정한다. + +현재 Workbench의 `revision`은 순환하는 적용 횟수다. ABI 버전·상태 스키마·비동기 티켓의 +대체물로 사용하지 않는다. 현재 중간 명령 번호나 Zig `Program` 메모리 배치를 모듈 파일 형식으로 고정하지 않는다. + +## 3. 첫 바이트 프로파일의 데이터 레코드 + +모든 필드는 고정 폭 정수다. 네이티브에서는 native-endian이며 C의 기본 정렬을 사용한다. +패킹 속성·C enum·bool·플랫폼별 `long`·포인터를 레코드에 넣지 않는다. +동일한 숫자 폭만으로 호출 ABI가 같아지는 것은 아니며, 함수 경계 검증은 별도다. + +| 레코드 | 크기 | 뜻 | +|---|---:|---| +| `pure_handle_v0` | 8 B | 슬롯·세대. 실행 영역 안에서만 유효 | +| `pure_span_v0` | 16 B | 버퍼 핸들·오프셋·길이 | +| `pure_step_result_v0` | 24 B | 레코드 크기·스트림 상태·오류·이번 소비량·유효 생성량·예약 필드 | +| `pure_stream_limits_v0` | 64 B | 레코드 크기·버전·프로파일·개수·예산·구간 상한·정렬 | + +이는 헤더의 크기 요구조건이며 메모리 사용량·전송량의 측정값이 아니다. +입출력 수와 `max_pending_per_context`는 첫 프로파일에서 각각 1이어야 한다. +`flags`와 `reserved`는 0이어야 한다. `struct_bytes`는 해당 크기와 정확히 일치해야 한다. +`max_input_bytes/max_output_bytes`는 1 이상이고 협상한 예산 이하, `buffer_alignment`는 +1 이상의 2의 거듭제곱이어야 한다. 호스트가 보장할 수 없는 정렬·예산은 로드 또는 open에서 거절한다. +0인 문맥·임시 예산은 해당 종류의 할당을 허용하지 않는다는 뜻이지 무제한이 아니다. + +한 요청의 길이는 `u32`, 모듈 예산·스트림의 누적 길이는 검사된 `u64`다. +JavaScript 어댑터는 `u64`를 BigInt 또는 명시적 두 워드로 전달하고 Number로 조용히 축약하지 않는다. +크기·개수·누적 산술은 실제 접근·할당 전에 검사한다. + +결과 검증 규칙: + +- `status`는 헤더의 여섯 값 중 하나다. `FAILED`에만 0이 아닌 `error`가 필요하고 다른 상태는 0이다. +- `consumed/produced`는 제공한 길이 이하이고 `MORE`는 둘 중 하나가 0보다 커야 한다. +- `reserved=0`, 레코드 크기가 일치해야 한다. 실패해도 검증된 부분 진행을 정확히 보존한다. +- 잘못된 완료는 모듈 프로토콜 오류다. 데이터는 공개하지 않고 문맥을 실패 상태로 만든다. + 접근 종료를 보장하는 신뢰 가능한 완료까지 대여를 보유한다. 길이 오류를 핑계로 진행 중 버퍼를 재사용하지 않는다. +- 헤더의 오류 번호는 이 초안의 별도 네임스페이스다. 현재 scalar VM의 오류 번호와 혼용하지 않는다. + +## 4. 논리적 제어 API + +아래는 다음 구현의 호출 의미다. 네이티브 C 함수 시그니처나 동적 로딩 심볼을 확정한 선언은 아니다. +첫 구현은 정적으로 등록한 내부 어댑터로 이 의미부터 검증한다. + +| 호출 | 결과와 수명 | +|---|---| +| `describe(module)` | 계약·기능·한도·구현 식별. 작업·대여 생성 없음 | +| `open(module, config, budget)` | 문맥 생성 또는 거절. 성공 시 문맥·숨은 내부 메모리의 예산 책임 시작 | +| `submit(context, input, output, END_INPUT)` | 런타임이 거절하거나 티켓을 접수. 준비/확정/백엔드 제출 구분은 연산 명세를 따름 | +| `cancel(ticket)` | 취소 요청을 전달한 결과. 스트림 결과나 버퍼 회수가 아님 | +| `reap(ticket)` | 아직 완료가 없으면 `WOULD_BLOCK`; 있으면 결과 회수·유효 길이 반영·대여 해제 | +| `close(context)` | 진행 작업이 있으면 `BUSY`; 없으면 내부 문맥·장기 자원 반환. 이후 핸들 무효 | + +사용하지 않는 입력이나 출력도 유효한 버퍼 핸들과 길이 0 구간으로 표시한다. +`open/submit`의 일시적 풀 포화는 `WOULD_BLOCK`, 고정 한도를 넘는 요구는 `LIMIT`, +이미 진행 요청이 있는 문맥은 `BUSY`, 잘못된 핸들·범위는 `INVALID`다. +이 접수 응답들은 `pure_step_result_v0`의 스트림 상태와 별개이며 해당 C 응답 레코드는 후속 구현에서 정한다. + +동기 어댑터도 `submit` 시 즉시 결과를 완료 슬롯에 보관하고 `reap`에서 전달할 수 있다. +호스트가 최적화로 내부 호출을 합치더라도 수명·오류·관찰 가능한 순서는 유지해야 한다. +개별 문맥은 순차 처리하며 다른 문맥 사이의 완료 순서는 제출 순서와 같다고 가정하지 않는다. + +`reap`은 요청의 완료다. 결과가 `NEED_INPUT`이면 문맥은 살아 있으며 다음 입력을 받을 수 있다. +문맥은 `DONE/FAILED/CANCELLED` 이후에도 `close`까지 예산을 차지한다. +프로그램·모듈 교체는 관련 요청·대여·열린 문맥이 모두 해소된 뒤에만 허용한다. + +## 5. 확장 범위 + +압축·인증 복호화·문서·미디어는 포맷 처리 모듈의 후보이며 모두 같은 복잡도를 갖지는 않는다. +여러 평면의 영상, 탐색 가능한 문서, 파일·네트워크의 부분 쓰기는 추가 프로파일이 필요하다. +현재 바이트 프로파일에 의미가 다른 모든 출력을 억지로 넣지 않는다. + +Pure 자체를 작은 OS로 설계한다는 [현재 방향](007-execution-domains.md)에 따라 실행·자원· +통신의 운영 모델은 Pure가 소유한다. 현재 호스트 OS는 구동 수단과 제약이며, 모듈의 의미를 +호스트의 프로세스·파일 디스크립터에 고정하지 않는다. 호스트 구동, kernel-bypass, 직접 하드웨어 +구동은 이를 실현하는 경로로 구분한다. 부팅·드라이버의 범위나 일정은 이 모듈 계약으로 확정하지 않는다. + +## 6. ABI를 구현했다고 말할 수 있는 조건 + +레코드의 `_Static_assert`가 타깃별로 컴파일되는 것은 데이터 레이아웃의 제한된 검사다. +실제 함수 호출·언어 간 호환·버전 협상·대여·완료의 검증을 대신하지 않는다. +후속 native adapter에서 함수 테이블 크기·호출 규약·오류 경계·호스트 할당 콜백·리소스 해제 책임을 +구체적인 선언으로 작성하고, 독립 C 생산자와 Zig 소비자의 실제 왕복 호출을 검사해야 한다. +Wasm 어댑터는 범위·리프트/로워·수명 검사와 복사량을 별도로 측정해야 한다. +이 단계 전에는 안정된 ABI·플러그인 호환·새 포맷 지원을 구현 완료로 보고하지 않는다. diff --git a/docs/design/004-prepared-connections.md b/docs/design/004-prepared-connections.md new file mode 100644 index 0000000..df4ccaf --- /dev/null +++ b/docs/design/004-prepared-connections.md @@ -0,0 +1,39 @@ +# 실행 준비에서 연결을 정하는 첫 범위 + +현재 필드·동적 입력 범위 확장은 [005](005-field-connections.md)를 따른다. 아래는 첫 상수 구간 프로파일의 기준이다. +조건부 바이트 부분 변경 `store.splice`와 네이티브 자동화 경로는 [023](../research/023-document-edits.md)에 추가했다. + +2026-09-23 착수 기준: + +- 바꿀 결정: 기존 저장·응답 순서를 C에 고정하지 않고 작성한 관계에서 접근 범위와 수명을 준비한다. +- 남은 질문: 중첩 구간·별칭을 실행 경로에서 없애면서 범위 오류·외부 효과·소유권을 유지할 수 있는가? +- 최소 확인: 기존 실제 TCP 저장·복원을 작성 파일로 실행하고, 일부만 필요한 응답은 최종 구간만 매핑한다. +- 중단 기준: 작성 변경이 실제 경로에 반영되고 의미 검사를 통과하면 범용 그래프 VM·새 쿼리 엔진으로 확장하지 않는다. + +첫 프로파일은 `on get`·`on put` 두 요청을 다룬다. 기존 숫자/화면 DSL과 파서를 공유하지 않는 +네이티브 연결 작성 실험이며 통합 언어의 확정 문법은 아니다. 준비는 서버 시작 시 한 번 한다. +동적 코드 생성·LLVM·JIT·요청별 그래프 순회는 필요하지 않은 범위다. + +값은 읽기 전용 바이트 범위와 저장 결과다. `let`은 별칭·정확한 `range` 또는 저장 호출을 +연결한다. `reply(result[, bytes])`는 결과 버전과 선택한 바이트를 호스트 응답에 연결한다. +저장 실패는 응답 오류로 전파하며, 본문 없는 reply도 저장 호출을 생략하지 않는다. +내구성 불확실 결과도 성공이나 롤백으로 바꾸지 않는다. + +읽기는 한 번의 `store.read(request.key)` 이후에 뷰를 정의한다. 쓰기는 모든 입력 뷰를 +정의한 뒤 한 번의 `store.write(request.key, request.revision, bytes)`를 호출한다. +쓰기를 수행한 뒤 실패할 수 있는 뷰 연산을 놓는 문법은 첫 프로파일에서 거절한다. +하나의 요청에서 여러 외부 효과·루프·동적 분기는 아직 지원하지 않는다. + +`range(bytes, offset, length)`는 정확한 범위를 요구한다. 잘라내거나 부족한 바이트를 +조용히 무시하지 않는다. 중첩 구간을 합칠 때 최종 범위만 검사하면 의미가 달라진다. +예: `range(range(body, 16, 64), 16, 16)`은 최종 `[32,48)`을 쓰지만 원본이 80 B 이상이어야 +한다. 준비된 경로는 최종 범위와 원본 최소 길이를 함께 보관한다. 사용되지 않는 구간의 +오류도 보존한다. 고정된 부모 구간을 넘는 자식 범위는 작성 오류로 시작 전에 거절한다. + +읽기 전송은 매핑의 수명을, 쓰기 결과와 함께 입력을 돌려주는 전송은 수신 구간의 수명을 +완료·중단까지 유지한다. 개발자가 해제 함수를 작성하지 않는다. 이 수명 선택은 현재 두 +생산자의 닫힌 계약에서 도출하며 임의 외부 모듈의 소유권 추론을 구현했다고 세지 않는다. + +이번에 제거할 것은 뷰 정의의 실행 단계와 불필요한 전체 본문 접근이다. 별칭·부분 범위는 +원래도 본문 복사를 요구하지 않는다. 이를 실제 복사를 제거한 수치로 꾸미지 않으며, +준비된 구간 길이를 RSS 절감으로 해석하지 않는다. 파일·커널·페이지 비용은 그대로 남는다. diff --git a/docs/design/005-field-connections.md b/docs/design/005-field-connections.md new file mode 100644 index 0000000..0993567 --- /dev/null +++ b/docs/design/005-field-connections.md @@ -0,0 +1,28 @@ +# 필드와 실제 입력 값으로 정하는 연결 + +2026-09-23 착수 기준: + +- 바꿀 결정: 작성자가 바이트 위치를 직접 관리하지 않고 필드의 배치·접근과 데이터에 따른 범위를 준비한다. +- 남은 질문: 이름·타입을 붙인 뒤에도 입력을 객체로 재구성하지 않고 실제 요청 값으로 저장·응답을 선택할 수 있는가? +- 최소 확인: 문서와 선택 시작·길이를 받는 실제 요청에서 선택한 내용을 저장·응답한다. 잘린 입력·잘못된 범위는 저장 전에 거절한다. +- 중단 기준: 이 경로가 검증되면 새 범용 직렬화 형식·타입 VM·동적 그래프 실행기로 범위를 늘리지 않는다. + +`layout`은 경계의 바이트 배치 선언이다. 저장소 스키마나 Pure 전체의 RAM 객체 모델이 아니다. +첫 필드 종류는 `u32le`, 고정 `bytes[N]`, 마지막 가변 길이 `bytes`다. 패딩 없이 선언 순서로 +배치하며 숫자는 명시한 바이트 순서로 읽는다. 기존 바이트 경로에는 선언을 강제하지 않는다. + +`view(Layout, source)`는 복사·할당 없이 타입이 있는 참조를 준비한다. 필드는 이름으로 선택한다. +고정 필드는 지정한 폭을 요구하며 마지막 bytes는 나머지 구간이다. 고정 필드만 있는 선언은 +접두부를 해석하고 여분의 바이트를 무시한다. 필드 의미·텍스트 인코딩·버전 변환까지 보장하지 않는다. + +`range`는 상수 또는 `u32le` 필드를 시작·길이로 사용한다. 상수만 필요한 연결은 기존처럼 +준비 단계에서 합친다. 실제 입력 값에 의존하는 범위만 요청 시 계산하며, 별칭은 실행 단계를 +추가하지 않는다. 쓰지 않는 동적 범위도 검사해 오류를 보존한다. 모든 검사가 저장 효과보다 앞선다. + +첫 동적 범위는 PUT 입력에 한정한다. GET의 이름 기반 고정 필드·나머지 구간 선택은 가능하나 +파일 내용의 숫자로 결정하는 동적 GET은 시작 시 거절한다. 후자는 같은 파일 버전에서 필요한 +숫자와 본문을 읽는 접근 계획이 추가로 필요하며, 전체 매핑으로 조용히 대체하지 않는다. + +배치 선언은 최대 4개, 선언당 필드는 16개, 입력 값에 의존하는 범위는 8단계로 제한한다. +가변 필드 뒤의 필드·중첩 배치·동적으로 선택한 바이트 위의 새 배치 해석은 아직 지원하지 않는다. +소비 완료까지 원본의 수명을 유지하며 입력의 일부만 선택해도 수신 할당 전체를 계상한다. diff --git a/docs/design/006-native-protocol.md b/docs/design/006-native-protocol.md new file mode 100644 index 0000000..9b51fec --- /dev/null +++ b/docs/design/006-native-protocol.md @@ -0,0 +1,139 @@ +# Pure의 자체 통신 형식과 보안 전송 경계 + +작성일: 2026-09-23, 구현 상태 갱신: 2026-09-24. 상태: 설계 방향과 제한된 첫 구현. +성능 결과나 QUIC 구현은 아니다. + +2026-09-24 구현 상태: 선택한 scalar 앱과 `editor.pure` 문서 앱에 자체 이진 형식을 +TLS 1.3/TCP 위에 실은 피어 호스트를 검증했다. [026 — scalar 피어](../research/026-secure-peer-link.md)와 +[030 — 문서 피어·출력 소비 선택](../research/030-authenticated-document-peer.md)을 본다. +이 설계의 일반 RPC·QUIC·새 전송 후보가 구현된 것은 아니다. + +## 결정 + +Pure 간 통신은 HTTP 메서드·URL·문자열 헤더를 필수 실행 모델로 삼지 않는다. +앱이 작성한 연결과 데이터 형식에서 자체 바이너리 통신 형식을 준비하는 방향으로 진행한다. +HTTP는 웹·기존 도구와 연결하는 호환 경계로 유지한다. 첫 검증 전송은 TLS 1.3/TCP이며, +최종 범용 전송 정책은 아직 선택하지 않았다. + +외부 런타임·서비스·프로그램과의 통신도 핵심 요구사항이다. Pure끼리의 자체 형식과 별도로 +상대가 사용하는 프로토콜·형식으로 요청을 받고 보낼 수 있어야 한다. 외부에 Pure 사용을 +요구하지 않으며, 자체 형식의 계약을 구현한 외부 프로그램과의 연결도 허용하는 방향이다. + +적용 범위 보완: [작은 OS로서의 Pure](007-execution-domains.md)가 연결의 의미와 운영 정책을 +소유한다. 공유가 허용된 내부 데이터에는 직접 호출·대여·소유권 전달을 사용하며, 이 문서의 +바이너리 경계 형식을 모든 내부 연결에 강제하지 않는다. 현재 호스트의 IPC·소켓은 가용한 +전달 수단이며 Pure 앱의 필수 개념이 아니다. + +`prototypes/connections/server.c`는 루프백 TCP 위의 평문 HTTP/1.1이며 요청마다 연결을 닫는다. +`on get` / `on put`과 요청 키도 그 작성 프로필에 들어 있다. 별도의 선택형 피어 호스트는 +해당 scalar 앱에서 HTTP 없이 입력·상태·저장을 주고받는다. 이 둘은 아직 하나의 범용 +프로토콜 선택기로 통합되지 않았다. 기존 HTTP 검증은 저장·범위·수명에 대한 것이며, +피어 검증도 인터넷 운영 보안이나 QUIC 대비 성능을 검증한 것이 아니다. 문서 피어는 +같은 `editor.pure` 효과를 C·Python 클라이언트에서 실행하며, 응답 본문을 소비하지 않을 +호출에서 그 바이트의 전송을 생략한다. + +사용자가 말한 socket 직접 통신은 일반 TCP/UDP 소켓으로도 구현할 수 있다. +IP 패킷을 직접 구성하는 `SOCK_RAW`나 커널 우회까지 필요한 것은 아니다. +각 선택은 실제 제거할 비용이 확인되면 별도로 판단한다. + +## HTTP 제거와 QUIC 대체는 다른 선택이다 + +QUIC은 HTTP 없이도 앱의 자체 프로토콜을 운반할 수 있다. +HTTP/3는 QUIC을 사용하는 앱 프로토콜 중 하나다. +따라서 자체 형식을 QUIC 스트림에 직접 싣는 경로를 첫 비교 후보로 둔다. +[RFC 9308 §1](https://www.rfc-editor.org/rfc/rfc9308.html#section-1) + +| 후보 | Pure가 정하는 부분 | 전송에서 사용하는 부분 | +|---|---|---| +| 자체 형식 / QUIC / UDP | 연결 대상, 값 배치, 호출·완료·취소 의미, 전송할 데이터 | QUIC의 보안 연결·스트림·복구·혼잡 제어 | +| 같은 자체 형식 / TLS 1.3 / TCP | 위 의미와 메시지 경계; 필요한 경우 호출 다중화 | TLS의 데이터 보호와 TCP의 순서 있는 바이트 전달 | +| 자체 형식 / 새 전송 / UDP | 위 부분과 새 전송의 모든 계약 | 재사용할 암호 구현 외에 전송 설계·검증도 필요 | + +두 번째 경로는 비교 및 UDP가 통하지 않는 환경의 연결 후보다. TCP+TLS를 QUIC과 모든 면에서 +동일하다고 보지 않는다. 예를 들어 TCP 바이트 스트림 하나에 여러 호출을 실으면 손실로 인한 +전달 대기를 공유하며, QUIC의 독립 스트림·경로 변경과 같은 기능은 자동으로 생기지 않는다. +보안 연결에 실패했을 때 평문으로 낮추는 동작은 후보에 포함하지 않는다. +[RFC 9308 §2](https://www.rfc-editor.org/rfc/rfc9308.html#section-2), +[RFC 9293 §2.2](https://www.rfc-editor.org/rfc/rfc9293.html#section-2.2) + +QUIC의 보안은 TLS 1.3을 통한 인증·키 설정과 패킷 보호를 포함한다. +신뢰성·안정성에는 손실 복구, 혼잡·흐름 제어, 주소 검증과 증폭 제한 등의 별도 계약도 있다. +암호 알고리즘 하나를 붙이거나 헤더를 줄인 것만으로 이 전체와 동등하다고 평가하지 않는다. +전송 보호와 앱의 접근 권한·저장 완료·응답 유실 후 중복 실행 처리도 구분한다. +[RFC 9001 §2](https://www.rfc-editor.org/rfc/rfc9001.html#section-2), +[RFC 9000 §4, §8](https://www.rfc-editor.org/rfc/rfc9000.html#section-4), +[RFC 9002](https://www.rfc-editor.org/rfc/rfc9002.html) + +## 연결에서 생략할 수 있는 정보 + +후보 흐름은 다음과 같다. 구체적인 패킷 배치나 필드 바이트 수는 아직 고정하지 않는다. + +1. 인증된 세션에서 허용된 입력·출력, 형식 버전, 한도와 전달 의미를 확인한다. +2. 대상·형식을 연결이나 스트림에 결부하거나 짧은 세션 내 식별자로 정한다. +3. 후속 메시지는 합의에서 추론할 수 없는 값과 식별 정보만 운반한다. +4. 실행·저장 결과를 앱이 관찰할 수 있는 완료로 전달한다. + +예를 들어 특정 문서의 변경 입력에 스트림을 결부했다면 매번 URL·메서드·필드 이름을 보낼 +필요가 없을 수 있다. 여러 대상이 섞이면 대상 식별이 필요하고, 가변 길이면 길이 또는 +명확한 종료 규칙이 필요하다. 조건부 갱신의 기대 버전, 동시 호출의 대응 관계, 오류 의미는 +해당 동작에 필요하면 남긴다. TCP의 한 번의 `read`를 메시지 하나로 간주하지 않는다. + +합의 비용도 비용이다. 추가 왕복, 세션별 표, 재연결, 형식 변경과 호환성 확인에 필요한 +시간·메모리를 포함한다. 형식 불일치는 해석 전에 거부하고, 원격 식별자를 포인터나 +임의 실행 권한으로 취급하지 않는다. 경계 형식은 명시적 바이트 순서·길이·한도를 가지며 +네이티브 구조체의 패딩이나 포인터를 그대로 전송하지 않는다. + +0-RTT로 저장 효과를 먼저 실행하는 동작은 초기 후보에서 제외한다. 보안 전송만으로 앱의 +중복 실행 문제가 해결되지는 않으며, 0-RTT 데이터에는 재전송 공격에 대한 별도 고려가 필요하다. +[RFC 9308 §3.1](https://www.rfc-editor.org/rfc/rfc9308.html#section-3.1) + +## Pure에서 확인할 연구 가설 + +바이너리 인코딩이나 연결별 형식 합의 자체를 신규 기술이라고 주장하지 않는다. +검증할 가설은 **준비된 연산·소비 관계를 전송까지 사용하면, 같은 앱 동작을 유지하면서 +반복 해석과 중간 보관·대기를 줄일 수 있는가**다. + +- 이미 정해진 대상·필드 접근을 송수신 경계에서 다시 문자열로 만들고 해석하는가? +- 소비자가 사용할 범위만 보낼 수 있는가? 부분 전송에서 필요한 형식·버전도 일치하는가? +- 전송의 수신 한도와 앱의 실제 소비·메모리 예산을 연결해 중복 큐를 줄일 수 있는가? +- 아직 전송하지 않은 최신 상태를 합쳐도 되는가? 모든 사건을 보존해야 하는 연결과 구분되는가? + +수신 한도를 소비에 연결할 때 큰 메시지의 나머지를 받기 위해 필요한 여유까지 막으면 +교착할 수 있다. 청크 소비 또는 메시지 단위 예산 예약 등으로 진행 가능성도 확인한다. +[RFC 9308 §4.4](https://www.rfc-editor.org/rfc/rfc9308.html#section-4.4) + +전송 라이브러리가 소유하는 평문·암호문·재전송 버퍼도 계산에 넣는다. 읽기 전용 저장 매핑을 +그대로 암호문으로 덮어쓸 수 없으며, `send`의 접수나 transport ACK를 앱의 저장 완료로 +취급하지 않는다. 참조 해제는 실제 전송 API의 소유권 계약에 따라 정한다. +현재의 본문 예산만으로 전체 통신 메모리가 제한된다고 주장하지 않는다. + +## 추가 구현과 비교의 경계 + +먼저 현재 get/put 전용 진입점을 앱 입력과 효과에 대한 내부 호출로 분리한다. +HTTP와 자체 형식은 같은 조건부 저장·응답 동작으로 연결한다. 별도의 범용 메시지 객체를 +항상 만들도록 강제하지 않는다. 기존 CLI와 저장·충돌·실패·수명 검증은 유지한다. + +첫 TLS 1.3 지속 연결은 [026](../research/026-secure-peer-link.md)에, 실제 문서 작업의 +연결과 선택 빌드·메모리 비용은 [030](../research/030-authenticated-document-peer.md)에 +기록했다. 다음에는 전체 앱의 실패망·혼합 작업 비용과 병목을 확인한다. 보안 없는 임시 소켓 경로를 +인터넷용 완료물로 보고하지 않는다. +새 UDP 전송은 이 단계에서 확인된 구체적인 병목을 기존 전송의 설정·연결 방식으로 +제거할 수 없을 때 별도 연구 후보로 연다. + +비교가 답할 질문은 분리한다. + +- 앱 형식의 효과: 같은 QUIC 구현에서 HTTP/3와 Pure 형식을 비교한다. +- 전송 선택의 효과: 같은 Pure 형식·동작을 QUIC과 TCP+TLS에서 비교한다. +- 새 전송의 효과: 동일한 보안·전달 요구를 충족한 뒤, 같은 Pure 형식을 기존 QUIC 구현과 비교한다. + +이 비교들을 모두 선행해야 한다는 뜻은 아니다. 실제 병목에 대한 선택이 필요할 때 해당 +비교만 수행한다. 연결 재사용·핸드셰이크·암호 설정·동시성·배치·내구성·완료 정의를 맞춘다. +현재의 평문·요청당 연결 종료 경로는 회귀 검증에 쓰며, QUIC 우위/열위의 근거로 쓰지 않는다. + +대표 작업은 기존 작업물의 작은 변경·저장, 큰 내용 전달 중 작은 변경, 느린 수신자와 +연결 종료 후 결과 확인이다. 새 전송 판단에는 지연·손실·재정렬이 있는 경로도 필요하다. +실제 성공 작업의 지연 분포, CPU, 전체 메모리와 연결별 비용, 전송 바이트·대기열을 +필요한 결정에 맞춰 기록한다. 헤더 바이트 절감만으로 전체 성능 우위를 선언하지 않는다. + +현재 결론은 HTTP를 내부 기본 모델로 요구하지 않는다는 것이다. +QUIC보다 빠르다는 결과나 새 전송의 보안 동등성은 아직 없다. diff --git a/docs/design/007-execution-domains.md b/docs/design/007-execution-domains.md new file mode 100644 index 0000000..01edb88 --- /dev/null +++ b/docs/design/007-execution-domains.md @@ -0,0 +1,222 @@ +# 작은 OS로 설계하는 마이크로 런타임과 실행 영역 + +작성일: 2026-09-23. 상태: 설계 방향. [022](../research/022-execution-core.md)에서 저장 프로필의 +영역별 작업 접수·완료·대여·회수를 구현했다. 작은 OS의 통합 코어·범용 실행·샌드박스는 아직 미완료다. + +## 마이크로 런타임의 크기 제약 + +**작은 OS라는 책임 범위와 마이크로 런타임이라는 크기 제약을 함께 지킨다.** +아래 기능 목록을 항상 포함되는 커널·서비스 목록으로 해석하지 않는다. + +- 공통 코어는 실제 작업에 필요한 실행 진행, 소유권·자원 한도, 접근 권한과 완료·회수 규칙에 + 한정한다. 사용하지 않는 기능을 위한 범용 객체·관리 계층을 미리 만들지 않는다. +- 네트워크 프로토콜·저장 구현·UI·미디어·호환 기능은 앱에 필요한 구성을 선택해 결합한다. + 초기 방식은 빌드 시 선택이며 동적 로더·플러그인 레지스트리를 기본 의존성으로 추가하지 않는다. + 기능 지원을 그 코드·상태·백그라운드 작업의 상시 포함으로 구현하지 않는다. +- 영역마다 전용 스레드·스택·소켓·큰 큐를 예약하지 않는다. 공유 가능한 준비 코드·연결은 공유하고 + 상태·대기 공간은 실제 수요와 명시적인 한도에 맞춘다. 유휴 풀의 보유 공간도 비용이다. +- 파서·컴파일러·작성 도구를 실행 전용 구성의 필수 의존성으로 만들지 않는다. 준비된 실행에 + 필요 없는 소스·AST·필드 이름표를 계속 보관하지 않는다. 특수화에 따른 코드 증가도 비용에 넣는다. + +코어의 코드·정적 데이터, 선택한 의존성을 포함한 배포 구성, 유휴 메모리, 영역·진행 작업별 +추가 비용을 구분한다. 실제 앱의 전체 피크와 외부 버퍼도 확인한다. 작은 코어 옆의 라이브러리나 +호스트로 비용을 옮긴 것으로 축소를 주장하지 않는다. 모듈을 분리했다는 사실만으로 미사용 +코드·상태가 빠졌다고 보지 않고 선택한 빌드와 실행에서 확인한다. + +새 코어 기능에는 현재 작업이 요구하는 이유와 더 작은 구현 가능성을 제시한다. 다음 구현의 +전후 크기·메모리 차이를 확인하고 설명되지 않는 증가가 있으면 범위를 줄인다. 수치 상한은 +실행 전용 코어·선택 구성·대상 기기별로 근거를 갖고 정하며 임의의 KiB 숫자를 달성치로 쓰지 않는다. +필수 권한·정확성·보안 계약을 제거해 작게 만들지 않는다. + +현재 저장 프로필은 `make core`로 파서·저장·HTTP 없는 아카이브만 빌드한다. release의 +`make check-core`가 코드 섹션·구조체 상한, 아카이브 구성과 외부 의존성을 검사한다. +상한 변경은 필요한 기능·더 작은 배치·전체 비용의 검토를 함께 기록한다. 현재 수치와 +선택 실행물의 비용 범위는 [023](../research/023-document-edits.md)에 있다. 이 제한된 코어의 +크기를 전체 앱·모든 플랫폼의 메모리 크기로 인용하지 않는다. + +## Pure가 소유하는 운영 모델 + +**Pure 런타임 자체를 하나의 작은 OS로 설계한다.** 실행·메모리·보호·자원 수명·저장·통신의 +의미와 정책을 Pure가 소유하고, 앱은 그 모델 위에서 동작한다. UI는 이 기반의 한 하위 체계다. +OS 프로세스는 현재 호스트에서 구동할 때 받는 실행·보호·자원 제약이며 Pure의 기본 개념이 아니다. +앞선 설명처럼 프로세스 배치 관리에서 설계를 시작하지 않는다. + +현재 호스트는 CPU 실행 기회, 메모리, 시계, 저장·통신·장치 접근을 제공한다. +Pure는 제공받은 범위 안에서 어떤 작업을 실행하고, 누구에게 자원을 할당하며, 어떤 연결과 +효과를 허용하고, 언제 회수할지 결정한다. 호스트의 PID·스레드·파일 디스크립터·소켓을 +앱의 정체성이나 공통 실행 계약으로 노출하지 않는다. 기존 네이티브 도구의 FFI는 별도 경계다. + +| Pure가 정의할 정책·의미 | 플랫폼 연결부가 제공할 수단·제약 | +|---|---| +| 실행 가능한 작업과 스케줄링, 기한·중단 계약 | 실행 기회, 깨우기·대기·시계, 사용 가능한 중단 수단 | +| 메모리 배치·소유권·대여·할당·회수와 예산 | 메모리 확보·반환·매핑·보호의 가용 기능과 실패 | +| 앱·자원 식별, 접근 권한과 실행 보호 | 해당 환경이 집행할 수 있는 보호 기능과 한계 | +| 내부 연결, 원격 호출과 완료·실패 의미 | 바이트·패킷 전달 수단과 실제 입출력 완료 | +| 저장 배치·공개·내구성·복구 계약 | 파일·블록 등 저장 수단, 동기화 결과와 장애 | +| UI 입력·배치·표시의 일관된 의미 | 창·입력·접근성·그래픽 등 환경 기능 | + +이 표는 새 중간 객체를 매번 만드는 API를 요구하지 않는다. 준비된 연결에서 계약을 보존하며 +불필요한 경계를 제거한다. 플랫폼이 어떤 계약을 집행하지 못하면 지원 범위로 명시한다. +호스트 밖의 메모리·CPU를 통제하거나 호스트 종료를 막는다고 주장하지 않는다. + +개발은 기존 OS에서 구동하는 형태로 시작한다. 직접 하드웨어를 구동하는 경로에서도 +Pure의 앱·실행 모델을 유지할 수 있도록 경계를 정하되, 드라이버·부팅 구현을 지금 완료한 +것으로 취급하지 않는다. 작은 OS라는 목표를 단순한 이름 변경이나 향후 부가 기능으로 미루지 않는다. + +## 외부 통신은 핵심 기능이다 + +Pure 외부와의 연결도 운영 모델의 일부다. Pure를 사용하지 않는 서비스·런타임·프로그램과 +요청을 보내고 받을 수 있어야 한다. 작은 OS라는 설계 방향을 폐쇄된 통신 환경으로 해석하지 않는다. + +- Pure 내부 연결은 준비된 연산·데이터 관계를 사용한다. +- Pure 간 원격 연결은 자체 형식과 보안 전송을 사용할 수 있다. +- 외부 서비스와의 연결은 상대가 사용하는 프로토콜·형식·인증·완료 의미를 지원한다. + HTTP(S), 기존 RPC, TCP/UDP 기반 프로토콜과 로컬 IPC 등 필요한 접점을 양방향으로 제공한다. + +이는 지원 목표이며 위 프로토콜들을 모두 구현했다는 목록이 아니다. 자체 형식도 상대가 +그 계약을 구현하면 Pure 외부에서 사용할 수 있어야 한다. 외부 프로그램의 내부 구조까지 +Pure 실행 영역으로 옮기거나 모든 상대에게 Pure 전용 프로토콜을 강제하지 않는다. + +외부 형식은 필요에 따라 검증·변환하되, 내부 데이터를 항상 외부 형식으로 보관하지 않는다. +그대로 소비할 수 있는 데이터에 불필요한 변환을 추가하지 않는다. 변환·암호화·대기·복사 비용은 +전체 경로에 포함하고, 해당 프로토콜의 주소·메서드·오류 등 앱에 필요한 의미는 접점에서 제공한다. +호환성을 이유로 그 개념을 모든 Pure 내부 연결의 필수 모델로 올리지는 않는다. + +## 기본 단위 + +**실행 영역**은 Pure 내부에서 상태·권한·수명을 귀속시키는 단위다. +기존 [메모리 계약](002-memory-lifetime.md)의 영역을 작은 OS의 운영 모델 안에서 구체화한다. +영역을 OS 프로세스의 대체 이름이나 컨테이너의 포장으로 정의하지 않는다. +VM 명령 집합의 채택 여부는 이 관리 단위를 정의하는 결정과 별개다. + +영역은 앱이 독립적으로 관리해야 하는 상태·권한·수명의 경계에 만든다. +함수·그래프 노드·요청마다 영역이나 메일박스를 하나씩 만들지 않는다. +첫 후보는 영역의 관리 상태를 한 제어 흐름이 변경하고, 독립 작업의 완료를 그 흐름에서 회수하는 방식이다. +영역마다 전용 OS 스레드·스택·소켓·이벤트 루프를 예약할 필요는 없다. +변경하지 않는 준비 프로그램은 공유하고, 인스턴스 상태와 사용 중인 작업만 개별 보유한다. + +| 영역이 관리할 것 | 의미 | +|---|---| +| 실행 식별자와 세대 | 재시작한 인스턴스에 이전 요청·완료가 섞이지 않음 | +| 프로그램과 상태 | 준비된 연산·연결, 영역이 소유한 변경 가능한 상태 | +| 자원 예산 | 보유 메모리, 진행·대기·완료 요청과 외부 자원의 귀속·한도 | +| 허용된 연결과 효과 | 호출 가능한 입력, 저장·파일·네트워크 등 접근 범위 | +| 실행·종료 상태 | 새 작업 접수, 진행 작업 배출, 취소 요청, 실제 접근 종료와 회수 | + +pod·VM은 사용자가 설명한 관리 경험의 비유이며 Pure의 내부 구조를 고정하는 명칭이 아니다. +묶음 수명과 보호 강도는 Pure의 요구에서 정한다. 영역마다 별도의 OS 이미지나 가상 장치를 +필수로 넣지 않는다. + +## 연결은 자원의 접근·공유 조건에서 준비한다 + +앱은 허용된 입력과 출력의 관계를 작성한다. 연결을 준비할 때 대상의 형식·권한·수명과 +배치를 확인하고 실행 경로를 정한다. 모든 호출을 범용 객체·직렬화·소켓으로 보내지 않는다. +경로를 선택해도 격리 정책과 관찰 가능한 실행 순서를 우회하지 않는다. + +| 가용 자원과 보호 조건 | 가능한 경로 | 남는 책임 | +|---|---|---| +| 데이터를 함께 접근할 수 있고 실행 순서를 바로 연결 가능 | 준비된 직접 호출·읽기 대여·소유권 이전 | 권한, 원본 수명, 접수 예산, 재진입·효과 순서 | +| 데이터를 공유할 수 있으나 실행 흐름이 독립적 | 제한된 작업 큐와 데이터 참조 전달 | 동기화·공개 순서, 대여 유지, 완료 회수 | +| 보호 경계가 직접 접근을 제한 | 허용된 공유 매핑 또는 바이트 전달 | 공유 영역의 권한·수명·동기화·검증, 종료 후 회수 | +| 메모리를 공유할 수 없는 원격 상대 | 자체 경계 형식과 보안 전송 | 인코딩, 흐름 제어, 기한, 응답 유실과 결과 불명 | + +현재 호스트 연결부에서는 위 조건을 스레드·메모리 매핑·IPC·소켓 등으로 구현할 수 있다. +이 구현 분류를 Pure 코어의 필수 프로세스·소켓 계층으로 올리지 않는다. + +같은 프로세스라도 격리된 샌드박스 사이에서는 임의 포인터 대여를 허용하지 않는다. +같은 기기라도 공유 메모리를 항상 사용하는 것으로 확정하지 않는다. 공유·매핑의 관리 비용과 +격리 요구에 따라 복사나 IPC가 더 적절할 수 있다. 서로 다른 프로세스에는 포인터를 전송하지 않는다. + +버퍼 핸들은 기존 계약처럼 발급 영역 안에서 유효하다. 영역 간 대여는 수신 영역에서 +검사할 수 있는 접근 권한과 대여 기록으로 연결하며, 발급 영역의 핸들 숫자만 복사하지 않는다. +물리 버퍼는 한 번만 계상하되, 각 소비자가 얼마나 오래 회수를 막는지도 기록한다. + +## 같은 호출 계약과 다른 실패 조건 + +접수, 결과, 완료, 취소, 기한의 의미는 [연산 계약](001-operation-semantics.md)과 +[모듈 계약](003-module-contract.md)을 따른다. 로컬 호출이 즉시 끝나더라도 제출 도중 앱에 +예상치 못한 재진입을 만들지 않는다. 관찰 가능한 순서·권한·예산을 보존할 수 있을 때 +내부 호출이나 완료 처리를 합칠 수 있으며 모든 경계마다 큐를 강제하지 않는다. + +원격 호출을 로컬 함수와 똑같이 성공·실패한다고 보이게 만들지는 않는다. +요청이 접수되기 전 거절과, 효과 실행 후 응답을 받지 못한 결과 불명은 구분한다. +취소는 이미 발생한 저장 효과를 되돌리지 않는다. 재연결 시 자동 재실행도 기본으로 가정하지 않는다. +같은 입력을 로컬·원격에 배치하려면 앱이 요구하는 순서·내구성·오류·공유 조건을 양쪽에서 +충족해야 한다. 충족하지 못하는 배치는 거절하거나 명시적으로 다른 계약을 선택한다. + +준비된 연결은 대상의 실행 세대와 결부한다. 대상이 재시작하면 이전 연결을 무효화하고 +형식·권한을 다시 확인한다. 늦은 완료로 새 인스턴스의 상태·버퍼를 변경하지 않는다. +이전 작업이 이미 실행됐을 가능성이 있으면 새 인스턴스에 조용히 재전달하지 않는다. +네트워크의 인증된 상대 식별과 영역 내부의 슬롯·세대는 별개이며 세대 번호 자체는 인증 수단이 아니다. + +## 자원 관리와 강제 격리 + +첫 단계의 신뢰하는 네이티브 코드 사이에서 가능한 것은 관리 계약에 따른 상태·자원 분리다. +현재 C 코드에 영역 구조체를 추가하는 것으로 임의 메모리 접근이나 프로세스 크래시를 +격리했다고 주장할 수 없다. 프로세스를 공유하면 OS 수준의 종료·주소 공간 장애도 공유한다. + +불신 코드에 필요한 격리 강도는 배치의 입력 조건으로 둔다. 후보는 제한된 실행 형식의 +검증·메모리 접근 제어, Wasm 같은 샌드박스, 별도 OS 프로세스 또는 VM 경계다. +메모리 안전한 실행 형식도 네이티브 확장·호스트 호출의 권한과 안전성을 자동 보장하지 않는다. +예를 들어 Wasmtime은 인스턴스 메모리 격리를 제공하고 호스트 API의 안전성도 별도로 다룬다. +이 문서는 Wasmtime 도입 결정이 아니다. [Wasmtime 보안 모델](https://docs.wasmtime.dev/security.html) + +메모리 예산에는 진행 작업뿐 아니라 완료 슬롯, 큐, 암호·재전송·외부 라이브러리의 보유 공간도 +포함하거나 미계측으로 명시한다. 같은 물리 공간의 중복 합산과 외부로 비용을 숨기는 일을 피한다. +공유 코드·스케줄러·유휴 풀은 런타임 공통 비용으로 따로 보고한다. + +CPU 시간의 강제 제한에는 중단 가능한 실행이나 OS의 집행 수단이 필요하다. +협력적 스케줄러의 실행 예산만으로 멈추지 않는 네이티브 함수나 동기 파일 호출을 제어할 수는 없다. +현재 서버의 동기 저장 호출은 이벤트 루프를 막을 수 있다. 영역을 나누는 것만으로 해결됐다고 세지 않는다. + +종료 요청은 새 접수를 막고 진행 작업·대여를 배출하는 단계다. 시간 초과만으로 참조 중인 +메모리를 재사용하지 않는다. 영역을 관리 목록에서 제외하더라도 남은 자원은 접근이 끝날 때까지 +명시적인 회수 책임과 예산 아래 둔다. 강제 종료·무중단 이동·복제는 별도 구현과 복구 계약이 필요하다. + +## BSD socket·Kitex 수준의 목표를 평가하는 방법 + +BSD socket은 통신 API이므로 단일 성능 수치를 뜻하지 않는다. 요구를 **같은 일을 하는 직접 +소켓 구현에 비해 런타임의 추가 비용을 작게 유지하고, 실제 RPC에서는 Kitex와 경쟁하는 것**으로 둔다. +그 수준에 도달했다는 결과는 아직 없다. + +Kitex는 Netpoll을 기본 전송 구현으로 사용하지만 다른 전송 구현도 연결할 수 있다. +연결 풀·프로토콜·실행 경로를 포함해 비교해야 한다. +[Kitex 전송 확장](https://www.cloudwego.io/docs/kitex/tutorials/framework-exten/transport/) + +현재 공식 문서는 긴 연결 풀을 기본으로 설명한다. `netpollmux`에 의존하는 +`WithMuxConnection` / `WithMuxTransport`는 유지보수 중단으로 deprecated 상태다. +이는 QUIC이나 gRPC를 포함한 모든 다중화가 불필요하다는 뜻이 아니다. +과거 벤치마크 숫자나 설정을 그대로 합격선으로 삼지 않는다. +[Kitex 연결 유형](https://www.cloudwego.io/docs/kitex/tutorials/basic-feature/connection_type/) + +비교는 실제 선택에 필요할 때만 수행한다. + +- 같은 주소 공간의 연결: 같은 작업·권한·수명·완료 조건을 가진 직접 구현에 비해 추가된 + 할당·큐·복사·스케줄링을 확인한다. 소켓을 생략한 결과를 네트워크 전송 성능으로 발표하지 않는다. +- 프로세스·기기 간 RPC: 성공 결과·형식·인증/암호화·연결 재사용·동시성·배치·내구성과 + 타임아웃·재시도 조건을 맞춘 직접 소켓 구현 및 유지보수되는 Kitex 구성과 비교한다. +- 기존 변경·저장 작업에서 지연 분포와 성공 처리량, 클라이언트·서버 CPU·전체 메모리, + 연결별 보유량을 확인한다. 큰 전달·느린 수신자·과부하가 작은 작업에 주는 영향도 포함한다. + +영역·핸들·바이너리 형식을 만들었다는 사실은 신규성이나 성능의 증명이 아니다. +연구 가설은 작성된 연결의 의미를 보존하면서 **배치에 불필요한 전송·복사·중간 보관을 +준비 단계에서 제거하고, 필요한 격리와 자원 관리는 유지할 수 있는가**다. + +## 다음 구현의 구체적 범위 + +[006](006-native-protocol.md)의 진입점 분리를 **Pure가 운영 정책을 소유하는 코어와 호스트 +수단의 분리**로 진행한다. 기존 선택·조건부 저장·응답 작업을 재사용해 실제 효과까지 연결한다. +기존 구현에서 필요한 책임만 분리하며, 사용처 없는 서비스 관리자·범용 모듈 체계를 먼저 만들지 않는다. +이번 경로에 필요한 코어 크기와 실행 비용을 기준으로 유지하고, 추가 기능의 비용은 선택 구성에 귀속시킨다. + +1. Pure가 요청 접수·진행 작업·완료·자원 회수를 소유하고, 호스트 연결부는 저장·전송·시계 등 + 실제 수단과 결과를 제공하도록 책임을 분리한다. PID·파일 디스크립터를 앱의 식별자로 삼지 않는다. +2. 영역별 프로그램·상태·예산·접근 권한을 이 코어에 귀속시킨다. 두 영역의 기존 작업을 + 내부 연결로 실행하고 외부 요청도 같은 앱 의미로 접수한다. 사용자에게 수명 관리를 떠넘기지 않는다. +3. 한 영역의 포화·정상 종료·재생성과 이전 완료 처리에서 다른 영역의 관리 상태를 보존하고, + 호스트의 자원 부족·입출력 실패가 Pure 결과로 정확히 전달되는지 확인한다. +4. 호스트에서 집행할 수 있는 격리와 보안 원격 경로를 확장한다. 각 기능의 검증 범위와 + 호스트 제약을 기록하며 관리 상태 분리를 네이티브 크래시 격리로 해석하지 않는다. + +첫 구현에 클러스터 스케줄러, 게스트 OS, 범용 바이트코드 VM, 자동 복제·라이브 이동을 추가하지 않는다. +고정 바이트 예산과 처리량 목표는 대상 작업·기기·보장을 확정한 뒤 결정한다. diff --git a/docs/design/abi-v0.h b/docs/design/abi-v0.h new file mode 100644 index 0000000..dc53310 --- /dev/null +++ b/docs/design/abi-v0.h @@ -0,0 +1,76 @@ +/* Draft data records for Pure's in-process byte-stream profile (0.1). + * No module loader or callable function table is implemented by this header. + * These records use native endianness and are NOT a file/wire format. + */ +#ifndef PURE_DRAFT_ABI_V0_H +#define PURE_DRAFT_ABI_V0_H + +#include +#include + +#define PURE_ABI_MAJOR_V0 UINT32_C(0) +#define PURE_ABI_MINOR_V0 UINT32_C(1) +#define PURE_PROFILE_BYTE_STREAM_V0 UINT32_C(1) + +#define PURE_STEP_MORE_V0 UINT32_C(0) +#define PURE_STEP_NEED_INPUT_V0 UINT32_C(1) +#define PURE_STEP_NEED_OUTPUT_V0 UINT32_C(2) +#define PURE_STEP_DONE_V0 UINT32_C(3) +#define PURE_STEP_FAILED_V0 UINT32_C(4) +#define PURE_STEP_CANCELLED_V0 UINT32_C(5) + +#define PURE_ERROR_NONE_V0 UINT32_C(0) +#define PURE_ERROR_INVALID_V0 UINT32_C(1) +#define PURE_ERROR_LIMIT_V0 UINT32_C(2) +#define PURE_ERROR_UNSUPPORTED_V0 UINT32_C(3) +#define PURE_ERROR_BACKEND_V0 UINT32_C(4) +#define PURE_ERROR_TRUNCATED_V0 UINT32_C(5) +#define PURE_ERROR_AUTH_V0 UINT32_C(6) + +typedef struct pure_handle_v0 { + uint32_t slot; + uint32_t generation; +} pure_handle_v0; + +typedef struct pure_span_v0 { + pure_handle_v0 buffer; + uint32_t offset; + uint32_t length; +} pure_span_v0; + +typedef struct pure_step_result_v0 { + uint32_t struct_bytes; + uint32_t status; + uint32_t error; + uint32_t consumed; + uint32_t produced; + uint32_t reserved; +} pure_step_result_v0; + +typedef struct pure_stream_limits_v0 { + uint32_t struct_bytes; + uint32_t abi_major; + uint32_t abi_minor; + uint32_t profile; + uint32_t flags; + uint32_t input_count; + uint32_t output_count; + uint32_t max_pending_per_context; + uint64_t context_budget_bytes; + uint64_t scratch_budget_bytes; + uint32_t max_input_bytes; + uint32_t max_output_bytes; + uint32_t buffer_alignment; + uint32_t reserved; +} pure_stream_limits_v0; + +_Static_assert(sizeof(pure_handle_v0) == 8, "handle record size"); +_Static_assert(sizeof(pure_span_v0) == 16, "span record size"); +_Static_assert(offsetof(pure_span_v0, offset) == 8, "span offset field"); +_Static_assert(sizeof(pure_step_result_v0) == 24, "step result size"); +_Static_assert(offsetof(pure_step_result_v0, produced) == 16, "produced field"); +_Static_assert(sizeof(pure_stream_limits_v0) == 64, "stream limits size"); +_Static_assert(offsetof(pure_stream_limits_v0, context_budget_bytes) == 32, "context budget field"); +_Static_assert(offsetof(pure_stream_limits_v0, max_input_bytes) == 48, "input bound field"); + +#endif diff --git a/docs/design/check-abi-layout.py b/docs/design/check-abi-layout.py new file mode 100644 index 0000000..7832aff --- /dev/null +++ b/docs/design/check-abi-layout.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Compile draft record assertions only; does not test a callable module ABI.""" +import argparse +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +root = Path(__file__).resolve().parent +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--zig', default='zig') +parser.add_argument('--output', type=Path) +args = parser.parse_args() +zig = shutil.which(args.zig) +if not zig: + parser.error('Zig 0.15.2 required; use --zig /path/to/zig') +version = subprocess.check_output([zig, 'version'], text=True).strip() +if version != '0.15.2': + parser.error(f'This check is pinned to Zig 0.15.2; found {version}') +header = root / 'abi-v0.h' +checks = [] +with tempfile.TemporaryDirectory(prefix='pure-abi-layout-') as cache: + env = dict(os.environ, ZIG_GLOBAL_CACHE_DIR=cache) + for target in ['aarch64-linux-gnu', 'aarch64-macos-none', 'wasm32-freestanding']: + command = [zig, 'cc', '-target', target, '-std=c17', '-ffreestanding', + '-Wall', '-Wextra', '-Werror', '-x', 'c', '-c', str(header), + '-o', str(Path(cache) / f'{target}.o')] + result = subprocess.run(command, env=env, capture_output=True, text=True) + checks.append({'target': target, 'command': command, 'exitCode': result.returncode, + 'stdout': result.stdout, 'stderr': result.stderr}) +report = {'recordedUtc': datetime.now(timezone.utc).isoformat(), 'zigVersion': version, + 'scope': 'Cross-compilation of C17 record sizes/offset assertions only. No target execution, function calls, module loading, memory enforcement, or performance measurement.', + 'headerSha256': hashlib.sha256(header.read_bytes()).hexdigest(), + 'checkerSha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'assertionsPerTarget': 8, 'checks': checks, + 'passed': all(check['exitCode'] == 0 for check in checks)} +encoded = json.dumps(report, indent=2) + '\n' +if args.output: + with args.output.open('x') as stream: + stream.write(encoded) +print(encoded, end='') +raise SystemExit(0 if report['passed'] else 1) diff --git a/docs/design/validation/2026-09-22-record-layout.json b/docs/design/validation/2026-09-22-record-layout.json new file mode 100644 index 0000000..7158525 --- /dev/null +++ b/docs/design/validation/2026-09-22-record-layout.json @@ -0,0 +1,80 @@ +{ + "recordedUtc": "2026-09-22T11:52:02.452411+00:00", + "zigVersion": "0.15.2", + "scope": "Cross-compilation of C17 record sizes/offset assertions only. No target execution, function calls, module loading, memory enforcement, or performance measurement.", + "headerSha256": "bbdd54e9b55df26c654a6f5fed2f16cd6f90aeca2d17f88f42774beec214e637", + "checkerSha256": "07bd674d124b74e7926dab8ada525b570c727bbaf94a5c17d98e8438b8ada56a", + "assertionsPerTarget": 8, + "checks": [ + { + "target": "aarch64-linux-gnu", + "command": [ + "/tmp/pure-zig-0.15.2/zig", + "cc", + "-target", + "aarch64-linux-gnu", + "-std=c17", + "-ffreestanding", + "-Wall", + "-Wextra", + "-Werror", + "-x", + "c", + "-c", + "/home/eunhhu/work/pure/docs/design/abi-v0.h", + "-o", + "/tmp/pure-abi-layout-w__551sp/aarch64-linux-gnu.o" + ], + "exitCode": 0, + "stdout": "", + "stderr": "" + }, + { + "target": "aarch64-macos-none", + "command": [ + "/tmp/pure-zig-0.15.2/zig", + "cc", + "-target", + "aarch64-macos-none", + "-std=c17", + "-ffreestanding", + "-Wall", + "-Wextra", + "-Werror", + "-x", + "c", + "-c", + "/home/eunhhu/work/pure/docs/design/abi-v0.h", + "-o", + "/tmp/pure-abi-layout-w__551sp/aarch64-macos-none.o" + ], + "exitCode": 0, + "stdout": "", + "stderr": "" + }, + { + "target": "wasm32-freestanding", + "command": [ + "/tmp/pure-zig-0.15.2/zig", + "cc", + "-target", + "wasm32-freestanding", + "-std=c17", + "-ffreestanding", + "-Wall", + "-Wextra", + "-Werror", + "-x", + "c", + "-c", + "/home/eunhhu/work/pure/docs/design/abi-v0.h", + "-o", + "/tmp/pure-abi-layout-w__551sp/wasm32-freestanding.o" + ], + "exitCode": 0, + "stdout": "", + "stderr": "" + } + ], + "passed": true +} diff --git a/docs/research/000-working-method.md b/docs/research/000-working-method.md new file mode 100644 index 0000000..7648fba --- /dev/null +++ b/docs/research/000-working-method.md @@ -0,0 +1,163 @@ +# Pure의 협업 방식 + +2026-09-22 사용자 요청에 따라 정한 역할 분담이다. + +Codex는 문헌 조사, 선행연구와의 차이 검토, 가설·실험 설계, 비교 구현, +정확성 검사, 측정, 결과 해석과 보고서를 맡는다. 일상적인 연구와 되돌릴 수 +있는 구현 선택은 스스로 진행한다. 추측과 측정 결과를 구분하고, 잘 안 된 +결과도 남긴다. + +사용자는 제품의 목적과 취향, 실제 사용·제작 경험을 제공한다. 이론이나 +저수준 구현을 먼저 이해해야 의견을 낼 수 있도록 요구하지 않는다. Codex는 +사용자의 관찰을 재현 가능한 작업과 기술적 검증 질문으로 바꾼다. + +각 연구 보고서의 앞부분에는 다음을 쉬운 말로 쓴다. + +- 이번에 알아낸 것과 아직 모르는 것. +- 사용 경험에 어떤 차이가 생길 수 있는지. +- 근거가 문헌, 계산, 실제 측정 중 무엇인지. +- 구현체에서 사용자가 직접 확인할 구체적인 동작. +- 다음 결정과 그 이유. + +사용자 피드백은 "하려던 일 / 실제로 일어난 일 / 기대했던 일" 정도로 +충분하다. 원인 분석과 해결책 설계는 Codex가 맡는다. 한 사람의 사용 경험을 +전체 사용자의 선호로 일반화하지 않는다. + +2026-09-22 제작 경험 기준 보완: 이미 만든 데모를 조작한 경험을 개발 경험으로 +기록하지 않는다. 사용자가 빈 작업물에서 대상·상태·입력·반응을 정의하고 바꿀 수 +있어야 한다. 새 동작마다 Codex가 실행기 내부를 수정해야 한다면 표현 도구의 +개발 경험을 검증한 것으로 보지 않는다. 자동화된 작성 흐름 검사도 실제 사용자의 +이해·학습·제작 시간 평가를 대신하지 않는다. + +프로젝트의 가치와 기능 우선순위가 크게 달라지는 선택은 대안과 근거를 +정리해 사용자에게 제시한다. 기술적 결정을 사용자에게 떠넘기는 형태로 +논문이나 전문 용어 중 하나를 선택하도록 요구하지 않는다. + +## 시간을 쓰는 기준 + +2026-09-22 사용자 요청에 따라, 작업량보다 **다음 결정을 바꾸는 근거**를 우선한다. +새 작업을 시작하기 전에 다음 네 가지를 짧게 적는다. 별도 보고서는 필요하지 않다. + +- 바꿀 결정: 결과에 따라 어떤 구현·채택·중단 결정이 달라지는가? +- 남은 질문: 기존 문헌·코드·측정으로 아직 답하지 못한 것은 무엇인가? +- 최소 확인: 그 질문을 구분할 가장 작은 조사·계산·실험은 무엇인가? +- 중단 기준: 어떤 결과면 채택·기각하거나 추가 작업을 멈추는가? + +기존 문서·공개 구현과 비용 계산으로 판단할 수 있으면 재구현하지 않는다. +실험이 필요하면 대표 조건 하나와 적절한 기존 해법부터 비교한다. 잔여 비용이 +확인될 때만 입력 행렬·의존성·비교 대상을 늘린다. 특정 후보가 이기는 조건을 +찾으려고 실험을 계속 추가하지 않는다. 필요한 정확성 검증은 유지하되, 변경이나 +새 우려가 없으면 이미 통과한 무거운 검사를 반복하지 않는다. + +새 문법·컴파일러·ABI·커널 우회·범용 기반 기능은 실제 작업에서 필요성이 +확인될 때 구현한다. 앞으로 쓸 수도 있다는 이유만으로 먼저 만들지 않는다. +기존 해법으로 충분하면 재사용하거나 후보를 닫는다. 차별화가 없다는 결론도 +유효한 결과지만, 같은 결론을 반복 확인하는 것은 새 연구 성과로 세지 않는다. + +[007](007-buffer-retention-results.md)·[008](008-json-fanout-results.md)의 공유·복사 +정책 비교는 현재 증거에서 종료한다. 구체적인 새 작업이나 요구 조건이 생길 때만 +재개한다. 다음 후보 P07/P08도 자동으로 비교 구현에 들어가지 않는다. 기존 증분 +해법으로 해결되지 않는 구체적인 비용과 Pure가 바꿀 수 있는 경로를 먼저 확인한다. +그 차이를 설명할 수 없으면 구현을 시작하지 않는다. + +보고는 결정·근거·남은 한계를 중심으로 짧게 한다. 원자료와 재현 방법은 보존하고, +작업 기준을 충족하지 못한 후보마다 새 데모나 긴 보고서를 만들지 않는다. + +## 자체 연구와 기술 구현의 우선순위 + +2026-09-22 추가 요청: 이미 알려진 방법의 재현을 주된 작업으로 삼지 않는다. +Pure의 요구에서 가설·실행 계약을 만들고 직접 구현하는 데 우선적으로 시간을 쓴다. +선행연구는 같은 발상을 반복하거나 성과를 과장하지 않기 위한 짧은 중복 확인과 +비교 대상으로 사용한다. 기존 기술을 배제하거나 새 이름으로 포장하라는 뜻으로 +해석하지 않는다. 직접 작성한 구현과 학술적인 신규성은 별도로 판단한다. + +첫 작업은 [기록을 버릴 수 있는 실행 코어](../../prototypes/forgetful-core/README.md)다. +값·추적 기록의 손실, 조건부 의존성, 작업 한도 초과를 하나의 작은 실행 계약으로 +구현했다. 이 계약 확인을 마친 뒤 같은 현상을 재현하는 실험을 늘리지 않는다. +제품 채택은 실제 작업의 비용과 기능을 근거로 결정한다. + +이 문서는 현재 작업에서의 역할 분담을 기록한다. 지속적인 백그라운드 실행이나 +정기 보고를 설정한 것은 아니다. + +## 목적과 아키텍처에 대한 사용자 정정 + +2026-09-23: 주 목적은 효율적인 인터페이스이며, 그 아래의 불필요한 추상화를 최대한 제거한다. +기존 앱·런타임·프레임워크의 경계와 개념을 필수 전제로 삼지 않는다. +입력에서 실제 출력까지의 의미와 비용에서 설계를 시작한다. 현재 DSL·VM·캐시도 교체 가능하다. +대용량 데이터·서비스·UI 작업은 전체 애플리케이션 런타임의 실용성과 규모를 검증한다. +첫 검증 작업의 범위가 제품 전체의 정의를 대신하지 않는다. +사용자에게 필요한 표현력·조작·제작 경험과 메모리·계산 비용을 함께 검증한다. +세부 로드맵은 [010](010-roadmap.md)을 따른다. 이전 CLI 우선 추천안은 이 정정으로 대체한다. + +같은 날 핵심 관점 보완: net·RPC·DB·UI는 입출력과 내부 연산을 연결하는 서로 다른 영역이다. +Pure의 중심은 이들 사이에서 무엇을 어디에 연결하고, 어떤 데이터 표현·수명·실행 시점으로 +연결을 효율적으로 수행할지다. 기능 목록이나 공통 API의 수가 연구 성과를 대신하지 않는다. +한 작업의 전체 경로에서 제거한 변환·복사·보관·대기와 그 대가를 확인한다. 이 목표를 현재 +구현으로 이미 해결한 성능 결과와 구분한다. 연결 모델을 특정 그래프·이벤트 버스 구현으로 +먼저 고정하지 않는다. + +참고 대화 후 보완: 개발자가 데이터·연산·소비 관계를 작성하면, 준비 단계와 런타임이 그 +관계에서 불필요한 변환·전달·보관을 제거할 수 있어야 한다. 공통 API나 연결부 수의 증가만으로 +이 목표를 충족했다고 판단하지 않는다. 고정 연결은 준비 단계에서 줄이고 실제 동적 관계에 +필요한 관리 비용을 둔다. 출력과 피드백은 연결 목적지로 표현할 수 있으나 시간 진행·효과의 +재실행 의미를 없애지 않는다. 개발자에게 수명·변환 관리를 떠넘겨 기계 비용만 줄이는 것도 +효율의 완성으로 보지 않는다. 최초의 제한된 작성·준비 구현은 [020](020-prepared-connections.md)이다. + +같은 날 저장 모델 정정: **SQL을 사용하지 않는다. RAM·storage를 SQL이나 관계형 스키마에 +맞추지 않는다.** DB라는 범위 이름은 데이터 접근·갱신·영속성을 다룬다는 뜻이다. 이를 +SQL 엔진 채택으로 해석한 것은 Codex의 오류다. 데이터 배치는 실제 소비 범위·연산·갱신· +수명에서 결정한다. SQL을 자체 질의 언어로 바꾸거나 모든 데이터를 하나의 범용 객체로 +포장하는 것도 자동 해법으로 삼지 않는다. 바이트 접근과 타입·의미 검증의 책임은 구분하며, +SQL 제거만으로 성능 우위를 주장하지 않는다. 018의 SQLite 경로는 역사적 검증 자료로 +보존하고 활성 구현은 [019](019-byte-storage.md)의 직접 바이트 저장으로 대체했다. + +## 독립 네이티브 애플리케이션 런타임과 표준 호환성 + +2026-09-23 추가 정정: **Pure 런타임 자체를 작은 OS로 설계한다.** 실행·메모리·보호·자원· +저장·통신의 운영 모델을 Pure가 소유한다. 현재 OS의 프로세스는 구동 환경이 부과하는 +제약이며 Pure의 기본 개념이나 설계 중심이 아니다. 이를 프로세스 배치·관리 계층으로 +축소하지 않는다. 기존 OS 위에서 개발하더라도 정책은 Pure 코어에, 환경별 수단과 한계는 +플랫폼 연결부에 둔다. UI는 이 작은 OS의 하위 체계다. 현재 프로토타입을 통합 OS 코어로 +구현 완료했다고 보고하지 않는다. [구체적인 경계와 다음 구현](../design/007-execution-domains.md). + +같은 정정의 보완: 외부 통신을 포기하지 않는다. Pure를 사용하지 않는 서비스·런타임· +프로그램과도 양방향으로 통신하고, 외부가 요구하는 프로토콜·형식을 접점에서 지원한다. +Pure끼리의 자체 통신 형식을 모든 상대에게 강제하는 폐쇄 구조로 해석하지 않는다. + +같은 정정의 크기 제약: **Pure는 마이크로 런타임이어야 한다.** 작은 OS라는 표현을 모든 +서비스의 기본 탑재나 코어 비대화의 근거로 삼지 않는다. 필요한 실행·소유권·자원·완료 규칙만 +공통 코어에 두고 나머지는 앱별 선택 구성으로 결합한다. 코드·의존성·유휴 메모리·영역 및 +작업별 관리 비용까지 확인한다. 동적 로더나 범용 서비스 관리자를 선행 구현하지 않는다. + +2026-09-23 사용자 정정: 저장·실행 표현과 미디어 포맷은 새로 설계해도 되지만, +구동 환경과 웹 호환성을 버리지 않는다. 표준 입출력을 유지하는 것과 내부 표현을 +그대로 따르는 것은 별개다. 변환 과정의 복사·동시 보관·품질 비용까지 포함해 판단한다. +현재 SVG 도형 예제나 작은 Wasm 예약 공간을 UI 전체의 메모리 효율로 해석하지 않는다. + +같은 날 추가 정정: **목표는 독립 네이티브 애플리케이션 런타임이며 UI는 그중 일부다. +앱 로직·net·RPC·DB와 영속성을 포함한다.** Codex가 웹 호환성을 브라우저에 실행을 맡기는 +것으로 해석하고, 이어 제품 전체를 UI 런타임으로 한정한 것은 잘못이다. 브라우저 편집기의 +동작 확인은 보존하되 그 실험으로 전체 아키텍처의 범위나 효율을 판단하지 않는다. + +공통 기반은 작업 실행·스케줄링, 데이터·자원의 소유권과 수명, 예산, 비동기 완료·취소·오류와 +과부하 처리의 계약을 다룬다. UI 없는 서버·CLI도 사용할 수 있어야 한다. net/RPC/DB를 +UI 완성 뒤의 부가 기능으로 미루지 않는다. 원격 호출·DB 트랜잭션·UI 조작의 서로 다른 +의미는 보존하며, 공통 API만으로 외부 효과까지 원자적으로 되돌리거나 중복을 없앤다고 약속하지 않는다. + +UI 하위 체계는 배치·텍스트·입력 대상 판정·포커스·조작·렌더링을 책임진다. 플랫폼 연결부는 +파일·소켓·시계·실행 자원뿐 아니라 창·장치 입력·IME·접근성·그래픽·글꼴 서비스를 연결한다. +전문 라이브러리·저장·프로토콜 구현은 위의 내부 모델을 강제하지 않는 범위에서 재사용할 수 있다. 모든 기능을 현재 숫자 VM에 +넣거나 모든 엔진을 직접 작성하는 것은 별도 결정이며 전체 범위를 지원한다는 말과 구분한다. +기능별로 지원하는 계약·검증한 범위·남은 의존성을 명시한다. + +웹은 호환 대상이자 선택 가능한 호스트다. 브라우저 없이 동작하는 경로를 첫 구현 기준에 둔다. +웹의 유연한 작성·연결·갱신 경험을 목표로 삼되, HTML/CSS/DOM의 전면 호환이나 새 브라우저 +엔진 제작을 사용자 요구로 추가하지 않는다. 표준 미디어·문서는 입출력 경계에서 지원하고, +새 내부 표현·포맷의 채택은 필요한 기능과 전체 비용으로 판단한다. + +다음 애플리케이션 작업의 착수 기준: + +- 바꿀 결정: 기존 실행 코어와 연산·수명 초안 중 무엇을 재사용하고, UI·RPC·DB가 공유할 계약을 어디까지 둘 것인가? +- 남은 질문: 비동기 요청과 저장을 연결하면서 중복 데이터·큐·변환을 줄이고 완료·취소·커밋의 의미를 유지할 수 있는가? +- 최소 확인: 기존 작업물의 변경·저장을 로컬 조작과 실제 네트워크 RPC에서 실행하고 DB 결과를 응답·표시에 반영한다. UI 없는 경로도 유지한다. +- 중단 기준: 한 작업의 수명·실패 경계가 확인되면 범용 모듈을 미리 늘리지 않는다. 의미를 잃는 공통화는 수정하고 다음 실제 기능으로 진행한다. 같은 도형·버퍼 실험을 반복하지 않는다. diff --git a/docs/research/001-research-brief.md b/docs/research/001-research-brief.md new file mode 100644 index 0000000..947f10e --- /dev/null +++ b/docs/research/001-research-brief.md @@ -0,0 +1,166 @@ +# Pure: 연구 의제와 첫 검증 설계 + +작성일: 2026-09-22 + +상태: **초기 문헌 조사에 근거한 제안**. 프로젝트의 확정 아키텍처가 아니다. 아래 가설의 신규성과 성능은 아직 검증하지 않았다. 이 초안 작성 시점에는 벤치마크를 실행하지 않았다. + +후속 기록: [첫 합성 실험 결과](002-checkpoint-results.md)와 [협업 방식](000-working-method.md)을 추가했다. 첫 실험은 고정된 계산 사슬에서 보관 간격을 비교하며, 아래의 전체 실행 모델이나 적응형 정책을 구현한 것은 아니다. + +후속 범위 보완: [제작 실험](004-authoring.md)의 사용자 피드백 이후 연산 의미·데이터 수명·모듈 계약을 +먼저 명세하기로 합의했다. 압축·암호화·문서·미디어·OS라는 장기 범위와 현재 구현의 경계는 +[기반 계약 보고서](005-foundation-contract.md)에 기록했다. 아래 초기 가설을 검증 완료로 바꾸는 내용은 아니다. + +## 목표와 연구의 관계 + +2026-09-23 방향 보완: 인터페이스 아래의 불필요한 추상화를 제거하는 것이 중심이다. +아래의 앱 모델·계산 그래프·실행기 구분은 고정 아키텍처가 아니다. +입력부터 출력까지의 표현·전달·메모리 비용을 기준으로 경계 자체를 재설계할 수 있다. +최신 작업 순서는 [010 로드맵](010-roadmap.md)에 기록한다. + +같은 날 범위 정정: Pure는 독립 네이티브 **애플리케이션 런타임**이다. 앱 로직·네트워크·RPC· +DB와 영속성·UI를 다루며, UI는 전체 범위의 일부다. 아래 UI 중심 가설은 한 연구 대상으로 +한정한다. 화면 없는 서버·CLI도 같은 실행 기반을 사용할 수 있어야 한다. + +Pure의 장기 목표는 적은 메모리로 실행되며, 입력과 출력을 유연하게 연결하고, 실행 중에 동작을 관찰·수정할 수 있는 애플리케이션 제작 환경이다. GUI와 터미널에서 같은 작업물을 다루고, 플랫폼이 바뀌어도 작업물의 의미를 보존한다. + +이 목표에는 실행 효율, 표현력, 제작 경험이라는 서로 다른 검증 대상이 있다. 실행기가 가벼워졌다는 결과만으로 제작 경험까지 좋아졌다고 주장하지 않는다. 각각 실험하되 같은 앱 모델과 변경 연산으로 연결한다. + +첫 연구 기여의 후보는 **편집 가능한 상호작용 프로그램에서, 관리 정보까지 포함한 메모리 예산과 응답 지연을 함께 고려하는 정보 보관·재계산 정책**이다. 기존 연구의 적용과 새로운 기여의 경계는 추가 조사와 실험으로 판단한다. + +## ByteDance 사례에서 확인한 방법 + +Sonic의 설계 문서는 당시 서비스 프로파일에서 JSON 직렬화·역직렬화가 상당한 CPU 비용을 차지했다고 보고한다. 여러 기존 구현을 비교한 뒤, 타입별 처리 코드의 특수화, SIMD와 스칼라 처리의 선택, 지연 파싱 등을 병목에 대응시킨다. 이 문서에 담긴 과거 Go 구현의 제약을 현재 모든 환경의 사실로 일반화하면 안 된다. [Sonic 설계 문서](https://github.com/bytedance/sonic/blob/main/docs/INTRODUCTION.md) + +Sonic의 사용 안내는 입력 문자열을 참조하는 방식이 원본 버퍼의 수명을 늘려 상주 데이터량을 증가시킬 수 있다고 설명한다. 따라서 복사량 감소와 메모리 감소는 따로 검증해야 한다. 호환 설정에 따라 정렬·이스케이프 등의 동작도 달라지므로 비교 실험에서는 동일한 의미를 맞춰야 한다. [Sonic README](https://github.com/bytedance/sonic#tips) + +Monoio는 작업이 스레드 사이로 이동하지 않는 실행 모델을 채택하며, 부하가 불균형하면 성능이 떨어질 수 있다는 제한도 명시한다. 특정 실행 조건을 이용해 비용을 줄이고 그 조건이 깨지는 경우까지 보고하는 사례다. [Monoio 설계 목표와 제한](https://github.com/monoio-rs/monoio) + +Pure가 채택할 연구 절차는 다음과 같다. + +1. 실제 작업에서 비용이 큰 위치를 찾는다. +2. 기존 구현과 논문이 이미 해결한 범위, 남는 비용, 성립 조건을 기록한다. +3. 바꿀 표현 또는 알고리즘과 예상 효과를 설명한다. +4. 정확성을 유지한 비교와 구성 요소별 실험으로 효과의 원인을 분리한다. +5. 다른 사람이 재실행할 수 있는 코드·입력·설정·원시 결과를 남긴다. + +## 초기 선행연구 지도 + +| 연구 | 이번에 확인한 내용 | Pure에서 확인할 질문 | +|---|---|---| +| [Self: Dynamic Deoptimization, PLDI 1992](https://bibliography.selflanguage.org/dynamic-deoptimization.html) | 최적화된 실행과 소스 수준 디버깅·실행 중 변경을 연결한다. 공개 논문은 이를 위한 의존성·디버깅 정보의 공간 비용도 다룬다. | 실행과 편집에 필요한 정보 중 무엇을 상주시킬 것인가? 필요할 때 재구성하면 편집 지연은 얼마나 생기는가? | +| [Memory Management for Self-Adjusting Computation, ISMM 2008](https://matthewhammer.org/ismm08/ismm08.pdf) | 변경 대응을 위해 유지하는 실행 기록과 메모리 회수 비용을 직접 연구한다. | 실행 기록이 많은 상황에서 일반적인 메모리 관리가 지불하는 비용은 무엇인가? | +| [Adapton, PLDI 2014](https://www.cs.umd.edu/~mwh/papers/hammer13adapton.html) | 출력의 필요 여부에 따라 재계산하고 기존 계산을 재사용하는 모델이다. | 전체 재계산보다 추적·무효화·보관 비용이 더 커지는 경계는 어디인가? | +| [Dynamic Tensor Rematerialization, ICLR 2021](https://arxiv.org/abs/2006.09616v4) | 제한된 메모리에서 중간 텐서를 버리고 필요할 때 다시 계산하는 온라인 정책을 제시한다. | 계산이 짧고 값이 작은 상호작용 프로그램에서도 정책·메타데이터 비용을 회수할 수 있는가? | +| [Simply RaTT, ICFP 2019](https://arxiv.org/abs/1903.05879) | 신호·이벤트를 표현하면서 의도치 않은 공간·시간 누수를 배제하는 타입 및 실행 규칙을 연구한다. | Pure의 입력·상태 모델에서 이전 상태가 불필요하게 유지되는 것을 어떤 규칙으로 방지할 것인가? | + +확인 범위: 공식 설계 설명, 저자·프로젝트의 초록, 공개 논문의 관련 부분이다. 모든 논문을 정독하거나 실험을 재현한 상태는 아니다. 후속 연구를 망라한 체계적 문헌 조사도 아직 아니다. + +새로운 기여라고 주장할 수 없는 아이디어는 이미 분명하다. 그래프로 계산을 표현하는 것, 변경된 부분만 재계산하는 것, 필요할 때 값을 재구성하는 것, 최적화된 프로그램을 실행 중 수정하는 것 자체에는 선행연구가 있다. Pure의 차별성은 적용 조건, 비용 모델, 표현, 알고리즘, 보장 또는 검증 결과에서 구체화해야 한다. + +## 우선 연구 질문 + +> 실행 중에 연결과 규칙을 수정할 수 있는 상호작용 프로그램에서, 응답 지연을 제한하면서 관리 정보까지 포함한 최대 메모리를 얼마나 줄일 수 있는가? + +일반적인 계산 그래프는 재계산을 줄이기 위해 값, 의존 관계, 변경 여부를 보관한다. 값이 몇 바이트에 불과하고 계산도 짧으면 그 관리 비용이 이득보다 클 수 있다. 이 문장은 Pure가 측정할 가설이다. 아직 Pure에서 확인한 현상이 아니다. + +초기 후보 정책은 작은 계산 묶음마다 다음 두 방식 중 하나를 선택한다. + +- 중간 결과와 변경 정보를 보관해 필요한 부분만 갱신한다. +- 중간 결과와 세부 추적 정보를 줄이고, 해당 묶음이 필요할 때 다시 계산한다. + +앱의 정의와 현재 상태는 보존한다. 제거한 의존성 추적 정보가 있으면, 정의를 순회하거나 더 큰 묶음을 무효화하는 비용이 생긴다. 이 비용까지 포함해 비교한다. + +초기 가설 H1: 변경이 드문 구역과 매번 바뀌는 구역을 구분하는 정책이, 전체 재계산과 세밀한 전체 추적 사이에서 더 유리한 메모리·지연 조합을 제공할 수 있다. + +귀무 가설 H0: 작은 값과 짧은 연산에서는 정책과 관리 정보의 비용 때문에, 잘 구현한 고정 정책보다 이득이 없다. + +단순한 묶음별 고정 정책으로 대부분의 이득이 나온다면 그것을 우선 채택한다. 자동 적응이 필요한지는 별도로 입증한다. + +## 첫 실험이 다룰 의미 + +첫 실험은 단일 스레드의 제한된 실행 모델로 시작한다. 이것은 제품 전체의 표현력이나 동시성에 대한 최종 결정이 아니다. + +- 입력은 순서와 시간이 명시된 기록으로 주어진다. 이벤트, 상태 샘플, 시간 진행을 구분한다. +- 한 처리 단계 안의 순수 계산은 비순환 구조로 제한한다. 피드백은 다음 단계로 넘기는 명시적인 상태를 통한다. +- 외부 입출력과 현재의 변경 가능한 상태를 재계산 가능한 임시 값으로 취급하지 않는다. 재계산으로 출력이 두 번 발생해서는 안 된다. +- 규칙 변경은 처리 단계 사이의 경계에서 적용한다. 초기에는 값·상수·연결·같은 타입의 연산 교체를 지원한다. 타입이나 상태 구조 변경은 명시적으로 거절한다. +- 같은 입력·시간·규칙 변경 기록에 대해 기준 실행기와 같은 상태 및 출력 기록을 생성해야 한다. 부동소수점 비교 규칙과 연산 순서는 사전에 고정한다. +- 처리할 수 있는 모델과 입력 큐의 용량을 명시한다. 예산을 넘으면 명시적인 거절 또는 미리 정의한 역압력으로 처리한다. 조용히 이벤트를 버린 결과를 최적화로 세지 않는다. + +모델 정의와 필수 상태만으로 예산을 넘는 입력은 지원 가능하다고 주장할 수 없다. 추적 정보나 중간 결과를 줄이는 것으로 모든 프로그램을 일정한 메모리에 넣을 수 있는 것은 아니다. + +## 비교 구현과 작업 부하 + +| 구현 | 역할 | +|---|---| +| A: 연속 배열 기반 전체 재계산 | 단순하고 최적화하기 쉬운 기준선. 관측 대상에 필요한 구역을 정상적으로 선별한다. | +| B: 세밀한 의존성 추적과 결과 보관 | 변경 전파를 활용하는 기준선. 불필요한 객체 할당으로 일부러 느리게 만들지 않는다. | +| C: 묶음별 보관·재계산 선택 | H1을 검증하는 후보. 정책 자체의 시간과 메모리를 포함한다. | +| R: 작업에 맞춰 직접 작성한 네이티브 로직 | 같은 계산의 실용적인 성능 참고점. 라이브 편집 기능이 같지 않다면 별도 표기하며, 수학적인 성능 하한으로 부르지 않는다. | + +A/B/C는 같은 언어, 연산 구현, 상태·출력 의미, 오류 처리와 편집 기능을 사용한다. 주 비교에서 바꾸는 것은 계산·보관 정책이다. 기존 증분 계산 라이브러리와의 비교도 필요하지만, 언어·GC·표현이 함께 달라지는 비교와 정책만 달라지는 비교는 별도로 보고한다. + +작업 부하는 세 종류로 정한다. + +1. 센서와 조건부 입력 연결: 값은 자주 들어오지만 일부 출력만 필요해지는 상황. +2. 목록·대시보드: 일부 항목 수정, 필터 변경, 보이는 영역 전환, 다수 항목의 일괄 갱신. +3. 애니메이션·작은 게임: 많은 상태가 매 처리 단계에서 바뀌는 상황. 앞서 이야기한 공룡 게임은 이 범주의 실사용 검증 사례다. + +노드 수만 늘리는 실험으로 끝내지 않는다. 변경 비율, 출력 요구 비율, 연결의 밀도, 연산 비용, 값의 크기, 편집 빈도를 독립적으로 바꾼다. 긴 연결, 넓은 전파, 빈번한 모드 전환, 느린 출력 소비자를 포함한다. 합성 입력과 실제 조작에서 얻은 입력 기록을 구분한다. + +정책을 조정하는 입력과 최종 평가 입력을 분리한다. 개발 중 유리했던 입력만 최종 결과에 남기지 않는다. + +## 측정 계약 + +핵심 목표는 메모리와 지연의 관계를 측정하는 것이다. 같은 메모리 예산에서의 지연, 같은 지연 한도에서의 메모리를 각각 비교한다. 목표 한도와 유의미한 개선 폭은 기준선과 측정 노이즈를 확인한 뒤, 후보 정책의 최종 평가 전에 고정한다. + +| 대상 | 기록할 값 | +|---|---| +| 코어 메모리 | 모델, 현재 상태, 중간 결과, 의존성·정책 정보, 입력·출력 큐, 임시 공간의 사용량과 확보 용량, 피크 | +| 장시간 보관 | 시간에 따른 살아 있는 바이트 수, 원본 버퍼를 붙잡는 참조의 수명, 가능한 경우 바이트×시간의 누적량 | +| 지연 | 입력 도착부터 결과까지의 P50/P95/P99, 최악 관측값, 규칙 변경의 적용 지연 | +| 실행 비용 | 실행한 연산 수, 추적·무효화·정책 선택·재계산에 쓴 시간, 할당량과 계측한 복사량 | +| 시스템 전체 | 바이너리·코드 크기, 프로세스 메모리, 편집 도구 메모리, 그래픽 자원, 시작 비용 | + +코어가 관리하는 바이트와 OS가 보고하는 상주 메모리는 서로 다른 측정값이다. CPU/GPU가 같은 메모리를 공유하는 환경에서는 같은 자원을 두 번 더하지 않는다. 실행 전용, 편집기 연결, 반복 편집 중의 결과를 각각 남긴다. 편집용 정보를 다른 프로세스나 디스크로 옮겼다면 그 공간과 다시 가져오는 시간도 기록한다. + +수동으로 감싼 복사 함수의 카운터를 모든 기계어 메모리 이동의 추적으로 표현하지 않는다. 계측 도구 자체의 메모리와 시간도 확인한다. + +반응 지연을 측정할 때 입력 발생 시점을 기록해 큐 대기 시간을 포함한다. 느린 실행기에 맞춰 입력 생성까지 느려지는 실험만 하면 실제 지연이 가려질 수 있다. 완료·거절·누락된 작업을 모두 집계한다. 백분위수는 충분한 표본과 반복 실행으로 보고하며 실시간 보장으로 과장하지 않는다. + +실험에 사용한 저장소 SHA, 컴파일러 버전, 최적화 설정, OS, CPU, 전원 상태, 입력 시드, 준비 구간, 반복 횟수를 저장한다. 초기 실행과 준비된 상태를 따로 보고하고 측정 순서를 섞어 열·순서 영향을 확인한다. + +사용자의 M3 macOS를 첫 실측 대상으로 제안한다. 현재 문서 작성 환경에서 M3 성능을 측정한 것은 아니다. 다른 ISA·OS와 임베디드에 대한 주장은 해당 타깃에서 확인한 뒤에 한다. + +## 실패 조건과 다음 결정 + +- 관리 정보까지 더하면 메모리 절감이 사라진다: 표현 또는 정책을 수정하거나 H1을 기각한다. +- 평균은 좋아져도 입력·편집 P99가 정한 한도를 넘는다: 해당 사용 조건에서 실패로 기록한다. +- 제한된 합성 입력에서만 이긴다: 적용 범위를 좁혀 보고하고 일반적인 런타임의 우월성을 주장하지 않는다. +- 단순 고정 정책이 같은 결과를 낸다: 적응형 알고리즘의 추가 비용을 정당화하지 않는다. +- 실행기는 개선되지만 앱 전체의 시간·메모리에 변화가 작다: 다음 연구에서 더 큰 비용을 차지하는 경로를 선택한다. +- 편집 기능을 끄거나 의미를 줄여야만 이긴다: 그 제한을 결과에 명시한다. +- 동일한 정책과 조건의 기존 연구가 확인된다: 기여를 재현·이식·개선으로 정확히 분류한다. + +## 첫 연구 주기의 산출물 + +1. **문헌 비교표 보강:** 위 연구와 후속 연구의 실행 모델, 유지 정보, 변경 지원, 메모리 측정 범위, 평가 조건을 정리한다. 증분 계산·캐시·재계산의 메모리 관리 분야를 우선 조사한다. +2. **기준 실행기와 입력 기록:** 상태 전이와 편집 의미를 고정하고 A/B를 구현한다. 첫 결과는 A/B가 각각 유리한 조건을 재현하는 것이다. +3. **비용 지도:** 어떤 데이터와 관리 정보가 얼마의 공간을 차지하는지, 변경 비율에 따라 시간이 어디에 쓰이는지 공개한다. 여기서 지배적인 비용이 다르면 연구 질문을 갱신한다. +4. **후보 정책과 구성 요소별 실험:** C를 구현하고, 보관 정책·묶음 크기·추적 방법을 각각 바꿔 원인을 설명한다. JIT·SIMD는 이 단계의 프로파일에서 정당화되는 경로에 적용한다. +5. **실사용 연결:** 같은 실행기를 TUI와 하나의 GUI 출력에 연결해 지연·메모리·변경 경험을 확인한다. 코어 결과가 전체 경험으로 이어지는지 측정한다. +6. **재현 패키지:** 실행 명령, 고정한 버전, 입력, 원시 결과, 분석 코드, 유리한 조건과 불리한 조건을 함께 남긴다. + +초기 문헌 조사에 시간 상한을 두고 위 주기를 반복한다. 모든 저수준 분야를 먼저 공부하는 방식으로 확장하지 않는다. 각 주기에서 필요한 이론은 해당 실험과 함께 학습한다. 첫 주기의 핵심은 자료의 수명, 의존 관계, 증분 계산, 측정 통계다. + +## 제작 경험의 별도 연구 + +실행 효율 연구와 함께, 제작 경험에는 다음 질문을 유지한다. + +> 대상·상태·동작·연결을 직접 편집하는 모델이, 기능을 추가하고 고치는 데 필요한 표현과 판단을 얼마나 줄이는가? + +GUI와 터미널이 같은 변경 연산을 사용하게 한다. 공룡 게임의 점프 조건 변경, 센서 연결 교체, 목록의 필터 규칙 변경처럼 완료 조건이 분명한 작업을 선정한다. 작업 완료 시간, 오류, 수정 지점, 모델을 우회한 코드, 학습 시간을 기록한다. AI 사용 조건도 비교에서 맞춘다. + +개발자 본인에게 익숙한 도구가 빠른 효과와 모델 자체의 효과를 구분하려면 이후 외부 사용자 평가가 필요하다. 실행 효율 연구가 성공해도 이 가설의 성공이 자동으로 따라오지 않는다. + +Pure의 연구 결과는 문제와 가설, 보존하는 동작, 바뀐 메커니즘, 비교 결과, 성립 조건까지 연결해 설명할 수 있어야 한다. 이 문서는 그 첫 질문을 선택하기 위한 초안이다. diff --git a/docs/research/002-checkpoint-results.md b/docs/research/002-checkpoint-results.md new file mode 100644 index 0000000..88c1354 --- /dev/null +++ b/docs/research/002-checkpoint-results.md @@ -0,0 +1,174 @@ +# Pure 연구 보고서 002: 중간 결과를 얼마나 보관해야 하는가 + +작성·측정일: 2026-09-22 +상태: **첫 합성 실험 완료. 제품 성능이나 새로운 알고리즘의 입증은 아님.** + +후속: [실행 중 규칙 변경 데모와 보관 공간 보고서](003-live-rules.md)를 추가했다. + +## 먼저 읽을 내용 + +중간 계산 결과를 모두 기억하면 수정 후에 다시 계산할 일이 줄어든다. +일부만 기억하면 메모리를 아끼지만, 수정한 위치에 따라 더 많이 계산해야 한다. +이번에는 이 관계를 확인할 작은 실행기와 재현 가능한 측정 도구를 만들었다. + +16,384개 연산으로 구성한 모델에서, 결과를 매 연산마다 저장하던 것을 +16개마다 저장하도록 바꾸자 **코어의 지속 보관 공간이 262,456바이트에서 +139,576바이트로 46.8% 줄었다.** 이는 자료 구조의 크기를 합산한 값이다. +프로세스 전체 RAM을 측정한 값은 아니다. + +수정이 끝부분에 집중되면 처리 시간은 **67.03ns에서 88.98ns로 약 33% 늘었다.** +다른 수정 패턴에서는 두 방식의 시간이 비슷하거나, 덜 저장하는 쪽이 짧았다. +즉, 메모리를 줄이는 것과 모든 작업을 빠르게 하는 것은 각각 확인해야 한다. + +이 차이가 사용자가 느끼는 반응 속도에 어떤 영향을 주는지는 아직 모른다. +위 숫자는 화면·입력 큐·편집기가 없는 작은 계산 실험의 처리 시간이다. +약 22ns의 차이 자체를 사용자가 느끼는 지연으로 해석하면 안 된다. + +이번 결정은 **저장 간격을 제품 기본값으로 정하지 않고, 비교 가능한 선택지로 +유지한다**는 것이다. 실제 앱에서 어떤 정보를 보관해야 하는지 먼저 확인한다. + +## 사용자가 확인할 경험 + +이번 산출물은 명령행 실험이어서 아직 직접 조작할 화면은 없다. +지금 사용자에게 저수준 실행 방식의 선택이나 성능 수치의 해석을 요구하지 않는다. + +다음 사용 경험 검증의 첫 작업은 앞서 제안한 작은 공룡 게임을 실행하면서 +점프 조건을 바꾸고, 바꾼 규칙과 현재 상태를 확인하는 것이다. +"바꾼 곳을 찾기 어려웠다", "수정하자 진행 상태가 사라졌다", +"왜 점프하지 않는지 확인할 수 없었다"처럼 구체적인 관찰을 받는다. +이를 위해 무엇을 상주시켜야 하는지, 무엇을 다시 계산할 수 있는지 분석하는 일은 +Codex가 맡는다. 이번에 이 데모까지 구현한 것은 아니다. + +## 무엇을 비교했는가 + +서로 연결되지 않은 16개의 계산 사슬을 만들었다. 각 사슬은 앞 연산의 결과를 +다음 연산이 사용한다. 길이는 256개와 1,024개를 각각 시험했다. +연산은 덧셈·XOR·홀수 곱셈·회전/XOR이며, 값은 64비트 부호 없는 정수다. +오버플로는 정해진 정수 규칙으로 처리한다. 관측하는 값은 각 사슬의 최종 결과다. + +연산의 종류·상수와 최초 입력은 실행 중 바꿀 수 있다. 연결 구조는 고정이다. +이는 앞선 연구 의제 전체를 검증하기 전에 계산·보관 비용만 분리한 실험이다. +상태 전이, 그래프 연결 편집, 센서, 외부 출력은 아직 포함하지 않았다. + +| 방식 | 기억하는 결과 | 수정 시 하는 일 | +|---|---|---| +| 전체 재계산 | 사슬의 최종 결과만 | 변경된 사슬을 처음부터 계산 | +| 매 연산 저장 | 모든 연산의 결과 | 첫 변경 지점부터 필요한 부분 계산 | +| 일정 간격 저장 | 4·16·64개 연산마다 결과 | 첫 변경 지점 앞의 저장 위치에서 계산 재개 | + +모든 방식이 같은 모델·연산·변경 기록을 사용한다. 변경되지 않은 사슬은 모두 +건너뛴다. 저장된 값과 같아져도, 같은 처리 단계에 적용된 마지막 변경 지점을 +지나기 전에는 계산을 멈추지 않는다. 전체 재계산과 매 연산 저장에는 전용 반복문을 +사용해, 저장 간격을 처리하는 일반 반복문의 비용을 불필요하게 부과하지 않았다. + +이 실험은 C17로 작성한 측정 장치다. Pure 제품의 언어를 Zig에서 C로 바꾸는 결정은 아니다. + +## 공간 결과: 캐시와 전체 보관 공간을 구분한다 + +아래는 사슬 16개 × 길이 1,024개인 경우다. KiB는 1,024바이트다. + +| 저장 간격 | 결과 캐시 | 코어 지속 보관 공간 | 매 연산 저장 대비 감소 | +|---|---:|---:|---:| +| 1개 | 128 KiB | 262,456 B | 기준 | +| 4개 | 32 KiB | 164,152 B | 37.5% | +| 16개 | 8 KiB | 139,576 B | 46.8% | +| 64개 | 2 KiB | 133,432 B | 49.2% | +| 1,024개: 최종 결과만 | 0.125 KiB | 131,512 B | 49.9% | + +16개마다 저장하면 캐시는 16분의 1이 된다. 하지만 연산 정의 131,072바이트, +입력·변경 범위 256바이트, 실행기 구조체 56바이트는 그대로 필요하다. +따라서 전체 보관 공간은 16분의 1이 되지 않는다. + +측정한 공간은 실행기 구조체 크기와 살아 있는 코어 배열의 요청 크기를 합산한 것이다. +계산식과 실제 할당 배열은 [실험 설명](../../research/experiments/001-checkpoint-granularity/README.md)에 있다. +할당자 내부 정보, 호출 스택, 실행 파일, OS 페이지는 포함하지 않았다. +별도로 미리 만든 변경 기록도 제외하며, 기록 크기는 작업에 따라 +4,112·65,552·524,304바이트였다. 이 제외 항목은 모든 방식에 같은 조건으로 적용했다. +전체 프로세스 메모리와 최대 메모리는 후속 측정 대상이다. + +## 시간 결과: 수정 위치에 따라 달라진다 + +아래도 사슬 16개 × 길이 1,024개다. 단위는 **한 변경 묶음 처리당 마이크로초(µs)**다. +각 숫자는 7회 측정에서 얻은 묶음 평균 처리 시간의 중앙값이며, 입력 지연의 +P50이나 P99가 아니다. 비교가 쉽도록 전체 재계산·매 연산 저장·간격 16을 나란히 적었다. + +| 변경 패턴 | 전체 재계산 | 매 연산 저장 | 16개마다 저장 | +|---|---:|---:|---:| +| 모든 사슬의 최초 입력 변경 | 107.713 | 115.260 | 112.930 | +| 임의 위치의 연산 하나 변경 | 6.339 | 3.401 | 3.375 | +| 끝 8개 중 연산 하나 변경 | 6.295 | 0.067 | 0.089 | +| 사슬마다 연산 8개 변경 | 115.972 | 110.094 | 113.426 | + +끝부분 수정에서 매 연산 저장은 평균 약 4.55개 연산을 다시 실행했고, +간격 16은 16개, 전체 재계산은 1,024개를 실행했다. 처리 시간 차이와 함께 +재계산량 증가를 직접 확인한 경우다. + +반대로 최초 입력이 모두 바뀌면 세 방식이 같은 16,384개 연산을 실행한다. +저장과 비교, 반복문 구조 등의 비용이 달라질 수 있다. 어느 비용이 시간 차이의 +주원인인지는 아직 프로파일링하지 않았다. + +길이 256에서 간격 16은 최초 입력 변경을 약 13.4% 짧은 시간에 처리했지만, +끝부분 수정에는 약 28.2% 더 걸렸다. 길이 1,024에서 관측한 1~3% 수준 차이 중에는 +반복 측정 범위가 겹치는 것도 있어 안정적인 속도 개선으로 결론 내리지 않는다. +간격 4는 두 크기의 네 작업 모두에서 매 연산 저장보다 느린 중앙값을 보였다. +보관 공간은 간격이 커질수록 줄었지만, 처리 시간은 단순히 같은 방향으로 움직이지 않았다. + +선택한 예시 외의 모든 조건과 최소·최대값은 +[전체 요약 표](../../research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/summary.csv)에 남겼다. + +## 검증과 재현 + +- 기준 구현은 각 사슬을 처음부터 계산한다. 별도 정확성 검사에서 매 변경 단계의 + 모든 출력을 기준 구현과 비교했다. +- 여러 시드, 길이 1과 홀수 길이, 한꺼번에 적용하는 변경, 아무 변화 없는 입력, + 서로 취소되는 변경, 앞의 변경은 취소되지만 뒤의 변경은 남는 경우를 검사했다. +- 최적화 빌드와 ASan·UBSan 빌드에서 **각각 108,000개의 변경 단계 검사**를 통과했다. + 이는 검사를 통과했다는 뜻이며, 모든 가능한 프로그램의 정확성 증명은 아니다. +- 성능 측정은 40개 조건 × 7회, 원시 표본 280개다. 각 측정 후 기준 출력 검사를 + 수행했고, 같은 크기·작업의 방식 간 최종 출력 체크섬도 일치했다. +- 초기 생성·기록 생성·준비 실행·출력 검사는 시간에서 제외했다. + 변경 적용, 실행 대상 선별, 재계산은 포함했다. 측정 구간 내 동적 할당은 없다. + +호스트는 **Raspberry Pi 5 / Linux aarch64**, 컴파일러는 GCC 14.2.0, 최적화는 `-O3`다. +한 CPU에 실행을 고정하고 측정 순서를 섞었다. 공유 호스트이며 전원·온도는 통제하지 않았다. +256개 처리 단계의 변경 기록을 반복 재생한 준비된 상태의 실험이다. 실제 사람의 입력 분포, +초기 실행, 장시간 실행과 다른 시드에 대한 성능 평가는 별도로 필요하다. +**사용자의 M3 macOS에서 측정한 결과가 아니다.** + +소스와 실행 스크립트의 해시, 컴파일러·호스트 정보, 검사 통과 결과는 +[실행 환경 기록](../../research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/environment.json)에 있다. +[원시 결과](../../research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/raw.csv)와 +[실행 방법·소스](../../research/experiments/001-checkpoint-granularity/README.md)를 함께 보관했다. +보고서 작성 시 현재 소스·스크립트의 해시가 측정 기록과 같은지 확인했다. + +## 선행연구와 이번 결과의 위치 + +계산을 어떤 크기의 단위로 재사용하는지가 성능에 영향을 준다는 문제는 기존 연구에 있다. +Acar 등의 논문은 의존 관계와 메모이제이션이 재사용하는 단위의 제약을 분석한다. +이번 고정 간격 실험이 그 논문의 알고리즘을 재현한 것은 아니다. +[An Experimental Analysis of Self-Adjusting Computation, TOPLAS 2009](https://www.cs.cmu.edu/~blelloch/papers/ABBHT09.pdf) + +중간 결과를 버렸다가 다시 계산해 메모리 예산을 맞추는 온라인 정책도 기존 연구에 있다. +DTR은 이를 텐서 계산에서 다룬다. 작은 스칼라 연산으로 된 이번 실험에 그 성능 결과를 +옮겨 적용할 수는 없다. +[Dynamic Tensor Rematerialization, ICLR 2021](https://arxiv.org/abs/2006.09616v4) + +따라서 이번 결과는 **알려진 공간·시간 관계를 Pure의 첫 측정 장치에서 확인한 것**이다. +독창적인 기여는 아직 입증하지 않았다. 문헌의 관련 부분을 확인했지만, 후속 연구를 +망라한 체계적 조사나 해당 논문들의 실험 재현을 완료한 상태도 아니다. + +## 다음 결정 + +단순한 사슬은 연결 정보가 암묵적이고, 최종 결과만 필요하다. 실제 앱에서는 하나의 값이 +여러 곳에 쓰이고 편집기가 중간 값을 관찰할 수도 있다. 이때 연결·관찰·편집 정보가 +이번 절감량을 얼마나 상쇄하는지가 다음 질문이다. + +1. 사용 경험 검증용 작은 앱의 모델과 변경 의미를 먼저 구체화한다. + 실행 중 규칙 변경과 현재 상태 관찰에 필요한 정보를 기록한다. +2. 그 모델의 분기·합류와 중간 값 관찰을 비교 실행기에 넣는다. + 보관 공간을 모델·필수 상태·연결·관찰 정보·캐시로 나누어 측정한다. +3. 실제로 지배적인 비용을 확인한 후 저장 정책을 선택한다. + 고정 간격으로 충분하면 자동 적응 정책의 비용을 추가할 이유가 없다. + +초기 의제의 H1인 "변경 특성에 적응하는 정책이 더 유리하다"는 이번 실험으로 +채택하거나 기각할 수 없다. 이번에는 고정 간격만 비교했다. diff --git a/docs/research/003-live-rules.md b/docs/research/003-live-rules.md new file mode 100644 index 0000000..b7c0349 --- /dev/null +++ b/docs/research/003-live-rules.md @@ -0,0 +1,139 @@ +# Pure 연구 보고서 003: 실행 중 규칙을 바꾸고 이유를 확인하기 + +작성일: 2026-09-22 +상태: **구현 검증 완료. 사용자 피드백 반영: 개발 경험 평가에는 부적합.** + +사용자는 이 Dino를 직접 개발하지 않았으므로 개발 경험을 평가할 수 없다고 지적했다. +이 데모로 확인한 범위는 정해진 규칙 변경·상태 보존·판정 표시다. +아래 기존 검사와 수치는 그 범위의 기록으로 보존한다. +새 동작을 직접 작성하는 평가는 [별도 제작 실험 004](004-authoring.md)로 옮겼다. + +## 먼저 직접 해볼 일 + +공룡이 점프하는 작은 연습 장면을 만들었다. 공중에서 두 번째 점프가 거절되면, +그 자리에서 규칙을 **두 번 점프**로 바꾸고 다시 시도할 수 있다. +규칙을 적용해도 위치·속도·진행 상태는 유지된다. 부딪혀도 연습은 계속된다. + +[실행 안내](../../prototypes/live-rules/README.md)에 따라 화면을 열고 다음을 확인하면 된다. + +1. 점프한 뒤 공중에서 다시 점프해, 거절 이유를 찾는다. +2. 공중에서 일시정지하고 **두 번 점프**로 바꿔 **규칙 적용**을 누른다. +3. 위치와 진행 상태가 유지되는지 보고 다시 점프한다. +4. 점프를 비활성화한 뒤, 무엇 때문에 입력이 거절됐는지 확인한다. + +시간을 맞추기 어려우면 정지 상태에서 점프하고 한 프레임씩 진행해도 된다. +정지 중에도 입력은 판정하지만 위치는 프레임을 진행할 때 움직인다. +**처음부터**는 규칙과 진행 상태를 함께 초기화한다. + +사용자가 알려줄 것은 “하려던 일 / 실제로 일어난 일 / 기대했던 일”이다. +예를 들어 “규칙을 바꿨는데 여전히 거절 표시가 보여 적용 실패로 생각했다”는 관찰이면 +충분하다. 그 원인을 재현하고, 화면과 실행 의미 중 무엇을 고칠지 분석하는 일은 Codex가 맡는다. + +## 무엇을 구현했는가 + +Zig로 작성한 코어가 점프 판정, 물리, 규칙 변경, 진행 상태와 사건 기록을 담당한다. +이를 WebAssembly로 빌드해 브라우저와 터미널에서 사용한다. 두 어댑터는 같은 Wasm 파일을 +각자의 인스턴스로 실행한다. 현재 두 화면의 진행 상태를 실시간으로 공유하는 기능은 없다. + +판정은 입력 요청·활성 여부·접지 여부·남은 점프 수를 읽고, 조건을 선택해 합류하는 +**고정된 여섯 노드 그래프**다. 화면에서 보는 노드 값은 코어가 실제 판정에 사용한 값이다. +바꿀 수 있는 것은 점프 조건·힘·활성화 세 항목이며, 모두 검증한 뒤 함께 적용한다. +지원하지 않는 값은 기존 규칙과 상태를 유지한 채 거절한다. + +연산이나 연결을 자유롭게 추가하는 편집기, 임의 프로그램의 상태 이전은 아직 구현하지 않았다. +이번에는 앞선 계산 사슬과 별도의 상호작용 모델에서 편집과 관찰의 의미를 확인했다. +새로운 알고리즘이나 일반 런타임의 성능 우월성을 입증한 결과는 아니다. + +## 규칙을 바꿨는데 왜 이전 판정이 남는가 + +판정 화면은 **마지막 입력을 처리하기 직전의 상태와 그때 사용한 규칙 버전**을 보여준다. +규칙을 수정했다고 과거의 입력이 새 규칙으로 처리된 것은 아니므로, 이 기록도 그대로 둔다. +다음 입력이 들어오면 새 규칙으로 판정하고 화면의 기록을 교체한다. + +따라서 현재 규칙이 버전 2이고 마지막 판정이 버전 1인 상태는 정상이다. +사용자가 이 차이를 이해할 수 있는지는 실제 조작으로 확인해야 한다. +기록을 더 보관하는 것만으로 설명이 쉬워진다고 가정하지 않는다. + +## 보관 공간에서 확인한 사실 + +아래 값은 Zig의 `@sizeOf`로 얻은 **선언된 자료 구조의 크기**다. + +| 항목 | 바이트 | 포함하는 정보 | +|---|---:|---| +| 규칙 | 16 | 조건·힘·활성화·버전 | +| 진행 상태 | 68 | 위치·속도·장애물·횟수 등 | +| 마지막 판정 | 24 | 판정 당시 상태·버전·여섯 노드 값 | +| 사건 기록 | 264 | 최근 사건 16개와 기록 위치 | +| 코어 구조체 | 372 | 위 네 항목을 포함한 크기 | +| 그래프 정의 | 24 | 여섯 노드의 고정 정의 | +| 선언 크기 합계 | **396** | 코어 구조체 + 그래프 정의 | + +마지막 판정과 사건 기록은 합쳐서 288바이트다. 이 작은 모델에서는 전체 선언 크기 +396바이트 중 약 72.7%를 차지한다. **사용자에게 실행 이유를 보여주기 위한 정보도 +따로 계산해야 한다**는 구체적인 사례다. 다른 앱에서도 같은 비중이라는 뜻은 아니다. + +이 합계는 실제 RAM 사용량이 아니다. 그래프 상수는 컴파일러가 명령어로 접을 수 있고, +Wasm 엔진과 화면에도 별도 비용이 있다. 코어 구조체는 앞의 네 항목을 포함하므로 +표의 모든 행을 더하면 중복 계산이 된다. + +실제 Wasm 선형 메모리의 용량은 **65,536바이트(64 KiB)**로, 초기값과 최대값이 같다. +코어 데이터와 예약한 16 KiB 스택은 이 공간 안에 포함된다. +선언 크기나 스택을 64 KiB에 다시 더하지 않는다. +빌드된 Wasm 파일은 **32,544바이트**이며, 파일 크기와 메모리 용량도 서로 다른 값이다. +브라우저·Node 엔진, DOM·렌더러, 실행 코드와 프로세스 전체 메모리는 이번 수치에 포함하지 않았다. + +최근 사건 16개를 넘으면 오래된 사건을 덮어쓴다. 이것은 완전한 실행 재생 로그가 아니다. +별도의 정해진 명령열로 재생 검사를 수행하며, 메모리에 모든 과거 상태를 보관하지 않는다. + +## 검증과 증거 + +- 네이티브 Zig 검사 **11개**를 통과했다. 규칙 변경 시 상태 보존, 잘못된 변경의 거절, + 정지·한 프레임 진행, 두 번 점프, 그래프 판정, 기록 한도, 재생 일치와 카운터 처리를 검사했다. +- 실제 Wasm을 사용하는 어댑터 검사 **6개**를 통과했다. 같은 입력 의미, 실행 중 변경, + 잘못된 숫자 입력, 반복 실행 후의 기록·메모리 한도와 재생 결과를 확인했다. +- 별도 코드 검토에서 코어와 어댑터의 수정 원자성, 범위 검사, 공간 집계 표현을 확인했다. + 터미널에 소수·범위를 넘는 정수·음수·지수 표기를 넣었을 때 거절되고 기존 상태가 유지되는지도 확인했다. + +검사를 통과했다는 것은 다룬 시나리오의 결과이며 모든 가능한 실행의 정확성 증명은 아니다. +검증 환경은 Linux aarch64, Zig 0.15.2와 Node 24다. **M3 macOS 측정은 수행하지 않았다.** +처리 속도, 사용자 입력 지연과 전체 앱 메모리를 비교하는 벤치마크도 수행하지 않았다. + +재현 명령은 [실행·검증 안내](../../prototypes/live-rules/README.md)에 있다. +소스·Wasm 해시, 구조체 크기와 명령별 상태를 담는 +[실행 기록](../../prototypes/live-rules/results/2026-09-22-linux-aarch64/storage-and-replay.json)은 +최종 소스와 대조해 저장했다. 이번 기록에는 네이티브 검사 결과와 빌드 정보도 포함한다. +향후 재실행에서 빌드 기록이 없으면 컴파일러 출처를 확인한 것으로 취급하지 않는다. + +## 실제 브라우저 조작 검증 + +Chromium 151과 Playwright CLI 0.1.21에서 실제 컨트롤을 조작해 **23개 확인 항목**을 통과했다. +JavaScript로 코어 상태를 직접 바꾸는 방식 대신 버튼·키보드·폼을 사용했고, +읽기 전용 관찰 함수로 결과를 확인했다. + +- 정지·실행·한 프레임 진행, 키보드와 버튼 입력, 키 반복과 중복 입력 방지를 확인했다. +- 초안은 적용 전까지 실행에 영향을 주지 않았고, 적용 시점의 높이 60.20px, 속도 5.60px/틱, + 진행 거리 32px, 처리 단계 8이 모두 그대로 유지됐다. 다음 점프는 새 규칙으로 허용됐다. +- 이전 버전 판정 표시, 세 번째 점프와 비활성화된 점프의 거절, 명시적 초기화를 확인했다. +- 최대 세기의 두 번 점프가 화면 밖으로 사라지지 않았다. 390px 폭에서 가로 넘침 없이 + 컨트롤을 조작했고, 공룡이 너무 작아 보이는 초기 표시를 확대해 확인했다. +- Wasm 파일을 불러오지 못할 때 실행 명령 안내와 비활성화된 컨트롤을 확인했다. + +최종 소스·Wasm 해시와 상태 전후 값은 +[브라우저 검증 기록](../../prototypes/live-rules/results/2026-09-22-linux-aarch64/browser-checks.json)에 있다. +[데스크톱 화면](../../prototypes/live-rules/results/2026-09-22-linux-aarch64/desktop.png)과 +[좁은 화면](../../prototypes/live-rules/results/2026-09-22-linux-aarch64/mobile.png)도 보관했다. +좁은 화면 검사는 데스크톱 브라우저의 뷰포트 검사이며 실제 휴대폰이나 터치 센서 검증은 아니다. +이 검증은 동작과 표시를 확인한 것이며, 사용자가 쉽게 이해하는지에 대한 평가를 대신하지 않는다. + +## 다음에 판단할 질문 + +아래는 당시의 후속 질문이다. 이후 사용자 피드백으로 **빈 작업물에서 직접 만드는 과정**을 +먼저 확보하기로 바꿨다. 이 데모의 평가를 다시 요구하지 않는다. + +먼저 사용자가 “왜 입력이 거절됐는지”와 “변경이 언제 적용됐는지”를 이 화면에서 +설명할 수 있는지 확인한다. 찾기 어렵다면 어떤 정보나 표현이 부족한지 재현한다. + +그다음 이 작은 모델에서 288바이트를 쓰는 판정·사건 기록 중 **항상 남겨야 할 정보와 +필요할 때 다시 계산할 수 있는 정보**를 나눈다. 이전 판정을 정확히 다시 설명하려면 +당시 상태와 규칙의 어떤 부분이 필요한지부터 확인해야 한다. +사용자가 실제로 찾는 정보와 정확성을 유지하는 조건이 정해진 뒤 보관·재계산 방식을 비교한다. diff --git a/docs/research/004-authoring.md b/docs/research/004-authoring.md new file mode 100644 index 0000000..6ced695 --- /dev/null +++ b/docs/research/004-authoring.md @@ -0,0 +1,176 @@ +# Pure 연구 보고서 004: 빈 작업물에서 직접 작성하기 + +작성일: 2026-09-22 + +**이번에는 빈 문서에서 상태·입력·도형·반응을 작성하는 도구를 구현했다.** +새 동작을 추가하는 데 실행기 내부를 수정하지 않아도 된다. 자동 검사와 실제 브라우저 조작은 +통과했지만, 사용자가 배우기 쉬운지와 기존 코드보다 제작하기 편한지는 아직 모른다. +실행 비용을 비교한 현재 구현은 고정된 JavaScript 예제보다 느렸다. + +후속 사용자 관찰: 문법은 매력적이고 기본 사용에 걸림돌이 없었지만, 현재 결과는 작은 DSL이며 +실제 큰 서비스·앱·CLI를 개발해야 가치를 판단할 수 있다는 피드백을 받았다. +이 관찰은 사용자 한 명의 현재 경험이다. 대규모 제작·성능·독창성은 여전히 미검증이다. +다음 설계는 [기반 계약 보고서 005](005-foundation-contract.md)에 기록했다. + +[제작 화면 실행 안내](../../prototypes/workbench/README.md)를 따라 자신의 작은 동작을 만들어볼 수 있다. +첫 화면은 비어 있고, 예제는 직접 선택했을 때만 편집기에 들어온다. +사용자가 전달할 관찰은 **하려던 일 / 실제로 일어난 일 / 기대했던 일**이면 된다. +도구의 표현이나 설명이 막히는 지점을 재현하고 개선하는 일은 Codex가 맡는다. + +## 이전 실험에서 바로잡은 점 + +사용자는 “Dino를 직접 만든 것이 아니므로 개발 경험을 평가할 수 없다”고 지적했다. +맞는 지적이다. 기존 데모는 정해진 동작의 설정 변경과 상태 보존을 시험했으며, +새 동작을 만드는 과정은 제공하지 않았다. 그 결과를 개발 경험의 증거로 쓰지 않는다. + +이번 실험에서는 사용자가 직접 다음을 정의한다. + +| 작성할 것 | 표현 | +|---|---| +| 기억할 값 | `state x = 80` | +| 입력 이름과 키 | `input right = ArrowRight` | +| 보여줄 대상과 상태 연결 | `circle seed(x, 180, 20, "#7ed9ae")` | +| 누름·놓음·시간 반응 | `on right.down`, `on right.up`, `on frame` | +| 계산·조건 | 대입, 산술·비교, `if`, `min/max/clamp` | + +점프·중력·충전·충돌은 코어에 내장하지 않았다. 충전 예제의 중력과 바닥 판정도 작성한 코드에 있다. +화면의 입력 버튼, 상태 목록과 도형은 선언한 내용에서 만들어진다. +현재 범위는 숫자 상태와 간단한 도형을 사용하는 작은 상호작용이다. 임의 앱, 센서, +자유로운 창 관리, 셰이더, 문자열·배열·함수 등을 지원한다는 뜻은 아니다. + +## 실제 제작 과정에서 확인한 의미 + +편집 중인 코드와 실행에 적용한 코드를 구분한다. 적용할 때 **같은 이름의 숫자 상태**는 유지한다. +상태 선언 순서를 바꿔도 이름으로 대응하며, 새 이름은 초깃값을 사용한다. +이름을 바꾸는 행위의 의미까지 추론하지는 않는다. 초깃값 변경을 현재 값에 반영하려면 초기화를 누른다. + +문법·이름·용량 오류가 있는 수정은 기존 작업을 유지한다. 계산 중 오류가 생기면 해당 이벤트에서 +실행한 모든 반응의 숫자 상태 변경을 함께 취소한다. 키보드와 포인터가 같은 입력을 동시에 +누르면 첫 누름과 마지막 놓음만 전달한다. 누른 상태에서는 새 코드를 적용할 수 없다. + +브라우저 검증 중 서로 다른 도형·입력·반응을 소스만 편집해 추가했다. 기존 실행기 변경은 없었다. +미적용 초안의 저장·불러오기도 확인했다. 실제 사용자의 자유로운 제작 성공률이나 소요 시간은 +이 자동화로 측정되지 않는다. + +## 이번에는 실제로 명령 해석기가 있다 + +초기 대화의 네이티브 C 함수 구성에는 자체 바이트코드 VM이 없었다. +이번에는 작성한 동작을 실행 중 교체하는 실험을 위해 **작은 중간 명령 해석기**를 도입했다. +구성이 달라졌으므로 바이트코드 저장량을 0이라고 말할 수 없다. + +```text +작성한 .pure 텍스트 + → JavaScript 컴파일러: 이름·타입의 범위와 용량 검사, 중간 명령 생성 + → Zig 코어: 중간 명령 실행, 상태·오류·적용 관리 + → 브라우저 SVG 또는 터미널 JSON +``` + +Zig 코어 자체는 WebAssembly로 빌드한다. 사용자가 작성한 중간 명령과 이 Wasm 파일은 +서로 다른 대상이다. 브라우저와 터미널은 같은 컴파일러·코어를 사용하지만 별도 인스턴스다. +이 선택이 최종 제품의 언어·컴파일 방식으로 확정된 것은 아니다. + +## 보관 공간과 실행 명령 수 + +[충전 예제](../../prototypes/workbench/examples/charge.pure)를 실제 컴파일·실행한 값이다. +소스는 674바이트, 상태 4개, 입력 1개, 출력 4개, 반응 3개이며 중간 명령은 66개다. + +| 항목 | 바이트 | 의미 | +|---|---:|---| +| 사용 중인 명령·반응 정보 | **1,092** | 명령 66 × 16 + 반응 3 × 12; 표시 계산 명령 포함 | +| 현재 숫자 상태 값 | **32** | 4 × 8 | +| 프로그램 한 개의 예약 공간 | 9,392 | 명령·반응·초깃값 등의 고정 용량 | +| 코어 구조체 예약 공간 | **19,072** | 실행 중 프로그램, 다음 적용을 위한 준비 공간, 상태·관리 정보 포함 | +| 그중 다음 프로그램 준비 공간 | 9,392 | 위 코어 구조체에 포함 | +| 한 번의 계산 평가 스택 용량 | 512 | 64 × 8; 영구 상태와 다른 임시 공간 | +| Wasm 선형 메모리 | **65,536** | 초기·최대값 고정; 코어와 예약한 16 KiB 호출 스택 포함 | +| 배포 Wasm 파일 | **48,182** | `ReleaseSafe`, 디버그 정보 제거한 파일 크기 | + +표의 포함 관계를 무시하고 합산하면 안 된다. 특히 32바이트 상태만으로 전체 프로그램이 +돌아간다는 뜻이 아니다. JS 컴파일러·소스·중간 명령 객체, 브라우저·Node 엔진, SVG, +기계어 코드와 프로세스 전체 메모리는 이 선형 메모리 수치에 포함되지 않는다. +JavaScript 비교 모델의 힙 사용량도 측정하지 않았으므로 메모리 우월성은 판단하지 않는다. + +고정 용량 덕분에 이 코어는 작업이 작아도 프로그램 두 개 분량을 예약한다. +작업에 비례해 메모리가 줄어드는 구현도 아니고, 이 19 KiB를 최적이라고 주장하지도 않는다. + +해당 예제에서 관찰한 이벤트당 중간 명령 실행 수는 다음과 같다. + +| 실제 상황 | 실행한 중간 명령 | +|---|---:| +| 바닥에 정지한 상태로 한 프레임 | 22 | +| 바닥에서 입력 누르기 | 8 | +| 충전 중 한 프레임 | 29 | +| 한 프레임 충전한 뒤 놓기 | 13 | +| 이후 공중에서 한 프레임 | 18 | + +이 값은 **해당 이벤트에서 해석한 명령** 수다. CPU 명령 수·CPU 사이클·함수 호출 횟수가 아니다. +컴파일, 호스트 입력 처리, 표시 표현식 평가와 렌더링도 제외한다. +[빌드·검사·명령 수 기록](../../prototypes/workbench/results/2026-09-22-linux-aarch64/verification.json)에 +당시 상태와 코드·Wasm 해시를 남겼다. + +## 일반 JavaScript와 비교한 결과 + +[독립 JavaScript 모델](../../prototypes/workbench/web/baseline.mjs)은 컴파일러나 Pure 코어를 사용하지 않는다. +같은 충전·해제·중력·바닥 계산과 출력 값을 일반 코드로 구현했다. +이 JS 모델에는 임의 프로그램의 실행 중 교체나 이름에 따른 상태 이전 기능이 없다. + +길거나 짧은 누름, 중복 누름, 두 입력원, 공중 입력, 놓기, 초기화를 포함하는 +**6,663회 호스트 호출**을 양쪽에 재생했다. 각 호출 뒤 상태·출력·틱·누른 입력을 비교했다. +초기 상태 포함 **6,664개 스냅샷**, 숫자 **126,616개**에서 차이가 0이었다. +이는 기록한 실행열에서의 일치다. 모든 가능한 실행의 동등성 증명은 아니다. +예를 들어 코어의 틱 카운터는 32비트에서 순환하지만 JS 모델의 틱은 같은 방식으로 순환하지 않는다. +이번 실행열은 그 경계에 도달하지 않는다. + +같은 실행열로 준비 실행 후 측정 순서를 교대하며 7회 측정했다. +환경은 **Linux aarch64, Cortex-A76, Node 24.20.0, Zig 0.15.2**다. +M3 macOS에서 얻은 결과가 아니다. + +| 경로 | 호출당 시간 중앙값 | 7회 측정값의 범위 | +|---|---:|---:| +| Pure 호스트 + Wasm 명령 해석기 | **569 ns** | 516~735 ns | +| 고정 JavaScript 모델 | **53.4 ns** | 52.7~74.6 ns | + +이 실행열에서는 Pure 경로가 약 **10.7배** 오래 걸렸다. +누름·놓음 처리, 프레임 계산, 초기화 등의 총 시간을 호출 수로 나눈 배치 처리량이다. +한 입력의 지연 분포나 화면 응답 시간은 아니다. Pure 쪽의 일반 명령 해석과 진단 관리 비용도 +함께 들어 있으므로 이 차이를 특정 연산이나 Wasm 경계 하나의 비용으로 단정하지 않는다. +컴파일·표시 표현식·스냅샷 생성·DOM 렌더링·코드 교체 시간은 제외했다. +CPU 고정·전력·온도 통제도 하지 않았으므로 절대 수치를 일반화하지 않는다. + +작성 파일은 `.pure` 34줄·674바이트, JS 모델 68줄·2,251바이트다. +이는 주석·빈 줄을 포함한 물리적 파일 길이이며, 편집기의 줄 수는 마지막 빈 줄까지 세어 다를 수 있다. +JS 모델에는 입력원 관리와 출력 데이터 생성 코드도 들어간다. Pure는 그 일부를 공통 실행기로 +옮겼으며 컴파일러·호스트 코드가 사라진 것이 아니다. 이 길이로 학습 부담이나 개발 시간을 판정하지 않는다. + +재현 가능한 실행열 생성기·모든 측정값·환경·해시는 +[비교 원자료](../../prototypes/workbench/results/2026-09-22-linux-aarch64/comparison.json)에 있다. + +## 검사와 남은 질문 + +- Zig 네이티브 18개: 이벤트 원자성, 범위·스택·분기, 상태 이전, 메모리 집계 등. +- 컴파일러 13개: 문법·이름·식·분기·용량·예제 작성 범위. +- 실제 Wasm 호스트 통합 13개: 새 동작 작성, 입력원 결합, 적용 실패 보존, 상태 이름 대응, + 오류 보존, 표시 계산의 순수성, 별도 인스턴스 일치, 고정 메모리. +- 독립 JS 모델 7개, 실제 터미널 프로세스 4개. +- Chromium 실제 조작 44개: 빈 작업물 작성부터 새 동작 추가, 편집 중 키 입력 격리, + 잘못된 수정·실행 오류, 초안 파일 저장·복원, 390px 화면과 Wasm 로드 실패 처리. + +검사 중 외부에서 반환된 프로그램 정보를 변경하면 다음 적용의 상태 대응이 틀어질 수 있는 문제를 +발견해 해당 정보를 변경 불가능하게 만들었다. 실패한 누름 뒤 정상적인 놓음이 오류를 지워버리는 +문제도 고쳤다. 마지막 실패와 가장 최근 입력을 따로 표시한다. + +[브라우저 증거](../../prototypes/workbench/results/2026-09-22-linux-aarch64/browser-checks.json), +[빈 화면](../../prototypes/workbench/results/2026-09-22-linux-aarch64/blank.png), +[작성한 화면](../../prototypes/workbench/results/2026-09-22-linux-aarch64/authored.png), +[좁은 화면](../../prototypes/workbench/results/2026-09-22-linux-aarch64/mobile.png)을 보관했다. +좁은 화면 검사는 데스크톱 브라우저의 뷰포트 검사이며 실제 휴대폰 센서·터치를 검증하지 않는다. + +다음 사용자 관찰은 **자신이 정한 동작을 이 표현으로 끝까지 작성할 수 있는가**다. +원하는 작은 숫자·도형 동작도 실행기 수정을 요청해야 한다면 그 지점이 표현 범위의 실패다. +구현 세부사항을 이해하지 못했다는 이유로 사용자의 책임으로 돌리지 않는다. + +다음 성능 연구에서는 같은 의미를 유지하면서 실제 사용하는 상태·명령 수가 달라질 때 +예약 공간, 상태 복사, 명령 해석, 호스트 경계, 표시 계산의 비용을 각각 분리해야 한다. +현재 전체 경로의 측정만으로 어느 부분을 최적화해야 할지 확정하지 않는다. +새 원리나 기존 런타임 대비 우월성은 아직 입증하지 않았다. 이번 산출물은 +그 판단에 필요한 제작 경험과 수치를 얻기 위한 실행 가능한 실험이다. diff --git a/docs/research/005-foundation-contract.md b/docs/research/005-foundation-contract.md new file mode 100644 index 0000000..842b732 --- /dev/null +++ b/docs/research/005-foundation-contract.md @@ -0,0 +1,139 @@ +# Pure 연구 보고서 005: 연산·수명·모듈 경계 + +작성일: 2026-09-22 +상태: **설계 초안과 데이터 레이아웃의 제한된 컴파일 검사 완료**. +버퍼 런타임·모듈 로더·포맷 지원·새 컴파일러가 구현됐다는 뜻은 아니다. + +## 이번에 정한 것 + +사용자는 현재 문법이 매력적이고 기본 사용에 걸림돌이 없었다고 평가했다. +동시에 현재 결과는 평범한 작은 DSL로 보이며 실제 서비스·앱·CLI 개발을 통해 가치를 확인해야 한다고 했다. +이 피드백을 특정 사용자의 현재 경험으로 기록한다. 대규모 개발 생산성이나 독창성 검증으로 확대하지 않는다. + +그다음 합의는 **연산 의미 → 데이터 수명·소유권 → 모듈 계약**을 먼저 명세하는 것이다. +커널 우회는 실제 입출력 병목이 확인될 때 별도 백엔드로 검토한다. +압축·암호화·문서·미디어·OS를 다루려는 장기 목표도 기록했다. +각 분야를 실제 지원하거나 새로운 알고리즘을 만드는 일은 별도의 구현·검증 범위를 갖는다. + +이번 산출물: + +| 문서 | 정한 경계 | +|---|---| +| [연산 의미](../design/001-operation-semantics.md) | 숫자 계산, 관리 상태 확정, 버퍼 변환, 외부 효과의 실패 범위 | +| [메모리·수명](../design/002-memory-lifetime.md) | 예산, 버퍼 소유자·대여, 비동기 완료·취소·반납 | +| [모듈 계약](../design/003-module-contract.md) | 프로파일·버전·진행 결과, 네이티브 호출과 영속 형식의 구분 | +| [C17 레코드 초안](../design/abi-v0.h) | 핸들·구간·스트림 결과·한도 정보의 필드 폭과 크기 | + +제작 화면의 문법·동작은 이번 작업에서 바꾸지 않았다. 새 데모를 조작해 평가할 과제도 없다. +현재 사용성 피드백을 유지하면서 다음 실제 앱이 요구할 기반을 명확히 하는 단계다. + +## 가장 중요한 세 가지 결정 + +**첫째, 취소 요청과 버퍼 반납은 다르다.** +파일·네트워크·장치 작업은 취소 요청 뒤에도 접근이 남을 수 있다. +접수된 작업의 대여는 최종 완료를 회수할 때까지 유지한다. 동기 백엔드도 같은 요청 계약을 사용한다. +libuv 역시 요청 관련 메모리를 완료 콜백 전에 해제하지 않도록 요구한다. +이 자료를 일반적인 수명 계약의 근거로 사용했고 libuv를 실제 의존성으로 추가하지는 않았다. +[libuv 요청 계약](https://docs.libuv.org/en/v1.x/request.html#c.uv_cancel) + +**둘째, 입력 소비·유효 출력·스트림 종료를 따로 센다.** +압축 해제는 입력을 다 읽은 뒤에도 출력과 종료 처리가 남을 수 있다. +첫 바이트 프로파일은 `consumed`, `produced`, `status`를 따로 반환한다. +코덱의 내부 윈도·문맥도 예산에 포함해야 하므로 외부 버퍼 크기만으로 전체 메모리를 판단하지 않는다. +근거로 읽은 Zstd 매뉴얼은 1.5.1이며 최신 라이브러리 버전 추천은 아니다. +[Zstandard 매뉴얼](https://facebook.github.io/zstd/zstd_manual.html) + +**셋째, 물리적으로 쓴 바이트와 사용 가능한 데이터는 다르다.** +첫 출력 프로파일은 유효 길이 0인 별도 버퍼에 쓰고, 완료 회수 시 검증된 접두부만 공개한다. +인증 복호화는 해당 인증 단위가 성공한 뒤에만 평문을 유효 출력으로 전달한다. +이것은 검증된 암호 라이브러리의 계약을 보존할 조건이며 자체 암호 알고리즘 제안이 아니다. +[libsodium 인증 암호화](https://doc.libsodium.org/secret-key_cryptography/secretbox) + +## 현재 구현과의 차이 + +현재 Workbench의 숫자 오류 복구는 32개 `f64` 배열을 임시 복사한 뒤 함께 확정하는 구조다. +외부 출력이나 제자리 버퍼 쓰기까지 되돌리는 기능은 없다. +그래서 새 초안은 **자원 예약 → 관리 상태·작업 목록 확정 → 실제 백엔드 제출 → 완료 처리**를 구분한다. +확정 전 자원 부족은 기존 상태를 보존하며, 확정 후 백엔드 실패는 이미 발급한 티켓의 완료로 보고한다. + +현재 `revision`은 적용 횟수이며 ABI 버전이 아니다. 내부 Zig 구조체의 `@sizeOf`도 +포맷·모듈 파일의 직렬화 규칙이 아니다. 64 KiB는 Wasm 선형 메모리이며 전체 컴파일러·앱의 상한이 아니다. +이 세 수치를 미래 계약의 보장으로 잘못 재사용하지 않는다. + +## 작게 만들기 위해 선택한 제약 + +- 첫 모듈은 바이트 스트림 프로파일로 제한한다. 파일·GPU·다중 평면 미디어의 결과는 별도 프로파일이 필요하다. +- 한 스트림 문맥에는 진행 요청 하나만 허용한다. 첫 제어 흐름은 실행 영역별 직렬 처리다. +- 입력과 출력 버퍼를 분리하고 버퍼 전체 단위로 읽기 공유 또는 쓰기 독점을 적용한다. +- 요청·완료·대여·버퍼 슬롯을 함께 예산에 넣는다. 느린 소비자로 포화되면 역압력을 반환한다. +- 진행 작업·대여·열린 문맥이 남아 있으면 교체를 거절한다. 첫 계약은 무중단 모듈 교체를 약속하지 않는다. +- 모듈마다 새 문법·전역 타입 분석·무제한 특수화를 컴파일러에 추가하지 않는다. +- 작은 핵심 계약과 검증된 외부 라이브러리의 연결을 먼저 시험한다. 내부 알고리즘 재구현은 독립적인 비교 근거가 필요하다. + +제약은 비용을 줄이기 위한 가설이지 성능 측정 결과가 아니다. +고정 풀도 미사용 예약 공간을 낭비할 수 있고, 공통 코드도 직접 특수화한 코드보다 느릴 수 있다. +어느 쪽이 유리한지는 실제 작업과 전체 비용으로 비교한다. + +## 장기 분야가 요구하는 검증 + +| 분야 | 공통 계약에 요구하는 것 | 해당 분야의 별도 책임 | +|---|---|---| +| 압축 | 부분 소비·생성, 종료 처리, 내부 메모리 상한 | 포맷 호환, 압축률·속도·입력 분포, 사전·윈도 | +| 암호화 | 출력 공개 시점, 키·임시 버퍼 수명 | 검증된 구현, 키·nonce·인증 단위·프로토콜의 올바른 사용 | +| 문서 | 범위가 명시된 읽기, 점진 처리, 오류 위치 | 구조·인코딩·참조·랜덤 접근·각 포맷의 의미 | +| 미디어 | 버퍼 대여, 비동기 완료, 시간·역압력 | 프레임·평면·코덱·GPU 동기화·실제 시간 제약 | +| OS·장치 | 외부 효과·핸들·완료·어댑터 경계 | 플랫폼 API·권한·드라이버·커널 경로, 자체 OS의 별도 설계 | + +이 표는 모든 분야를 하나의 바이트 함수로 표현할 수 있다는 주장이 아니다. +공통으로 보존해야 할 수명·진행·비용 규칙을 정하고, 의미가 다른 부분은 각 모듈이 책임지게 한다. +지원할 실제 포맷·버전·플랫폼의 목록이나 전체 OS 개발 일정은 아직 정하지 않았다. + +## 검증한 범위와 증거 + +코드·문서 검토에서 다음 모순을 찾아 초안에서 해결했다. + +- 런타임 접수 전 거절과 관리 상태 확정 뒤 백엔드 제출 실패의 혼동. +- 출력 오프셋이 임의일 때 단일 유효 길이로 초기화되지 않은 구간까지 공개할 수 있는 모호함. +- 취소 중 공유 입력 읽기를 허용하는 규칙과 모든 읽기를 금지하는 검사 항목의 충돌. +- 동기 백엔드 반환과 런타임의 완료 회수 중 어느 시점에 대여가 끝나는지의 충돌. + +그 결과 v0 출력은 빈 버퍼·오프셋 0으로 제한하고, 모든 접수 요청의 대여는 완료 회수까지 유지한다. +입력의 공유 읽기는 허용하지만 충돌하는 쓰기·재사용은 거절한다. + +Zig 0.15.2의 C 컴파일러로 **AArch64 Linux, AArch64 macOS, wasm32**를 대상으로 +레코드 크기·오프셋의 `_Static_assert` 8개씩을 컴파일했다. 세 타깃 모두 성공했다. +이는 헤더의 제한된 데이터 배치 검사다. M3 실행·함수 호출 ABI·버퍼 수명·성능 검증이 아니다. +[컴파일 기록과 해시](../design/validation/2026-09-22-record-layout.json)를 보관했다. + +```sh +python3 docs/design/check-abi-layout.py --zig /path/to/zig --output /tmp/pure-abi-new.json +``` + +출력 파일은 덮어쓰지 않는다. native ABI의 다음 검증은 독립 C 생산자와 Zig 소비자 사이의 +실제 호출, 버전 거절, 수명·예산 처리다. 현재 헤더에는 함수 테이블이나 동적 로더 구현이 없다. + +## 다음 구현의 합격 기준 + +2026-09-22 후속 결정: [006의 문제 조사](006-problem-landscape.md)에 따라, +아래 구현에 앞서 기존의 잘 구성한 구현에서 실제 비용과 남은 개선 여지를 확인한다. +이 계약은 후보 설계이며, 조사·비교 결과에 따라 변경한다. + +먼저 작은 고정 버퍼 풀과 요청 슬롯에서 제출·완료·반납을 실행하고, +포화·부분 출력·취소 경합·늦은 완료·오래된 핸들·교체 거절을 재현한다. +이는 수명 계약 검사이며 실제 앱 사용성 평가로 대신하지 않는다. + +그다음 실제 파일 스트림과 검증된 변환 라이브러리를 연결해 +풀의 총 용량보다 큰 파일, 느린 출력, 잘린 입력, 출력 실패를 다룬다. +직접 라이브러리를 호출하는 일반 구현과 결과·오류 의미를 맞춘 뒤 다음을 비교한다. + +| 측정 축 | 기록할 대상 | +|---|---| +| 컴파일러 | 소스·토큰·AST·IR 용량, 최대 작업 메모리, 컴파일 시간, 산출물 크기 | +| 실행 메모리 | 확보·사용·유지 바이트, 내부 문맥·완료 큐·계측 공간, 전체 RSS | +| 복사 | 계측한 경로별 바이트, 원본 버퍼 유지 시간, 측정하지 못한 경로 | +| 처리 | 입출력 총량·정확성·처리량·큐 대기 포함 지연·완료/거절/취소 수 | +| 제작 | 실제 앱에서 새 기능을 위해 계약 밖 코드나 실행기 변경이 필요했던 지점 | + +새 API가 모듈 내부 메모리를 숨기거나, 풀·완료 큐·이전 문맥을 포함하면 이득이 사라지거나, +부분 실패에서 데이터를 잃으면 해당 설계를 수정한다. 기존 구현에 없던 제약을 붙여 이긴 경우에도 +같은 조건의 비교로 별도 보고한다. 이번 초안에서 새로운 원리·성능 우월성을 입증하지 않았다. diff --git a/docs/research/006-problem-landscape.md b/docs/research/006-problem-landscape.md new file mode 100644 index 0000000..cbfd6b7 --- /dev/null +++ b/docs/research/006-problem-landscape.md @@ -0,0 +1,263 @@ +# Pure 연구 보고서 006: 먼저 풀 문제를 고르기 + +작성일·자료 확인일: 2026-09-22 +상태: **1차 문헌 조사와 실험 전략. 새 성능 측정이나 독창성 입증은 없음.** + +## 이번 판단 + +언어·플랫폼별 실제 문제를 먼저 조사하고 Pure의 전략을 정하는 방향이 맞다. +좋은 문제는 개선의 실용적 가치를 결정하고, 실제 성능은 알고리즘·구현·작업 조건으로 검증해야 한다. +각 언어의 약점 목록에서 출발하되, 최종 연구 단위는 **작업 → 비용 발생 원인 → 기존 최선의 해결책 → 남은 비용**으로 잡는다. + +이 조사에서는 서비스·개발 도구의 운영 보고와 공식 구현 문서에서 10개 후보를 검토했다. +여러 사례에는 이미 좋은 해결책이 있다. 따라서 지금 정한 ABI·버퍼 풀·연산 규칙은 +문제에 맞춰 수정할 후보 설계다. 먼저 구현하고 그 구조가 유리한 문제를 뒤에서 고르는 순서를 피한다. + +첫 후보는 **여러 처리 단계에 걸친 버퍼 수명과 메모리 예산**이다. +사용 경험으로는 큰 파일이나 연속 데이터를 다룰 때 메모리가 예측 가능하고, +출력이 느려져도 조작·취소가 정상적으로 처리되는 것으로 나타나야 한다. +이번에는 사용자가 조작할 새 구현체가 없다. 기존 제작 화면도 이 가설을 검증하지 않는다. + +## 조사 범위와 해석 한계 + +- Discord·Instagram·VS Code 자료는 실제 시스템을 개선한 당사자의 역사적 보고다. 당시 원인이 지금도 그대로 남았다는 증거는 아니다. +- Sonic·Node.js·.NET·Rust·Java·SwiftUI·Metal·Arrow 자료는 공식 기능·구현 설명이다. 그 자체를 대규모 운영 장애나 Pure의 성능 증거로 취급하지 않는다. +- 전체 언어·플랫폼을 망라한 조사나 체계적 논문 검토가 아니다. 동적 문서의 현재 내용을 확인했지만 재현할 버전·커밋은 아직 고정하지 않았다. +- 아래의 ‘Pure 가설’과 우선순위는 자료를 바탕으로 한 우리의 추론이다. 원문 저자들이 Pure의 필요성을 주장한 것이 아니다. + +## 10개 후보와 현재 해법 + +### P01. Go: 큰 캐시의 메모리와 지연 + +Discord는 2020년 Read States 서비스에서 큰 캐시의 GC 비용과 주기적인 지연을 보고했다. +캐시를 줄이면 데이터베이스 접근이 증가하는 상충 관계가 있었다. Rust 전환에는 자료구조와 +복사 경로 등의 변경도 포함되어 언어만 바꾼 통제 실험으로 읽을 수 없다. +[Discord 운영 보고](https://discord.com/blog/why-discord-is-switching-from-go-to-rust). + +**기존 해법:** 캐시 정책·자료 배치·할당 감소·GC 설정과 메모리 제한을 먼저 검토한다. +Go의 공식 GC 안내도 CPU와 메모리 사이의 비용 관계를 설명한다. 이 안내는 Go 1.19를 기준으로 +설명한다고 명시하므로 재현 시 선택한 버전에서 다시 확인한다. +[Go GC guide](https://go.dev/doc/gc-guide). + +**Pure 가설:** 수명이 다른 데이터를 구분하면 회수 비용과 메모리 유지량을 줄일 수 있는가? +동등하게 최적화한 Go 및 C/Rust 구현이 비교 대상이다. GC가 없는 구현의 이득만 확인되면 +Pure 고유의 기여는 아직 없다. 캐시 적중률·DB 작업량도 같아야 한다. **우선순위: 보조 후보.** + +### P02. Python: 읽기 위주 데이터도 프로세스마다 복제되는 비용 + +Instagram의 pre-fork 환경에서는 참조 횟수 등 객체 관리 정보의 쓰기가 공유 페이지의 +copy-on-write를 유발했다. Meta는 immortal objects로 이 문제를 줄이는 작업을 보고했고, +PEP 683은 Python 3.12를 대상으로 채택되었다. 모든 사용자 객체가 자동으로 이 혜택을 +받는다는 의미는 아니다. +[Meta, 2023](https://engineering.fb.com/2023/08/15/developer-tools/immortal-objects-for-python-instagram-meta/), +[PEP 683](https://peps.python.org/pep-0683/). + +**Pure 가설:** 읽기 전용 본문과 변경되는 관리 정보를 분리할 가치가 남아 있는가? +기존 Python 개선과 명시적 공유 메모리가 비교 대상이다. 프로세스별 RSS를 단순 합산하지 않고 +공유·전용 페이지를 구분한다. 이 문제의 pre-fork 전제를 모바일·브라우저에 일반화하지 않는다. +**우선순위: 해당 실행 환경을 선택할 때 재검토.** + +### P03. Node.js / .NET: 느린 소비자와 버퍼 누적 + +Node.js는 streams의 역압력으로 생산 속도를 제어한다. .NET Pipelines도 풀링된 버퍼와 +writer 중단·재개 임계값을 제공하며, 소비·검사 위치를 잘못 전달하면 메모리 문제나 정체가 +생길 수 있음을 문서화한다. 역압력과 버퍼 재사용은 이미 제공되는 기능이다. +[Node.js streams](https://nodejs.org/learn/modules/backpressuring-in-streams), +[System.IO.Pipelines](https://learn.microsoft.com/en-us/dotnet/standard/io/pipelines). + +**Pure 가설:** 변환·분기·캐시·완료 대기를 합친 예산을 공유하면, 각 단계가 개별적으로 +제한을 두는 구현보다 같은 처리량·지연 조건에서 메모리를 덜 유지하는가? +올바른 역압력과 전역 예산을 추가한 기존 구현까지 비교해야 한다. +느린 소비자에서 입력을 무조건 버려 얻은 수치는 동일 기능의 성능 개선이 아니다. +**우선순위: 첫 연구, P04와 함께 검토.** + +### P04. ByteDance Sonic: 복사를 생략한 결과 원본 전체가 살아남는 비용 + +Sonic은 디코딩한 문자열이 원본 JSON을 참조하면서, 작은 결과를 보관해도 원본 버퍼가 +유지될 수 있음을 설명한다. `CopyString` 및 검색 결과의 `CopyReturn` 같은 선택지도 제공한다. +복사량과 장기 메모리 유지량 사이에 상충 관계가 있다는 구체적인 사례다. +[Sonic 공식 저장소](https://github.com/bytedance/sonic#tips). + +**Pure 가설:** 원본 크기·남기는 범위·메모리 압력을 이용해 공유와 복사를 선택하면 +일괄 정책보다 유리한가? 항상 복사, 항상 공유, 단순 크기 임계값, 기존 라이브러리 설정이 +비교 대상이다. 미래의 실제 보관 시간을 미리 아는 정책을 실용 구현처럼 비교하지 않는다. +**우선순위: 첫 연구. 기존 옵션보다 남는 이득이 있는지부터 확인.** + +### P05. Rust: 특수화의 실행 성능과 빌드 비용 + +rustc는 제네릭의 구체 타입별 코드를 생성하는 monomorphization을 사용한다. +공식 가이드는 실행 성능의 이점과 컴파일 시간·바이너리 크기 비용을 함께 설명한다. +이것만으로 Rust 컴파일러 전체의 크기나 어떤 앱의 메모리 사용 원인을 설명할 수는 없다. +[Rust Compiler Development Guide](https://rustc-dev-guide.rust-lang.org/backend/monomorph.html). + +**Pure 가설:** 공통 실행과 제한적 특수화를 조합해 원하는 실행 성능에서 빌드 비용을 +통제할 수 있는가? 컴파일러 배포 크기, 빌드 시간·최대 RAM, 산출물 크기, 실행 RAM을 +별도로 기록한다. 동등한 기능을 가진 비교가 필요하며 현재 작은 DSL의 크기를 범용 언어와 +직접 비교하지 않는다. **우선순위: 모든 단계의 관리 지표. 첫 대표 문제로는 보류.** + +### P06. JVM: 이미 해결된 제약을 연구 대상으로 고르는 위험 + +JDK 24는 `synchronized` 안에서 대기하는 virtual thread가 carrier thread를 붙잡는 +제약을 완화하는 JEP 491을 반영했다. 이전 버전의 대표 문제를 최신 Java 전체의 +한계로 삼으면 출발점부터 비교가 틀어진다. 모든 native/FFI 상황의 제약이 없어졌다는 뜻은 아니다. +[Oracle JDK 24 변경 사항](https://docs.oracle.com/en/java/javase/24/migrate/significant-changes-jdk-24.html). + +**Pure 판단:** 이 과거 제약은 첫 연구 후보에서 제외한다. 실제 작업의 남은 대기 비용이 +확인되면 지원 중인 대상 JDK와 비교한다. 작은 스케줄러를 만드는 것만으로 우월성을 주장하지 않는다. + +### P07. TypeScript / VS Code: 작은 변경에 큰 문서를 다시 처리하는 비용 + +VS Code의 2021년 괄호 색상 개선은 확장 API의 제약을 넘어서 토큰 정보와 증분 자료구조를 +활용했다. 보고된 큰 개선은 특정 괄호 갱신 작업에 대한 것으로, 앱 전체나 언어 간 속도 비율이 아니다. +기존 확장 호스트의 격리는 무거운 작업 중 UI 반응성을 지키는 역할도 했다. +[VS Code 개발팀 보고](https://code.visualstudio.com/blogs/2021/09/29/bracket-pair-colorization). + +**Pure 가설:** 작은 수정이 유발하는 재계산·데이터 이동 범위를 예산 안에서 제한할 수 있는가? +비교 대상도 증분 알고리즘을 사용해야 한다. 의존성 추적 메모리와 초기 구축·대규모 수정 비용을 +포함한다. **우선순위: 두 번째 연구 후보, 실제 문서·데이터 편집 기능과 연결.** + +### P08. SwiftUI: 상태 의존성과 갱신 범위 + +Apple은 SwiftUI 성능 분석에서 의존성, 뷰의 identity, 비용이 큰 갱신 작업을 다룬다. +SwiftUI 자체가 의존성을 추적하므로 변경된 부분만 처리한다는 발상만으로 차별화되지는 않는다. +[Apple WWDC23: Demystify SwiftUI performance](https://developer.apple.com/videos/play/wwdc2023/10160/). + +**Pure 가설:** 상태·입력·출력의 연결 정보로 불필요한 갱신을 줄이면서 추적 메모리도 +제한할 수 있는가? 적절히 상태를 분리한 기존 구현과 비교한다. 렌더러 차이와 상태 처리 차이를 +분리하고 동일한 화면·상호작용을 유지한다. **우선순위: P07과 묶어 후속 검토.** + +### P09. Apple Metal: 통합 메모리에서도 남는 자원 수명과 동기화 + +Apple GPU는 시스템 메모리를 공유하지만 자원 접근 방식에 따라 shared/private/memoryless를 +선택한다. shared 자원도 CPU·GPU 접근 순서를 동기화해야 한다. +[Apple 저장 모드 안내](https://developer.apple.com/documentation/metal/choosing-a-resource-storage-mode-for-apple-gpus), +[shared 접근 계약](https://developer.apple.com/documentation/metal/mtlstoragemode/shared). + +**Pure 가설:** 입력에서 GPU 완료까지 자원 수명을 연결해 임시 자원과 대기를 줄일 수 있는가? +잘 구성한 Metal 직접 구현이 비교 대상이다. 동시에 진행하는 프레임 수와 출력 품질을 맞추고 +CPU·GPU 대기 및 메모리를 함께 측정한다. 현재 Linux 측정은 사용자의 Apple Silicon에서 +실행한 증거가 아니다. **우선순위: 실제 Mac 프로파일을 확보한 뒤.** + +### P10. C/C++ 및 다중 언어 경계: 이미 있는 무복사 ABI + +Apache Arrow C Data Interface는 같은 프로세스 안에서 언어·런타임 간 columnar 데이터를 +공유할 수 있는 작은 C 인터페이스와 해제 계약을 제공한다. 프로세스 간 전송이나 영속 저장 형식은 +그 인터페이스의 범위가 아니다. +[Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html). + +**Pure 가설:** 기존 인터페이스로 표현되지 않는 비동기 수명·예산 관리가 필요한가? +Arrow에 맞는 데이터는 이를 활용하는 방안을 먼저 비교한다. 자체 ABI의 필요성은 함수 호출, +버전 처리, 소유권, 호환 비용까지 보여야 한다. **우선순위: 재사용·연동 기준.** + +## 첫 연구 질문 + +> 여러 단계가 입력을 나눠 읽고 일부 결과를 오래 보관하는 작업에서, +> 전체 예산과 원본 버퍼 수명을 함께 관리하면 기존의 잘 구성한 구현보다 +> 같은 처리량·지연 조건에서 메모리 유지량을 줄일 수 있는가? + +**귀무가설 H0:** 기존의 제한된 큐, 메모리 풀, 단순 복사 정책을 조합하면 Pure 후보와 +동등하거나 더 좋은 비용을 얻는다. 이 경우 공통 라이브러리나 어댑터 수준으로 해결하고 +그 문제를 이유로 새 언어·ABI·스케줄러를 확대하지 않는다. + +**대안 가설 H1:** 단계 간 예산과 수명 정보를 공유하는 정책이, 입력 분포가 달라져도 +단순 정책 대비 유리한 메모리·처리량·지연 조합을 만든다. 그 이득이 관리 정보·계측·호스트 +비용을 포함해 남고 다른 작업에도 적용되는지 확인한다. 이 가설은 아직 검증되지 않았다. + +현재 초안의 버퍼 전체 단위 대여와 완료 회수 시점도 수정 대상이다. +큰 원본의 작은 구간 때문에 전체 할당이 유지되거나 완료 회수가 밀리면 오히려 비용이 커질 수 있다. +복사를 줄이는 지표 하나만 목표로 삼지 않는다. + +## 재현할 작업과 비교 순서 + +첫 작업 후보는 **큰 로그·구조화 데이터에서 필드를 추출하고, 일부 결과를 보관하면서 +느린 출력으로 내보내는 CLI**다. 이는 연구용 작업 제안이며 제품 범위를 확정한 것은 아니다. +후속으로 압축 입력과 문서 탐색 화면을 연결할 수 있지만 첫 비교에 모두 넣지 않는다. + +1. **기존 구현에서 비용 재현.** 단일 프로세스의 제한된 처리 흐름으로 시작한다. + 입력 크기, 레코드 크기 분포, 보관 비율, 소비자 속도, 분기 수를 독립적으로 바꾼다. + 합성 데이터는 생성 규칙·seed를 공개하고 실제 운영 추적이라고 부르지 않는다. +2. **비교 구현 개선.** 항상 복사·공유·단순 임계값 정책을 비교하고, + 올바른 역압력·전역 예산·버퍼 풀까지 적용한다. 튜닝에 쓴 입력과 평가 입력을 분리한다. +3. **남은 비용 분석.** 원본 유지·대기열·파싱·할당·I/O 중 비용이 어디에 있는지 확인한다. + Pure가 건드릴 수 있는 경로의 비중이 작으면 우선순위를 낮춘다. +4. **최소한의 Pure 정책 비교.** 같은 언어·파서·I/O 경로에서 정책만 바꿔 원인을 분리한다. + 외부 언어·플랫폼 비교는 별도 실험으로 보고한다. 파서 교체로 얻은 이득을 수명 정책의 이득으로 돌리지 않는다. +5. **실제 제작 경험.** 가치가 확인된 작업을 사용자가 빈 작업물에서 작성·수정하게 한다. + 새 필터·출력·보관 정책을 추가할 때마다 실행기 코드를 고쳐야 하는지 기록한다. + +큰 파일 하나만으로 대규모 서비스의 특성을 대체할 수 없다. 데이터/메모리 비율, +동시 작업 수, 보관 시간 분포, 부하 급증, 느린 소비자의 분포를 드러내야 한다. +네트워크를 붙일 때는 도착 부하와 큐 대기 시간을 기록하고, 느려진 만큼 요청 발생도 늦어져 +지연이 작아 보이는 측정 오류를 피한다. 분산 저장소·핫키·질의 설계의 비용은 별도로 분석한다. + +## 측정과 탈락 기준 + +| 축 | 반드시 포함할 것 | +|---|---| +| 정확성 | 출력 내용·순서, 잘린 입력, 실패·취소, 완료 후 자원 반환 | +| 메모리 | 예약·사용·유지 바이트, 큐·풀·관리 정보, 라이브러리 내부 할당, 프로세스 메모리 | +| 수명 | 원본 할당별 마지막 참조 시점, 유지 바이트의 시간 적분(byte·seconds), 최고 유지량 | +| 복사 | 경로별 복사 바이트, 공유로 연장된 수명, 계측 밖 경로 | +| 처리 | 완료한 유효 작업량, 처리량, 큐 대기를 포함한 지연 분포, 거절·손실·취소 수 | +| 구축 비용 | 도구 버전, 빌드 시간·최대 RAM, 실행 파일·모듈·의존성 크기 | +| 측정 조건 | OS·CPU·RAM, 빌드 옵션·커밋, 입력·설정, 워밍업, 반복 분산, 계측 자체 비용 | + +공유 원본은 할당별로 한 번만 센다. 풀 예약량·RSS·GPU 통계를 무조건 더하지 않는다. +관리하는 버퍼 예산과 전체 프로세스 메모리 상한을 구분한다. 취소가 접수됐다는 이유로 +아직 접근 중인 메모리를 반납해 성능을 높이는 구현은 실패다. + +실험 전에 작업의 메모리 한도와 필요한 지연·처리량을 정한다. 숫자는 실제 사용 조건에서 정하고 +유리한 결과를 본 뒤 고르지 않는다. 같은 조건에서 반복 측정의 변동보다 확실한 차이가 있는지, +다른 입력에서는 어떤 손해가 생기는지 함께 보고한다. + +다음 중 하나면 가설을 기각하거나 범위를 줄인다. + +- 단순한 기존 정책을 튜닝하면 차이가 사라진다. +- 관리 정보·라이브러리·호스트 비용을 포함하면 이득이 사라진다. +- 메모리를 줄인 대가로 필요한 처리량·지연·정확성을 만족하지 못한다. +- 특정 입력에만 유리하며 평가에 쓰지 않은 입력에서 재현되지 않는다. +- 새 런타임 없이 작은 라이브러리 변경으로 같은 결과를 얻는다. 이 경우 그 변경 자체의 가치를 평가한다. + +## 연구 기여를 판단하기 위한 다음 문헌 단계 + +운영 사례는 문제의 존재를 보여주지만 새로운 해법의 독창성을 증명하지 않는다. +첫 문제의 잔여 비용이 확인되면 다음 주제의 원 논문·공개 구현과 비교표를 만든다. + +- 영역 기반 메모리 관리와 소유권/대여: 회수 단위와 수명 표현의 기존 해법. +- 스트리밍 실행기의 admission control·역압력·메모리 예약: 전역 예산의 기존 해법. +- 공유 버퍼의 보관·복사 정책: 미래 수명을 모르는 온라인 결정과 관리 비용. +- 증분 계산·질의 처리: 입력 변화에 비례한 작업량과 추적 상태 비용. + +각 연구의 전제, 보장, 비용, 실패 조건, 재현 가능 구현을 기록한다. +현재는 이 논문 검토를 완료하지 않았으므로 새로운 원리나 세계 최초라는 표현을 사용하지 않는다. + +## 기존 계획에 대한 결정 + +[005의 계약 초안](005-foundation-contract.md)은 유지하되, 구현 우선순위 앞에 +**기존 해법의 재현과 남은 비용 확인**을 둔다. 버퍼 풀·자체 ABI·커널 우회 중 무엇이 필요한지는 +그 결과에서 결정한다. 컴파일러 크기 관리는 병행하는 측정 항목이다. + +다음 산출물은 새 문법 데모가 아니라 첫 후보의 **재현 가능한 비교 구현과 비용 보고서**다. +첫 후보에서 차별화가 확인되지 않으면 그 결과를 남기고 P07/P08 또는 실제 Mac 작업을 검토한다. +압축·암호화·문서·미디어·OS의 장기 목표는 유지하지만, 한 실험의 결과를 모든 분야의 성능으로 확대하지 않는다. + +2026-09-22 진행 기록: [007의 첫 비교 실험](007-buffer-retention-results.md)에서 단일 FIFO의 +원본 유지 비용을 재현했다. 기존 복사·선별 읽기 정책으로 개선됐으며 Pure 고유의 이점은 확인하지 못했다. +007의 범위에는 여러 소비자와 실제 파서의 잔여 비용 검증이 포함되지 않았다. + +추가 진행: [008](008-json-fanout-results.md)에서 실제 JSON 파서와 다중 소비자의 제한된 작업을 +검증했다. 기존 정책 대비 고유한 이점을 확인하지 못해 P03/P04는 기반 품질 과제로 유지하고, +다음 대표 연구 후보는 P07/P08의 증분 편집·갱신 비용으로 옮긴다. 실제 운영 작업 전체에 대한 결론은 아니다. + +작업 범위 보완: 공유·복사 정책 비교는 여기서 종료하고 원자료를 보존한다. +P07/P08는 아직 구현 대상으로 확정하지 않는다. 먼저 기존 문헌·공개 구현에서 +구체적인 편집 작업의 비용과 기존 해법의 한계를 확인한다. **기존 해법 채택으로 +충분한지, Pure의 실행 방식 변경이 필요한지**를 가르는 근거만 조사한다. +작은 변경을 전체 재계산과 비교해 빠르다는 사실을 재현하는 데 그치면 탈락이다. +남은 비용과 최소 확인 방법을 특정할 수 있을 때만 비교 구현으로 넘어간다. +세부 작업 기준은 [협업 방식](000-working-method.md)을 따른다. + +자체 구현 진행: P07/P08의 일반적인 증분 처리 벤치마크 대신, 초기 연구 질문 중 +추적 기록의 제거를 [작은 실행 코어](../../prototypes/forgetful-core/README.md)로 +구체화했다. 변경 기록이 부족할 때의 재계산, 조건부 의존성의 보존, 작업 한도 내 +거절을 확인했다. 이는 자체 실행 계약 구현이며 신규성·성능 우위의 결론은 아니다. diff --git a/docs/research/007-buffer-retention-results.md b/docs/research/007-buffer-retention-results.md new file mode 100644 index 0000000..36fb684 --- /dev/null +++ b/docs/research/007-buffer-retention-results.md @@ -0,0 +1,195 @@ +# Pure 연구 보고서 007: 버퍼를 공유할 때와 복사할 때 + +작성일: 2026-09-22 +상태: **비교 CLI 구현·정확성 검사·Linux AArch64 측정 완료. Pure의 성능 우월성은 확인하지 못함.** + +## 이번에 알아낸 것 + +첫 후보의 문제는 재현됐다. 작은 결과 때문에 큰 원본 버퍼가 오래 유지되면 메모리가 커진다. +하지만 이번 범위에서는 **일반적인 크기 기준 복사와 필드 선별 읽기만으로 큰 개선이 가능했다.** +현재 메모리 압력에 따라 복사를 결정하는 후보 정책은 그 단순한 방법보다 뚜렷하게 낫지 않았다. +이 결과를 근거로 새로운 런타임·ABI·컴파일러를 확대할 이유는 아직 없다. + +같은 64개 큐 한도에서 64 KiB 레코드의 64바이트만 남기는 작업을 비교했다. +공유 방식의 최대 관리 메모리는 약 **4.070 MiB**, 일반 복사 방식은 **0.137 MiB**였다. +이는 실험 코드가 요청한 저장 공간의 약 30배 차이다. 전체 앱 RAM의 30배 개선이 아니다. +해당 조건의 Linux RSS 근삿값은 각각 약 5.05 MiB와 1.56 MiB였다. + +사용 경험으로 이어질 수 있는 차이는 큰 데이터를 처리할 때 적은 메모리로 작업을 지속하는 것이다. +다만 메모리가 줄면 큐가 더 길어져 응답이 늦어질 수도 있었다. 버퍼 용량과 지연을 함께 제한해야 한다. +이번 근거는 합성 입력을 사용한 실제 네이티브 실행 측정이다. 실제 서비스·Mac·Pure 제작 경험의 측정은 아니다. + +사용자는 이번에 새 데모를 평가할 필요가 없다. 실행 가능한 산출물은 +[비교 CLI와 재현 방법](../../research/experiments/002-buffer-retention/README.md)이며, +현재 제작 화면의 문법과 동작은 바꾸지 않았다. + +## 구현한 비교 + +같은 C17 프로그램 안에서 입력 처리·큐·출력·메모리 예산·계측을 공유하고 정책을 바꾼다. +Pure VM을 호출하지 않으며 새로운 메모리 관리 원리를 구현했다고 주장하지 않는다. + +| 방식 | 처리 | +|---|---| +| `borrow` | 선택한 부분이 필요할 동안 원본 전체를 보관 | +| `copy` | 선택한 부분을 별도 버퍼에 복사한 뒤 원본 반납 | +| `threshold2/4/8` | 원본/선택 버퍼의 할당 용량 비율이 2·4·8 이상이면 복사 | +| `pressure` | 현재 활성 버퍼가 가용 예산의 절반을 넘으면 더 작은 결과를 복사 | +| `project` | 입력에서 필요한 부분만 결과 버퍼로 읽고 나머지는 작은 임시 공간으로 읽어서 버림 | +| `copy_no_pool` | 일반 복사와 같은 정책·용량 구분을 쓰되 버퍼 재사용 캐시를 끔 | + +모든 방식에 전체 예산과 큐 개수 제한을 적용했다. 캐시의 빈 버퍼도 예산에 넣고 필요하면 반납한다. +복사 중 원본과 결과가 동시에 존재하는 공간도 포함했다. 원본 크기만큼 필요한 공간이 없으면 +대기 중 출력을 처리하고, 그것으로도 불가능하면 명시적으로 실패한다. + +입력은 길이·선택 범위가 헤더에 담긴 실험용 바이너리 레코드다. 모든 입력 바이트를 실제로 읽는다. +JSON 파서나 압축 라이브러리를 비교한 것이 아니다. `project`의 범위는 이처럼 필요한 위치를 +알 수 있는 입력이다. 범용 문서에서 같은 효과를 얻으려면 별도 검증이 필요하다. + +## 실행 조건과 검증 + +- 호스트: Raspberry Pi 계열 Linux AArch64, Cortex-A76, GCC 14.2.0. 사용자 Apple Silicon에서 실행하지 않았다. +- 빌드: C17, `-O2`, 경고를 오류로 처리, 진단용 assert·계측 활성화. 최적화 빌드와 ASan/UBSan 빌드를 따로 검사했다. +- 입력: 조건별 2,048개 레코드, 약 128~169 MiB. seed 20260922, 크기·선택 범위·바이트 생성 규칙과 해시 보관. +- 일반 조건: 관리 공간 8 MiB, 큐 최대 512개. 좁은 예산 조건은 2 MiB. 별도 민감도 검사는 큐를 64개로 제한했다. +- 느린 소비자: 각 결과는 enqueue 뒤 최소 5 ms 보관, 연속 출력 완료 사이 최소 20 μs 간격. + 실제 sleep과 파일 I/O를 수행하지만 이 지연은 합성 조건이다. OS 기상 지연 때문에 정확히 20 μs마다 출력되는 것은 아니다. +- 6개 조건 × 8개 방식 × 5회 = **240개 측정**. 방식별 워밍업 후 각 반복에서 순서를 섞었고 모든 출력 해시가 일치했다. +- 일반 빌드와 ASan/UBSan 합계 **88개 검사 그룹** 통과. 바이트 단위 출력 비교, 잘린 입력, + 잘못된 범위, 예산 부족, 완료 출력의 순서, 취소, SIGTERM, 실제 출력 실패(`/dev/full`), 버퍼 회수를 검사했다. + +입력 생성·빌드·결과 검사는 시간 측정 밖이다. 측정에는 읽기·쓰기·복사·대기·계측·버퍼 정리가 포함된다. +파일 캐시를 사용하는 실행이며 출력에 `fsync`를 하지 않는다. 한 CPU로 고정했으나 온도·전력·다른 작업은 통제하지 않았다. + +## 결과 1: 같은 큐 한도에서도 기존 복사 정책이 효과적 + +64 KiB 레코드에서 64바이트를 선택하고, 모든 방식의 큐를 64개로 제한했다. +아래는 5회 중앙값이다. 메모리는 고정 관리 공간·임시 공간·버퍼·빈 캐시를 포함한 요청 바이트다. +시간은 전체 2,048개 처리가 끝날 때까지의 값이며 합성 대기가 포함된다. + +| 방식 | 최대 관리 공간 MiB | RSS 근사 MiB | 완료 시간 ms | 프로세스 내부 P99 상한 ms | +|---|---:|---:|---:|---:| +| 원본 공유 | 4.070 | 5.05 | 167.22 | 7.34 | +| 항상 복사 | 0.137 | 1.56 | 168.36 | 7.86 | +| 크기 비율 2 기준 | 0.137 | 1.56 | 167.15 | 7.86 | +| 메모리 압력 기준 | 4.070 | 5.50 | 167.12 | 7.34 | +| 필요한 부분만 읽기 | 0.074 | 1.52 | 167.51 | 7.86 | + +공유의 완료 시간 범위는 167.16~169.33 ms, 복사는 166.68~177.07 ms였다. +이 표로 작은 시간 차이의 우열이나 동일한 서비스 SLO 충족을 주장하지 않는다. +여기서 분명한 관찰은 **작은 결과를 독립시키면 유지해야 하는 원본 공간이 줄어든다**는 것이다. + +항상 복사해도 전체 명시적 복사는 128 KiB였다. 원본 입력 약 128 MiB 전체를 복사한 것이 아니다. +형식이 허용한 선별 읽기는 그 복사도 없앨 수 있었다. 다만 read/write 등 다른 데이터 이동은 여전히 있다. + +크기 관계로도 설명할 수 있다. 대략 Q개 결과가 남아 있을 때 원본 공유는 `Q × 원본 용량`, +복사는 `Q × 선택 용량 + 처리 중인 원본 용량`을 요구한다. 실제 측정에는 관리 공간·캐시·할당 반올림이 더해진다. +이 비용 관계를 Pure의 새 발견으로 취급하지 않는다. + +## 결과 2: 메모리만 줄이면 큐 대기는 커질 수 있다 + +큐 한도를 512개로 둔 같은 작은 선택 조건에서, 공유는 예산 때문에 최대 126개를 보관했고 +복사는 512개까지 보관했다. 최대 관리 공간은 공유 7.963 MiB에서 복사 0.188 MiB로 줄었지만, +프로세스 내부 P99 상한 중앙값은 약 9.44 ms에서 41.94 ms로 커졌다. + +그만큼 작업이 큐 안에서 기다렸다. 입력을 아직 읽지 않아 밖에서 대기한 시간은 이 지연에 포함되지 않는다. +따라서 공유 방식의 서비스 전체 지연이 더 짧다고 해석할 수도 없다. +**메모리 예산·큐 길이·외부 도착률을 함께 정해야 지연에 관한 비교가 가능하다.** + +이 관찰 후 추가한 것이 앞의 64개 큐 검사다. 사후에 추가한 탐색 실험임을 +[v2 계획](../../research/experiments/002-buffer-retention/plan.json)에 명시했다. + +## 결과 3: 복사를 많이 하면 항상 좋은 것도 아니다 + +64 KiB 중 48 KiB를 남기는 조건에서는 이번 할당기의 용량 반올림 때문에 선택 버퍼도 64 KiB가 된다. +공유와 복사 모두 최대 관리 공간이 약 7.963 MiB였고, 항상 복사 방식은 추가로 96 MiB를 복사했다. +크기 기준 정책은 이 불필요한 복사를 하지 않았다. + +즉, 원본과 선택 데이터의 관계뿐 아니라 **실제 할당 용량**을 고려해야 한다. +다른 용량 구분·정확한 크기 할당기에서는 결과가 달라질 수 있다. 이번 작은 풀을 최선의 할당기라고 주장하지 않는다. + +메모리 부족이 없는 빠른 소비자 조건에서는 대부분의 방식이 약 0.149 MiB만 관리했고, +전체 완료 시간 중앙값도 약 20~23 ms 범위였다. 느린 소비자에서 관찰한 큰 유지량 차이를 +모든 실행에 일반화할 수 없다. + +## 결과 4: 좁은 예산에서는 원본 유지가 진행 자체를 늦출 수 있다 + +4~256 KiB 원본과 다양한 선택 비율이 섞인 입력을 2 MiB 예산으로 처리했다. +모든 방식이 거의 같은 최대 관리 공간까지 사용한다. 이 경우에는 완료 시간과 복사량이 달라졌다. + +| 방식 | 완료 시간 중앙값 ms | 측정 범위 ms | 명시적 복사 MiB | +|---|---:|---:|---:| +| 원본 공유 | 493.63 | 491.78~495.15 | 0 | +| 항상 복사 | 191.25 | 191.04~191.77 | 51.24 | +| 크기 비율 2 기준 | 188.82 | 188.31~201.97 | 27.16 | +| 크기 비율 4 기준 | 225.45 | 224.73~229.05 | 10.71 | +| 크기 비율 8 기준 | 283.68 | 282.26~289.85 | 3.65 | +| 메모리 압력 기준 | 190.69 | 189.79~193.71 | 27.07 | +| 필요한 부분만 읽기 | 175.31 | 174.67~179.28 | 0 | + +명시적 복사 바이트가 적다고 완료가 빠르지는 않았다. 원본을 오래 보관하면 다음 입력을 받아들일 +공간이 부족해지고, 합성 보관 시간을 기다리는 동안 작업 진행이 제한된다. +압력 기준 정책은 단순한 크기 비율 2 정책과 비슷한 결과였다. 후보 정책의 고유한 이점은 확인하지 못했다. + +이 결과는 해당 로컬 파이프라인의 처리 시간이다. 약 2.6배의 시간 차이를 +서버·파서·Pure 전체의 성능 개선으로 표기하지 않는다. + +버퍼 재사용의 효과도 별도로 확인했다. 64개 큐의 작은 선택 조건에서 복사 방식의 실제 할당 호출은 +캐시를 끄면 4,096회, 켜면 65회였지만 완료 시간의 뚜렷한 개선은 확인하지 못했다. +할당 횟수 감소와 사용자 체감 성능 개선은 서로 다른 주장이다. + +## 계측 중 발견하고 수정한 문제 + +첫 실행에서 모든 RSS가 약 27.42 MiB로 동일했다. 이를 결과로 확정하지 않고 조사했다. +Linux `getrusage`의 자원 통계는 exec를 지나서도 보존되므로, 이 실행 경로의 값에는 +프로그램 실행 전 이력이 섞여 있었다. +[getrusage 문서](https://man7.org/linux/man-pages/man2/getrusage.2.html). + +현재 결과는 Linux `/proc/self/status`의 현재 메모리 이미지 `VmHWM`을 별도로 읽고, +원래 `rusage`도 구분하여 남긴다. Linux 문서는 이 RSS 계측도 근삿값이라고 설명한다. +정확한 요청 바이트 계수와 동일한 지표가 아니다. +[proc status 문서](https://man7.org/linux/man-pages/man5/proc_pid_status.5.html). + +[첫 실행 기록](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/audit.json)과 +당시 소스는 보존했다. 본문의 수치는 모두 수정된 v2 실행에서 가져왔다. +수정 후 큰 원본 유지와 작은 선택 결과가 RSS에서도 구분되는지 회귀 검사를 추가했다. + +## 이 실험이 아직 다루지 않은 것 + +- 하나의 원본을 여러 소비자가 공유하는 분기, 영속 캐시, 독립적인 여러 처리 단계의 예산 조정. +- Sonic·Node.js·.NET·Arrow 자체의 현재 구현과 직접 비교. +- 일반 JSON·문서·압축 파서와 외부 라이브러리 내부 메모리. +- 독립적인 요청 도착, 서비스 전체 지연, 네트워크·GPU·비동기 취소 경합. +- 온도·전력 통제, 별도 계측 오버헤드 보정, Mac 실행, 통계적 유의성 검정. +- 사용자에게 필요한 실제 작업의 메모리·지연 목표와 새 도구의 개발 경험. + +따라서 006의 여러 단계에 걸친 가설 전체를 기각한 것은 아니다. +**단일 입력·단일 소비자에서의 공유/복사 선택만으로는 Pure의 필요성을 입증하지 못했다**는 결론이다. + +## 다음 설계 결정 + +1. 무복사나 전용 할당기 자체를 대표 차별점으로 삼지 않는다. 기존 크기 기준 정책을 필수 비교 대상으로 유지한다. +2. [버퍼 수명 초안](../design/002-memory-lifetime.md)에는 필요한 계측 질문이 생겼다. + 원본 할당과 선택 범위, 마지막 소비자, 빈 풀의 유지량, 큐 밖의 대기까지 구분해야 한다. + 이 실험만으로 현재 계약의 비동기 보장이 구현·검증된 것은 아니다. +3. 다음 후보 검증은 실제 파서와 여러 소비자가 같은 원본을 공유하는 작업으로 한정한다. + 그 작업의 기존 구현에서도 남는 비용이 확인될 때 전역 수명·예산 정책과 선행연구 비교를 진행한다. + 같은 해법으로 충분하면 이 주제의 차별화 우선순위를 낮추고 증분 편집 후보로 이동한다. + +## 재현·원자료 + +- [비교 코드](../../research/experiments/002-buffer-retention/benchmark.c), [실행·검증 도구](../../research/experiments/002-buffer-retention/run.py) +- [실행 환경·빌드·소스 해시](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/environment.json) +- [입력·출력 해시](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/fixtures.json) +- [검증 기록](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/verification.json) +- [240개 원자료](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.csv), [중앙값·범위](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/summary.json) + +```sh +python3 research/experiments/002-buffer-retention/run.py --check-only +python3 research/experiments/002-buffer-retention/run.py --output /tmp/pure-retention-results +``` + +출력 경로는 새 디렉터리여야 한다. 측정 도구는 기존 결과를 덮어쓰지 않는다. + +2026-09-22 후속: [008](008-json-fanout-results.md)에서 실제 JSON 파서와 최대 세 소비자로 +확장했다. 그 범위에서도 기존 복사·복사본 공유 정책이 효과적이었으며, 대표 연구 문제로서의 +우선순위를 낮추고 증분 편집·상태 갱신 후보로 이동하기로 했다. diff --git a/docs/research/008-json-fanout-results.md b/docs/research/008-json-fanout-results.md new file mode 100644 index 0000000..5fcc5d0 --- /dev/null +++ b/docs/research/008-json-fanout-results.md @@ -0,0 +1,202 @@ +# Pure 연구 보고서 008: 실제 JSON 파서와 여러 소비자의 버퍼 수명 + +작성일: 2026-09-22 +상태: **실제 파서를 연결한 비교 구현·검증·210회 측정 완료. Pure 고유의 우위는 확인하지 못함.** + +## 이번 판단 + +실제 JSON과 여러 소비자를 붙여도 원본 유지 비용은 재현됐다. +그리고 이번 범위에서도 **작은 결과를 일찍 복사하고 같은 결과의 복사본을 공유하는 기존 방법이 효과적이었다.** +소비자가 하나만 남았을 때 뒤늦게 복사하는 후보는 원본의 유지 시간을 줄였지만, +최대 메모리를 충분히 줄이지 못하거나 단순 크기 기준보다 진행이 느렸다. + +이제 ‘공유 버퍼를 어떻게 복사할 것인가’만으로 새로운 언어·실행기·ABI의 필요성을 설명하지 않는다. +수명·예산 관리는 Pure의 기본 품질 요건으로 남기고, 연구의 대표 문제로서 우선순위를 낮춘다. +다음 우선 후보는 006에서 정한 **작은 변경이 큰 재계산을 일으키는 편집·상태 갱신 문제**다. + +사용 경험에 연결되는 관찰은 분명하다. 작은 메타데이터를 느린 소비자에게 넘겼다는 이유로 +이미 처리한 큰 본문까지 붙잡을 필요는 없다. 다만 이것을 사용자 앱이나 Pure 문법으로 구현한 것은 아니다. +이번 산출물은 [실행 가능한 비교 CLI](../../research/experiments/003-json-fanout/README.md)이며, +사용자가 새 데모를 조작해 평가할 과제는 없다. + +## 이전 실험에서 보완한 것 + +[007](007-buffer-retention-results.md)은 미리 알려진 바이트 범위와 단일 소비자만 다뤘다. +이번에는 다음을 추가했다. + +- **실제 JSON 파싱:** 버전을 고정한 yyjson 0.12.0으로 줄마다 JSON 전체를 파싱한다. + 선택하지 않는 필드도 구문 검사를 거친다. Python의 별도 JSON 디코더와 실제 출력 바이트를 비교한다. +- **최대 세 소비자:** 서로 다른 필드 또는 같은 필드를 소비하고, 각각 다른 시점에 완료한다. + 소비자별 순서는 유지하지만 소비자 사이의 출력 순서는 달라질 수 있다. +- **공유 소유권:** 같은 원본을 여러 번 세지 않는다. 마지막 참조가 반납되어야 해당 할당을 반환한다. +- **파서 내부 메모리:** yyjson의 할당·재할당·해제 콜백을 같은 예산에 연결했다. + 입력·결과만 작게 보이게 하고 파서 작업 공간을 제외하는 비교를 피했다. + +yyjson은 입력을 직접 사용해 문자열을 디코딩하는 in-situ 옵션을 제공한다. +필요한 문자열의 위치와 길이를 얻은 뒤 파서 문서 메타데이터는 즉시 해제하고, +문자열이 있는 입력 버퍼의 소유권만 유지했다. +실험에 사용한 [고정 버전 소스](https://github.com/ibireme/yyjson/tree/8b4a38dc994a110abaec8a400615567bd996105f)와 +MIT 라이선스·해시는 저장소에 함께 보관했다. + +## 비교 조건 + +모든 방식이 같은 파서, 읽기 경로, 소비자 스케줄러, 예산 검사와 출력 형식을 사용한다. +각 소비자의 큐는 최대 64개다. 기본 지연은 첫 소비자 0 ms, 둘째 5 ms, 셋째 8 ms다. +이는 실제 sleep으로 넣은 **합성 보관 시간**이며 네트워크나 서비스의 관측 지연이 아니다. + +| 방식 | 결정 | +|---|---| +| `borrow` | 모든 소비자가 원본을 참조 | +| `copy` | 소비자마다 필요한 문자열을 별도로 복사 | +| `shared_copy` | 같은 원본 범위를 선택한 소비자끼리 복사본 하나를 공유 | +| `threshold2/8` | 원본/결과의 할당 용량 비율이 2·8 이상이면 복사하고 같은 복사본을 공유 | +| `compact` | 소비자 완료 후 원본 참조가 하나만 남으면 작은 결과를 복사 시도 | +| `copy_no_pool` | 독립 복사와 같은 용량 구분을 쓰되 빈 버퍼 캐시는 끔 | + +`compact`는 현재 참조 상태만 보는 단순 후보다. 미래의 보관 시간을 미리 알지 않는다. +다른 소비자가 여전히 원본을 참조할 때 그 버퍼를 옮기지 않는다. +단일 소비자 조건에서는 완료 전 참조 수 변화가 없으므로 이 후보의 복사 시도도 없다. +이 특정 후보가 모든 동적 정책을 대표하지는 않는다. + +6개 조건 × 7개 방식 × 5회로 **210개 측정**을 수행했다. +기본 입력은 본문 32 KiB·패딩 8 KiB와 작은 문자열·메타데이터를 포함한 JSON 1,024개, 약 40.13 MiB다. +혼합 입력은 약 47.57 MiB, 메타데이터가 많은 입력은 512개·약 5.02 MiB다. +모두 생성 규칙과 seed를 공개한 합성 데이터다. 실제 운영 트래픽은 아니다. + +호스트는 Linux AArch64, Cortex-A76, GCC 14.2.0이다. 한 CPU로 고정했으나 온도·전력은 통제하지 않았다. +워밍업 뒤 방식 순서를 반복마다 섞었다. 아래 값은 5회 중앙값이며 통계적 유의성 검정은 하지 않았다. +사용자 Mac에서의 측정도 아니다. + +## 결과 1: 느린 작은 소비자가 큰 원본을 붙잡는다 + +첫 소비자가 본문을 바로 출력하고, 두 느린 소비자가 작은 `tag` 문자열을 보관하는 조건이다. +‘관리 공간’은 고정 상태·큐·입력·결과·파서·빈 버퍼 캐시를 포함한 최대 요청 바이트다. 전체 RSS와 다르다. + +| 방식 | 최대 관리 공간 MiB | 명시적 선택 복사 MiB | 전체 완료 시간 ms | +|---|---:|---:|---:| +| 원본 공유 | 4.230 | 0 | 132.60 | +| 소비자마다 복사 | 0.304 | 32.109 | 132.28 | +| 복사본 공유 | 0.297 | 32.055 | 132.28 | +| 크기 비율 2 기준 | 0.297 | 32.055 | 132.24 | +| 크기 비율 8 기준 | 0.297 | 0.055 | 132.18 | +| 마지막 참조에서 복사 | 4.231 | 0.055 | 132.70 | + +크기 비율 8 정책은 빠른 본문 소비자에게 원본을 잠깐 빌려주고 작은 문자열만 독립시켰다. +이 조건에서는 약 **4.23 → 0.297 MiB**로 관리 공간이 줄면서 명시적 복사도 작았다. +이것은 일반 C 코드의 정책 변경으로 얻은 결과다. Pure를 호출하거나 컴파일하지 않았다. + +원본 공유와 마지막 참조에서의 복사는 최대 공간이 거의 같았다. +둘째 소비자가 완료될 때까지는 원본을 반납할 수 없기 때문이다. +원본 활성 바이트·시간의 중앙값은 약 540,593에서 340,108 byte·seconds로 줄었지만, +이미 형성된 최대 유지량은 해결하지 못했다. + +모든 방식에서 최대 큐 개수는 `[1, 64, 64]`였다. 느린 두 소비자의 내부 P99 상한도 각각 +약 5.24 ms, 8.39 ms였다. 이 수치는 입력이 프로그램 밖에서 대기한 시간을 포함하지 않는다. +완료 시간은 합성 지연의 영향을 크게 받으므로 1 ms 미만의 차이를 서비스 속도 개선으로 해석하지 않는다. + +## 결과 2: 소비자마다 복사하면 같은 결과도 중복된다 + +세 소비자가 모두 같은 32 KiB 본문을 선택하도록 바꿨다. + +| 방식 | 최대 관리 공간 MiB | 명시적 선택 복사 MiB | 완료 시간 ms | +|---|---:|---:|---:| +| 원본 공유 | 4.230 | 0 | 133.81 | +| 소비자마다 복사 | 4.296 | 96 | 134.44 | +| 복사본 공유 | 2.262 | 32 | 133.59 | +| 크기 비율 2 기준 | 2.262 | 32 | 133.66 | +| 마지막 참조에서 복사 | 4.512 | 32 | 134.20 | + +소비자별 독립 복사는 원본을 해제하면서도 결과를 중복 보관했다. +**한 번 만든 복사본을 공유하는 기존 기법**으로 복사량과 관리 공간을 줄일 수 있었다. +같은 내용의 모든 문자열을 검색·통합한 것은 아니다. 한 레코드에서 같은 위치와 길이를 선택한 경우만 공유했다. + +늦은 복사는 복사 중 원본과 결과의 공존, 빈 버퍼 캐시 때문에 최대 공간이 더 커질 수도 있었다. +‘마지막 순간에 복사하면 항상 메모리가 작다’는 결론은 성립하지 않았다. + +## 결과 3: 좁은 예산에서는 단순한 정책이 작업 정체도 줄인다 + +본문·패딩 크기가 섞인 JSON을 2 MiB 예산으로 처리했다. +빠른 소비자는 본문을, 느린 소비자들은 작은 문자열을 선택한다. + +| 방식 | 최대 관리 공간 MiB | 완료 시간 중앙값 ms | 측정 범위 ms | +|---|---:|---:|---:| +| 원본 공유 | 2.000 | 516.88 | 516.42~519.74 | +| 복사본 공유 | 1.047 | 134.70 | 134.66~134.73 | +| 크기 비율 2 기준 | 1.047 | 134.65 | 134.60~134.81 | +| 크기 비율 8 기준 | 1.047 | 134.63 | 134.51~135.14 | +| 마지막 참조에서 복사 | 2.000 | 330.51 | 330.33~332.81 | + +큰 원본이 남아 있으면 다음 입력과 파서가 사용할 공간이 부족해진다. +늦은 복사도 정체를 완화했지만, 이 작업에서는 작은 결과를 일찍 분리하는 방법이 더 효과적이었다. +이 수치는 특정 파이프라인의 처리 시간이며 서버나 Pure 전체가 몇 배 빨라진다는 뜻은 아니다. + +큰 본문을 **느린** 소비자에게 보내는 별도 조건에서는 크기 비율 8 정책의 최대 관리 공간이 +4.237 MiB, 비율 2 정책은 2.268 MiB였다. 복사량을 적게 만드는 기준이 메모리까지 항상 최소화하지 않는다. +소비자의 역할과 실제 할당 크기를 같이 봐야 한다. + +## 파서와 풀을 빼면 결과가 왜곡된다 + +메타데이터가 많은 입력에서는 크기 기준 정책의 최대 관리 공간이 약 0.172 MiB였다. +그 실행의 파서 활성 할당 최고치는 약 0.094 MiB였다. +혼합 입력에서는 파서 최고치가 약 0.500 MiB였다. 입력·출력만 계측해서는 이 공간을 설명할 수 없다. +각 영역의 최고치는 발생 시점이 다를 수 있으므로 서로 더해서 총 최고치라고 하지 않는다. + +파서 콜백이 메모리 부족 시 기존 소비자를 진행시키도록 했으므로, +`parse_call_ns`에는 그 진행과 대기 시간이 섞일 수 있다. 순수 파서 CPU 비용으로 사용하지 않는다. +전체 처리 시간·프로세스 CPU·영역별 메모리를 별도로 기록했다. + +또한 빈 버퍼를 보관하는 풀은 할당을 재사용하는 대신 공간을 유지한다. +세 소비자의 다른 속도 조건에서 독립 복사는 풀을 켜면 약 0.304 MiB, +끄면 약 0.241 MiB였다. 풀의 존재만으로 메모리 최적화를 주장하지 않는다. +이번 작은 풀을 각 플랫폼의 최선의 할당기와 비교한 것도 아니다. + +## 검증과 재현 가능한 범위 + +일반 빌드와 ASan·UBSan 빌드에서 총 **108개 검사 그룹**이 통과했다. +파서 자체도 계측해서 빌드했다. 검사는 다음을 포함한다. + +- 1·2·3 소비자의 독립 출력, 큐 슬롯 재사용, Unicode·이스케이프·NUL·빈 문자열. +- 파서 문서 해제 뒤에도 남아 있는 입력/복사본의 유효성, 공유 결과의 마지막 반납. +- 잘린 JSON, 잘못된 필드 타입·누락, 잘못된 UTF-8, 선택하지 않은 필드의 구문 오류. +- 파서 작업 공간이 예산을 넘는 경우의 실패와 반납, 너무 긴 레코드 거절. +- 취소·SIGTERM·실제 출력 실패와 남은 소유권 정리. + +210개 측정 모두 Python JSON 디코더로 만든 소비자별 기대 해시·개수와 일치했고, +모든 측정에서 관리 예산을 지켰으며 종료 후 남은 버퍼 할당은 0이었다. +이 검사가 yyjson의 모든 동작이나 다중 스레드·장치 취소 경합을 검증한 것은 아니다. + +## 남은 한계와 다음 결정 + +이 실험은 실제 파서를 쓰지만 입력·소비자 지연은 합성이고 실행은 한 스레드다. +JSON 한 줄을 모두 읽은 뒤 처리하며 지속 캐시·네트워크·독립적인 요청 도착·GPU를 다루지 않는다. +Sonic·Node.js·.NET 자체의 비교도 아니다. 운영 부하에서 자동 정책이 유리할 가능성까지 기각하지 않는다. + +현재 증거로 결정할 수 있는 범위는 다음과 같다. + +1. 버퍼 수명·역압력·정확한 예산 계측은 **필요한 기반 기능**으로 유지한다. + 자체 ABI나 커널 우회가 이 개선에 필요했다는 주장은 하지 않는다. +2. 공유/복사 정책만으로 Pure의 차별화를 설명하는 이번 비교 연구는 여기서 종료한다. + 남은 특수 조건을 계속 추가해서 후보 정책이 이기는 사례만 찾지 않는다. + 구체적인 새 작업이나 요구 조건이 생기면 보존한 증거부터 검토한다. +3. 다음 조사 후보는 006의 P07/P08, 즉 **큰 문서·상태에서 작은 변경에 필요한 + 계산과 갱신 범위를 줄이는 문제**다. 기존 증분 구현의 잔여 비용을 먼저 확인하고, + Pure가 바꿀 수 있는 경로가 있을 때만 기준 구현을 만든다. 전체 재계산 대비 + 증분 처리의 알려진 이점을 확인하는 것만으로 새 실험을 정당화하지 않는다. + 이 후보에서도 증분 정보를 유지하는 메모리 비용을 포함해야 한다. + +## 코드와 원자료 + +- [실험 설명·실행 방법](../../research/experiments/003-json-fanout/README.md) +- [비교 구현](../../research/experiments/003-json-fanout/benchmark.c), [실행·검증 도구](../../research/experiments/003-json-fanout/run.py) +- [실험 계획](../../research/experiments/003-json-fanout/plan.json) +- [환경·버전·빌드·소스 해시](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/environment.json) +- [입력과 소비자별 출력 해시](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/fixtures.json) +- [검증 기록](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/verification.json) +- [210개 원자료](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.csv), [중앙값·범위](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/summary.json) + +```sh +python3 research/experiments/003-json-fanout/run.py --check-only +python3 research/experiments/003-json-fanout/run.py --output /tmp/pure-json-fanout-results +``` + +새 결과 디렉터리만 허용한다. 의존성은 고정 버전으로 포함되어 있어 재현 시 다운로드가 필요 없다. diff --git a/docs/research/009-bounded-display-integration.md b/docs/research/009-bounded-display-integration.md new file mode 100644 index 0000000..0269d34 --- /dev/null +++ b/docs/research/009-bounded-display-integration.md @@ -0,0 +1,141 @@ +# Pure: 기록을 버리는 실행 계약을 작성물에 연결 + +2026-09-22. **구현·기능 검증 완료. 캐시는 선택 기능으로 유지한다.** + +2026-09-23 후속: **기록 공간을 코드 교체 준비 공간과 공유하도록 구현했다.** +현재 코어는 19,128 B이며, 아래 첫 통합의 20,272 B에서 1,144 B 줄었다. +현재 비용과 검증은 문서 끝의 ‘공유 작업 공간’에 구분해 기록한다. + +이제 기존 `.pure` 작성물의 표시 계산에서 결과를 보관하거나 기록을 지우고, +한 번의 장면 계산에 쓸 작업량을 제한할 수 있다. 소수점 계산·입력 순서·실패 시 +상태 복구는 기존 의미를 보존한다. [앞선 C 코어](../../prototypes/forgetful-core/README.md)의 +정수 DAG를 교체 삽입하지 않고, 실행 계약을 기존 Zig/Wasm 코어에 이식했다. + +## 이번에 끝낸 범위 + +- 전체 장면에 하나의 작업 한도. 부족하면 부분 장면 없이 명시적인 표시 오류. +- 결과와 의존성 요약을 함께 버리는 고정 크기 캐시, 버릴 수 있는 변경 일지. +- 증거가 부족하면 재계산. 입력 이벤트나 외부 효과는 재실행하지 않는다. +- 이벤트 실패, 상태 재정렬, 프로그램 교체, 초기화, signed zero를 포함한 보존 검사. +- 터미널의 `limits`·`forget`·`costs`와 브라우저/Node 공통 API. + +조건부 추적 기록을 다룬 첫 코어와 달리, 현재 작성물의 표시식에는 분기가 없다. +논리 연산도 기존처럼 양쪽을 모두 계산한다. 실제 LOAD에서 상태 의존성을 모으며, +호스트가 전달한 의존성 주석을 신뢰하지 않는다. 캐시는 입력이 없는 상수의 재사용에 +변경 일지를 요구하지 않는다. 코드 교체는 상수 캐시도 폐기한다. + +## 채택 확인: 기존 작성물 세 개 + +같은 새 실행기에서 캐시 0개와 32개를 비교했다. 각 파일마다 관측 100회와 +600프레임, 주기적인 누름·놓음 20회를 재생했다. **총 2,160개 스냅샷이 일치**했다. +이는 정해진 예제 입력 기록이며 사용자 제작 경험이나 대규모 앱 평가가 아니다. + +| 작성물 | 실행된 표시 명령: 캐시 끔 → 켬 | 전체 호출 시간 중앙값 ms: 끔 → 켬 | +|---|---:|---:| +| counter | 9,360 → 93 | 3.111 → 2.693 | +| toggle | 10,080 → 54 | 3.066 → 2.663 | +| charge | 11,520 → 566 | 4.587 → 4.100 | + +시간은 입력 처리와 snapshot 구성까지 포함한 720개 호출 묶음이다. 로딩·컴파일·DOM· +통계 읽기·동등성 검사는 제외했다. 5회씩 순서를 번갈아 측정했으며 Linux AArch64 +한 기기에서 CPU 고정·전력 통제를 하지 않았다. P99 지연이나 macOS 성능이 아니다. +표시 명령이 크게 줄어도 전체 호출 시간의 감소는 이번 측정에서 약 11~13%였다. + +charge의 계측 작업 단위는 오히려 21,600에서 26,909로 늘었다. 변경 기록 확인도 +작업이기 때문이다. 이 단위는 VM 명령 개수나 CPU 시간과 같지 않다. + +## 첫 통합의 비용과 결정 — 별도 기록 공간을 둔 빌드 + +| 영역 | 이전 → 현재 | +|---|---:| +| 코어 고정 구조 | 19,072 → 20,272 B | +| Wasm 파일 | 48,182 → 53,125 B | +| Wasm 선형 메모리 용량 | 65,536 → 65,536 B | + +**1,200바이트의 추가 기록 공간을 소비한다. 메모리를 절감한 결과가 아니다.** +캐시를 꺼도 이 고정 공간은 예약된다. 코드·상태·캐시를 모두 임의로 늘릴 수 있는 +범용 할당기, 기록 공간의 실제 반납, 큰 문서의 의존성 표현은 구현하지 않았다. + +따라서 작업량 제한은 기본 적용하되 캐시는 기본으로 켜지 않는다. 작은 예제의 +이득만으로 모든 작업의 기본 정책을 바꿀 근거는 부족하다. 사용자는 CLI나 임베딩 +API에서 선택할 수 있다. 현재 한도에서는 기본 작업량 2,048로 모든 정상 표시식을 +완료할 수 있다. 실제 응답 시간이나 앱 전체 작업량의 보장은 아니다. + +이 단계의 완료 기준은 **실제 작성물로 연결, 기존 의미 보존, 한도 초과·복구 검증, +추가 비용 확인**이었다. 모두 확인했으므로 같은 예제의 측정 확장은 하지 않는다. +학술적 신규성·대규모 서비스 우위·Pure 전체 완성은 이번 결과의 주장이 아니다. + +## 검증과 재실행 + +Zig 네이티브 20개와 JavaScript/CLI/Wasm 45개 검사가 통과했다. 이 통합은 Zig의 +검사와 실제 Wasm 실행으로 검증했다. 앞선 C 코어의 ASan·UBSan 결과를 이번 +Zig 변경의 검사로 대신하지 않는다. + +실제 Chromium에서도 작성·수정·기록 삭제·한도 초과·복구의 11개 확인을 통과했다. +UI의 한도 초과 검사는 테스트 세션의 모듈 응답에서 기본 작업량만 1로 바꿔 +진입했으며 제품 파일은 바꾸지 않았다. 검사 뒤 원래 설정과 빈 화면으로 복원했다. + +- [통합 소스](../../prototypes/workbench/evaluation.zig), [실행기](../../prototypes/workbench/web/runtime.mjs) +- [사용 방법](../../prototypes/workbench/README.md), [정확한 계약](../../prototypes/workbench/CONTRACT.md) +- [빌드·검사·소스 해시](../../prototypes/workbench/results/2026-09-22-evaluation/verification.json) +- [비교 원자료](../../prototypes/workbench/results/2026-09-22-evaluation/comparison.json) +- [첫 통합의 브라우저 확인 절차](../../prototypes/workbench/results/2026-09-22-evaluation/sources/qa-evaluation.js) +- [브라우저 결과](../../prototypes/workbench/results/2026-09-22-evaluation/browser.json), + [한도 초과 화면](../../output/playwright/pure-evaluation-limit.png) + +```sh +node prototypes/workbench/cli.mjs prototypes/workbench/examples/charge.pure +node prototypes/workbench/evaluation.test.mjs +node prototypes/workbench/evaluation-check.mjs --output /tmp/pure-evaluation-new.json +``` + +## 공유 작업 공간 — 2026-09-23 + +평소의 계산 기록과 교체할 코드를 준비하는 공간이 동시에 필요하지 않다는 +점을 이용했다. 두 자료를 한 공간에 번갈아 담고, 현재 실행 중인 프로그램과 +상태는 별도로 유지한다. 교체 준비 중에는 기존 표시식을 재계산한다. +교체를 시작한 뒤 발생한 입력·초기화도 새 프로그램에 상태를 보존할 때 반영한다. +교체가 끝나거나 취소되면 같은 공간을 빈 계산 기록용으로 돌려준다. + +| 항목 | 별도 공간 → 공유 공간 | +|---|---:| +| 전체 코어 고정 구조 | 20,272 → **19,128 B** | +| 기능 추가 전 19,072 B 대비 증가량 | 1,200 → **56 B** | +| Wasm 파일 | 53,125 → 53,952 B | +| Wasm 선형 메모리 | 65,536 → 65,536 B | + +56 B는 제어 정보 48 B와 Zig의 유니언 안전성 확인용 8 B다. 캐시·변경 기록 +1,152 B는 기존 9,392 B 준비 공간 안에 들어간다. 코어 자료구조에서 1,144 B를 +줄였지만 Wasm 파일은 827 B 늘었다. OS 페이지 반납이나 프로세스 전체 RAM의 +절감을 측정한 결과는 아니다. 첫 통합의 시간 측정도 이 빌드의 속도 증거로 재사용하지 않는다. + +이전 빌드와 771개 스냅샷에서 상태·출력·진단 및 표시 작업량·명령 수·캐시 적중이 +일치했다. 입력·초기화·재적용·기록 삭제를 섞었다. 별도로 교체 준비 중 기존 이벤트와 +표시를 실행하고, 512개 명령을 준비하는 동안 설정·기록 삭제·초기화를 반복해 +교체할 코드가 훼손되지 않는지 검증했다. 거절·취소·중복 취소도 확인했다. +Zig 네이티브 23개, JavaScript/CLI/Wasm 47개 검사가 통과했다. +최신 Wasm을 실제 Chromium에 올린 12개 확인도 통과했다. 작성·수정·기록 삭제· +한도 초과·복구와 공유 공간의 크기를 확인했으며, 처리되지 않은 브라우저 오류는 없었다. +UI 한도 초과는 첫 통합과 같은 테스트 전용 모듈 응답 변경으로 유도했고, +검사 뒤 원래 설정과 빈 작업물로 복원했다. + +트레이드오프는 코드 교체를 시작하면 캐시를 잃는다는 것이다. 실패·취소하더라도 +캐시까지 보존하지 않으며, 아주 작은 작업 한도는 재계산에 부족할 수 있다. +현재 프로그램·상태·이벤트 진단은 보존한다. 동시 스레드나 비동기 재진입은 지원하지 않는다. + +- [현재 빌드·검증 기록](../../prototypes/workbench/results/2026-09-23-workspace/verification.json) +- [이전 빌드와 비교한 원자료](../../prototypes/workbench/results/2026-09-23-workspace/comparison.json) +- [보존한 이전 소스·Wasm 목록](../../prototypes/workbench/results/2026-09-22-evaluation/sources/manifest.json) +- [현재 Wasm에서 교체 중 실행 검사](../../prototypes/workbench/workspace.test.mjs) +- [현재 브라우저 검사 절차](../../prototypes/workbench/qa-evaluation.js), + [12개 확인 결과](../../prototypes/workbench/results/2026-09-23-workspace/browser.json), + [한도 초과 화면](../../output/playwright/pure-workspace-limit.png) + +```sh +node prototypes/workbench/workspace.test.mjs +node prototypes/workbench/workspace-check.mjs --output /tmp/pure-workspace-new.json +``` + +이로써 표시 계산 계약의 통합과 그 추가 공간을 줄이는 후속 구현을 마무리한다. +이 기능의 합성 측정은 더 늘리지 않는다. 큰 앱과 문서의 메모리 문제는 +별도의 실제 작업으로 확인해야 하며 이번 결과로 해결했다고 주장하지 않는다. diff --git a/docs/research/010-roadmap.md b/docs/research/010-roadmap.md new file mode 100644 index 0000000..a9ae94d --- /dev/null +++ b/docs/research/010-roadmap.md @@ -0,0 +1,288 @@ +# Pure: 작은 OS로 설계하는 네이티브 애플리케이션 런타임 + +수정일: 2026-09-23. 상태: 연구·구현 방향. 새 아키텍처의 성능이나 신규성을 입증한 문서가 아니다. + +## 목적과 이번 수정 + +**Pure는 하나의 작은 OS로 설계하는 독립 네이티브 애플리케이션 런타임이다.** +**마이크로 런타임의 크기 제약을 최상위 조건으로 유지한다.** 항상 필요한 실행·소유권·자원· +완료 규칙을 코어에 두고, 프로토콜·저장 구현·UI·미디어는 앱이 필요한 구성만 결합한다. +전체 기능을 기본 탑재하거나 영역마다 관리 서비스·스레드·큰 큐를 예약하지 않는다. +코어·선택한 전체 빌드·유휴 메모리·영역과 진행 작업별 비용을 확인하며, 의존성으로 옮긴 비용도 포함한다. +실행·스케줄링·메모리·보호·자원 수명·저장·통신의 운영 모델을 Pure가 소유한다. +입출력·연산·상태 사이의 연결을 효율적으로 표현하고 실행하는 것이 중심 목적이다. +현재 호스트 OS와 그 프로세스는 Pure를 구동할 때 받는 수단·제약이며 앱의 기본 모델이 아니다. +프로세스 배치 관리에서 출발하거나 작은 OS라는 목표를 장기 부가 기능으로 미루지 않는다. + +**Pure 외부와의 통신도 핵심 요구사항이다.** 일반 웹 서비스·다른 런타임·외부 프로그램과 +상대의 프로토콜·형식으로 요청을 주고받을 수 있어야 한다. Pure끼리의 자체 형식을 모든 +상대에게 요구하지 않는다. 표준 호환 접점의 의미와 비용을 지원하면서 내부 운영 모델은 Pure가 정한다. + +net·RPC·DB·UI는 런타임이 연결하는 관점에서 모두 +입출력과 그 안의 연산을 가진 영역이다. 핵심 설계 대상은 어디의 어떤 데이터를 어떤 연산과 +출력에 연결하며, 어떤 표현·소유권·수명·실행 시점으로 그 연결을 수행할지다. + +서버·CLI·데스크톱 앱이 같은 기반을 사용하며, 화면 없는 앱에 UI 모듈이나 프레임 루프를 +요구하지 않는다. 이 문서는 목표와 연구 방향이며 지원 완료나 성능 우위의 선언이 아니다. + +**SQL을 사용하지 않는다.** 여기서 DB는 데이터 접근·갱신·영속성의 범위를 뜻한다. +RAM·storage를 테이블·행·SQL 실행 모델에 맞추지 않고, 소비자가 필요한 범위와 연산·갱신· +수명에서 데이터 배치를 정한다. 단순히 자체 쿼리 문법이나 범용 객체 저장소로 교체하는 것도 +목표가 아니다. 저장 위치가 달라도 필요한 데이터를 연결하되, RAM의 쓰기 완료와 영속화 +완료가 같다고 취급하지 않는다. + +효율적인 인터페이스와 그 아래의 불필요한 추상화 제거는 전체 애플리케이션 경로에 적용한다. +기존 앱·프레임워크·런타임의 구조나 개념을 필수 전제로 삼지 않는다. 첫 UI 실험의 범위를 +제품 전체의 정의로 확대한 것은 Codex의 오류다. UI를 완성한 뒤 net/RPC/DB를 부가 기능으로 +붙이는 순서도 철회한다. 반대로 데이터 처리 도구만으로 UI 사용성을 검증한 것으로 세지 않는다. + +웹과 같은 유연한 작성·연결·갱신 경험을 제공하되 브라우저를 필수 실행 기반으로 삼지 않는다. +표준 포맷·웹 호환성은 유지한다. 017에서 웹 호환성을 브라우저 소유의 레이아웃·편집으로 +해석한 것은 잘못이며, 해당 실험으로 네이티브 UI 기반이 불필요하다는 결론을 내릴 수 없다. + +효율은 사용자의 표현·조작 비용과 기계의 메모리·계산·전달 비용을 함께 뜻한다. +입력에서 실제 출력까지 필요한 표현·변환·복사·대기·수명 관리에서 설계를 시작한다. +현재 DSL, VM, 상태 객체, 이벤트 버스, 뷰 트리, 렌더러 경계도 변경 가능한 구현 선택이다. +파일을 합치거나 추상화에 새 이름을 붙인 것으로 비용이 제거됐다고 판단하지 않는다. +009는 작은 코어의 계약과 공간 재사용을 검증했다. 전체 인터페이스 경로의 효율은 아직 검증하지 않았다. + +## 연결에서 해결할 문제 + +사용자는 필요한 입력·연산·상태·출력의 관계와 관찰 가능한 동작을 표현한다. Pure는 그 의미를 +보존하는 실행 경로에서 중간 표현·전달·보관·대기를 줄일 수 있는지를 다룬다. 연구 단위는 +개별 API의 호출 비용뿐 아니라 여러 경계를 지나는 한 작업의 전체 데이터·실행 경로다. + +- 연결: 실제로 필요한 값·범위·변경과 그것을 소비하는 연산·출력은 무엇인가? +- 표현과 이동: 어떤 변환이 필요한가? 같은 데이터를 다시 포장하거나 복사하는 단계는 제거 가능한가? +- 보관과 수명: 누가 언제까지 데이터를 쓰는가? 공유·이동·복사·재계산 중 전체 비용에 맞는 선택은 무엇인가? +- 실행과 전달: 언제 계산하고 공개해야 하는가? 합치거나 미루거나 생략해도 결과·순서·기한이 유지되는가? + +이 질문은 설계 기준이다. 현재 구현이 모든 연결의 최적 경로를 자동으로 찾는다는 뜻은 아니다. +예를 들어 DB 결과의 일부만 화면과 RPC 응답에 필요할 때, 필요한 부분을 전달하는 경로와 +전체 결과를 여러 객체·버퍼로 바꾸는 경로를 같은 동작으로 비교할 수 있다. 저장·전송 포맷이 +요구하는 변환과 비동기 소비자의 수명도 비용에 포함한다. API 모양을 통일한 것만으로 성공으로 세지 않는다. + +## 연결되는 영역과 보존할 의미 + +| 범위 | 책임 | +|---|---| +| 공통 실행 기반 | 앱 로직·상태, 작업 실행·스케줄링, 데이터와 자원의 소유권·수명·예산, 비동기 완료·취소·오류, 대기열 한도와 과부하 처리 | +| net | 연결·송수신·스트림·프로토콜 연결, 부분 입출력과 버퍼 수명, 느린 상대와 연결 종료 처리 | +| RPC | 요청·응답·스트리밍, 인코딩과 호출 식별, 기한·원격 실패·재시도와 중복 실행의 의미 | +| 데이터·영속성 | 필요한 범위의 접근·갱신·일관성, 참조의 수명, 변경 공개·내구성·복구의 경계; SQL·관계형 스키마를 내부 모델로 요구하지 않음 | +| UI | UI 표현·레이아웃·텍스트·입력 대상 판정·포커스·캡처·스크롤·렌더링; 공통 실행 기반을 사용하는 하위 체계 | +| 플랫폼 연결부 | Pure의 운영 정책을 실행하는 자원 수단·완료·오류·지원 한도; 현재 호스트의 메모리·시계·실행·저장·통신·UI 접근을 연결 | +| 표준 호환 경계 | 기존 문서·미디어의 읽기·쓰기와 외부 환경 연결; 내부 표현과 변환·복사 비용을 명시 | +| 선택 가능한 웹 호스트 | 배포·호환·검증 경로 중 하나; 네이티브 애플리케이션의 필수 실행 의존성이 아님 | + +이 표는 연결에 참여하는 영역의 의미를 정리한 것이다. 기능별 엔진을 먼저 완성하고 나중에 +접착 계층을 추가하는 구현 순서가 아니다. 계층 수·패키지 구조·하나의 범용 이벤트 버스를 고정하지 않는다. +필요한 기능만 연결할 수 있어야 한다. DB 지원을 곧 새 저장 엔진 개발로, net 지원을 곧 자체 +네트워크 스택 개발로 확정하지 않는다. 기존 구현을 연결하더라도 앱에서 쓸 수 있는 실행· +수명·오류 계약까지 제공해야 하며 외부 라이브러리 이름만 나열한 것으로 지원을 세지 않는다. + +공통 계약의 후보는 [연산 의미](../design/001-operation-semantics.md)와 +[메모리·수명](../design/002-memory-lifetime.md)에 이미 있다. 구현·검증 상태를 확인해 +재사용하거나 수정한다. 모든 기능을 현재 숫자 VM에 넣지는 않는다. UI는 일관된 조작 의미를 +소유하면서 글꼴·래스터화 등의 전문 구현 및 OS의 입력·접근성 서비스와 연결한다. + +## 다음 연구 질문과 첫 작업물 + +> 필요한 입출력과 연산의 관계에서 실행 경로를 구성할 때, +> 관찰 가능한 동작과 각 영역의 의미를 유지하면서 +> 표현·이동·보관·대기의 전체 비용을 얼마나 줄일 수 있는가? + +- 바꿀 결정: 어떤 표현을 권위 있는 원본으로 두고, 어떤 중간 표현·계층을 없애거나 필요할 때만 생성할 것인가? +- 남은 질문: 전체 입력→출력 경로에서 중복 상태·변환·갱신과 관리 비용이 실제로 어디에 집중되는가? +- 최소 확인: 같은 작업물의 변경을 로컬 조작과 RPC로 요청하고 DB에 저장한 뒤, 확정된 결과를 응답과 UI에 반영하는 경로. UI 없이도 앱 로직·RPC·DB가 동작해야 한다. +- 채택 기준: 같은 표현력·관찰 가능한 동작에서 경로 전체의 비용이 줄고 사용자 작성·수정이 가능해야 한다. +- 중단 기준: 튜닝한 단순 구현과 차이가 없거나 표현력·정확성·반응성을 희생해야 하면 해당 가설을 닫는다. + +기존 첫 작업물은 사용자가 대상·값·연결·입력 동작을 정의할 수 있는 작은 공간형 작업물이다. +예를 들어 데이터 항목을 두 영역에 배치하고 키보드나 드래그로 이동하며 연결된 값·표시를 바꾼다. +실행 중 연결·표현을 수정하고 저장·복원할 수 있어야 한다. +새 동작을 만들 때 코어 수정이 필요하면 제작 도구로서의 기준을 통과하지 못한다. +현재 작성 도구에서 이어 만들 수 있는 부분은 재사용하고 별도 조작 데모를 반복해서 만들지 않는다. + +다음 작업은 기존 작성물의 변경·저장 기능을 애플리케이션 경로로 확장하는 것이다. +로컬 조작 또는 네트워크 RPC → 앱의 변경 처리 → DB 저장 → 응답·표시를 하나의 실제 +작업으로 연결한다. 먼저 요청 데이터·작업·버퍼의 수명과 실패 경계를 정하고 필요한 범위만 +구현한다. 이는 제안하는 첫 검증 작업이며 제품 전체를 공동 편집기로 한정하는 결정은 아니다. + +네이티브 UI 경로도 이 작업의 일부다. 창 크기와 배율이 바뀌면 Pure의 배치 결과에서 렌더링과 +입력 대상 판정이 함께 갱신되어야 한다. UI 없이도 동일한 앱의 변경·저장·요청 처리를 사용할 +수 있어야 한다. 모의 RPC·메모리 배열 저장만으로 net/DB를 검증했다고 세지 않는다. + +입력→변경 확정→계산→출력 전체를 첫 단계부터 연결한다. +대상 하나의 변경에 전체 작업물의 호스트 객체·스냅샷·출력 표현을 다시 만드는지 확인하고, +그 비용을 제거하는 후보와 같은 기능의 단순 직접 구현을 비교한다. +필수 데이터, 계산 기록, 출력 자료, 편집 자료의 비용을 구분한다. + +## 설계 자유와 지켜야 할 의미 + +하나의 원본 정의에서 실행에 필요한 접근·갱신 경로를 준비하고 중복 해석과 모델 동기화를 줄이는 +방식을 후보로 둔다. 그래프·트리·바이트코드는 필요성이 확인되면 선택한다. +사용자가 보는 표현과 CPU/GPU에 유리한 배치가 같아야 한다고 강제하지 않는다. +파생 표현은 생성·동기화·저장 비용보다 이익이 있을 때 유지한다. + +입력을 같은 연결 규칙에서 다루되 의미 차이는 보존한다. 버튼 누름·해제처럼 빠뜨리면 동작이 +달라지는 사건과 최신 위치를 사용할 수 있는 연속 값은 같은 버리기 정책을 쓰지 않는다. +출력도 GUI·TUI·장치의 지원 기능을 명시하고, 공통 모델 때문에 최소 공통 기능에 가두지 않는다. +지원하지 않는 기능을 조용히 다른 의미로 바꾸지 않는다. + +경계를 없애거나 공간을 공유하더라도 소유권, 효과 실행 순서, 비동기 완료와 자원 수명을 정의한다. +공통 실행 계약이 외부 효과까지 원자적으로 되돌려 주는 것으로 표현하지 않는다. 설계에서 +취소 요청과 실제 완료, RPC 응답 유실과 원격 실행 실패, DB 커밋과 화면 반영을 구분한다. +재시도·중복 제거·복구의 보장 범위를 명시하고, UI 규칙의 숫자 상태 롤백을 재사용해 대신하지 않는다. +격리나 플랫폼 접근 제한을 없앤 것으로 효율을 만들지는 않는다. +커널 우회·자체 ABI도 금지된 선택이 아니다. 없애려는 구체적인 경로와 비용이 근거가 되어야 한다. +현재의 캐시·공유 공간 구현도 목표 달성을 방해하면 교체한다. + +## 단계와 완료 조건 + +| 단계 | 산출물 | 완료 기준 | +|---|---|---| +| 1. 네이티브 애플리케이션 경로 | 기존 작업의 로직·실제 net/RPC·DB와 네이티브 UI를 필요한 범위로 연결 | UI 없는 실행도 가능; 완료·취소·저장 실패·연결 종료에서 결과와 수명을 설명하고 브라우저 의존 없이 동작 | +| 2. 실용 앱 완성 | 같은 앱의 작성·조회·변경·저장·복구, UI의 텍스트·스크롤·편집 | 실행 중 수정, RPC 기한·재시도, DB 트랜잭션·재시작, IME·포커스·접근성을 실제 사용 흐름에서 확인 | +| 3. 규모 확대 | 동시 요청·큰 데이터·느린 소비자·지속 실행과 UI 부하 | 전체 메모리·대기열·지연·처리량·누락을 실제 작업에 맞춰 측정하고 기능별 비용을 설명 | +| 4. 환경·분야 확대 | 추가 네이티브 환경·프로토콜·DB·미디어·웹 호환 경로 | 공통 계약의 재사용과 분야별 의미·표현력·호환성을 함께 유지 | +| 5. 운영 규모 검증 | 다중 사용자·장애 복구·배포와 장기 실행 | 충돌·과부하·권한·격리·내구성 보장을 검증한 범위에서 서비스 규모를 주장 | + +앞 단계의 작업물을 발전시킨다. UI·서버·CLI 중 첫 구현의 편의가 전체 제품 범위를 결정하지 +않도록 한다. 현재 Zig 실행 코어와 웹 호스트는 부분 계약을 검증한 구현이다. 통합 net/RPC/DB와 +네이티브 UI 경로는 아직 구현하지 않았다. 기존 코어의 재사용 여부도 새 요구에 따라 판단한다. +네이티브 숫자 코어 테스트나 독립 데이터 처리 실험만으로 애플리케이션 런타임 완료로 세지 않는다. + +첫 목표 환경은 사용자가 밝힌 M칩 macOS다. 현재 작업 환경인 Linux의 빌드·검사 결과로 +macOS의 창·Metal·IME·접근성 동작을 검증했다고 주장하지 않는다. 플랫폼 선택은 런타임의 +애플리케이션 의미와 분리하고, 실제 macOS 검증이 남으면 완료 보고에 명시한다. + +## 측정과 규모 선택 + +입력·요청 도착부터 작업 결과·RPC 응답·DB 커밋·화면 제출까지 해당 경로의 지연을 구분해 잰다. +UI가 없는 작업에 화면 지표를 요구하지 않는다. 실제 화면 표시 시점을 확보할 수 없는 +환경에서는 제출 시점과 구분한다. 큐 대기, 취소·누락, 편집 적용 비용도 포함한다. +데이터 복사, 파생 객체 생성, 동기화할 표현의 수, 살아 있는 바이트, 관리 정보, +호스트·GPU·I/O 버퍼·DB 캐시·드라이버·전체 프로세스 비용을 기록한다. 외부 DB 서버의 비용을 +클라이언트 밖으로 옮겨 절감으로 세지 않는다. 같은 물리 공간을 중복 합산하지 않는다. +컴파일러 배포 크기·빌드 시간·피크 RAM·산출물 크기도 별도로 관리한다. + +첫 크기는 실제 상호작용과 필수 상태 크기로 정한다. 대상 수와 함께 관계 수, +변경 비율, 보이는 범위, 계산 비용을 기록한다. 이전 안의 100만 행·128 MiB를 +인터페이스의 보편적 합격 기준으로 고정하지 않는다. +사용자 작업과 목표 기기의 제약으로 예산·응답 목표를 정하고 후보 측정 전에 확정한다. + +같은 상호작용을 잘 구현한 직접 경로와 비교하며 기존 구현에도 부분 갱신·재사용을 허용한다. +특정 계층의 비용을 호스트·드라이버·디스크로 옮긴 것만으로 성공으로 세지 않는다. +대용량 질의가 실제 병목이 될 때만 해당 데이터 도구와 추가 비교한다. +선행연구는 중복·과장을 피하기 위해 확인하되 기존 아키텍처 채택을 강제하는 기준으로 쓰지 않는다. + +다음 착수 항목은 **기존 작업의 변경·저장을 기준으로 UI·RPC·DB가 공유할 실행·수명 계약을 +구체화하고, 그 계약을 쓰는 네이티브 애플리케이션 경로를 구현하는 것**이다. 범용 기반을 +한꺼번에 만들지 않고 한 요청의 접수·실행·외부 효과·완료·자원 회수부터 연결한다. +각 기능의 지원 범위와 남은 의존성을 기록하고, 실제 병목과 비교 선택지가 있을 때 측정한다. +공통 API나 직접 렌더링을 만들었다는 사실만으로 메모리 효율이나 신규성을 주장하지 않는다. + +현재 진행 — [019](019-byte-storage.md): [018](018-native-connections.md)에서 잘못 도입한 +SQLite를 제거하고 실제 TCP → 바이트 저장 → 전송 경로로 대체했다. 호출자의 본문을 그대로 +저장하고 필요한 범위를 매핑해 전송까지 유지한다. 버전 충돌·동시 쓰기·부분 전송·갱신 중 +이전 참조·공개 전후의 실패·프로세스 종료·재시작을 확인했다. 전체 성능 우위나 최종 데이터 +배치를 결정한 것은 아니다. + +후속 [020](020-prepared-connections.md): `.pure`로 저장·응답할 구간을 작성하고 서버 시작 때 +별칭·중첩 구간을 최종 접근 범위와 원본 길이 조건으로 준비한다. 실제 TCP에서 작성 변경과 +오류·효과·원본 수명을 확인했다. + +후속 [021](021-field-connections.md): 경계의 필드를 이름으로 선언하고 실제 입력 숫자에 따라 +선택할 구간을 계산한다. 한글·이모지 문서에서 선택한 내용만 저장·응답하는 네이티브 경로를 +확인했다. 필드 배치·별칭은 준비 때 해소하고 데이터 의존 단계만 실행한다. 현재는 u32le· +고정 바이트·가변 꼬리 필드와 입력 기반 범위에 한정되며 일반 레코드 런타임은 아니다. +다음에는 앱의 읽기·변경·저장 경로에 필요한 연산·버전 관계를 다룬다. 데이터 전체를 객체로 +재구성하지 않으면서 서로 다른 생산자의 값·수명을 연결할 필요가 있는지 실제 작업에서 결정한다. +필드 종류나 예제 숫자를 늘리는 것만으로 진행하지 않는다. 일반 피드백·동적 분기·여러 효과는 별도 계약과 +검증이 필요하며 전역 최적 경로를 자동으로 찾는다고 주장하지 않는다. +네이티브 UI와 범용 RPC·데이터 실행 모델까지 구현한 것은 아니므로 1단계 전체 완료로 세지 않는다. + +통신 방향 추가 — [자체 통신 형식 설계](../design/006-native-protocol.md): HTTP는 호환 경계이며 +Pure 간 통신의 필수 모델이 아니다. 작성된 연결·필드에서 바이너리 형식을 준비하는 방향으로 +진행한다. 먼저 현재 get/put 전용 진입점을 앱 입력·효과와 분리해 같은 저장 작업을 연결한다. +이는 위의 읽기·변경·저장 확장에 앞서 정리할 경계다. 이후 제한된 scalar 피어 +[026](026-secure-peer-link.md)와 같은 문서 작업의 인증 피어 [030](030-authenticated-document-peer.md)에서 +TLS 1.3/TCP를 선택 전송으로 구현했다. 범용 전송 정책·QUIC은 미결정이다. 새 자체 전송은 +구체적인 병목과 동등한 보안·전달 조건에서 판단한다. 현재 평문 HTTP나 제한된 TLS 피어의 +결과로 QUIC 대비 우위를 주장하지 않는다. + +운영 모델 정정 — [작은 OS로서의 Pure](../design/007-execution-domains.md): Pure 코어가 실행· +자원·권한·연결·수명을 관리하고 플랫폼 연결부는 현재 환경의 수단·결과·제약을 제공한다. +다음 진입점 분리에서 요청 접수·진행·완료·회수의 소유자를 이 코어로 정하고, 기존 저장 +작업을 내부 연결과 외부 입력 양쪽에서 실행한다. 실행 영역은 Pure 내부 단위이며 프로세스 +관리자의 포장으로 만들지 않는다. 호스트의 자원 부족·입출력 실패도 실제 검증 범위에 포함한다. +원격 형식과 영역별 스레드·소켓·VM을 필수로 요구하지 않는다. 직접 소켓 구현의 낮은 추가 +비용과 Kitex 수준의 RPC를 목표로 삼되, 동등한 작업·보장으로 확인하기 전 성능 달성을 +주장하지 않는다. 통합 OS 코어·영역 관리자·샌드박스·직접 하드웨어 구동은 아직 미구현이다. + +후속 [022](022-execution-core.md): 저장 프로필에 한정된 실행 영역의 작업 슬롯·접수 예산· +준비 대기열·완료·결과 대여를 구현하고 기존 HTTP 서버를 연결했다. 내부 결과 전달은 소켓· +직렬화·추가 본문 할당 없이 원본의 수명을 유지한다. 실제 파일 저장·갱신 및 기존 TCP 회귀와 +sanitizer 검증을 통과했다. 위의 미구현 항목 중 영역별 기본 수명 관리를 일부 구현한 상태이며 +통합 OS·일반 앱 작성·자체 보안 RPC·네이티브 UI 완료는 아니다. +첫 공개 목표와 남은 요건은 [체크포인트](../../CHECKPOINT.md)에서 유지한다. + +후속 [023](023-document-edits.md): 같은 저장 프로필에 조건부 바이트 부분 변경을 추가했고 +네이티브 CLI와 HTTP가 같은 준비된 효과·버전을 사용한다. 큰 문서의 작은 변경은 변경 구간만 +입력하지만 저장소의 전체 파일 재작성 비용은 남는다. core/editor/server 선택 빌드와 release +코어의 크기·의존성 회귀 검사를 추가했다. 일반·sanitizer 빌드의 36개 통합 시나리오를 통과했다. +현재 코어의 작은 수치로 전체 앱 메모리나 범용 런타임 완성을 주장하지 않는다. 다음 작업도 +작성 모델·실제 네이티브 앱·보안 통신의 남은 연결을 대상으로 하며 기능 목록을 늘리지 않는다. + +후속 [024](024-native-app-instances.md): 기존 Workbench 계산을 명시적인 인스턴스로 분리하고, +같은 .pure를 네이티브 CLI로 빌드했다. 입력·스냅샷·조건부 저장·복원이 공통 C 코어를 통과한다. +현재 scalar 모듈의 한도와 앱별 고정 예약 비용은 남는다. 공통 코어에는 VM·컴파일러를 추가하지 +않았다. 실제 창, 실시간 작성 적용, JSON 프로젝트 변환, 보안 RPC·비동기 효과·규모 검증은 +계속 체크포인트의 미완료 항목이다. + +## 이전 구현 기록 + +아래 기록의 당시 ‘다음 작업’은 이 문서 앞부분의 애플리케이션 런타임 범위·우선순위보다 우선하지 않는다. + +진행 기록: [011](011-direct-presentation.md)에서 현재 작성 도구의 전체 장면 객체와 DOM 재생성 +경로를 제거했다. 결과·오류·작성 흐름은 보존했지만 속도·전체 RAM 개선은 입증하지 못했다. +공간형 직접 조작이나 다양한 출력의 구현까지 완료한 것으로 세지 않는다. + +후속 [012](012-refresh-ownership.md): 프로파일에서 실행 입력마다 편집기 정보를 갱신하던 비용을 +확인하고 연결을 제거했다. 실제 작성 흐름을 보존하면서 동일 조건의 동기 입력 처리 시간을 줄였다. +원격 서비스·대규모 작업이나 실제 화면 표시 지연의 우위로 일반화하지 않는다. + + +후속 [013](013-authored-pointer-input.md): 대상에 연결한 좌표 입력을 추가하고 이동·제한·취소를 +작성 코드로 처리했다. 실제 마우스·터치·동시 조작·CLI 재생을 검증했다. 속도 비교는 추가하지 않았다. +다음은 배치 결과를 저장·복원하고 여러 대상을 편집하는 하나의 실제 작업물이다. +현재의 소스 저장은 현재 배치 상태를 보존하지 않는다는 제한부터 해결해야 한다. +규모나 성능 측정은 이 작업의 구체적인 병목과 선택지를 확인할 때 수행한다. + + +후속 [014](014-layout-persistence.md): 여러 대상의 배치를 파일로 저장하고 새 실행기에 복원하는 +경로를 구현했다. 적용된 정의·현재 값·미적용 초안을 구분하고 전체 복원을 원자적으로 확정한다. +다음 설계 문제는 대상마다 반복되는 상태·입력·반응 선언이다. 도형 숫자를 억지로 늘린 벤치마크보다, +대상 집합에 하나의 반응을 적용하면서 값과 소유권을 분리할 수 있는 실행 표현을 먼저 검토한다. + + +후속 [015](015-shared-rules.md): record/item의 연속 필드 구간과 한 번만 저장되는 rule 본문을 +연결했다. 편집·선언 순서 변경·동시 조작·저장 복원에서 대상별 값이 유지된다. +현재는 정적인 이름 기반 대상이며 동적 생명주기 핸들은 아니다. 다음 제약은 대상 수만큼 +늘어나는 입력 연결과 키 사건의 모든 연결 순회다. 실제 추가·삭제 작업을 지원할 때 +대상 식별과 선택한 대상만 실행하는 경로를 함께 설계해야 한다. + + +후속 [016](016-pointer-routing.md): 여러 도형을 하나의 포인터 입력에 연결하고 사건의 대상 +필드 구간을 규칙에 전달한다. 대상별 네 반응의 복제를 없애고 접촉 소유권은 따로 유지한다. +키보드 선택은 여전히 작성한 ID 조건에 의존한다. 다음은 선택 없음·대상 제거·저장 복원과 +선택/캡처의 차이를 정의한 뒤 선택 대상에 키 사건을 직접 전달하는 경로다. 정적 대상 번호를 +동적 생명주기 식별자로 취급하지 않는다. + + +후속 [017](017-web-layout-boundary.md): Codex가 웹 호환성 요구를 잘못 해석해 브라우저의 +문서 작업을 우선 구현했다. CSS 레이아웃·textarea·스크롤·표준 UTF-8 파일과 Pure의 숫자 +입력을 연결한 검증은 유효하지만, 브라우저에 UI를 맡기는 구조는 네이티브 목표를 충족하지 +않는다. ‘자체 레이아웃은 필요 없다’는 프로젝트 차원의 결론을 철회한다. 이 호스트의 추가 +최적화는 현재 우선 작업에서 내리고, 파일 호환·입력 정책·취소 동작의 참고 자료로 보존한다. diff --git a/docs/research/011-direct-presentation.md b/docs/research/011-direct-presentation.md new file mode 100644 index 0000000..00f8577 --- /dev/null +++ b/docs/research/011-direct-presentation.md @@ -0,0 +1,102 @@ +# Pure 011: 입력에서 출력까지, 중간 장면 객체 제거 + +작성일: 2026-09-23. 실제 작성 도구의 호스트·출력 경로 변경과 브라우저 검증. + +후속 [012](012-refresh-ownership.md)에서 남은 비용을 프로파일하고 실행 입력과 편집기 갱신의 +연결을 제거했다. 아래 수치는 이 보고서 시점의 구현 결과로 유지한다. + +## 알아낸 것과 사용 경험 + +입력마다 전체 장면 스냅샷과 DOM 요소를 다시 만드는 경로를 제거했다. +계산 결과를 기존 출력 요소에 직접 전달하고 값이 달라진 속성·텍스트만 쓴다. +사용자가 코드를 적용하고 키보드·화면 버튼으로 동작시키는 기존 작성 흐름에 연결했다. +별도의 고정 동작 데모를 만들지 않았다. 작성 중인 코드와 적용된 작업의 구분도 유지한다. + +**중간 표현과 요소 재생성을 없앤 것은 확인했다. 속도 향상이나 전체 RAM 절감은 입증하지 못했다.** +이번 계측에서 새 경로의 시간 중앙값은 오히려 더 컸고, 표본 간 편차도 컸다. +이를 새 범용 아키텍처나 학술적인 신규성으로 주장하지 않는다. + +직접 확인할 동작은 자신이 작성한 값·도형을 입력으로 바꾸고, 정의를 수정하거나 초기화하는 것이다. +입력 갱신 중 같은 SVG 요소를 유지한다. 실제 정의 변경 시에는 출력 구조를 다시 만들 수 있다. +표시 오류가 발생하면 해당 출력을 숨기고, 복구되면 같은 요소를 다시 표시한다. +전체 표시 계산 한도 초과에서는 모든 출력을 숨기되 성공한 입력의 상태는 유지한다. + +## 제거한 것과 남긴 것 + +이전 경로: + + 입력 → Wasm 상태 변경 → 전체 상태·장면 JS 객체 → 새 DOM 요소 → 전체 교체 + +현재 경로: + + 입력 → Wasm 상태 변경 → 표시 계산 검증 → 기존 DOM 목적지에 값 전달 + +`runtime.present(target)`은 상태 맵·장면 객체를 만들지 않는다. +`output.mjs`는 코드 적용 시 선언과 실제 DOM 목적지를 연결하고, 값은 DOM과 비교한다. +독립적인 숫자 상태 복사본이나 가상 장면 트리를 추가하지 않았다. +DOM은 출력을 보관하며 동작을 결정하는 상태는 여전히 Wasm 안에 있다. +저장·CLI·검사용 `snapshot()`은 명시적으로 호출할 때 유지한다. + +작업 한도가 중간에 끝나도 일부 결과만 내보내지 않기 위해 숫자 결과 96개와 유효 표시 24개를 +담는 **792 B의 호스트 버퍼**를 재사용한다. typed array 객체 오버헤드, 적용 시 만드는 binding +참조 배열, DOM 목적지 참조는 별도 추가 공간이다. 이 비용을 Wasm 메모리 수치 안에 숨기지 않는다. +무효한 출력도 DOM 요소를 보관하므로 전부 제거하는 이전 경로보다 해당 상황의 유지량이 늘 수 있다. + +기존 계산 순서·작업량 제한은 유지한다. 매 갱신의 전체 바인딩 평가, 진단 객체 생성, +편집기 부가 정보 갱신까지 없앤 것은 아니다. ‘모든 할당 0’이나 ‘변경 크기에 비례하는 실행’은 아니다. +출력 콜백 중 실행기 재진입은 거절하며, 출력 어댑터는 신뢰된 동기 코드다. +임의 어댑터 예외의 외부 효과까지 원자적으로 되돌리지는 않는다. + +## 실제 브라우저 비교 + +이전 app/runtime 소스를 보존하고, 테스트 세션의 응답만 바꿔 같은 페이지·Wasm·컴파일러에서 비교했다. +24개 출력과 상태 2개를 사용했다. 이동 입력은 x를 0.25 늘리고, 도형 위치와 텍스트 하나만 바꾼다. +각 실행에서 준비 입력 20회 뒤 입력 100회(누름·해제 200사건)를 처리했다. +이전/현재를 번갈아 각각 3회 실행했으며 모든 최종 상태·출력이 일치했다. + +| 관찰 항목, 입력 100회당 | 이전 | 현재 | +|---|---:|---:| +| DOM 요소 생성 호출 | 9,200 | **0** | +| 전체 snapshot 호출 | 200 | **0** | +| 장면 전체 교체 | 200 | **0** | +| 기존 장면 요소의 동일성 유지 | 아니오 | 예 | +| 동기 실행 시간 중앙값 | 648.7 ms | 733.1 ms | +| 동기 실행 시간 범위 | 633.6–941.2 ms | 579.1–751.4 ms | + +현재 장면의 변경은 위치 속성 100회와 값 텍스트 100회였다. 텍스트 노드의 교체까지 +없어진 것은 아니다. 상태 표시와 비용 표시도 요소를 유지하지만 변경된 텍스트는 갱신한다. + +시간은 **브라우저 안의 합성 키보드 dispatch 시작부터 DOM 제출까지**이며 계측 비용을 포함한다. +OS 입력 도착·레이아웃·페인트·실제 화면 표시 시간은 포함하지 않는다. +실제 키보드·포인터 흐름은 별도의 브라우저 조작 검사로 확인했다. +세 표본에서 성능 우위를 주장할 수 없다. 유리한 수치를 얻기 위해 반복 측정을 늘리지 않았다. + +app/runtime/output의 비압축 소스 합은 29,334 → 32,558 B로 **3,224 B 증가**했다. +Wasm 파일 53,952 B, 코어 구조 19,128 B, 선형 메모리 65,536 B는 변경하지 않았다. +호스트 heap·브라우저 전체 RSS·GC 시간·GPU 비용의 절감은 측정하지 않았다. + +## 검증과 재현 + +JS/CLI/Wasm 검사 52개가 통과했다. 새 검사에는 보존한 이전 실행기와 결과·계산량 비교, +0~80 작업 한도, 표시 오류, 이벤트 롤백, 부호 있는 0, 정의 교체, 재진입 거절과 어댑터 예외 복구가 있다. +Zig 소스·Wasm은 변경하지 않았으므로 이전 네이티브 검사를 다시 실행한 것으로 세지 않는다. + +브라우저에서는 새 경로 비교 13개, 기존 작성 흐름 44개, 표시 한도·복구 12개를 확인했다. +후자의 한도 초과는 테스트 전용 모듈 응답에서 한도를 1로 바꿔 유도한다. +검사 후 원래 설정과 빈 작업물로 복원한다. 이전 증거의 이미지는 덮어쓰지 않았다. + +- [변경 전 소스 목록과 해시](../../prototypes/workbench/results/2026-09-23-presentation/before/manifest.json) +- [검사와 현재 소스 해시](../../prototypes/workbench/results/2026-09-23-presentation/verification.json) +- [브라우저 비교 원자료](../../prototypes/workbench/results/2026-09-23-presentation/browser.json) +- [작성 흐름](../../prototypes/workbench/results/2026-09-23-presentation/browser-authoring.json), + [표시 한도](../../prototypes/workbench/results/2026-09-23-presentation/browser-limit.json) +- [실행 계약](../../prototypes/workbench/CONTRACT.md), [재실행 방법](../../prototypes/workbench/README.md) +- [직접 출력 구현](../../prototypes/workbench/web/output.mjs), [실행기](../../prototypes/workbench/web/runtime.mjs) + +## 다음 결정 + +출력 요소 유지와 스냅샷 경로 분리는 실제 작성 도구에 남긴다. 구조 제거의 효과를 관찰할 수 있는 +첫 구현이며, 속도·메모리 효율 가설의 합격 판정은 보류한다. 아직 일반 SDK로 고정하지 않는다. +이 결과만으로 데이터 규모를 키우거나 새 캐시·ABI를 추가하지 않는다. +다음 변경의 근거는 현재 경로의 남은 비용과 더 풍부한 직접 조작의 요구여야 한다. +사용자가 대상·관계를 직접 조작하는 기능과 여러 출력 방식의 의미 보존은 이후 작업으로 남아 있다. diff --git a/docs/research/012-refresh-ownership.md b/docs/research/012-refresh-ownership.md new file mode 100644 index 0000000..d9fde5d --- /dev/null +++ b/docs/research/012-refresh-ownership.md @@ -0,0 +1,100 @@ +# Pure 012: 실행 입력이 편집기를 다시 계산하던 경로 제거 + +작성일: 2026-09-23. 실제 작성 도구 변경, 단계별 프로파일과 브라우저 비교. + +## 알아낸 것과 바뀐 동작 + +011에서 DOM 재생성을 없앴지만 시간이 줄지 않았다. 다음 변경에 앞서 비용을 나눠 측정하니 +출력 계산·전달보다 **바뀌지도 않은 소스 편집기의 정보를 갱신하는 경로**가 더 컸다. +입력 이벤트마다 `refresh()`가 줄 번호, UTF-8 바이트 수, 스크롤, 커서 위치를 갱신했다. +입력과 코드 편집은 독립적인데 하나의 갱신 함수로 묶여 있었다. + +실행 갱신에서 이 경로를 제거했다. 실제 소스 변경에서만 편집기 정보를 갱신하고, +코드 적용은 초안과 적용된 정의의 일치 상태만 갱신한다. +화면에 이미 같은 값이 있으면 상태·진단·버튼 속성도 다시 쓰지 않는다. +새 캐시, 구독 그래프, 이벤트 버스, VM 명령이나 런타임 계층을 추가하지 않았다. + +사용자는 전과 같이 코드를 작성하고, 키보드·화면 버튼·프레임으로 동작시킨다. +실행 중에도 미적용 초안은 그대로 남고 코드 편집·적용·오류 위치 이동의 표시가 갱신된다. +이번 결과는 **불필요한 갱신 연결을 제거한 구현 개선**이다. 새로운 계산 원리의 발견은 아니다. + +## 먼저 확인한 비용 + +24개 출력, 상태 하나, 입력 하나를 작성했다. 이동 입력은 x를 0.25 늘리고 도형과 텍스트에 반영된다. +준비 입력 20회 뒤 입력 100회(누름·해제 200사건)를 처리했다. +기존 app 모듈의 테스트 전용 응답에 타이머를 삽입해 한 차례 프로파일했다. + +| 구간 | 호출 수 | 누적 시간 | +|---|---:|---:| +| 계산·출력 전달 `present` | 200 | 26.6 ms | +| 비용 표시 | 200 | 56.6 ms | +| 편집기 정보 갱신 | 200 | 259.1 ms | +| 전체 `refresh` | 200 | 359.6 ms | + +전체 입력 묶음은 375.9 ms였다. 부분 구간은 refresh 안에 포함되므로 합산하지 않는다. +CDP는 이 구간 주변에서 레이아웃 201회와 레이아웃 시간 177.7 ms를 기록했다. +줄 번호를 쓴 뒤 스크롤 위치를 읽는 경로도 있었다. 이 관찰을 근거로 편집기 갱신 연결을 +먼저 제거했으며, 바인딩 캐시나 출력 평가 정책은 변경하지 않았다. + +## 변경 전후 비교 + +직전 구현의 소스를 보존했다. before 실행에서는 app.js만 테스트 응답으로 교체한다. +런타임·출력 어댑터·컴파일러·Wasm·작성된 소스는 양쪽이 동일하다. +프로파일 타이머는 이 비교에 넣지 않았다. 인코딩 호출과 편집기 DOM 변경을 계측했다. +before/after 순서를 섞어 각각 3회 실행했으며 최종 상태·출력·이벤트 수와 실제 SVG 위치가 모두 같았다. + +| 입력 100회당 관찰값 | 이전 | 현재 | +|---|---:|---:| +| 소스 UTF-8 인코딩 호출 | 400 | **0** | +| 인코딩 결과 배열의 누적 바이트 | 410,800 B | **0 B** | +| 편집기 정보의 DOM 변경 기록 | 800 | **0** | +| CDP 레이아웃 횟수, 매 실행 | 201 | **1** | +| 동기 처리 시간 중앙값 | 837.9 ms | **152.8 ms** | +| 동기 처리 시간 범위 | 709.7–1,151.8 ms | **137.8–327.4 ms** | + +중앙값 기준 약 81.8% 감소다. 세 표본 모두 현재 쪽 시간이 더 짧았다. +다만 표본 수가 작고 편차가 크며 CPU·전력·다른 프로세스를 고정하지 않았다. +이 수치는 동일 세션의 **동기 합성 입력 dispatch부터 DOM 제출까지**다. +실제 키 입력 도착부터 화면 표시까지의 지연이나 큰 앱의 처리량으로 일반화하지 않는다. +계측 자체의 비용도 포함한다. CDP 카운터는 입력 묶음 전후의 브라우저 작업을 포함할 수 있다. +이전 보고서의 별도 실행 시간과 비율을 곱하거나 서로 직접 비교하지 않는다. + +410,800 B는 없어진 인코딩 결과의 누적 용량이다. 피크 RAM 절감량도 전체 복사 바이트도 아니다. +추가 런타임 버퍼는 없다. app.js의 비압축 소스는 16,014 → 16,510 B로 496 B 증가했다. +코어·Wasm·011의 792 B 출력 버퍼와 바인딩 평가 순서는 그대로다. + +## 보존한 계약과 검사 + +소스 입력, Tab 삽입, 코드 조각·파일·예제 삽입은 편집기 정보 갱신 경로를 사용한다. +성공·실패한 코드 적용은 초안 상태를 다시 확인한다. 커서 이동·스크롤·오류 위치 이동은 +해당 표시만 갱신한다. 실행 입력·프레임·초기화는 소스 내용을 재계산하지 않는다. +UI의 오류 문구는 완성된 문자열을 한 번에 갱신하고 기존 사건 오류·표시 오류 구분을 유지한다. + +브라우저 비교·편집 분리 검사 13개, 기존 작성 흐름 44개, 표시 한도·복구 12개가 통과했다. +검사에는 Unicode 바이트 수, 줄 번호, 미적용 초안을 둔 실행·초기화, 성공/실패 적용, +오류 위치 이동, Tab·코드 조각 삽입, 저장·복원, 키보드·포인터·모바일 동작이 포함된다. +Zig·컴파일러·런타임은 변경하지 않았으므로 기존 52개 JS/CLI/Wasm 검사나 네이티브 검사를 +다시 실행한 것으로 세지 않는다. 이번 변경의 검증은 실제 브라우저에서 수행한다. + +- [변경 전 소스·해시](../../prototypes/workbench/results/2026-09-23-refresh/before/manifest.json) +- [현재 소스·검증 기록](../../prototypes/workbench/results/2026-09-23-refresh/verification.json) +- [원인 프로파일](../../prototypes/workbench/results/2026-09-23-refresh/profile-before.json) +- [6회 비교와 편집 분리 검사](../../prototypes/workbench/results/2026-09-23-refresh/browser.json) +- [작성 흐름 검사](../../prototypes/workbench/results/2026-09-23-refresh/browser-authoring.json) +- [표시 한도·복구 검사](../../prototypes/workbench/results/2026-09-23-refresh/browser-limit.json) +- [비교 절차](../../prototypes/workbench/qa-refresh.js), [실행 스크립트](../../prototypes/workbench/qa-refresh.py) + +```sh +python3 prototypes/workbench/qa-refresh.py --cli /path/to/playwright-cli.js --session pure-path --output /tmp/pure-refresh-new.json +``` + +사전에 로컬 페이지를 연 Playwright CLI 0.1.21 세션이 필요하다. 결과 파일은 덮어쓰지 않는다. +검사 종료 후 원래 모듈과 빈 작업물로 복원한다. + +## 결정과 남은 범위 + +실행과 편집의 갱신 경로 분리를 채택한다. 현재 질문에 답했으므로 같은 입력의 추가 측정은 하지 않는다. +이 결과를 ‘모든 추상화 제거가 빠르다’는 근거로 삼지 않는다. 비용이 컸던 것은 실제로 변하지 않은 +편집 자료까지 갱신하도록 연결한 경로였다. 필요한 연결과 수명은 유지한다. +전체 표시식 평가·비용 숫자 포맷·호스트 진단 생성은 남아 있다. 자동으로 다음 미세 최적화에 들어가지 않는다. +다음 인터페이스 확장은 사용자가 대상과 관계를 직접 조작하는 실제 요구를 기준으로 잡는다. diff --git a/docs/research/013-authored-pointer-input.md b/docs/research/013-authored-pointer-input.md new file mode 100644 index 0000000..57d76e9 --- /dev/null +++ b/docs/research/013-authored-pointer-input.md @@ -0,0 +1,109 @@ +# 013 — 작성한 규칙으로 직접 조작하기 + +2026-09-23. 이번 변경은 입력 표현력과 실제 조작 경로의 구현이다. +속도·RAM 비교나 합성 대규모 벤치마크는 수행하지 않았다. + +## 문제와 이번 결정 + +기존 작성물은 키 또는 버튼의 누름·놓음으로만 조작할 수 있었다. +화면에 보이는 대상을 잡고 이동시키려면 좌표 입력이 필요했다. +호스트에 별도 이동 모델을 만들면 작성한 상태와 화면 위치를 동기화해야 한다. +이번에는 좌표를 기존 사건 실행의 일시적인 피연산자로 전달하고, +이동·제한·취소·연결된 출력은 작성한 상태와 표현식으로 처리했다. + +```text +input drag = pointer(tile) +on drag.move { + x = clamp(event_x - grab_x, 20, 564) + y = clamp(event_y - grab_y, 100, 276) +} +``` + +전체 [직접 조작 예제](../../prototypes/workbench/examples/direct.pure)는 +`.down`에서 잡은 위치와 시작 위치를 기억하고, `.up`에서 마지막 위치를 반영한다. +`.cancel`은 시작 위치로 복귀한다. 좌우 키도 같은 x 상태를 바꾼다. +오른쪽 영역에 들어갔는지는 같은 상태를 읽는 다른 출력으로 표시한다. +제한이나 복귀 정책을 바꿀 때 실행기·렌더러 수정은 필요 없다. + +## 구현 경계 + +브라우저는 대상 찾기, 좌표 변환, 포인터 캡처와 해제를 담당한다. +SVG 루트가 캡처를 유지하므로 대상 또는 장면 밖에서도 조작을 끝낼 수 있다. +클라이언트 좌표는 SVG 변환의 역행렬로 장면 좌표에 연결한다. +축소된 모바일 화면에서도 작성 코드는 같은 좌표계를 사용한다. + +코어에는 좌표 읽기 명령 두 개와 `dispatch_point(event,x,y)`가 추가됐다. +좌표는 호출 동안만 존재하며 영구 상태 슬롯을 만들지 않는다. +기존 명령·키 입력 번호와 사건의 원자적 확정, 변경 기록, 출력 경로는 유지한다. +호스트의 소유권 기록은 활성 포인터 입력마다 출처와 마지막 좌표를 가진다. +조작 위치의 권위 있는 값은 작성한 상태에만 있다. + +하나의 입력은 한 포인터가 소유하며 다른 대상에는 독립적인 접촉을 허용한다. +놓기는 마지막 좌표를 반영한다. 취소·캡처 상실·초점 상실은 별도 cancel 사건이다. +이미 종료된 포인터의 늦은 놓기나 이동이 두 번째 사건을 만들지 않는다. +새 큐, 프레임 단위 묶기, 최신 좌표만 남기는 정책은 추가하지 않았다. +브라우저가 전달한 move를 처리하며 하드웨어의 모든 샘플 수신을 보장하지는 않는다. + +하나의 사건에서 뒤쪽 반응이 실패하면 앞쪽 상태 변경도 확정하지 않는다. +이는 전체 드래그를 자동 되돌리는 것과 다르다. 전체 복귀는 작성한 cancel 반응이다. +취소 반응 자체가 실패하더라도 호스트의 입력 소유권은 해제하고 오류를 보존한다. +CLI의 `point NAME PHASE X Y`도 같은 호스트 API와 코어를 사용한다. + +## 검증 목적과 결과 + +검증 질문은 **작성한 직접 조작이 입력의 시작·변경·종료·취소에서 일관되는가**다. +처리량이나 실행 시간을 연구 결과로 제시하지 않는다. 테스트 도구가 자동 출력하는 +실행 시간도 성능 증거로 해석하지 않는다. + +- 네이티브 26개: 좌표 문맥, 유한 값과 사건 범위, 표시 계산에서 좌표 접근 거부, + 여러 반응에 걸친 실패 시 원자성 및 기존 코어 동작. +- JavaScript 59개: 컴파일러·런타임·출력·CLI 회귀, 입력 소유권, 중복/오래된 사건, + 마지막 놓기 좌표, 좌표로 변경한 상태의 출력 갱신, CLI 재생 동등성. +- 브라우저 직접 조작 26개: 실제 마우스로 장면 밖까지 끌기와 놓기, + 캡처 해제, 초점 상실과 취소, 작성 코드의 제한 변경, 실패 후 정리, + 축소 화면의 터치와 서로 다른 대상의 두 접촉, 동일 대상의 중복 접촉. + 페이지 오류는 없었다. +- 기존 작성 흐름 브라우저 44개: 빈 문서 시작, 편집·적용·초기화, 키 입력, + 파일 저장·불러오기, 오류 표시와 모바일 레이아웃의 회귀 검증. + +브라우저는 Linux AArch64 Chromium이다. 터치는 Chromium CDP 입력을 사용했다. +초점 상실과 단일 포인터 cancel은 사건 주입, 캡처 상실은 실제 캡처 해제 API, +마우스 조작은 Playwright 마우스 입력으로 확인했다. +실제 휴대폰·Safari·펜 압력·기울기 동작을 검증했다고 주장하지 않는다. + +근거: [검증 로그](../../prototypes/workbench/results/2026-09-23-pointer/verification.json), +[브라우저 검사](../../prototypes/workbench/results/2026-09-23-pointer/pointer-browser.json), +[작성 흐름 회귀](../../prototypes/workbench/results/2026-09-23-pointer/authoring-browser.json), +[재현 스크립트](../../prototypes/workbench/qa-pointer.js), +[상세 계약](../../prototypes/workbench/CONTRACT.md). + +## 이번 결과의 의미와 다음 작업 + +새 제스처 이론이나 대규모 성능 우위를 입증한 결과는 아니다. +화면의 대상에 좌표 입력을 연결하고, 작성 코드가 동작을 정하며, +기존 상태 확정과 출력 경로로 이어지는 기능을 확보했다. +별도 호스트 이동 상태 없이 이 경로가 동작하는 것이 이번 단계의 결과다. + +현재 상태 32개·입력 8개·출력 24개라는 한도는 그대로다. +실제 컨테이너 소속 변경, 다중 선택, 확대·축소, 배열·가변 개체 집합은 구현하지 않았다. +두 배경 영역을 가로지르는 예제는 컨테이너 간 재배치 구현이 아니다. +저장 기능도 현재는 소스 저장이며 조작 후 배치 상태의 저장·복원은 아니다. + +다음은 같은 작업물을 **여러 대상을 배치하고 결과를 저장·복원하는 도구**로 연결한다. +작업 중 발생하는 상태·정의·파일의 일관성과 현재 고정 개수 모델의 제약을 확인한다. +규모를 억지로 키운 숫자 대신, 실제 작업을 막는 제약이 확인될 때 구조 변경과 측정을 택한다. + + +## 사용자 피드백에 따른 놓기 수정 + +Codex 내장 브라우저에서 놓으면 되돌아가며 마지막 사건이 up 또는 cancel로 표시된다는 +보고가 있었다. 기존 Chromium 검사가 내장 브라우저의 정상 동작까지 입증하지는 않는다. +놓기가 SVG 밖에 전달된 뒤 캡처를 잃는 순서를 주입하면 기존 코드에서 cancel과 +원위치 복귀가 재현됐다. 내장 브라우저의 실제 사건 순서를 직접 확보한 것은 아니다. + +놓기·취소를 창의 캡처 단계에서 받고, pointerup 없이 전달된 기본 mouseup도 활성 +마우스 입력에 한해 처리한다. 종료 후 늦은 캡처 해제는 중복 종료를 만들지 않는다. +예제의 up은 위치를 다시 계산하지 않고 마지막으로 표시된 위치를 확정하도록 변경했다. +놓기 좌표가 누르기 좌표를 반복하더라도 이동 결과가 튀지 않는다. 좌표가 필요한 작성자는 +여전히 up에서 event_x/event_y를 읽을 수 있다. 실제 cancel/초점 상실의 복귀는 유지한다. +이 수정의 재현 검사는 [qa-release.js](../../prototypes/workbench/qa-release.js)에 있다. diff --git a/docs/research/014-layout-persistence.md b/docs/research/014-layout-persistence.md new file mode 100644 index 0000000..5746552 --- /dev/null +++ b/docs/research/014-layout-persistence.md @@ -0,0 +1,93 @@ +# 014 — 여러 대상을 배치하고 작업을 다시 열기 + +2026-09-23. 이번 단계는 작성한 인터페이스를 한 번 조작하고 끝내지 않고, +작업 결과를 저장한 뒤 새 실행기에서 계속 편집할 수 있게 하는 것이다. +처리량·지연·대규모 합성 벤치마크는 수행하지 않았다. + +## 실제로 할 수 있는 일 + +[세 도형 배치](../../prototypes/workbench/examples/layout.pure)를 불러와 적용하면 +각 대상을 따로 움직일 수 있다. 마지막으로 잡은 대상은 방향키로 조정한다. +**배치 저장** 후 파일을 **파일 열기**로 다시 열면 적용된 코드와 현재 배치가 복원된다. +아직 적용하지 않은 미완성 코드도 별도 초안으로 남는다. +새 페이지 또는 CLI에서 열어도 기존 실행기의 값을 참조하지 않는다. + +색상·이동·선택·키 반응은 예제 소스에 있다. 호스트에는 세 도형 전용 상태나 +이동 코드를 추가하지 않았다. 이전 드래그 놓기 수정도 유지했다. +기존 .pure 저장과 코드 JSON 저장은 계속 초안만 저장하므로 배치 저장과 구별된다. + +## 무엇이 일치해야 하는가 + +한 작업물에 서로 다른 세 종류의 정보가 있다. + +| 정보 | 저장·복원 의미 | +|---|---| +| 적용된 코드 | 복원한 값을 해석하고 실행할 정의 | +| 현재 작성 상태 | 대상 위치·선택 등 선언된 모든 숫자 값 | +| 편집 초안 | 미적용·문법 미완성이어도 보존, 자동 실행하지 않음 | + +현재 값을 코드의 초깃값으로 덮어쓰면 원래의 초기화 의미가 사라진다. +반대로 코드만 저장하면 조작 결과가 사라진다. 따라서 소스와 현재 값을 별도로 저장하고, +복원 시 한 번에 확정한다. **초깃값으로**는 여전히 작성한 소스의 초기값으로 돌아간다. + +입력 소유권·시계·오류 기록·계산 캐시·렌더러 객체는 작업 결과에 포함하지 않는다. +복원은 정지 상태에서 시작하며 입력을 재생하지 않는다. 저장 전에 조작과 시간 진행을 +끝내도록 한다. 실행 실패가 남아 있으면 해결한 뒤 저장한다. +작성자가 상태로 선언한 드래그 보조 값은 현재 모두 저장한다. 영속·일시 상태를 +구분하는 언어 기능이 생겼다고 주장하지 않는다. + +## 복원 구현 + +version2 파일은 적용된 소스, 별도 초안, 이름과 값의 목록을 가진다. +모든 상태 이름이 정확히 한 번씩 있어야 하며, 값은 유한한 숫자여야 한다. +순서는 의미가 없다. 음의 0은 JSON 숫자 직렬화에서 사라지므로 전용 문자열 `"-0"`로 +보존한다. 다른 숫자 문자열은 허용하지 않는다. 파일 크기와 코드 한도를 검사한다. +version1은 기존 코드 전용 파일로 계속 지원한다. + +호스트는 정의와 상태 목록을 검사한 뒤 코어의 준비 공간에 넣는다. +코어는 전체 정의와 모든 복원 값을 검증한 후 활성 정의·상태를 함께 교체한다. +값 하나를 넣다 실패해 현재 배치가 절반만 바뀌는 경로가 없다. +새 준비 값은 기존 로드/계산 기록 공유 공간에 들어가며, 활성 프로그램 형식은 늘리지 않았다. + +32개 복원 값과 유효성 비트·정렬 때문에 준비 공간은 264 B 늘었다. +현재 Wasm 코어 예약 구조체는 19,392 B, 선형 메모리 한도는 여전히 64 KiB다. +이는 원자적 복원에 사용한 저장 공간의 회계이며 성능 개선 주장이나 프로세스 RAM 측정이 아니다. + +브라우저의 비동기 파일 읽기에는 초안 변경 번호, 적용 정의의 동일성, 성공 사건 수를 확인한다. +읽는 도중 편집하거나 도형을 움직였다면 파일을 다시 열도록 안내한다. +느린 파일 읽기가 뒤늦게 현재 작업을 덮어쓰지 않는다. +CLI `save-project`와 `restore`도 같은 검증·복원 API를 사용하고 기존 파일은 덮어쓰지 않는다. + +## 검증 + +- 네이티브 29개: 정의·복원 값의 원자적 확정, 부족·중복·비유한 값 거절, + 0개·32개 상태 경계, 기존 활성 실행과 준비 값의 분리, 원래 초기값 유지. +- JavaScript 68개: 소스·상태·미완성 초안의 왕복, 상태 순서 변경, 음의 0, + 잘못된 파일의 비변경, 캐시 무효화, 조작 중 거절, CLI 파일 왕복과 기존 동작. +- 실제 브라우저 작성·저장 17개: 세 대상 이동, 키보드 조정, 다운로드 파일로 + 새 페이지 복원, 잘못된 파일, 편집·입력과 비동기 읽기의 충돌, 조작 중 저장 거절, + 모바일에서 추가 버튼의 가로 넘침 여부. +- 기존 브라우저 작성 흐름 44개: 코드 저장·불러오기, 편집·적용·오류·키 입력 등 회귀 검사. + +브라우저 검증은 Linux AArch64 Chromium에서 수행했다. Codex 내장 브라우저에서 +이 새 파일 흐름까지 사용자가 검증한 것은 아니다. 앞선 놓기 수정은 사용자가 정상 동작을 확인했다. +테스트 실행 도구의 경과 시간은 성능 근거로 해석하지 않는다. + +근거: [검증 로그](../../prototypes/workbench/results/2026-09-23-layout/verification.json), +[코어 빌드·테스트](../../prototypes/workbench/results/2026-09-23-layout/build.json), +[브라우저 결과](../../prototypes/workbench/results/2026-09-23-layout/layout-browser.json), +[기존 작성 흐름](../../prototypes/workbench/results/2026-09-23-layout/authoring-browser.json), +[브라우저 재현](../../prototypes/workbench/qa-layout.js), +[문서 계약](../../prototypes/workbench/CONTRACT.md). + +## 다음에 풀 제약 + +세 도형을 만들면서 대상마다 위치·잡은 위치·취소 위치·입력·반응을 반복 선언해야 했다. +같은 이동 규칙이 대상 수만큼 커지는 구조가 현재의 구체적인 제약이다. +현재 예제만으로 대규모 도구의 적합성이나 개발 경험 우위를 주장하지 않는다. + +다음 후보는 **대상 집합의 데이터는 분리하고, 같은 반응은 한 번만 정의하는 실행 표현**이다. +포인터가 소유한 대상, 키보드가 선택한 대상, 저장한 대상의 식별이 일치해야 한다. +개체 추가·삭제 뒤 잘못된 대상에 입력이 적용되지 않는 조건도 함께 검토해야 한다. +문법을 꾸미거나 기존 수치를 반복 측정하기보다 이 반복과 식별 문제를 먼저 해결한다. +현재 파일 저장은 수동 저장이며 자동 저장·실행 취소·다중 사용자 동기화는 구현하지 않았다. diff --git a/docs/research/015-shared-rules.md b/docs/research/015-shared-rules.md new file mode 100644 index 0000000..44f6544 --- /dev/null +++ b/docs/research/015-shared-rules.md @@ -0,0 +1,115 @@ +# 015 — 대상별 값과 공유 규칙 본문 + +2026-09-23. 여러 도형을 배치하는 작업에서 같은 이동·놓기·취소 반응이 대상마다 +복제되던 문제를 다뤘다. 규칙을 한 번 작성하고, 컴파일된 명령도 한 벌만 보관하며, +각 입력 연결은 대상의 연속된 상태 구간을 지정한다. + +## 사용하는 형태 + +```text +record Position { x=0; y=0 } +item green = Position(x=80,y=120) +item blue = Position(x=280,y=160) +rule move(t: Position) { + t.x = event_x + t.y = event_y +} +on drag_green.move use move(green) +on drag_blue.move use move(blue) +``` + +전체 작성물 [shared-layout.pure](../../prototypes/workbench/examples/shared-layout.pure)는 +잡은 위치 유지, 제한된 영역의 이동, 놓기 확정, 취소 복귀, 선택한 대상의 방향키 조정을 포함한다. +화면의 **공유 규칙으로 배치**에서 불러와 적용할 수 있다. +`go_right` 한 곳을 고치면 연결된 세 대상이 같은 변경을 사용한다. +대상별 입력과 출력 연결은 명시적으로 작성한다. + +## 실행 표현 + +`record`는 필드의 기본값, `item`은 그 필드의 실제 값, `rule`은 공유되는 반응이다. +각 item의 필드는 기존 숫자 상태 배열의 연속 구간에 놓인다. `green.x` 같은 이름은 +작성·수정·저장의 식별자이며, 이벤트마다 문자열을 찾아 실행하지 않는다. + +규칙 본문은 첫 사용 위치에서 한 번만 내린다. 연결마다 본문 범위와 대상 필드의 +시작 위치·개수를 기록한다. 읽기·쓰기는 `base + field_offset`으로 실제 상태에 접근한다. +필드 번호 목록, 인자 값 복사, 별도 레코드 객체, VM 호출 스택을 추가하지 않았다. +표시 표현식은 직접 해석된 기존 상태 읽기를 쓰므로 계산 캐시도 실제 필드 변경을 추적한다. + +매개변수 하나는 정확히 같은 record 타입의 item에 연결한다. 다른 타입, 누락된 필드, +범위를 벗어난 슬롯, 잘못된 본문 공유는 거절한다. 사용하지 않은 규칙도 이름과 필드를 +검사하지만 명령 저장량에는 포함하지 않는다. 매개변수 이름이 전역 item 이름과 겹칠 때 +없는 필드를 전역 값으로 잘못 해석하지 않도록 검사한다. + +한 사건의 모든 연결은 기존처럼 작성 순서대로 실행한다. 뒤쪽 연결이 실패하면 앞쪽 +연결의 변경과 전역 상태 변경까지 함께 취소한다. 같은 item을 두 번 연결하면 두 번 실행한다. +코드를 공유한다는 이유로 입력이나 효과를 생략하지 않는다. + +## 확인한 구조적 차이 + +동일한 최종 코어에서 기존 `layout.pure`와 공유 규칙 작성물을 컴파일한 회계다. +실행 시간 비교나 처리량 벤치마크는 아니다. + +| 항목 | 기존 명시적 반복 | 공유 규칙 | +|---|---:|---:| +| 저장된 명령 | 319 | 117 | +| 이벤트 연결 | 16 | 24 | +| 사용 중인 명령·연결 자료 | 5,360 B | 2,256 B | +| 숫자 상태 | 22 | 25 | + +공유 버전의 추가 세 상태는 예제가 명시한 대상별 `id`다. 기존 키 반응의 대상별 조건을 +각 연결에 나누면서 연결 수는 늘었다. 코드 저장량 감소를 전체 메모리 감소나 속도 향상으로 +해석하면 안 된다. 각 대상의 값과 입력 소유권은 계속 필요하다. + +연결 구조체는 12 B에서 16 B로 늘었다. 활성·준비 프로그램의 고정 연결 배열 때문에 +코어 예약 공간은 이전 19,392 B에서 **19,904 B**로 512 B 늘었다. +Wasm 선형 메모리 한도는 64 KiB다. 사용 중인 자료와 고정 예약 공간을 별도로 기록한다. +이 구조가 모든 작은 작성물에 메모리 이득이라는 주장은 하지 않는다. +[구조 회계](../../prototypes/workbench/results/2026-09-23-shared-rules/representation.json)는 +동일 ABI의 두 작성물을 비교하며 이전 12 B 연결 구조체의 수치를 섞지 않는다. + +## 의미 보존과 검증 + +기존 작성물과 공유 버전에 같은 입력을 전달해 모든 대상의 위치·잡은 위치·취소 위치· +조작 여부·선택·출력 값을 비교했다. 두 대상의 동시 조작, 한 대상의 취소, 키보드 조정, +영역 밖 이동과 놓기 이후의 결과가 일치했다. + +선언 순서와 record 필드 순서를 바꿔도 `item.field` 이름으로 값을 보존하고 물리 연결을 +다시 만든다. 삭제한 item의 값은 사라지며 재선언은 새로운 초기값에서 시작한다. +배치 파일은 물리 슬롯 번호를 저장하지 않는다. 새 실행기에서 소스를 다시 컴파일하고 +이름을 검사한 다음 정의와 값을 함께 확정한다. 이전 version2 형식은 유지한다. +생성되는 필드 이름이 소스보다 길어질 수 있어, 최대 크기의 유효한 저장 파일을 자체 +로더가 거절하지 않도록 파일 크기 상한도 해당 이름 수를 포함해 정했다. + +- 네이티브 32개: 공유 명령의 독립 필드 접근, 같은 대상의 반복 연결, 여러 연결의 + 원자성, 누락·잘못된 범위·부분 중첩 거절과 기존 코어 기능. +- JavaScript 75개: 규칙 하나의 명령 수가 연결 증가로 복제되지 않음, 컴파일 오류, + 이전 작업물과 의미 동등성, 순서 변경·제거·복원, 캐시 무효화, 기존 작성·파일 기능. +- 브라우저 23개: 공유 규칙 한 곳 수정, 서로 다른 대상에 적용, 선언 순서 변경, + 실제 마우스와 두 접촉의 독립 이동·취소, 다운로드와 새 페이지 복원, 파일 읽기 충돌. + +기존 작성 흐름의 브라우저 회귀 검사 44개도 통과했다. + +브라우저는 Linux Chromium이며 터치는 CDP 입력이다. 실제 모바일 하드웨어나 Codex 내장 +브라우저에서 이번 공유 규칙 흐름까지 사용자가 확인한 것은 아니다. + +근거: [Node 검증](../../prototypes/workbench/results/2026-09-23-shared-rules/verification.json), +[코어 빌드](../../prototypes/workbench/results/2026-09-23-shared-rules/build.json), +[브라우저 결과](../../prototypes/workbench/results/2026-09-23-shared-rules/shared-browser.json), +[재현 스크립트](../../prototypes/workbench/qa-shared-rules.js), +[계약](../../prototypes/workbench/CONTRACT.md). + +## 남은 제약과 다음 방향 + +매개변수화된 규칙은 기존 프로그래밍 개념이다. 이번 결과는 이 작성·실행·저장 경로에서 +실제 명령 복제를 없애면서 상태와 조작 의미를 보존한 구현이다. +현재 record당 필드는 1~8개이고, 모든 item의 필드는 합쳐서 상태 32개 한도에 포함된다. +입력 8개·출력 24개 한도도 그대로다. 일반 함수 호출, 동적 대상 생성·삭제는 없다. + +이름으로 보존하는 정적 필드와 동적 대상의 생명주기 핸들은 다르다. +예제의 `id`와 `selected`는 작성한 숫자 상태라서 중복 방지나 선택 정책을 실행기가 +자동 보장하지 않는다. 재사용한 ID를 오래된 입력이 가리키는 문제까지 해결한 것으로 세지 않는다. + +다음 제약은 대상마다 필요한 입력 연결과 키 사건의 모든 연결 순회다. +**실행 중 대상 추가·삭제와 선택한 대상만 실행하는 경로**를 검토할 때, +삭제·재사용 뒤 오래된 입력과 저장된 선택이 잘못된 대상에 적용되지 않는 식별 규칙이 필요하다. +규모를 키운 속도 수치보다 이 작업 조건을 먼저 고정한다. diff --git a/docs/research/016-pointer-routing.md b/docs/research/016-pointer-routing.md new file mode 100644 index 0000000..933e505 --- /dev/null +++ b/docs/research/016-pointer-routing.md @@ -0,0 +1,98 @@ +# 016 — 대상마다 복제하던 포인터 연결 제거 + +2026-09-23. 앞 단계는 규칙 본문을 공유했지만, 세 도형의 드래그를 위해 입력 세 개와 +반응 연결 열두 개를 따로 만들었다. 이번에는 **입력 하나와 반응 네 개에 대상 표를 연결**한다. +현재 잡은 도형의 상태 구간을 규칙에 전달하고, 접촉 소유권은 도형별로 유지한다. + +## 작성과 사용 + +```text +input drag = pointer { + green_box: green + blue_box: blue + amber_box: amber +} +on drag.down use grab(drag.target) +on drag.move use move(drag.target) +on drag.up use drop(drag.target) +on drag.cancel use cancel(drag.target) +``` + +작업 화면을 새로고침하고 **입력 묶어서 배치 → 예제 불러오기 → 코드 적용**을 누른다. +[전체 작성물](../../prototypes/workbench/examples/routed-layout.pure)은 이전과 같은 +잡은 위치 유지, 경계 제한, 놓기 확정, 취소 복귀, 선택한 도형의 방향키 조정을 제공한다. +한 그룹 안의 도형 두 개를 동시에 움직일 수 있다. 하나를 놓거나 취소해도 다른 도형의 +소유권은 유지되며, 이미 놓은 도형은 다른 접촉을 취소해도 되돌아가지 않는다. + +같은 record 타입의 item끼리 묶는다. 도형과 대상의 연결은 명시하지만 네 반응은 +대상마다 작성하거나 컴파일해 복제하지 않는다. 마지막 입력 표시에는 대상 도형도 나온다. +CLI는 `point-target drag green_box move 210 220`으로 같은 경로를 실행한다. + +## 실제 실행 표현 + +컴파일러는 도형 이름을 item의 연속 필드 구간에 연결한다. VM에 저장되는 대상 하나는 +`input, base, count` 세 바이트다. 그룹 반응의 메타데이터에는 ‘이번 사건의 대상’을 +사용한다는 표시와 필드 수만 둔다. 명령 본문과 연결 수는 도형 수만큼 늘어나지 않는다. + +브라우저는 처음 잡은 도형을 캡처 기록에 유지한다. 런타임은 코드 적용 시 만든 이름 +조회 표로 대상 번호를 찾고, VM은 그 번호로 활성 대상 표를 직접 읽는다. +포인터가 움직일 때마다 그룹의 모든 도형을 검색하거나 각 도형의 규칙을 실행하지 않는다. +기존의 제한된 핸들러 목록에서 사건 번호를 비교하는 순회는 남아 있다. + +소유권 키는 입력 이름과 도형 이름의 조합이다. 같은 도형의 두 번째 접촉은 소유권을 +가져갈 수 없다. `releaseAll`은 잡고 있는 도형 각각에 취소를 한 번씩 보낸다. +그룹끼리 같은 item을 중복 연결하는 것도 거절해 필드 구간이 겹치는 캡처를 막는다. +작성자가 다른 전역 상태나 명시적 대상에 쓰는 일반 반응까지 금지하는 정책은 아니다. + +VM은 잘못된 대상 번호, 다른 입력의 대상, 누락된 대상, 범위를 벗어난 필드를 거절한다. +하나의 사건에서 앞의 반응이 쓴 값도 뒤의 반응이 실패하면 함께 취소한다. +준비 중인 연결 표는 활성 표를 바꾸지 않는다. 소스 적용과 파일 복원은 입력을 모두 +놓은 뒤에만 가능하고, 이름을 기준으로 표를 다시 만든다. 파일에는 물리 대상 번호를 저장하지 않는다. + +## 확인한 비용과 한계 + +동일한 최종 코어에 두 작성물을 넣은 저장 구조 회계다. 속도 벤치마크는 하지 않았다. + +| 항목 | 공유 규칙 + 대상별 입력 | 공유 규칙 + 그룹 입력 | +|---|---:|---:| +| 전체 입력 | 7 | 5 | +| 포인터 반응 연결 | 12 | 4 | +| 전체 반응 연결 | 24 | 16 | +| 저장 명령 | 117 | 117 | +| 대상 표 | 0 B | 9 B | +| 사용 중인 명령·연결·대상 표 | 2,256 B | 2,137 B | +| 숫자 상태 | 25 | 25 | + +대상 표 24칸을 예약하면서 활성/준비 프로그램에 각각 80 B가 추가됐다. +코어 고정 예약은 **19,904 → 20,064 B**, Wasm 선형 메모리는 64 KiB로 유지된다. +현재 예제의 사용 자료는 줄었지만, 고정 예약은 늘었다. JS의 조회 Map과 메타데이터, +브라우저 전체 메모리는 이 수치에 포함하지 않는다. 전체 메모리나 실행 속도의 우위로 해석하지 않는다. +[구조 회계](../../prototypes/workbench/results/2026-09-23-pointer-routing/representation.json). + +1·3·9·24개의 작은 대상을 컴파일해 하나의 입력과 하나의 이동 반응이 유지되는지도 검사했다. +이는 기존 입력 8개 한도와 대상 수를 분리했는지 확인하는 경계 검사다. +큰 앱이나 많은 도형의 성능 실험으로 세지 않는다. 총 상태 32개, 출력 24개 등의 +기존 한도는 그대로이며, 현재 Tile의 8필드 구성으로 24개 도형을 만들 수 있다는 뜻도 아니다. + +## 검증과 남은 작업 + +코어 검사 35개와 Node/CLI 검사 83개가 통과했다. 기존 배치와 동일한 입력을 전달해 +모든 필드와 출력을 비교했고, 동시 접촉·일부 취소·소유권 침범 거절·그룹 혼동 거절· +실패 원자성·표시 캐시 갱신·선언 순서 변경·제거 후 재선언·파일 복원을 확인했다. + +그룹 예제의 브라우저 확인 30개, 기존 작성 흐름 44개, 이전 대상별 포인터 흐름 +23개도 통과했다. +브라우저에서는 실제 마우스와 Chromium CDP 터치로 조작한다. 개별 접촉 종료 검사에서 +처음에 남겨둘 접촉을 종료하도록 보낸 오류를 바로잡았다. 기록된 실제 `pointerup`과 +남은 소유권을 함께 확인했다. 이동 검사는 CDP 명령 응답 이후 실제 사건이 +반영될 때까지 기다린다. 이 재현은 실제 모바일 하드웨어 검증을 대신하지 않는다. +[검증 자료와 재현](../../prototypes/workbench/results/2026-09-23-pointer-routing/README.md). + +이 결과는 입력마다 대상을 별도 연결하던 비용을 분리한 구현이다. 독창적인 새 +프로그래밍 개념이나 대규모 서비스 최적화의 증거로 제시하지 않는다. + +다음 문제는 **선택 대상의 식별과 키 입력의 직접 전달**이다. 현재 예제의 `selected`와 +`id`는 작성한 숫자 상태이며, 방향키는 여전히 대상별 조건을 검사한다. 이를 해결할 때는 +선택 없음, 선택 대상 제거, 저장 복원 뒤 선택, 잡고 있는 대상과 선택이 다를 때의 정책을 +먼저 고정해야 한다. 동적 생성·삭제에는 오래된 입력이 재사용된 슬롯으로 전달되지 않는 +수명 식별도 필요하다. 이번 대상 번호는 정적 정의 안의 주소이며 그 역할을 대신하지 않는다. diff --git a/docs/research/017-web-layout-boundary.md b/docs/research/017-web-layout-boundary.md new file mode 100644 index 0000000..042c969 --- /dev/null +++ b/docs/research/017-web-layout-boundary.md @@ -0,0 +1,116 @@ +# 017 — 웹 레이아웃을 복제하지 않는 문서 작업 + +**범위 정정 — 2026-09-23:** 아래 결과는 브라우저 호스트에 한정한다. Pure의 목표는 앱 로직· +net·RPC·DB·UI를 다루는 독립 네이티브 애플리케이션 런타임이다. UI는 일부다. +웹 호환성을 브라우저에 레이아웃·편집을 맡기는 것으로 해석한 것은 +Codex의 오류다. 이 실험의 기능 검증은 보존하지만, Pure 자체의 레이아웃·텍스트·입력 체계가 +불필요하다는 근거로 사용하지 않는다. 현재 구현 방향은 [010 로드맵](010-roadmap.md)을 따른다. + +2026-09-23. 일반 텍스트를 열고 편집·읽기·저장하는 화면에 Pure를 연결했다. +**이 작업에서는 레이아웃 트리·텍스트·스크롤 위치를 VM으로 옮길 필요가 없었다.** +크기 변경과 줄바꿈은 브라우저가 처리하고, Pure에는 사용자가 정한 패널 비율 하나만 둔다. + +이것은 실제 UI 조건을 확인한 구현·기능 검증이다. 전체 메모리 절감, 새로운 압축 원리, +대규모 앱 성능 우위의 증거는 아니다. 화면 하나의 분할 비율을 위해 VM을 추가할 성능상의 +근거도 얻지 않았다. 기존 실행 규칙을 표준 웹 호스트에 연결하는 경계를 확인한 단계다. + +## 직접 확인하기 + +기존 작업 화면을 새로고침하고 **문서 편집 · 반응형 레이아웃 작업 화면** 링크를 연다. +직접 주소는 서버의 `/document.html`이다. + +1. 일반 UTF-8 `.txt` 파일을 열거나 빈 문서에 작성한다. **예제 문서**도 있다. +2. 가운데 경계를 끌거나 방향키·슬라이더로 두 패널의 폭을 조절한다. +3. 조작 중 창 크기를 바꾸거나 Esc로 취소한다. 좁은 화면에서는 두 패널이 위아래로 놓인다. +4. 양쪽을 독립적으로 스크롤하고, 텍스트를 고치고 실행 취소한다. +5. `.txt 저장`으로 다른 편집기에서도 읽을 수 있는 파일을 내려받는다. +6. **분할 규칙 수정**에서 `clamp(event_value,20,80)`을 `clamp(event_value,30,70)`으로 + 바꾸어 적용한다. HTML 어댑터를 바꾸지 않아도 같은 컨트롤의 제한이 바뀐다. + +파일은 텍스트만 저장한다. 패널 비율·Pure 규칙을 묶는 자체 문서 형식은 만들지 않았다. +임포트/익스포트는 파일당 2 MiB까지이고, UTF-8 오류는 현재 문서를 유지한 채 거절한다. +브라우저 textarea의 줄바꿈 정규화 때문에 저장은 LF·BOM 없음으로 이루어진다. +원본 파일의 모든 바이트를 그대로 보존하는 편집기라는 뜻은 아니다. + +## 어디에서 무엇을 소유하는가 + +| 자료/작업 | 소유자 | 연결에서 하는 일 | +|---|---|---| +| 작성 중인 텍스트·선택·실행 취소 | 브라우저 기본 textarea | 입력 후 읽기 화면에 텍스트 전달 | +| 읽기 화면의 글자 배치·줄바꿈·스크롤 | DOM/CSS/브라우저 | 문서가 바뀔 때만 텍스트 갱신 | +| 패널의 실제 크기·좁은 화면 배치 | CSS Grid/media query | 비율 하나를 CSS 변수로 전달 | +| 사용자가 요청한 분할 비율의 정책 | Pure 숫자 상태·규칙 | range/포인터/키보드가 같은 숫자 입력을 사용 | +| 접촉 소유권·취소 시작값·기하 캐시 | 브라우저 어댑터 | 조작 시작과 무효화 뒤 필요한 좌표만 읽음 | + +문서 크기나 줄 수가 늘어나도 이번 정책의 VM 상태는 비율 하나다. viewport width, +DOMRect 배열, 텍스트 AST, 스크롤 위치를 VM 상태로 복제하지 않는다. 레이아웃 연산이 +없어진 것은 아니다. 브라우저가 그 연산과 관련 저장 공간을 담당한다. + +읽기 화면은 textarea와 별도의 텍스트 표현을 갖는다. 따라서 **제로 카피 문서 모델은 아니다.** +별도 JS 문서 모델과 Wasm 문자열 사본은 만들지 않았지만, 브라우저 내부의 텍스트·undo· +레이아웃 저장 공간과 파일 읽기/인코딩의 임시 버퍼는 여전히 필요하다. + +## 필요한 경계만 추가했다 + +기존 실행기는 키·포인터·프레임 입력은 받았지만 네이티브 숫자 컨트롤의 값을 직접 받을 +경로가 없었다. 다음 입력을 추가했다. + +```text +state ratio = 50 +input split = value +on split.change { ratio = clamp(event_value,20,80) } +``` + +호스트는 `runtime.change('split',65,'range')`, CLI는 `change split 65`를 사용한다. +값은 유한한 숫자이고 사건 동안만 존재한다. 규칙이 저장한 값만 상태가 된다. +기존 원자적 이벤트 실행·실패 취소·공유 규칙·표시 캐시 무효화를 그대로 사용한다. +문서 호스트는 HTML로 출력하므로 가짜 도형 선언이나 SVG 장면을 만들지 않는다. +출력 도형이 없는 프로그램에서는 사용하지 않는 표시값 배열도 생성하지 않도록 했다. + +[정의](../../prototypes/workbench/examples/document.pure)의 실제 표현은 상태 1개, +입력 2개, 반응 2개, 명령 7개다. 활성 상태는 8 B, 명령·연결은 144 B다. +하지만 코어는 여전히 **20,064 B를 예약하고 Wasm 선형 메모리는 64 KiB**다. +이 숫자는 브라우저 앱 전체 메모리가 아니며, 작은 상태만으로 전체 효율을 주장하지 않는다. + +포인터 이동은 프레임당 마지막 좌표만 전달한다. 놓을 때 대기 중인 이동을 먼저 처리하고, +취소는 시작 비율로 복귀한다. 크기 변경·스크롤은 기하 캐시를 무효화하지만 그 콜백에서 +레이아웃을 읽거나 쓰지 않는다. 다음 조작이 실제로 필요할 때 읽는다. +관찰된 0회는 애플리케이션의 해당 API 호출 수이며 브라우저 내부 레이아웃 비용 0을 뜻하지 않는다. + +## 확인한 것과 한계 + +- 네이티브 37개, Node/CLI 87개 검사 통과. 새 숫자 사건의 컨텍스트, 잘못된 입력, + 비유한 수, 여러 반응의 실패 원자성, 공유 규칙, 캐시, 저장·복원을 포함한다. +- 문서 브라우저 검사 32개: 창 크기 변경·독립 스크롤·네이티브 컨트롤·실제 마우스와 + CDP 터치·조작 중 resize·취소·표준 파일 다운로드와 새 페이지 왕복·늦게 끝난 파일 읽기 + 거절·Unicode 편집·네이티브 undo·미완성 조합 중 출력 보류·규칙 수정·Wasm 실패 시 문서 기능 유지. +- 기존 작성 흐름 44개도 통과했다. 기본 화면의 숫자 입력 컨트롤 3개, 잘못된 출력·조작 중 교체·파일 크기 등의 + 추가 확인 6개도 통과했다. +- 창 크기 변경과 스크롤에서 Pure 이벤트·문서 재전달·앱 기하 읽기가 증가하지 않음을 + 확인했다. 비율 변경도 문서 재전달을 일으키지 않는다. 이는 소유권 검증이며 속도 비교가 아니다. + +환경은 Linux Chromium이다. 한글 조합의 순서는 합성 composition 사건으로 확인했고, +실제 운영체제 IME·물리 모바일 기기·전체 접근성 적합성을 검증한 것은 아니다. +분할 조작은 [W3C의 separator 의미와 키보드 지침](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)을 +참고했다. 포인터 캡처·취소는 [Pointer Events](https://www.w3.org/TR/pointerevents3/), +크기 관찰 경계는 [Resize Observer](https://drafts.csswg.org/resize-observer/)를 참고했다. +표준을 재발명한 것으로 세지 않는다. + +[검증 자료와 재현](../../prototypes/workbench/results/2026-09-23-web-layout/README.md), +[실행 계약](../../prototypes/workbench/CONTRACT.md), +[문서 호스트](../../prototypes/workbench/web/document.mjs), +[입력 어댑터](../../prototypes/workbench/web/split-input.mjs). + +## 당시 결정의 정정과 남은 한계 + +당시 ‘기존 환경과의 경계가 이미 목적을 충족하므로 자체 레이아웃을 추가하지 않는다’는 +결론은 철회한다. 브라우저를 필수 기반으로 삼은 이 구현은 독립 네이티브 UI 목표를 충족하지 +않는다. 현재 HTML 화면은 직접 작성한 호스트이며 현재 DSL이 일반 UI 전체를 표현하지도 않는다. +Pure의 UI 체계가 배치·입력·출력을 소유하는 일은 애플리케이션 런타임 구현의 일부다. +다음 우선순위는 로드맵의 실행·수명 계약과 UI·RPC·DB를 연결하는 실제 작업을 따른다. +이는 모든 기능을 현재 VM에 넣거나 새 문서·미디어 포맷을 먼저 만들겠다는 뜻은 아니다. + +남아 있는 구체적 비용은 **텍스트 한 부분을 편집해도 읽기 화면의 전체 문자열을 갱신하는 것**이다. +좁은 화면에서 읽기 패널이 보이지 않을 때도 지금은 갱신한다. 이 웹 호스트의 추가 최적화는 +미룬다. 표준 파일 왕복·숫자 입력·취소 등의 동작은 네이티브 구현에서 참고하되, DOM/CSS에 +맡긴 비용이 제거됐다고 계산하지 않는다. 표준 파일을 포기하거나 새 포맷의 우위를 가정하지 않는다. diff --git a/docs/research/018-native-connections.md b/docs/research/018-native-connections.md new file mode 100644 index 0000000..f2c26bc --- /dev/null +++ b/docs/research/018-native-connections.md @@ -0,0 +1,81 @@ +# 018 — 요청·DB 결과·전송의 수명을 연결한 네이티브 경로 + +> 역사적 기록: 이 문서의 SQLite 구현은 사용자 요구와 맞지 않아 +> [019의 SQL 없는 바이트 저장](019-byte-storage.md)으로 교체했다. 아래 결과와 원자료는 +> 당시 구현의 기록이며 현재 구현의 의존성·검증 결과를 뜻하지 않는다. + +2026-09-23. **기존 작성물의 저장·복원을 실제 TCP와 SQLite에 연결했다.** 한 실행기에서 +이동한 배치를 저장하고 다른 실행기에서 복원할 수 있다. 새 네이티브 서버는 브라우저 없이 +실행되며, 기존 CLI가 요청한다. 이번 결과는 실제 연결의 구현·정확성 확인이다. 속도·전체 +메모리 우위나 새 알고리즘의 증거는 아니다. + +알아낸 것은 연결의 책임 범위다. DB 결과를 응답 본문 배열로 다시 복사하지 않으려면, +전송이 끝날 때까지 원본의 수명도 이어져야 한다. 느린 수신자가 있으면 데이터뿐 아니라 +그 결과를 유지하는 DB 자원도 살아 있다. 본문 길이·동시 연결·대기 시간을 함께 제한했다. +이 방식이 복사하는 방식보다 전체적으로 유리한지는 이번 검사로 결정하지 않는다. + +## 실제 작성 흐름 + +기존 `.pure` 정의에서 도형을 움직인 뒤 CLI의 `connect`, `push 0`으로 저장한다. +빈 실행기를 열어 `connect`, `pull`하면 적용 코드·현재 값·초안이 복원된다. 서버를 재시작한 +뒤에도 남는다. 다른 실행기가 먼저 갱신하면 이전 revision의 쓰기를 거절해 덮어쓰지 않는다. +실행 방법은 [구현 README](../../prototypes/connections/README.md)에 있다. + +숫자 VM 자체에 DB나 네트워크를 넣지는 않았다. 네이티브 연결 경로는 C17로 작성했고 기존 +Node/Wasm CLI를 재사용했다. C는 현재 SQLite·POSIX 연결을 직접 확인하기 위한 구현 선택이다. +언어 선택의 성능 우위를 주장하거나 기존 Zig 코어를 다시 작성한 것이 아니다. + +## 연결에서 구현한 계약 + +| 경계 | 구현 | 실패·완료 | +|---|---|---| +| 요청 → 입력 구간 | Content-Length 확인 후 한도 안에서 할당하고 부분 수신 | 불완전한 본문은 DB에 제출하지 않고 회수 | +| 입력 구간 → DB | 같은 바이트 구간을 `SQLITE_STATIC`으로 바인드 | 모든 경로에서 statement를 먼저 해제한 뒤 입력을 회수 | +| DB 변경 → 응답 | revision 검사와 갱신을 트랜잭션으로 실행 | COMMIT 뒤에만 성공 응답; 실패·충돌은 기존 저장 내용 유지 | +| DB 결과 → 전송 | 같은 읽기 스냅샷에서 크기를 확인하고 결과 BLOB을 대여 | 별도 응답 본문 사본 없이 헤더·본문 구간을 순서대로 전송 | +| 전송 → 회수 | 부분 전송 위치와 소유자 해제 함수를 연결 | 완료·연결 오류·기한 종료에서 원본을 한 번 회수 | + +SQLite의 바인딩 규칙에 따라 입력 포인터를 statement 종료까지 유지했다. +[공식 바인딩 수명 규칙](https://www.sqlite.org/c3ref/bind_blob.html). +조회 포인터도 변환·step·reset·finalize로 무효화될 수 있으므로 전송 중에는 해당 statement를 +진행하거나 해제하지 않는다. [공식 결과 포인터 수명 규칙](https://www.sqlite.org/c3ref/column_blob.html). +본문 전송 완료는 `send`가 바이트를 받아들인 시점이다. 상대 앱의 처리 완료까지 보장하지 않는다. + +DB 커밋과 응답 전달은 별개다. 응답을 읽지 않은 클라이언트가 끊어져도 이미 끝난 커밋은 +남는다. 조건부 갱신으로 오래된 revision의 재시도를 거절하지만, 임의 RPC의 exactly-once나 +분산 트랜잭션을 제공하지 않는다. [SQLite 트랜잭션 경계](https://www.sqlite.org/lang_transaction.html). + +## 검증과 한계 + +Linux AArch64, GCC 14.2.0, SQLite 3.46.1에서 확인했다. + +- 부분 송수신, 이진 본문, 버전 충돌, DB 잠금·오류, 불완전한 요청, 다른 쓰기 중의 읽기 + 스냅샷, 수신 종료, 자원 포화·기한 종료, 실제 작성물 복원·재시작 등 **15개 통합 시나리오**. +- 코어 검사: 전송 중 조기 해제 금지·중복 회수 방지, 저장 실패 후 내용 보존, revision 소진, + 빈 본문과 읽기 예산 검사. 일반 빌드와 ASan·UBSan 빌드 모두 통과했다. +- 기존 CLI 회귀 검사 8개 통과. 복원 실패·누르고 있는 입력이 있을 때 로컬 상태와 원격 + revision이 바뀌지 않는 것도 통합 검사에 포함했다. +- 검사 종료 후 연결과 보유 본문이 모두 0이고, 본문 접수 한도와 연결 슬롯 수를 넘지 않았다. + 이는 지정한 자원의 수명·한도 검사이며 전체 RAM 상한이나 처리 성능 측정이 아니다. + +SQLite 내부 할당·복사, 페이지 캐시·WAL, OS 버퍼, 클라이언트의 직렬화 비용은 남는다. +수신 헤더에 함께 들어온 본문 접두부도 한 번 복사한다. 장기 읽기는 WAL checkpoint 진행을 +늦출 수 있어 디스크 비용도 포함한 판단이 필요하다. [SQLite WAL 읽기·checkpoint 규칙](https://www.sqlite.org/wal.html#concurrency). +DB 호출은 아직 동기식이므로 디스크 작업 중 다른 연결 처리가 지연될 수 있다. macOS 실행, +전원 장애·디스크 고장, 원격 배포, 인증·TLS, 범용 HTTP/RPC, 네이티브 UI는 검증 범위 밖이다. +동적 링크한 SQLite 자체를 sanitizer로 다시 빌드한 것은 아니다. + +[원자료·재현](../../prototypes/connections/results/2026-09-23-native-connections/README.md), +[전송 수명](../../prototypes/connections/flow.c), +[DB 연결](../../prototypes/connections/store.c), +[호스트](../../prototypes/connections/server.c), +[기존 CLI 연결](../../prototypes/workbench/remote.mjs). + +## 다음 결정 + +현재 경로와 바이트 구간의 수명은 네이티브 코드에 고정되어 있다. Pure 정의에서 이 연결을 +작성하거나 실행 준비 단계에서 불필요한 연산·전달을 제거하는 기능은 아직 없다. +다음은 **이 저장 작업의 입력·검증·외부 효과·응답 관계를 실행 가능한 연결 정의로 분리하는 것**이다. +기존 문법·숫자 VM을 그대로 강제하지 않고, 외부 효과를 재계산하거나 롤백했다고 잘못 표현하지 +않도록 범위를 정한다. 소비자의 요구와 수명에서 판단 가능한 전달 비용부터 다룬다. +새 DB·RPC 기능을 병렬로 늘리거나 종료한 공유/복사 벤치마크를 다시 실행하지 않는다. diff --git a/docs/research/019-byte-storage.md b/docs/research/019-byte-storage.md new file mode 100644 index 0000000..faefecf --- /dev/null +++ b/docs/research/019-byte-storage.md @@ -0,0 +1,84 @@ +# 019 — SQL을 제거한 바이트 접근·저장 경로 + +> 후속 [020](020-prepared-connections.md)에서 저장·응답 구간을 작성 파일로 정의하도록 +> 확장했다. 아래는 그 이전 구현의 기록이며 현재 실행에는 `--program FILE`도 필요하다. + +2026-09-23. **RAM·storage를 SQL에 맞추지 않는다는 요구에 따라 활성 구현의 SQLite를 +제거했다.** 기존 CLI의 저장·복원 흐름은 유지하고 서버를 호출자의 바이트를 직접 기록하는 +C17/POSIX 어댑터로 교체했다. 새 저장 기술의 성능을 입증한 연구 결과가 아니라, 잘못 +도입한 내부 모델을 제거하고 연결의 의미를 확인한 구현 결과다. + +사용자는 이전처럼 작업물을 만들고 `push`·`pull`로 저장·복원한다. 서버 실행 옵션은 +`--db FILE`에서 `--store DIRECTORY`로 바뀌었다. 기존 파일은 자동 변환하거나 덮어쓰지 않는다. +실행 예는 [구현 README](../../prototypes/connections/README.md)에 있다. + +## 내부 모델의 기준 + +필요한 데이터의 범위·접근·갱신·수명에서 배치와 실행 경로를 결정한다. 테이블·행·질의 +실행기에 맞춘 뒤 다시 소비자의 표현으로 바꾸는 단계를 필수로 두지 않는다. DB는 데이터 +접근·일관성·영속성을 다룬다는 범위 이름이며 SQL 엔진 채택을 뜻하지 않는다. + +이번 경로는 RAM에 수신한 본문과 저장 파일의 읽기 범위를 같은 `PureSpan`으로 전달한다. +소비자는 포인터와 길이를 사용하며, 소유자는 소비 종료까지 데이터를 유지한다. 저장소의 +주소를 영속 포인터로 기록하거나 RAM과 저장 매체의 물리적 성질이 같다고 가정하지 않는다. +타입·의미 검증, 표현 변환이 필요한 경계, 공개·영속화·전송 완료의 차이는 남는다. + +현재 Workbench는 JSON 전체를 저장·복원한다. 이 경로에서 직렬화를 제거했다고 주장하지 +않으며, JSON이나 `PureSpan`만으로 전체 애플리케이션의 데이터 표현을 완성한 것도 아니다. +파일별 전체 교체는 지금 작업의 어댑터 선택이다. Pure 전체를 키-값 저장소로 고정하지 않는다. + +## 구현한 경로 + +쓰기: 수신 구간 → 새 파일에 원본 바이트 기록 → 파일 fsync → 원자적 이름 교체 → +디렉터리 fsync → 성공 응답. 버전 비교와 공개는 디렉터리별 쓰기 잠금 안에서 수행한다. +파일의 24 B 헤더는 식별자·버전·본문 길이만 담는다. 본문은 변환하지 않는다. + +읽기: 파일·길이 검사 → 필요한 범위의 읽기 전용 매핑 → 부분 전송 → 매핑 해제. +저장 API는 오프셋·길이 접근을 지원하며 현재 HTTP 경로는 전체 본문을 요청한다. +페이지 정렬에 필요한 접두부도 매핑하므로 요청한 바이트 수가 실제 매핑/RSS와 같지는 않다. +파일 디스크립터는 매핑 후 닫을 수 있다. [mmap 계약](https://man7.org/linux/man-pages/man2/mmap.2.html). + +저장 버전을 제자리에서 수정하지 않고 새 파일로 교체해, 읽고 있는 이전 버전과 새 요청이 +보는 버전을 분리한다. 이는 불변 파일과 이름 교체의 조합이며 `MAP_PRIVATE`만으로 외부 +파일 변경까지 격리되는 것은 아니다. [rename 계약](https://man7.org/linux/man-pages/man2/rename.2.html). + +| 쓰기 결과 | 의미 | +|---|---| +| 예상 버전 불일치 | 충돌, 기존 바이트 유지 | +| 공개 전 실패 | 기존 바이트 유지, 이 호출의 임시 파일 제거 시도 | +| 공개 후 디렉터리 fsync 실패 | 새 바이트는 보이지만 영속화 확인 불가; `PURE_UNCERTAIN` / `durability_unknown` | +| 모든 동기화 성공 | 어댑터의 저장 성공; 상대 앱의 응답 수신 완료와는 별개 | + +파일 fsync만으로 디렉터리 항목의 동기화를 대신할 수 없어 두 시점을 구분했다. +[fsync 계약](https://man7.org/linux/man-pages/man2/fsync.2.html). 이 순서를 구현했다는 사실과 +실제 전원 장애에서 파일시스템·장치의 내구성을 검증했다는 주장은 별개다. + +## 확인한 것과 남은 비용 + +Linux AArch64에서 일반 빌드와 ASan·UBSan 빌드가 모두 통과했다. SQL 라이브러리 링크는 없다. + +- 저장 단위 검사: 범위·페이지 경계·예산, 갱신 중 이전 참조, 충돌·빈 본문·버전 소진, + 잘못된 저장 파일, 공개 전후 실패 주입, 공개 전 프로세스 종료와 재개. +- 실제 TCP 통합 16개 시나리오: 기존 작성물 복원, 두 프로세스의 쓰기 경쟁, 부분 송수신, + 읽는 중 다른 저장, 연결 종료, 자원 포화와 회수, 응답 미수신, 재시작 등. +- 검사 종료 후 활성 연결과 참조 본문 길이는 0이다. 현재 기록은 지정한 자원의 정확성 + 검사이며 지연·처리량·전체 RAM 비교가 아니다. + +현재 경로는 별도 응답 본문 사본을 만들지 않고 SQL 엔진도 경유하지 않는다. 커널 복사· +페이지 폴트·파일 동기화·클라이언트 직렬화는 남는다. 동기 파일 작업은 다른 연결의 처리를 지연시킬 수 있다. +이전 매핑은 이전 파일의 디스크 수명도 연장한다. 공개 전 종료로 남은 임시 파일을 자동으로 +정리하지 않으므로 장기 디스크 사용량의 상한도 아직 보장하지 않는다. 본문 체크섬·부분 쓰기· +다중 객체 트랜잭션은 없고, macOS·전원 장애·디스크 고장도 검사하지 않았다. + +[원자료와 재현](../../prototypes/connections/results/2026-09-23-byte-storage/README.md), +[직접 저장 구현](../../prototypes/connections/store.c), +[구간·수명 API](../../prototypes/connections/store.h). +018의 원자료는 당시 SQLite 구현의 기록으로 별도 보존한다. + +## 다음 결정 + +현재 연결 순서는 여전히 C에 고정돼 있다. 다음은 **필요한 데이터 범위·연산·외부 효과· +소비 완료의 관계를 실행 가능한 Pure 정의로 표현하는 것**이다. 이미 있는 작업의 저장과 +응답 경로에서 시작하고, 공개됐지만 영속화가 불확실한 결과를 단순 실패나 롤백으로 +합치지 않는다. 데이터의 배치는 실제 소비·갱신 패턴에서 결정하며, 새 쿼리 엔진이나 +SQL 대체 제품 개발을 다음 목표로 추가하지 않는다. diff --git a/docs/research/020-prepared-connections.md b/docs/research/020-prepared-connections.md new file mode 100644 index 0000000..89039b1 --- /dev/null +++ b/docs/research/020-prepared-connections.md @@ -0,0 +1,87 @@ +# 020 — 작성한 저장·응답 관계에서 실행 경로 준비 + +> 후속 [021](021-field-connections.md)에서 이름 기반 필드와 입력 값에 의존하는 범위를 추가했다. +> 이 문서의 구조체 크기와 상수 구간 한정 설명은 당시 구현의 기록이다. + +2026-09-23. **이제 C 서버를 고치지 않고 저장할 구간과 응답할 구간을 `.pure`에 작성할 수 +있다.** 시작 때 별칭과 중첩 구간을 최종 접근 범위로 합치고, 요청 처리에는 그 결과만 쓴다. +응답 소비자가 필요한 범위가 실제 파일 매핑까지 전달된다. 작성 관계가 실행 경로에 영향을 +주는 첫 제한된 구현이다. 문법을 추가한 사실이나 새 알고리즘의 성능 우위를 성과로 삼지 않는다. + +기존 작업물 전체를 저장·복원하는 [storage.pure](../../prototypes/connections/examples/storage.pure)를 +기본 사용 예로 유지했다. [excerpt.pure](../../prototypes/connections/examples/excerpt.pure)를 고치면 +같은 서버 실행 파일로 다른 구간을 응답할 수 있다. 실행 중 교체는 지원하지 않으므로 재시작이 +필요하다. [실행·작성 방법](../../prototypes/connections/README.md). + +## 작성 관계가 바꾼 것 + +```text +on get { + let object = store.read(request.key); + let window = range(object.body, 16, 64); + let excerpt = range(window, 16, 16); + reply(object, excerpt); +} +``` + +이 정의의 준비 결과는 다음과 같다. + +- 파일 길이는 **80 B 이상**이어야 한다. 최종 응답에 들어가지 않는 부모 구간의 조건도 남는다. +- 응답 범위는 **오프셋 32부터 16 B**다. 파일 헤더를 확인한 뒤 이 범위만 매핑한다. +- 별칭·중간 구간을 위한 실행 노드나 본문 배열은 만들지 않는다. 매핑·최종 주소 준비·검사는 남는다. +- 파일의 존재·형식 오류와 응답의 revision을 유지한다. 범위 검사와 매핑은 같은 파일 버전에서 수행한다. + +실제 1 MiB 객체에서 이 16 B를 응답했고 논리적 본문 접수 예산도 16 B로 처리했다. +이는 요구 범위가 저장 경로까지 전달됐다는 정확성 확인이다. 실제 매핑 페이지·RSS·디스크 +사용량이 16 B라는 뜻이 아니며, 전체 응답과의 처리량 비교도 아니다. + +쓰기에서도 입력의 일부를 저장하고 같은 부분을 응답할 수 있다. 그 경우 수신 버퍼를 +저장 호출 직후 해제하지 않고 전송 완료·중단까지 유지한다. 요청에서 일부만 참조하더라도 +원본 수신 할당 전체를 보유하는 비용은 남는다. 사용자는 복사나 해제 콜백을 작성하지 않는다. + +## 제거하면 안 되는 작업 + +최종 `[32,48)`이 유효해도 부모 `[16,80)`이 유효하지 않으면 실패해야 한다. 사용되지 않는 +`range`도 길이 부족을 관찰할 수 있으므로 검사까지 제거하지 않는다. 현재 모든 범위 오류는 +같은 실패 종류이며 같은 원본에 대한 최소 길이 조건을 최댓값 하나로 합칠 수 있다. +구간별 오류 위치·다른 효과·서로 다른 버전을 관찰하는 연산이 생기면 이 규칙을 그대로 +확장할 수 없다. + +쓰기의 뷰 정의는 저장 효과보다 앞에 둔다. 실패할 입력으로 저장한 뒤 나중에 오류를 +발견하는 것을 막는다. 본문 없는 응답도 저장 효과나 읽기 대상의 존재 검사를 없애지 않는다. +공개 후 영속화 불확실 결과는 기존 `durability_unknown`으로 전달하며 성공으로 바꾸지 않는다. + +현재 서버는 준비된 범위·조건을 사용해 직접 저장 API와 전송기를 호출한다. 요청마다 +노드 목록을 순회하거나 소스를 다시 해석하지 않는다. `--check-program`의 +`runtime_view_steps: 0`은 이 구현 구조의 설명값이며 CPU 명령 수나 실행 중 측정값이 아니다. +일반/검사 빌드에서 준비된 프로그램 구조체는 112 B다. 이는 전체 서버·컴파일러 메모리가 아니다. + +## 검증과 범위 + +Linux AArch64에서 일반 빌드와 ASan·UBSan 빌드가 모두 통과했다. + +- 기존 실제 TCP 저장·Workbench 복원 등 16개 시나리오와 새 작성·범위·효과·수명 시나리오 + 6개를 합쳐 22개. SQL 의존성은 다시 도입하지 않았다. +- 중간 부모의 범위 오류, 쓰지 않는 뷰의 범위 오류, 본문 없는 읽기의 파일 오류를 보존한다. +- 입력을 돌려주는 응답의 부분 전송·완료·중단과 원본 회수, 중단 후 저장 내용 유지. +- 작성 오류는 저장소 생성·네트워크 접수 전에 거절한다. 파싱 실패는 준비 결과를 변경하지 않는다. +- 순서대로 범위를 검사하는 참조 계산과 준비된 범위의 결과·실패를 경계 조건에서 대조했다. + 이 단위 검사는 실행 속도 비교가 아니다. + +[원자료·소스 사본](../../prototypes/connections/results/2026-09-23-prepared-connections/README.md), +[준비 구현](../../prototypes/connections/program.c), +[실제 실행 경로](../../prototypes/connections/server.c), +[작성 계약](../design/004-prepared-connections.md). + +지금은 등록된 읽기·쓰기·응답과 상수 바이트 범위에 한정한다. 요청당 효과는 하나이며, +숫자/화면 DSL과도 파서를 통합하지 않았다. 동적 범위·조건 분기·피드백·임의 연산·다중 소비자· +여러 외부 효과·비동기 저장을 지원하는 일반 실행 모델은 아직 아니다. macOS 실행, 실제 +전원 장애, 전체 지연·RAM 우위도 검증하지 않았다. + +## 다음 결정 + +현재는 작성자가 바이트 위치를 알아야 한다. 이 부담을 제품의 정상적인 사용성으로 굳히면 +안 된다. 다음에는 **실제 데이터의 필드·연산·소비 관계를 표현하고, 그 관계에서 배치와 +접근을 정하는 것**을 다룬다. SQL·범용 객체를 먼저 정하지 않고 실제 앱 데이터에서 필요한 +조회·갱신과 외부 형식의 변환 지점을 찾는다. 이번 상수 구간 데모의 크기나 벤치마크 행렬은 +늘리지 않는다. 시간 진행과 효과 재실행 의미가 필요한 피드백은 별도 계약 없이 추가하지 않는다. diff --git a/docs/research/021-field-connections.md b/docs/research/021-field-connections.md new file mode 100644 index 0000000..ad1e98e --- /dev/null +++ b/docs/research/021-field-connections.md @@ -0,0 +1,89 @@ +# 021 — 필드와 입력 값으로 정하는 저장·응답 + +2026-09-23. **바이트 위치를 연산마다 직접 쓰지 않고, 필드 이름과 실제 입력 값으로 저장할 +내용을 정할 수 있게 했다.** 문서·선택 시작·길이를 받아 선택한 내용만 저장하고 응답하는 +네이티브 경로를 구현했다. 한글·이모지가 있는 UTF-8 파일을 예제 클라이언트에서 선택하고 +다시 조회해 같은 바이트가 남는 것을 확인했다. + +이 단계는 작성 표현과 실제 계산·저장·소비를 연결한 기능 결과다. 범용 타입 시스템이나 +새 직렬화 표준을 확정한 것이 아니며 성능 우위도 주장하지 않는다. + +## 사용자가 작성하는 관계 + +[selection.pure](../../prototypes/connections/examples/selection.pure)는 경계의 필드를 선언한다. + +```text +layout Selection { + start: u32le; + count: u32le; + document: bytes; +} + +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); +} +``` + +`layout`은 이 입력 경계의 배치다. 선언 순서와 타입 폭으로 접근 위치를 정하며, RAM 전체나 +저장소를 같은 레코드로 강제하지 않는다. 이 예제의 저장 본문은 선택한 원본 문서 바이트이고 +입력의 start·count 필드는 저장되지 않는다. 기존 전체 바이트 저장 경로에는 layout도 필요 없다. + +이름·타입·별칭은 서버 시작 때 해소한다. 요청에서는 두 u32 값을 정렬되지 않은 위치에서도 +little-endian으로 읽고, 범위를 검사한 뒤 원본의 해당 구간을 참조한다. 레코드 객체·필드 사전· +선택 본문 사본을 추가하지 않는다. 저장과 응답은 같은 계산 결과를 쓰며 응답 완료까지 원본을 유지한다. + +실제 사용은 [Python 예제 클라이언트](../../prototypes/connections/examples/select-text.py)가 +UTF-8 텍스트 파일과 문자 위치를 받아 요청한다. 클라이언트는 Unicode 코드 포인트 위치를 +바이트 위치로 바꾼다. 서버의 bytes 연산 자체는 UTF-8이나 사용자에게 보이는 글자 덩어리를 +검증하지 않는다. [실행 방법](../../prototypes/connections/README.md). + +## 준비할 수 있는 것과 실행해야 하는 것 + +상수 구간은 기존처럼 합친다. 필드의 실제 값에 의존하는 구간만 준비된 범위 연산 목록에 +남긴다. 예제는 한 단계이며 그 결과에 별칭을 더 붙여도 단계 수는 늘지 않는다. +동적 구간의 하위 구간도 표현할 수 있다. 사용하지 않는 동적 구간의 실패도 관찰 가능한 +동작으로 유지하고, 모든 입력 검사가 성공한 뒤에만 쓰기를 호출한다. + +GET에서는 이름 기반 고정 필드와 가변 꼬리 구간을 선택한다. 꼬리의 길이는 같은 파일 버전의 +전체 크기에서 구하므로 본문 전체를 별도 배열로 읽지 않는다. GET의 숫자 필드 값에 의존하는 +범위는 현재 거절한다. 필요한 숫자와 선택 본문을 같은 버전에서 읽는 계획을 구현하지 않고 +전체 매핑으로 대체하지 않았다. + +지원 타입은 u32le·고정 bytes[N]·마지막 가변 bytes다. 선언은 최대 4개, 필드는 선언당 16개, +입력 값에 의존하는 범위는 8단계다. 버전 식별·마이그레이션·일반 산술·중첩 배치·여러 가변 +필드를 지원하는 최종 애플리케이션 데이터 모델은 아직 아니다. 송수신자가 배치에 합의해야 한다. + +## 비용과 검증 + +Linux AArch64에서 일반 빌드와 ASan·UBSan 빌드가 통과했다. + +- 기존 22개 통합 시나리오에 실제 문서 선택, 잘린 입력·동적 범위, 필드 순서·정렬 변경, + 사용되지 않은 계산의 오류, 저장된 가변 꼬리 조회, 부분 전송 수명을 추가해 28개를 확인했다. +- 선언 순서를 바꾸면 실제 숫자 접근 위치도 달라진다. 고정된 헤더 위치를 서버에 숨겨 넣은 것이 아니다. +- 정렬되지 않은 u32, 여러 바이트에 걸친 수, 최대 u32, 빈 구간·끝 위치·범위 초과를 검사했다. +- 오류가 나면 기존 내용·revision을 유지한다. 느린 응답에서는 선택 본문 길이만 계상하는 대신 + 보유 중인 원본 수신 할당 전체를 계상하고, 완료 뒤 반납한다. + +이 기능에는 비용이 추가됐다. 준비 구조체는 프로그램당 **112 B에서 648 B로 증가**했다. +최대 8개의 동적 범위 계획도 고정 예약하는 현재 구현의 크기이며 전체 RSS·컴파일러 크기가 아니다. +요청 계산에는 고정된 임시 span 배열을 쓰고 이 기능 때문에 힙 객체를 만들지는 않는다. +기존 수신 할당·파일 쓰기·페이지 매핑·커널 복사·클라이언트 인코딩 비용은 남는다. +따라서 이번 결과를 전체 메모리 절감이나 처리량 개선으로 해석하지 않는다. + +[원자료·소스 사본](../../prototypes/connections/results/2026-09-23-field-connections/README.md), +[필드와 범위 준비·계산](../../prototypes/connections/program.c), +[계약](../design/005-field-connections.md). + +macOS 실행, 전원 장애, 대규모 서비스, 네이티브 UI, 현재 숫자/화면 DSL과의 언어 통합은 +검증 범위 밖이다. 저장 효과는 여전히 요청당 하나이고 파일 작업은 동기식이다. + +## 다음 결정 + +현재 입력 값과 소비자를 연결했지만, 저장된 값의 조회·변경·다시 저장은 한 요청에서 작성할 +수 없다. 다음은 **실제 앱의 읽기·변경·저장 경로에서 연산과 버전 관계를 연결하는 것**이다. +읽은 버전과 조건부 쓰기의 관계, 서로 다른 생산자가 제공하는 값의 수명을 먼저 정한다. +숫자 타입·문법을 늘리기보다 기존 실행 코어에서 재사용할 연산과 필요한 외부 효과 경계를 +구체적인 작업에서 결정한다. 이번 문서 선택 데모의 크기를 키운 반복 측정은 하지 않는다. diff --git a/docs/research/022-execution-core.md b/docs/research/022-execution-core.md new file mode 100644 index 0000000..dfe826e --- /dev/null +++ b/docs/research/022-execution-core.md @@ -0,0 +1,72 @@ +# 022 — HTTP 연결에서 분리한 작은 실행 코어 + +2026-09-23. 상태: 저장·범위 선택 프로필의 구현과 정확성 검증. +첫 공개 체크포인트의 일부이며 통합 OS·범용 앱 실행·고성능 RPC 완성 보고가 아니다. + +## 실제로 바뀐 책임 + +이전에는 HTTP `Connection`이 본문 할당·예산·저장 호출·해제를 소유했다. +이제 `runtime.c`가 작업 슬롯과 세대, 입력 예산과 소유권, 준비 대기열, 효과 실행, +결과 보유·회수를 관리한다. 서버는 바이트를 받아 접수된 입력을 채우고, 완료 결과를 전송한다. +본문이 일부만 도착하고 끊기면 준비되지 않은 작업을 해제하므로 저장 효과가 발생하지 않는다. + +코어는 호출자가 제공한 작업 슬롯을 사용한다. 영역마다 스레드·소켓·힙·큰 큐를 만들지 않는다. +준비된 `PureProgram`은 읽기 전용으로 공유할 수 있다. 현재 효과는 영역에 연결한 `PureHost`의 +동기 읽기·쓰기·할당 함수다. 파일·매핑·할당 구현은 `store-port.c`에 있고 HTTP는 `server.c`에 있다. +이 API는 C 내부 연결 계약이며 메모리 격리·안정된 외부 ABI·포괄적 권한 시스템은 아니다. + +준비된 프로그램의 실행을 `evaluate.c`로 분리했다. `libpure-execution.a`는 `runtime.c`와 +`evaluate.c`만 포함한다. 서버는 소스 로딩을 위해 여전히 별도로 `program.c`를 링크한다. +코어 아카이브가 작다는 이유로 서버 전체에서 파서 비용이 사라졌다고 해석하지 않는다. + +## 내부 연결과 수명 + +완료된 결과를 다른 영역의 입력으로 전달하면 코어가 원본 작업을 pin한다. 중간 버퍼 할당· +직렬화·소켓 전송 없이 같은 데이터 구간을 사용하며, 생산자 쪽 호출자는 자신의 티켓을 +먼저 해제할 수 있다. 마지막 소비가 끝나야 원본 메모리·매핑이 반환된다. +한 결과의 여러 소비자 중 하나가 취소돼도 나머지 소비자의 참조는 유지한다. +전달된 입력을 응답에서 사용하지 않는 프로그램은 저장 효과가 끝나면 원본을 일찍 해제한다. + +긴 전달 연결의 회수는 반복문으로 처리한다. 1,024개 연결 검사는 재귀적 해제로 스택이 +증가하지 않고 할당 한 개를 마지막에 한 번 회수하는지 확인하는 수명 검사다. 성능 벤치마크가 아니다. +각 영역의 예산은 자신의 유지 입력·출력에 대한 접수 크레딧이다. 공유된 데이터를 영역별로 +청구하므로 여러 영역의 크레딧을 합해 물리 메모리라고 주장하지 않는다. + +종료는 새 접수를 막고 기존 작업을 배출한다. 진행·완료·대여 작업이 남은 영역의 재시작은 +거절하며, 완전히 배출된 뒤 재시작해도 티켓의 단조 증가 세대는 유지한다. +이전 티켓은 새 작업의 결과를 읽거나 해제할 수 없다. 실행 전 취소와 효과 실행 후 결과는 구분한다. + +## 검증 근거 + +[원시 결과](../../prototypes/connections/results/2026-09-23-execution-core/)의 native·ASan/UBSan +빌드에서 기존 28개 실제 TCP 시나리오와 실행 코어 검증을 통과했다. +기존 범위 의미 비교 3,584개와 필드 경계 사례 48개, 저장 실패·매핑·전송 검사도 유지했다. + +추가로 확인한 것은 다음과 같다. + +- 준비되지 않은 입력·취소·대기열 제거는 저장 효과를 실행하지 않는다. +- 슬롯·바이트 예산 포화, 할당 실패, 제공되지 않은 쓰기 권한은 접수/실행 경계에서 처리한다. +- 호스트 호출 중 같은 영역의 재진입을 거절한다. 읽기 호스트가 반환 한도를 어겨도 자원을 회수한다. +- 완료 결과의 두 소비자 중 하나를 취소하고 생산자의 티켓을 먼저 해제해도 나머지 결과는 유효하다. +- 실제 입력 숫자로 선택한 `bcd`를 첫 영역에 저장하고 두 번째 영역으로 전달해 별도 저장소에 보존한다. +- 파일을 매핑한 결과를 다른 영역에 넘긴 뒤 원래 저장 객체를 교체해도 이전 내용으로 완료한다. +- 잘못된 입력 범위는 실제 저장 파일을 만들지 않는다. 종료 뒤 모든 영역의 작업·크레딧은 0이다. + +## 크기와 한계 + +Linux AArch64, GCC `-O2`에서 확인한 구조체 크기는 영역 192 B, 작업 슬롯 224 B, +티켓 24 B, 공유 가능한 준비 프로그램 648 B다. 본문·호스트 자원·스택·allocator·RSS는 별도다. +GNU `size`의 코어 두 오브젝트 `text` 합은 5,672 B이며 데이터·BSS는 0 B다. +이는 코드/읽기 전용 섹션 등에 대한 도구 수치이며 전체 프로세스 RAM이나 아카이브 파일 크기가 아니다. +코어 아카이브의 외부 심볼은 `memcpy`, `strlen`과 아카이브 내부에서 해결되는 +`pure_program_input`이다. 파서·할당기·파일·소켓·스레드 API를 직접 참조하지 않는다. + +8개 작업 슬롯을 추가한 서버 구조체는 이전 39,824 B에서 41,488 B로 **1,664 B 증가**했다. +HTTP 버퍼가 여전히 대부분을 차지한다. 이 변경은 RAM 절감 결과가 아니라 영역별 작업·수명 +관리를 추가한 비용을 명시한 것이다. 선택 기능을 제거한 전체 실행물과 실제 앱의 비용 검증은 남았다. + +현재 쓰기는 동기 파일 호출이며 오래 걸리는 효과를 선점하거나 다른 실행 영역의 지연을 +보장하지 못한다. core 제어 API는 한 소유 스레드에서만 사용한다. 입력은 여전히 전체 본문을 +받은 뒤 실행하며, 작성 프로필도 get/put·하나의 저장 효과에 한정된다. 소스 문법의 HTTP 명칭 +제거, 일반 앱 연산, 비동기 효과, 자체 RPC·TLS, 네이티브 UI, macOS 실사용 검증은 남아 있다. +이 결과로 Kitex·QUIC보다 빠르거나 네이티브 코드를 강제 격리한다고 주장하지 않는다. diff --git a/docs/research/023-document-edits.md b/docs/research/023-document-edits.md new file mode 100644 index 0000000..d2bcd5e --- /dev/null +++ b/docs/research/023-document-edits.md @@ -0,0 +1,102 @@ +# 023 — 문서 부분 변경과 코어 크기 제한 + +2026-09-23. Linux AArch64, GCC 14.2.0. 제한된 저장 프로필의 구현·정확성·구조 비용 기록이다. +첫 공개 체크포인트는 진행 중이며 네이티브 UI, 통합 앱 작성 모델, 보안 RPC는 아직 남아 있다. + +## 이번 결정 + +실제 편집에서는 문서 전체를 다시 입력하기보다 바꿀 위치·삭제 길이·삽입 내용으로 변경을 +표현해야 한다. 이 효과를 기존 작성·접수·완료·회수 경로에 추가했다. 별도 편집 VM, 서비스 +관리자, 도메인별 스레드·힙·소켓은 만들지 않았다. 코어가 선택 기능의 구현을 끌어오지 않는지, +추가한 관리 비용이 얼마인지 함께 검사한다. + +## 작성과 실행 + +[editor.pure](../../prototypes/connections/examples/editor.pure)는 입력의 두 `u32le` 필드와 +삽입 바이트를 다음 효과에 연결한다. + +```text +store.splice(request.key, request.revision, edit.start, edit.remove, edit.insert) +``` + +효과는 저장된 값의 `[0,start)`와 삽입 내용, `[start+remove,end)`를 이어 새 버전을 공개한다. +예상 버전과 저장 값의 범위 검사는 같은 작성자 잠금 안에서 수행한다. 존재하지 않는 값은 +예상 버전 0, 위치 0, 삭제 길이 0일 때 만들 수 있다. 빈 변경도 성공하면 버전이 증가한다. +바이트 위치이며 문자열·문자소·UTF-8 유효성을 자동 검사하는 연산은 아니다. + +한 핸들러에 저장 효과는 여전히 하나다. 입력 뷰의 검증은 효과 전에 완료하며 반환 본문은 +입력 바이트를 빌린다. 보통 쓰기 권한만 제공된 호스트에서 splice를 접수하면 거절한다. +저장 성공 이후 응답이 끊겨도 효과를 되돌리지 않는다. 파일 공개 후 디렉터리 동기화가 실패하면 +`durability_unknown`을 반환한다. 같은 예상 버전으로 무조건 재시도하면 충돌할 수 있다. + +네이티브 C CLI `pure-edit`가 같은 준비 프로그램과 실행 코어를 사용한다. `edit`은 준비된 +필드 위치에 인자를 채운다. 선언 순서를 바꾸거나 숫자가 비정렬 위치에 있어도 대응한다. +고정 위치·동적 범위 등 편의 명령으로 표현할 수 없는 입력은 `apply`로 원시 입력 파일을 +전달한다. 로컬 실행에는 소켓이 필요 없다. 별도 프로세스의 HTTP 클라이언트와도 같은 +저장 버전으로 변경·충돌·복원을 확인했다. 이 CLI를 네이티브 UI 완성으로 세지 않는다. + +## 메모리와 저장 비용을 구분한다 + +1 MiB 이진 문서 중 3 B를 바꾸는 경로에서 입력은 위치 4 B + 삭제 길이 4 B + 삽입 3 B다. +네이티브 CLI와 실제 TCP 서버 모두 **입력 크레딧 11 B**로 실행했고, 서버에서 더 큰 입력은 +효과 전에 거절했다. 저장소는 변경하지 않은 부분을 **4 KiB 호스트 스택 버퍼**로 옮긴다. +새 문서 전체를 앱의 힙 버퍼로 조립하지 않지만 **저장 파일 전체는 여전히 다시 쓴다.** + +11 B는 전체 RAM·RSS·피크 메모리가 아니다. 코어 구조체·스택·파서·allocator·HTTP 버퍼· +매핑 페이지·커널 캐시·파일 I/O가 따로 있다. 재작성의 디스크 대역폭이나 동기 `fsync` 지연을 +없앤 결과도 아니다. 현재 입력 본문 자체는 전부 받은 뒤 실행한다. 다음 저장 배치 최적화는 +실제 앱에서 재작성 비용이 문제가 되는 근거에 따라 결정한다. + +## 코어가 커지는 것을 검출한다 + +`make core`, `make editor`, `make server`로 구성을 각각 빌드한다. 깨끗한 출력 디렉터리에서 +`make core`가 만드는 것은 `runtime.o`, `evaluate.o`, `libpure-execution.a`뿐임을 확인했다. +편집기와 서버는 선택한 저장·소스 파서를 추가로 포함한다. 실행 전용 코어에는 필요하지 않다. + +`make check-core`는 [예산 파일](../../prototypes/connections/core-budget.json)에 따라 현재 +프로필의 GNU `size` text 합 **6 KiB**, data/BSS **0 B**, 아래 구조체의 현재 크기를 상한으로 +검사한다. 아카이브 멤버와 미해결 외부 심볼도 확인한다. 전용 힙·파일·소켓·스레드 API를 +연결하는 변경은 검사 대상이다. 6 KiB는 현재 5,932 B의 증가를 감지하기 위해 정한 개발 상한이며 +범용 런타임 전체의 크기 약속이나 실측 RAM이 아니다. 다른 플랫폼·도구에서는 다시 검증한다. +확장에 필요하다는 이유만으로 상한을 자동으로 올리지 않고 기능 위치와 비용을 먼저 검토한다. + +| 항목 | 022 | 현재 | 증가 | +|---|---:|---:|---:| +| 실행 코어 두 오브젝트의 `size text` | 5,672 B | 5,932 B | 260 B | +| 실행 영역 구조체 | 192 B | 200 B | 8 B | +| 작업 슬롯 구조체 | 224 B | 224 B | 0 B | +| 공유 가능한 준비 프로그램 | 648 B | 680 B | 32 B | +| 티켓 | 24 B | 24 B | 0 B | +| HTTP 서버 구조체 | 41,488 B | 41,528 B | 40 B | + +이번 추가 비용은 선택한 효과를 호출하는 분기·인자 읽기, 호스트 함수 포인터 하나, +준비된 두 위치 값이다. splice를 사용하지 않는 프로그램에도 현재 준비 구조체의 증가가 +적용된다. 따라서 선택 저장 구현을 분리했다는 이유로 모든 관리 비용까지 선택적이라고 하지 않는다. + +코어의 실제 외부 심볼은 `memcpy`, `strlen`이다. 크기 상한을 낮추면 실패하고, 검사용 아카이브에 +`malloc` 의존성·4 KiB 전역 상태를 추가하면 실패하는 것도 확인했다. 계측 도구 자체는 코어에 없다. + +전체 선택 실행물의 수치는 다음과 같다. 둘 다 소스 파서를 포함하고 직접 동적 의존성은 +이 환경의 `libc.so.6`다. 공유 라이브러리·로더의 메모리를 아래 파일 크기에 포함한 것은 아니다. + +| 구성 | `size` text/data/bss 합 | 심볼 제거 후 ELF 파일 | +|---|---:|---:| +| 네이티브 편집 CLI | 36,767 B | 67,904 B | +| 루프백 HTTP 서버 | 43,817 B | 68,000 B | + +파일 정렬·헤더와 디버그 정보가 있으므로 디스크 파일 크기, 섹션 크기, 구조체 크기는 서로 다르다. +실제 앱의 유휴·피크 메모리와 선택 라이브러리 전체 비용 검증은 여전히 남아 있다. + +## 검증과 남은 범위 + +[동결된 근거](../../prototypes/connections/results/2026-09-23-document-edits/)에 일반·ASan/UBSan +빌드 결과, 소스 37개와 해시, 코어 및 선택 빌드 크기를 보관했다. 각 빌드의 통합 시나리오 36개, +기존 구간 의미 비교 3,584개·필드 사례 48개, 실행 수명 검사를 통과했다. 통합 검사에는 +네이티브 CLI와 실제 TCP를 통한 왕복·두 작성자의 동일 버전 경쟁·불완전 입력이 포함된다. +새 저장 단위 검사는 이진 접두부/접미부 보존·이전 매핑 유지·위치 및 크기 초과·버전 소진· +공개 전 실패·공개 후 내구성 불확실·임시 파일 회수를 확인한다. 모든 서버 종료 시 작업과 +본문 크레딧은 0이다. 이는 정확성과 크기 경계의 증거이며 처리량 우위나 실제 정전 시험이 아니다. + +여전히 단일 소유 스레드, 동기 저장, get/put 작성 프로필이다. 문자열 편집 UI·일반 연산· +비동기 효과·작성 모델 통합·자체 보안 RPC·macOS 실사용은 [체크포인트](../../CHECKPOINT.md)에 +남긴다. 이 기능과 검사를 새 범용 프레임워크의 근거로 확장하지 않는다. diff --git a/docs/research/024-native-app-instances.md b/docs/research/024-native-app-instances.md new file mode 100644 index 0000000..fee556d --- /dev/null +++ b/docs/research/024-native-app-instances.md @@ -0,0 +1,109 @@ +# 024 — 기존 작성물을 네이티브 앱 상태·저장 경로에 연결 + +2026-09-23. Linux AArch64, Zig 0.15.2 / GCC 14.2.0. +첫 공개 체크포인트 진행 중이다. 이번 실행 호스트는 터미널이며 네이티브 창은 아직 없다. + +## 바뀐 경계 + +기존 Workbench의 계산 실행기는 전역 `engine` 하나를 사용했다. Wasm 인스턴스마다 메모리가 +분리되는 브라우저에서는 여러 작성물을 실행할 수 있었지만, 같은 코드를 네이티브 라이브러리로 +붙이면 앱마다 독립된 상태를 제공하지 못했다. + +실제 계산을 `engine.zig`의 명시적인 `Engine` 인스턴스로 옮겼다. 기존 `core.zig`는 브라우저 +ABI와 회귀 검사를 유지하는 얇은 연결부다. 네이티브 `native.zig`는 호출자가 제공한 정렬된 +메모리에 인스턴스를 초기화하고 C 함수로 접근한다. 네이티브 모듈에는 기본 VM 인스턴스, +할당기, 파일·소켓·스레드가 없다. 실행 정의의 준비·실패·복원·평가 기록도 해당 인스턴스에 속한다. + +이 계산 모듈은 **앱이 선택하는 기존의 제한된 scalar 실행기**다. 모든 Pure 실행 영역에 +자동으로 포함하는 새 공통 VM이 아니다. 작업 접수·수명·대여·예산을 맡는 C 코어는 변경하지 않았다. + +## 작성에서 네이티브 실행까지 + +새 문법이나 별도 파서를 만들지 않았다. `native/prepare.mjs`가 기존 `compiler.mjs`로 `.pure`를 +준비하고 읽기 전용 C 데이터로 출력한다. 상태·입력 이름, 공유 규칙의 바인딩, 대상 라우팅, +표시 식이 그대로 들어간다. 문자열은 UTF-8 바이트의 C escape로 출력해 소스 문자열이 C 코드가 +되지 않게 한다. 이 데이터와 실행 모듈·호스트를 링크해 독립 실행 파일을 만든다. + +Node·Zig·C 컴파일러는 **빌드 도구**다. 만들어진 실행물은 Node·브라우저·Wasm 없이 구동한다. +실행 시 소스 파서나 AST를 보관하지 않는다. 다만 연산은 기존 준비 명령을 해석하며, 앱의 각 +연산을 기계어로 직접 생성하는 AOT 최적화 컴파일러를 구현한 것은 아니다. 현재 작성 변경은 +다시 빌드해서 반영한다. 실시간 소스 편집·교체는 아직 네이티브 호스트에 연결하지 않았다. + +## 하나의 실행 코어를 통과하는 앱 경로 + +`native/app.c`는 준비된 앱의 입력과 상태를 기존 C 실행 코어에 연결한다. + +```text +입력 → 코어의 접수·예산·작업 슬롯 → 선택한 계산 인스턴스 → 상태/표시 값 +상태 스냅샷 → 코어의 내부 결과 대여 → 조건부 바이트 저장 +저장된 바이트의 매핑 → 코어의 내부 결과 대여 → 검증 후 상태 복원 +``` + +저장과 복원은 기존 `pure_runtime_forward`를 사용한다. 전달을 위한 두 번째 본문 버퍼나 +소켓 직렬화를 추가하지 않는다. 단, **현재 상태를 스냅샷으로 만들 때는 복사한다.** 이후 입력이 +상태를 바꾸어도 저장할 내용이 달라지지 않도록 44 B의 헤더와 상태당 8 B를 별도 소유한다. +같은 데이터의 두 작업이 유지되는 동안 각 작업의 크레딧을 계산하므로 이를 물리 메모리로 합산하지 않는다. + +예제의 상태 25개는 스냅샷 244 B다. 버전 충돌은 상대 값을 덮어쓰지 않고 현재 앱 상태도 +바꾸지 않는다. 복원은 정의 식별자·상태 수·길이·유한한 값을 검사한 뒤 전체 정의와 값을 함께 +설치한다. 실패하면 기존 앱 상태와 채택한 저장 버전은 유지한다. 초기값과 현재 값은 별개라 +복원 후 `reset`은 작성된 초기값으로 돌아간다. + +정의 식별자는 빌드 도구가 만든 SHA-256이다. 의미만 같은 다른 소스나 위치 정보가 달라진 +정의를 자동으로 호환 처리하지 않으며, 정의가 다르면 복원을 거절한다. 이는 인증이나 서명이 +아니다. 현재 스냅샷은 브라우저 Workbench의 JSON 프로젝트 형식과 별개이며 변환기는 미구현이다. +수치 표현도 현재 IEEE binary64 경계에 한정한다. 모든 Pure 데이터를 이 배치로 강제하지 않는다. + +포인터는 입력 그룹이 아닌 실제 대상마다 소유자를 가진다. 같은 라우팅 입력의 두 대상을 +동시에 조작하고 하나만 취소할 수 있다. 다른 소유자의 이동·해제는 거절한다. 키 입력은 여러 +출처가 눌러도 첫 down과 마지막 up만 작성 규칙에 전달한다. 진행 중인 입력이 있으면 저장· +복원을 거절한다. 이벤트의 계산 실패는 상태 쓰기를 원자적으로 되돌리며 실제 입력 해제와 구분한다. +네이티브 명령 인터페이스의 오류 표현·무시 정책 전체가 브라우저 UI와 같다는 주장은 아니다. + +## micro 비용 + +일반 네이티브 라이브러리 설정은 기본 panic 보고·심볼 해석과 compiler-rt 아카이브를 포함했다. +이 선택 모듈에는 그 전체 기능이 필요하지 않았다. `ReleaseSafe`의 범위·오버플로 검사를 유지하고 +예상하지 못한 내부 불변식 위반은 trap으로 중단하도록 바꿨다. 정상적인 작성 오류는 기존 상태 +코드로 반환한다. 실패를 무시하거나 안전 검사를 꺼서 줄인 것은 아니다. 내부 버그 발생 시 +이 모듈이 자체 스택 추적을 출력하지 않는 비용 절충은 남으며, 네이티브 크래시 격리도 아니다. + +필요하지 않은 compiler-rt 전체를 포함하지 않은 뒤 실제 외부 심볼은 `memcpy`, `memset`뿐이다. +초기 아카이브 수치는 동결 자료에 보관했다. 링크 전 아카이브 합을 최종 실행물 크기 감소율로 +해석하지 않는다. + +| 범위 | 확인한 비용 | +|---|---:| +| 공통 C 실행 코어의 GNU `size text` | 5,932 B, 이전과 동일 | +| 선택한 scalar 라이브러리의 `size text` | 33,020 B | +| scalar 라이브러리의 data/BSS | 0 B / 0 B | +| 앱별 scalar 인스턴스 | 20,064 B, 정렬 8 B | +| 앱 컨트롤러 | 928 B, 실행 코어와 작업 슬롯 2개 포함 | +| 앱 입력 값 | 32 B | +| 상태 25개의 저장 스냅샷 | 244 B | +| 라우팅 예제 전체 CLI의 text/data/BSS 합 | 64,883 B | +| 같은 실행물의 심볼 제거 후 ELF 파일 | 68,000 B | + +계산 인스턴스의 20,064 B에는 최대 용량의 준비 정의·교체 작업 공간·상태·평가 기록이 들어간다. +읽기 전용 C 준비 데이터도 실행물에 존재한다. 현재 배열의 고정 예약과 정의 복사 비용을 +없앴다고 하지 않는다. 입력·스냅샷의 호스트 할당, 스택, libc·로더·커널·페이지·유휴/피크 RSS는 +위 구조체 수치와 별개다. 현재 ELF의 직접 동적 의존성은 `libc.so.6`이며 전체 의존 메모리 +비용을 측정한 결과가 아니다. 코어 크기 검사는 그대로 통과한다. + +## 실제로 확인한 범위 + +[동결된 근거](../../prototypes/workbench/results/2026-09-23-native-instances/)에 다음을 보관했다. + +- 기존 Zig 의미 검사 37개, 브라우저/Wasm 및 Node CLI 회귀 검사 87개 통과. +- 기존 카운터·토글·프레임 계산·드래그·공유 대상·숫자 입력과 빈 정의·계산 실패·문자열 경계 등 + 작성물 9개의 입력 156단계에서 네이티브와 Wasm의 상태·표시 값 일치. +- 각 네이티브 작성물을 저장한 뒤 새 프로세스에서 같은 정의로 복원해 값과 표시 식을 확인. +- 같은 프로세스의 인스턴스 3개에서 상태·준비 실패·평가 설정·입력 소유권·예산이 섞이지 않음을 확인. +- 실제 저장소의 버전 충돌, 다른 정의, 비유한 상태 복원 거절과 모든 작업·크레딧 회수 확인. +- 일반 C 호스트와 ASan/UBSan C 호스트에서 위 네이티브 경로 통과. Zig 오브젝트는 둘 다 + `ReleaseSafe`이며 ASan으로 계측된 Zig 빌드라고 주장하지 않는다. + +동시 소유 스레드 간 실행, 네이티브 창·IME·접근성·크기 변경, macOS 실사용은 이번 검증 범위에 없다. +기존 scalar 한도(상태 32, 입력 8, 표시 24, 명령 512)도 유지된다. 대규모 앱 전체를 이 한도에 +맞추라는 뜻이 아니라 이전 작성 모델을 실제 네이티브·저장 경로로 연결한 중간 구현이다. +전체 체크포인트의 일반 앱 표현·비동기 효과·보안 RPC·실제 화면·규모 검증은 계속 남아 있다. diff --git a/docs/research/025-native-authoring-shell.md b/docs/research/025-native-authoring-shell.md new file mode 100644 index 0000000..960ae7a --- /dev/null +++ b/docs/research/025-native-authoring-shell.md @@ -0,0 +1,103 @@ +# 025 — 네이티브 작성 창을 실행 코어에 연결하고 비용을 분리 + +2026-09-24. Linux AArch64, GCC 14.2.0, GTK 4.18.6, Zig 0.15.2. +첫 공개 체크포인트의 중간 결과다. **네이티브 창을 만들었다는 사실만으로 마이크로 앱 런타임이나 +네이티브 UI 모델이 완성된 것은 아니다.** 이번에 확인한 경계는 편집 도구를 붙이면서도 실행 전용 +구성을 이전 크기로 유지할 수 있는지다. + +## 실제 작업 경로 + +`manage.py native --desktop`은 기존 `.pure` 정의와 같은 C 실행 코어·독립 scalar 인스턴스를 +링크한 Linux GTK4 작성 창을 만든다. 왼쪽은 GTK의 `GtkTextView`, 오른쪽은 현재 제한된 +box/circle/text 출력과 입력을 보여 주는 화면이다. 작성자는 소스를 고치고 **Apply source**를 +누른다. 기존 JS 컴파일러는 편집 호스트의 자식 프로세스에서 준비 정의를 만들고, C 컴파일러는 +그 읽기 전용 데이터만 공유 라이브러리로 만든다. 편집 호스트가 이를 열어 후보를 제출한다. + +```text +GTK 입력·편집 → 선택한 C 호스트 → 실행 코어의 작업 접수·완료 → scalar 앱 인스턴스 + ↓ + SQL 없는 상태 저장·복원 +``` + +새 정의 설치도 코어의 `plan` 입력으로 보낸다. 진행 중인 입력·작업이 있으면 거절하고, 같은 이름의 +상태는 이어받는다. 컴파일·정의 검증에 실패하면 이전 실행 정의와 상태가 남는다. 성공해야 소스 +파일을 기록하며, 상태 스냅샷은 별도 **Save state**로 조건부 저장한다. 정의가 달라진 스냅샷은 +자동 호환시키지 않는다. 재시작 때 편집 호스트가 소스를 준비·적용한 뒤 **Load state**를 +요청할 수 있다. 준비된 `.pure`를 실행하는 CLI에는 GTK·Node·컴파일러·동적 모듈 로더가 없다. + +실시간 적용은 **선택한 개발 호스트**의 기능이다. 작성된 실행물의 앱 연산 전체를 AOT 기계어로 +컴파일한 것은 아니다. 로컬에서 신뢰하는 `.pure`와 도구체인을 전제로 하며, 공유 라이브러리 +적재를 격리나 원격 코드 실행 경계로 제시하지 않는다. 소스 문자열은 기존 `prepare.mjs`에서 +바이트 escape로 생성하므로 작성한 문자열을 C 문장으로 삽입하지 않는다. + +소스 입력은 GTK의 일반 텍스트 위젯을 써서 선택·편집·실행 취소를 제공한다. 이 구성은 +[GTK의 텍스트 위젯](https://docs.gtk.org/gtk4/class.TextView.html)과 +[입력 컨트롤러](https://docs.gtk.org/gtk4/input-handling.html)에 의존한다. 별도의 macOS IME· +접근성 실사용 검증을 했다는 뜻은 아니다. 표시는 GTK `DrawingArea`의 콜백이며 그 안에서 +앱 상태를 바꾸지 않는다. 이는 [GTK의 DrawingArea 계약](https://docs.gtk.org/gtk4/method.DrawingArea.set_draw_func.html)을 따른다. + +## 실제 창에서 확인한 동작 + +[근거 폴더](../../prototypes/workbench/results/2026-09-24-native-desktop/)에 Xvfb의 실제 GTK 창을 +조작한 화면과 검사 결과를 보관했다. + +- 세 도형 예제의 녹색 도형을 드래그해 놓고 상태를 저장했다. 새 CLI 프로세스가 같은 스냅샷을 + 복원했으며 `green.x=167.07482993197277`, `green.y=154.82993197278913`, `held=0`이었다. +- 창의 소스 끝에 `text live(20,20,selected,"LIVE: ")`를 추가해 적용했다. 기존 선택·도형 위치가 + 유지됐고 출력 `LIVE: 1`이 나타났다. +- `invalid source line`을 추가하면 `Unknown declaration invalid (73:1)`을 한 줄로 보여 주고, + 직전 화면·상태·저장된 소스를 유지했다. 잘못된 줄을 지우고 다시 적용한 뒤 새 정의의 상태를 + 저장했다. 새 CLI 프로세스는 저장 버전 2에서 기존 도형 위치와 새 `live` 표시 값을 복원했다. +- 창을 1320×720에서 900×600으로 줄인 뒤 같은 내용을 확인했다. 별도 시나리오에서 축소한 + 창에서 드래그·방향키 입력 후 저장한 값을 CLI가 복원했다. +- `document.pure`의 숫자 입력에 99를 넣자 작성한 `clamp` 규칙에 따라 저장 값 `ratio=80`을 + CLI에서 확인했다. 실제 한국어 IME 입력 시나리오는 아직 수행하지 않았다. + +기존 작성물 9개·입력 156단계의 네이티브/Wasm 상태·표시 일치, 독립 인스턴스, 실패·저장 충돌· +정의 재설치 검사는 일반 C 호스트와 ASan/UBSan C 호스트에서 통과했다. Zig 모듈은 ReleaseSafe로 +빌드했으며 ASan 대상이었다고 주장하지 않는다. 기존 Node 회귀 검사 87개도 통과했다. + +## 마이크로 비용: 범위를 섞지 않음 + +GNU `size`의 `text/data/bss`는 ELF 섹션의 합이며 RSS가 아니다. 아래 RSS·PSS는 이 호스트에서 +한 번 관찰한 프로세스 수치다. 컴파일러 자식 프로세스와 Xvfb 자체는 편집 창 프로세스 수치에 +포함하지 않는다. + +| 선택한 범위 | Linux AArch64에서 관찰한 비용 | +|---|---:| +| 공통 실행 코어 아카이브 `text/data/bss` | 5,932 / 0 / 0 B, 기존 6 KiB 예산 통과 | +| 선택 scalar 라이브러리 `text` | 33,020 B (기존 결과) | +| scalar 인스턴스 / 앱 컨트롤러 | 20,064 / 928 B | +| 실행 전용 CLI `text/data/bss` | 62,827 / 2,040 / 16 B | +| 실행 전용 CLI stripped ELF | 68,000 B; 직접 동적 의존성 `libc.so.6` | +| 실행 전용 CLI 유휴 VmRSS | 1,552 KiB | +| GTK 작성 창 `text/data/bss` | 78,964 / 2,824 / 16 B | +| GTK 작성 창 stripped ELF | 134,224 B; GTK/Pango/Cairo/GLib 동적 의존성 추가 | +| GTK 작성 창 유휴 VmRSS / PSS | 36,512 / 23,618 KiB, `GSK_RENDERER=cairo GDK_DISABLE=gl,vulkan` | +| 같은 창의 기본 렌더러 유휴 VmRSS / PSS | 173,808 / 139,710 KiB, 별도 한 번의 관찰 | + +Xvfb에서는 GPU 가속이 없으므로 마지막 두 값은 렌더러 설정에 민감했다. GTK가 문서화한 +[`GSK_RENDERER`와 `GDK_DISABLE`](https://docs.gtk.org/gtk4/running.html) 설정으로 비교했으며, +실제 GPU 환경이나 macOS 수치로 확대하지 않는다. 36 MiB조차 1.5 MiB 실행 전용 앱과 같지 +않다. 편집기 전체를 마이크로라고 주장하지 않는다. + +작성 전용 정의 교체를 공통 `app.c`에 무조건 링크했을 때 CLI `text`가 63,507 B로 늘고, +AArch64 ELF 세그먼트 정렬 경계를 넘으면서 stripped 파일이 133,536 B가 됐다. +`PURE_NATIVE_AUTHORING` 선택 빌드로 분리한 뒤 CLI는 이전의 `text=62,827 B`, stripped +68,000 B로 돌아왔다. 단순히 함수 바이트 수만 보면 놓치는 **최종 실행물의 계단식 비용**이다. +코어 자체의 코드나 외부 의존성은 증가하지 않았다. + +## 아직 성립하지 않은 주장 + +현재 보기는 기존 `.pure`의 고정된 640×360 논리 좌표를 창 크기에 맞춰 확대·축소한다. 실제 +제약 기반 레이아웃, 터치·멀티 포인터의 네이티브 검증, UI 요소의 컨테이너 이동이나 완전한 +접근성은 아직 없다. GTK 편집 호스트는 소스 적용 동안 동기적으로 컴파일하므로 긴 컴파일은 +화면 응답을 막을 수 있다. 정의 설치 후 소스 파일 쓰기가 실패하면 새 정의는 메모리에만 남고 +오류를 표시한다. 재시도는 가능하지만 정의·파일을 하나의 트랜잭션으로 확정하지 않는다. +소스 파일 저장에는 다른 편집자의 변경에 대한 조건부 갱신도 아직 없다. macOS Apple Silicon의 +AppKit/Metal 경로는 구현·검증하지 않았다. + +더 중요한 전체 체크포인트 요구인 일반 앱 표현·비동기 I/O·Pure 간 인증된 RPC·큰 실서비스· +동등 조건 성능 비교도 남았다. UI 작성 창은 런타임 전체가 아니다. 다음 판단은 실행 전용 +구성을 작은 상태로 유지하면서 **실제 앱 작업**에서 네트워크·저장·UI를 연결할 수 있는지에 +달려 있다. diff --git a/docs/research/026-secure-peer-link.md b/docs/research/026-secure-peer-link.md new file mode 100644 index 0000000..de31986 --- /dev/null +++ b/docs/research/026-secure-peer-link.md @@ -0,0 +1,105 @@ +# 026 — 선택형 Pure 피어 연결과 실제 비용 + +2026-09-24. Linux AArch64, GCC 14.2, Zig 0.15.2, OpenSSL 3.x. 첫 공개 체크포인트의 **중간 결과**다. +이 문서의 크기는 전체 용량 scalar 모듈을 사용한 첫 구현 기준이다. 이후 고정 정의 빌드의 +비용 변화는 [027](027-fixed-native-capacity.md)에 기록했다. + +## 구현한 경계 + +기존 `routed-layout.pure`를 준비한 같은 앱 인스턴스에 `pure-peer`가 입력·조회·저장·복원을 제출한다. +서버는 TCP 소켓에서 TLS 1.3을 수립한 뒤 길이가 명시된 작은 바이너리 메시지를 읽고, 기존 +`PureNativeApp`과 실행 코어를 호출한다. HTTP 문자열 헤더·메서드·URL이나 SQL 저장소를 이 +연결의 내부 형식으로 사용하지 않는다. 일반 Python `ssl` 클라이언트도 이 형식을 구현해 +상태를 읽고 작업을 제출했다. 기존 평문 HTTP 경계도 별도로 유지한다. + +```text +외부 TCP → TLS 1.3 → 선택형 peer.c/peer-wire.c → 같은 앱 작업 → 실행 코어 → scalar·저장 +``` + +이것은 **앱 선택 호스트**다. `runtime.c`, scalar 모듈, 실행 전용 `pure-native`에는 소켓·TLS +형식이 들어가지 않는다. OpenSSL을 직접 구현하지 않고 검증 가능한 라이브러리로 사용한다. +TLS에서 체인 검증과 이름 검증은 별도 확인이 필요하다. `SSL_get_verify_result()`의 성공 값만 +으로 피어 인증서를 받았다고 판단하지 않으며 실제 인증서 존재도 검사한다. +[OpenSSL 검증 결과](https://docs.openssl.org/3.5/man3/SSL_get_verify_result/), +[호스트 이름 검증](https://docs.openssl.org/3.5/man3/SSL_set1_host/). + +서버와 클라이언트 모두 CA·인증서·개인키를 지정하고 서로의 DNS 이름을 검증한다. TLS 1.3과 +ALPN `pure/1`이 맞지 않으면 종료하며 평문으로 낮추지 않는다. 0-RTT는 사용하지 않는다. +부작용 있는 작업을 0-RTT에서 안전하게 재생할 수 있다고 가정하지 않는 선택이다. +[TLS 1.3 RFC §8](https://www.rfc-editor.org/rfc/rfc8446.html#section-8), +[OpenSSL ALPN](https://docs.openssl.org/3.3/man3/SSL_CTX_set_alpn_select_cb/). + +## 이번 앱에 필요한 최소 메시지 + +TLS는 바이트 스트림이므로 각 메시지 앞에 2바이트 big-endian 길이를 둔다. 핸드셰이크 후 +서버가 `PUR1` 4바이트, 작성 정의 SHA-256 32바이트, 상태 수 1바이트, 로컬/저장 버전 각 +8바이트로 된 53바이트 hello를 보낸다. Pure 클라이언트는 정의가 다르면 작업 전에 거절한다. +일반 클라이언트는 필요에 맞게 이 식별자를 확인할 수 있다. + +| 메시지 | payload 길이 | 의미 | +|---|---:|---| +| 상태 조회 | 1 B | 현재 값과 버전. 작업 효과 없음 | +| 저장·복원 | 9 B | 동작 1 B + 기대 로컬 버전 8 B | +| 키·프레임·초기화 | 12 B | 동작·버전·입력·위상·대상 | +| 수치 입력 | 20 B | 위 필드 + IEEE binary64 값 | +| 포인터 입력 | 28 B | 위 필드 + 좌표 2개 | +| 결과 | 26 + 상태 수 × 8 B | 명시적 결과 코드·버전·오류·held·숫자 상태 | + +선택한 앱의 상태·입력 한도에 따라 요청은 최대 28 B, 결과는 최대 282 B다. 앱 내부 구조체, +포인터, 패딩을 직렬화하지 않는다. 필드 길이·바이트 순서·유한 숫자·입력 종류와 대상·상태 수를 +확인한 뒤 실행한다. 슬롯 숫자는 **현재 준비 정의의 연결 인덱스**이며 범용 안정 ABI가 아니다. +이 때문에 정의의 identity를 세션 시작에 확인한다. + +서버는 작업의 `expected revision`이 현재 로컬 버전과 다르면 상태를 바꾸지 않고 충돌을 +돌려준다. 한 연결에서 동시에 대기하는 작업은 하나로 제한한다. 응답이 유실되면 성공 여부를 +알 수 없으므로 클라이언트가 자동 재시도하지 않는다. 다시 연결해 상태·저장 버전을 읽어야 +한다. 연결이 끊어지면 해당 연결이 누른 키와 포인터 입력을 해제·취소한다. + +## 재현한 동작과 비용 + +[실행 결과](../../prototypes/workbench/results/2026-09-24-secure-peer/peer-check.json), +[빌드 식별](../../prototypes/workbench/results/2026-09-24-secure-peer/build.json), +[크기 원자료](../../prototypes/workbench/results/2026-09-24-secure-peer/size.txt). +테스트는 1일짜리 임시 CA·서버·클라이언트 인증서를 `/tmp`에 만들고 종료 시 삭제한다. +실제 비밀키를 저장소에 넣지 않는다. + +- 일반 Python TLS 클라이언트가 정의 hello와 현재 상태를 읽고, 버전 불일치 작업의 충돌과 + 상태 불변을 확인했다. 연결을 끊은 뒤 서버가 held 입력을 해제하는 것도 확인했다. +- 불명 동작·과장된 길이의 프레임, 인증서 없음·잘못된 클라이언트 이름, 잘못된 서버 이름, + ALPN 불일치를 거절했다. 인증을 끝낸 뒤 요청을 보내지 않는 클라이언트도 약 5초의 + 요청 기한 후 연결을 닫고, 다음 클라이언트가 같은 앱을 읽을 수 있었다. +- Pure 클라이언트가 녹색 상자를 잡아 이동·놓고 저장했다. 새 서버 프로세스의 첫 상태가 + `green.x=220`, `green.y=200`, 저장 버전 1, held 0임을 확인했다. +- 기존 네이티브/Wasm 9개 작성물·156개 이벤트의 일치 검사가 일반·C 호스트 ASan/UBSan에서 + 통과했고, Workbench Node 회귀 검사 87개가 통과했다. 같은 피어 통합 시나리오도 C 호스트를 + ASan/UBSan으로 계측한 실행물에서 통과했다. Zig 모듈과 OpenSSL 라이브러리까지 계측했다는 + 뜻은 아니다. + +동일한 준비 정의에 대해 `-O2 -g`로 만든 실행 파일을 비교했다. `size`의 text는 ELF 섹션 +합계이며 실제 상주 메모리와 다르다. stripped 파일 크기도 라이브러리 파일을 포함하지 않는다. + +| 범위 | 이 호스트에서 관찰한 값 | +|---|---:| +| 공통 실행 코어 아카이브 `text/data/bss`, `make check-core` | 5,932 / 0 / 0 B, 6 KiB 예산 통과 | +| `runtime.c` 단일 오브젝트 `text`, 이 명령의 `cc -O2` | 5,012 B | +| 기존 실행 전용 CLI `text`, stripped ELF | 62,827 B, 68,000 B | +| 선택형 피어 실행물 `text`, stripped ELF | 75,969 B, 133,936 B | +| 피어 실행물에 추가된 `text` | 13,142 B | +| 피어 서버 유휴 VmRSS / 인증 연결 중 VmRSS | 7,904 / 8,480 KiB, 각각 한 번 관찰 | +| 피어 실행물의 직접 동적 의존성 | OpenSSL `libssl.so.3`, `libcrypto.so.3`, libc 및 라이브러리 하위 의존성 | + +이 피어 프로세스를 “8 MiB 이하 런타임”이라 일반화하지 않는다. 인증서·TLS 연결·동적 +라이브러리·호스트 환경의 비용이 포함되며 동시 연결, 실제 부하, macOS에서는 아직 측정하지 +않았다. 실행 전용 CLI는 OpenSSL을 링크하지 않는다. 이 분리가 마이크로 비용의 현재 증거다. + +## 남은 한계와 다음 판단 + +서버는 단일 앱 인스턴스에서 한 피어씩 처리한다. DNS 조회와 동기 저장 효과는 현재 5초 +네트워크 기한에 포함되지 않는다. 원격 정의 교체, 다중 작업·스트림, 느린 소비자의 장기 예산, +권한별 역할, 인증서 교체 운영, 실제 인터넷 장애·손실·재연결 정책은 미구현이다. 5초는 현재 +프로토타입의 고정값이다. macOS Apple Silicon에서는 빌드·실행을 확인하지 못했다. + +TLS 1.3 위의 인증된 앱 작업 **정확성**을 확인했을 뿐 QUIC 수준 전송 특성이나 Kitex 수준 +성능을 보이지 않았다. 추가 성능 실험은 유휴 숫자 반복이 아니라 같은 앱 작업·보안·완료 +조건에서 실제 병목이 나타날 때 진행한다. 체크포인트에는 큰 작성물과 혼합 작업, 전체 +피크 메모리·작업별 비용, 사용자 작성·배포 경로가 더 필요하다. diff --git a/docs/research/027-fixed-native-capacity.md b/docs/research/027-fixed-native-capacity.md new file mode 100644 index 0000000..4783634 --- /dev/null +++ b/docs/research/027-fixed-native-capacity.md @@ -0,0 +1,90 @@ +# 027 — 작성 정의만큼만 예약하는 네이티브 실행물 + +2026-09-24. Linux AArch64, GCC 14.2, Zig 0.15.2. 첫 공개 체크포인트의 중간 결과다. +**기본 실행 코어의 범위를 늘리지 않고, 선택한 앱 인스턴스의 반복 비용을 줄였다.** +이 문서는 당시의 고정 용량 빌드 결과다. 이후 정의와 상태를 분리한 +[028 불변 프로그램 공유](028-shared-fixed-program.md)가 현재 CLI·피어 구조와 측정값이다. + +## 제거한 비용 + +기존 native scalar 모듈은 작은 작성물에도 상태 32개, 입력 8개, 명령 512개, 반응 64개, +포인터 대상 24개의 최대 배열을 **실행 정의와 교체 준비 공간 각각에** 예약했다. 실행 전용 CLI와 +피어는 빌드된 정의를 실시간 교체하지 않지만 인스턴스마다 20,064 B를 차지했다. 이 비용은 +동일한 작성물을 여러 독립 인스턴스로 운영할수록 반복된다. + +`prepare.mjs`가 이미 검증한 `.pure` 정의의 상태·입력·명령·반응·대상 개수를 Zig 빌드 프로필에 +함께 기록하도록 했다. `native-fixed.zig`는 기존 `engine.zig`를 해당 용량으로 빌드한다. +배열이 비어 있을 때 Zig가 인덱스 연산을 컴파일할 수 있도록 0개 항목에는 1칸만 예약한다. +언어의 상한 32/8/512/64/24는 그대로 검사한다. + +```text +.pure → 기존 컴파일러의 검증된 정의 → application.c + fixed-profile.zig + ↓ + 같은 scalar VM의 정확한 배열 용량 +``` + +고정 정의를 쓰는 `manage.py native` CLI와 `--peer`만 이 프로필을 선택한다. 작성 중에 새 +정의를 적용하는 `--desktop`과 브라우저 Wasm은 여전히 전체 용량을 가진다. 고정 실행물에서 +다른 정의로 바꾸려면 다시 빌드한다. 입력 하나의 실행 중에 크기 검사나 동적 배열 할당을 +추가하지 않았고, 새 파서나 별도 인터프리터도 만들지 않았다. 앱 계산은 여전히 기존 scalar +VM이며 AOT로 모든 연산을 기계어로 펼친 결과는 아니다. + +## 동작·한도 검증 + +[전체 용량 결과](../../prototypes/workbench/results/2026-09-24-fixed-capacity/general-parity-final.json)와 +[고정 용량 결과](../../prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-parity-final.json)는 +동일한 기존 작성물 11개, 입력 170단계에 대해 네이티브 상태·출력을 Wasm 결과와 대조한다. +명령 319개인 `layout.pure`, 공통 규칙·대상별 배치를 포함한다. 두 C 호스트는 ASan/UBSan으로 +실행했고 Zig 모듈은 ReleaseSafe였다. 고정 `routed-layout.pure`에서는 용량보다 큰 상태·입력· +명령 정의가 거절되고 기존 활성 상태·버전이 남는 것도 확인했다. 브라우저/CLI 회귀 87개, +Zig 코어 37개, TLS 피어의 [인증·저장·오류 시나리오](../../prototypes/workbench/results/2026-09-24-fixed-capacity/peer-check.json)도 통과했다. + +| 작성물 | 명령 수 | 전체 용량 인스턴스 | 고정 용량 인스턴스 | +|---|---:|---:|---:| +| 비어 있는 작성물 | 0 | 20,064 B | 1,320 B | +| 카운터 | 17 | 20,064 B | 1,576 B | +| 직접 조작 | 82 | 20,064 B | 3,248 B | +| 대상별 공유 규칙 배치 | 117 | 20,064 B | 5,712 B | +| 피어와 CLI의 묶음 배치 | 117 | 20,064 B | 5,456 B | +| 명시적 세 도형 배치 | 319 | 20,064 B | 11,792 B | + +이는 `pure_scalar_bytes()`가 요구하는 인스턴스 저장 공간이다. 앱 컨트롤러, TLS·GTK, +스택·저장소·힙 관리 비용을 포함한 값이 아니다. + +반복 비용의 실제 효과를 별도로 확인했다. 같은 묶음 배치 정의로 독립 scalar 인스턴스 +1,000개를 초기화한 Linux 프로세스에서, 초기화 전후 RSS 차이의 3회 중앙값은 전체 용량 +**19,808 KiB**, 고정 용량 **5,536 KiB**였다. 둘 다 같은 실제 준비 정의를 적재했고 +각 인스턴스의 버전을 읽어 유효한 초기화를 확인했다. [원자료](../../prototypes/workbench/results/2026-09-24-fixed-capacity/instance-memory.json)와 +[재현 하네스](../../prototypes/workbench/native/instance-memory.c)를 보관했다. 이 수치는 +**scalar 인스턴스 1,000개의 메모리 확대 비용**만 비교한다. 1,000명 서비스의 전체 메모리나 +처리량·지연 개선이라는 뜻은 아니다. + +## 실행물 전체 비용 + +동일한 `routed-layout.pure`와 `-O2 -g` C 빌드의 [ELF 섹션 원자료](../../prototypes/workbench/results/2026-09-24-fixed-capacity/size.txt), +[stripped 파일 크기](../../prototypes/workbench/results/2026-09-24-fixed-capacity/stripped.txt), +[CLI 빌드 식별](../../prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-cli-build.json), +[피어 빌드 식별](../../prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-peer-build.json). + +| 선택한 범위 | 전체 용량 | 고정 용량 | +|---|---:|---:| +| scalar 오브젝트 `text` | 33,020 B | 18,252 B | +| CLI 실행물 `text` | 62,827 B | 48,059 B | +| CLI stripped ELF | 68,000 B | 68,000 B | +| TLS 피어 실행물 `text` | [026의 기준](026-secure-peer-link.md) 75,969 B | 61,201 B | +| TLS 피어 stripped ELF | [026의 기준](026-secure-peer-link.md) 133,936 B | 68,400 B | + +CLI의 `text`가 줄어도 stripped 파일은 이 호스트의 ELF 배치·정렬 때문에 같은 68,000 B였다. +따라서 파일 크기 개선이라고 보고하지 않는다. 반면 피어는 세그먼트 경계를 넘어 실제 +stripped 파일도 작아졌다. 동적 OpenSSL의 파일·메모리 비용은 여전히 별도이며, 이번 피어 서버 +유휴 RSS 한 번의 관찰은 7,888 KiB였다. 공통 실행 코어 아카이브의 5,932 B 예산 및 CLI가 +libc만 직접 요구하는 성질은 그대로다. + +## 다음에 제거할 중복 + +고정 용량 안에서도 각 인스턴스가 동일한 실행 정의를 **두 벌의 배열 공간에** 보관한다. +이번 변경은 최대 크기 예약을 줄였지만 준비된 명령을 인스턴스 간 공유하지는 않는다. +따라서 많은 독립 앱을 운영하는 다음 검증에서는 불변 프로그램을 공유하고 변경 가능한 상태· +입력 소유권·평가 기록만 인스턴스에 둘 수 있는지 확인해야 한다. 정의 교체의 원자성, +동시 실행의 수명, 스냅샷 호환성을 유지하면서 실제 반복 메모리를 더 줄일 수 있을 때 +구조를 채택한다. 큰 서비스 성능이나 컴파일 시간 우위는 아직 입증하지 않았다. diff --git a/docs/research/028-shared-fixed-program.md b/docs/research/028-shared-fixed-program.md new file mode 100644 index 0000000..3c48e97 --- /dev/null +++ b/docs/research/028-shared-fixed-program.md @@ -0,0 +1,79 @@ +# 028 — 고정 네이티브 앱의 불변 프로그램 공유 + +2026-09-24. Linux AArch64, Zig 0.15.2, GCC 14.2. 첫 공개 체크포인트의 중간 결과다. +마이크로 런타임의 기본 비용을 지키기 위해, **같은 정의로 실행하는 독립 인스턴스의 반복 +메모리에서 프로그램·교체 공간·선택형 캐시를 제거했다.** 코어의 기능이나 의존성을 늘리지 않았다. + +## 구조와 경계 + +[027의 고정 용량 빌드](027-fixed-native-capacity.md)는 최대 예약을 줄였지만, 한 앱의 +명령·반응·대상 정보와 교체 준비 공간을 인스턴스마다 보유했다. CLI·피어는 정의를 실행 중 +교체하지 않으므로 준비 단계가 만든 동일한 정의를 Zig 빌드의 불변 `Program`으로 한 번 +링크한다. 인스턴스에는 설치 여부, 각자의 상태·진단·평가 기록만 남긴다. 고정 빌드에서 +C의 명령·반응 배열도 다시 링크하지 않는다. + +```text +작성된 .pure → 기존 준비기 → 고정 빌드의 불변 Program (앱마다 한 벌) + ├─ scalar 인스턴스 A: 상태·진단·평가 + └─ scalar 인스턴스 B: 상태·진단·평가 +``` + +설치할 때 준비 정의의 SHA-256 identity와 상태·입력·명령·반응·대상 개수를 빌드 정의와 +대조한다. 복원 값은 모두 유한한지 확인한 뒤에야 상태·버전을 바꾼다. 여기서 identity는 +저장 상태의 **호환성 검사**이며 출처 인증이 아니다. 고정 실행물에서 다른 정의를 쓰려면 +재빌드해야 한다. 정의 staging API와 선택형 평가 캐시는 이 프로필에서 용량 오류를 반환한다. +캐시 없는 평가와 정해진 작업 예산은 계속 사용한다. 정의 교체가 필요한 GTK 작성 창과 +브라우저 Wasm은 일반 프로필을 유지한다. + +초기 버전에는 설치 전의 빈 상태를 표현하려고 2,480 B짜리 빈 `Program`이 실행 파일에 +추가로 들어갔고, 기본 포인터 초기값은 읽기 전용 영역의 동적 재배치도 만들었다. +최종 버전은 인스턴스의 설치 여부로 빈 상태를 표현한다. [실행물 의존성 검사](../../prototypes/workbench/results/2026-09-24-shared-program/dependencies.txt)에서 +CLI·피어 모두 `TEXTREL`이 없고, [심볼 검사](../../prototypes/workbench/results/2026-09-24-shared-program/symbols.txt)에서 +2,480 B `engine.shared_program` 한 벌만 남은 것을 확인했다. + +## 반복 비용과 동작 + +`routed-layout.pure`의 [최종 크기·RSS 원자료](../../prototypes/workbench/results/2026-09-24-shared-program/instance-memory.json): + +| 범위 | 일반 작성 프로필 | 앞선 고정 용량 [027](027-fixed-native-capacity.md) | 공유 고정 프로필 | +|---|---:|---:|---:| +| scalar 인스턴스 요구 저장 공간 | 20,064 B | 5,456 B | **280 B** | +| 같은 scalar 인스턴스 1,000개 초기화의 RSS 증가, 3회 중앙값 | 19,808 KiB | 5,536 KiB | **480 KiB** | + +각 인스턴스를 실제 할당·초기화·설치한 뒤 버전 합을 검사했다. RSS는 Linux의 초기화 +전후 차이이며 allocator·페이지 단위의 영향도 포함한다. 이 값은 **scalar 인스턴스 +반복 비용**이지 사용자 1,000명의 서비스 메모리·처리량이나 전체 피크가 아니다. +고정 프로그램 자체는 앱마다 정적 저장 공간 2,480 B를 한 번 사용한다. 별도의 C 앱 +컨트롤러는 이 사례에서 928 B이고, 저장·입출력·스택·TLS·GTK는 위 280 B에 없다. + +[고정 프로필](../../prototypes/workbench/results/2026-09-24-shared-program/fixed-parity.json)과 +[일반 프로필](../../prototypes/workbench/results/2026-09-24-shared-program/general-parity.json)은 +기존 작성물 11개, 입력 170단계의 상태·출력을 Wasm과 비교했다. 두 C 호스트는 ASan/UBSan, +Zig 모듈은 ReleaseSafe였다. 고정 단위 검사는 독립 상태, 설치 전 빈 상태, 잘못된 정의 +identity·개수 거부, 복원 실패의 원자성, 부호 있는 0과 reset을 확인했다. 일반 프로필의 +정의 staging·교체 검사도 통과했다. Zig 코어 37개와 브라우저/CLI 회귀 87개를 통과했다. + +## 선택한 실행물의 비용 + +같은 `routed-layout.pure`, C `-O2 -g`의 [섹션 크기](../../prototypes/workbench/results/2026-09-24-shared-program/size.txt), +[stripped 파일 크기](../../prototypes/workbench/results/2026-09-24-shared-program/stripped.txt), +[CLI 빌드 식별](../../prototypes/workbench/results/2026-09-24-shared-program/cli-build.json), +[피어 빌드 식별](../../prototypes/workbench/results/2026-09-24-shared-program/peer-build.json). + +| 선택한 범위 | 앞선 고정 용량 [027](027-fixed-native-capacity.md) | 공유 고정 프로필 | +|---|---:|---:| +| scalar 오브젝트 `text` | 18,252 B | **10,396 B** | +| CLI 실행물 `text` | 48,059 B | **37,135 B** | +| CLI stripped ELF | 68,000 B | **68,000 B** | +| TLS 피어 실행물 `text` | 61,201 B | **50,277 B** | +| TLS 피어 stripped ELF | 68,400 B | **68,400 B** | + +이 호스트의 ELF 배치·정렬 때문에 이번에는 **stripped 파일 크기가 줄지 않았다**. +CLI는 libc, 피어는 libc·OpenSSL 동적 라이브러리를 요구한다. 피어의 유휴 RSS는 +[최종 인증·복원·오류 검사](../../prototypes/workbench/results/2026-09-24-shared-program/peer-check.json)에서 +7,856 KiB였다. 공통 저장 실행 코어의 별도 `text` 예산 5,932 B는 그대로 통과했다. + +이제 작은 scalar 인스턴스만 더 줄이는 측정은 우선순위가 아니다. 첫 공개 체크포인트에는 +실제 크기의 작성·저장·원격 작업을 포함한 **앱 전체 비용과 실패 조건**이 필요하다. +이번 결과는 반복 메모리 제거와 기존 동작 유지의 증거이며 큰 서비스의 지연·처리량 우위나 +macOS 메모리 특성에 대한 증거는 아니다. diff --git a/docs/research/029-native-document-window.md b/docs/research/029-native-document-window.md new file mode 100644 index 0000000..c5084f3 --- /dev/null +++ b/docs/research/029-native-document-window.md @@ -0,0 +1,62 @@ +# 029 — 작성된 바이트 편집을 실제 네이티브 창에서 실행 + +2026-09-24. Linux AArch64, C17/GCC 14.2, GTK 4.18. 첫 공개 체크포인트의 중간 결과다. +기존 [문서 편집 연결](023-document-edits.md)을 GTK 창에 붙였다. CLI·HTTP 서버·창이 +동일한 [`editor.pure`](../../prototypes/connections/examples/editor.pure) 정의를 준비해 +동일한 실행 코어와 조건부 바이트 저장 효과를 사용한다. GTK는 **선택한 앱의 호스트 +어댑터**이며 기본 코어·CLI·서버에 들어가지 않는다. + +![Linux GTK 문서 창](../../prototypes/connections/results/2026-09-24-native-document-window/window.png) + +## 실제 사용 경로 + +`make -C prototypes/connections window`로 선택 빌드한다. 실행 명령은 +[사용 안내](../../prototypes/connections/README.md#네이티브-문서-창)와 같다. +창은 저장된 UTF-8 문서를 열고, GTK 텍스트 버퍼에서 편집하며, 이전 내용과 새 내용의 +공통 앞·뒤 구간을 찾아 바뀐 바이트 구간만 `store.splice`에 보낸다. 공통 구간이 UTF-8 +코드 포인트의 중간에서 끝나면 양쪽을 문자 경계로 넓힌다. 수정하지 않은 기존 본문은 +입력 메시지에 다시 싣지 않는다. 저장 구현 자체는 새 파일에 전체 범위를 다시 기록한다. + +저장 조건은 읽은 문서 버전이다. 다른 실행기가 먼저 저장했을 때 창은 충돌을 표시하고 +현재 초안을 유지한다. `Reload`는 미저장 초안이 있으면 거절하고, `Discard & reload`만 +명시적으로 초안을 버린다. 창 닫기·Ctrl+W도 미저장 변경을 확인한다. 수정 없이 저장하면 +저장 버전을 올리지 않는다. 이 창은 일반 바이트 편집기가 아니어서 유효하지 않은 UTF-8이나 +NUL이 든 저장 문서는 열지 않고 기존 화면을 유지한다. 해당 바이트는 기존 CLI가 다룬다. + +## 실제 프로세스 검증 + +[Xvfb/GTK 사용자 흐름 결과](../../prototypes/connections/results/2026-09-24-native-document-window/gtk-flow.json)와 +[ASan/UBSan으로 계측한 C 호스트 결과](../../prototypes/connections/results/2026-09-24-native-document-window/gtk-sanitized.json)는 +실제 창의 입력·저장·재시작을 검사한다. 빈 문서 생성·복원, CLI에서 창 저장 내용 확인, +HTTP 서버가 먼저 문서를 갱신한 뒤 창의 충돌·초안 보존, UTF-8에서 첫 두 바이트가 같은 +다른 글자로 교체, 변경 없는 저장, 미저장 종료 방지가 통과했다. 1 MiB 문서를 창에서 열고 +마지막 한 바이트를 바꾼 뒤 CLI로 전체 결과를 대조했다. 계측 빌드에서 GTK 라이브러리 +자체가 ASan으로 재빌드된 것은 아니다. + +기존 [연결·저장 회귀 검사](../../prototypes/connections/results/2026-09-24-native-document-window/connection-check.json)도 +통과했고, [실행 코어 예산 검사](../../prototypes/connections/results/2026-09-24-native-document-window/core-budget.json)의 +`text`는 이전과 동일한 5,932 B였다. 창용 코드는 `make all`의 필수 대상이 아니다. + +## 비용은 계층별로 기록 + +같은 호스트의 [ELF 섹션 원자료](../../prototypes/connections/results/2026-09-24-native-document-window/size.txt), +[stripped 파일 크기](../../prototypes/connections/results/2026-09-24-native-document-window/stripped.txt), +[직접 동적 의존성](../../prototypes/connections/results/2026-09-24-native-document-window/dependencies.txt), +[빌드·소스 해시](../../prototypes/connections/results/2026-09-24-native-document-window/build.json). + +| 선택 실행물 | ELF `text` | stripped 파일 | 직접 동적 의존성 | +|---|---:|---:|---| +| 문서 CLI | 35,807 B | 67,904 B | libc | +| HTTP 호환 서버 | 42,761 B | 68,000 B | libc | +| GTK 문서 창 | 43,308 B | 68,280 B | GTK·GIO·GObject·GLib·libc | + +창에서 1 MiB 문서를 연 뒤 Linux `/proc`로 본 **창 프로세스 RSS는 44,208 KiB**였다. +Xvfb·커널 캐시·다른 프로세스는 포함하지 않은 수치다. ASan 빌드의 RSS는 143,168 KiB로 +별도 검증용이며 제품 비용과 비교하지 않는다. GTK의 공유 라이브러리·렌더링·텍스트 버퍼 +비용을 무시하고 68,280 B 파일을 앱 전체 크기라 부르지 않는다. 이것은 마이크로 코어를 +실제 문서 앱에 연결한 결과지만, **전체 GTK 앱이 임베디드급 메모리라는 증거는 아니다.** + +현재 한계는 단일 문서, 동기 저장, 버전 충돌 후 수동 재적용, GTK/Linux 호스트, 바이트 +기반 저장이다. IME·문자소 단위 위치·큰 문서의 대화식 지연·macOS 실사용은 검증하지 +않았다. 이후 [030](030-authenticated-document-peer.md)에서 선택형 인증 TLS 피어까지 +**같은 문서 작업**으로 연결했다. 실제 앱의 전체 피크·실패·사용성 비용은 계속 검증해야 한다. diff --git a/docs/research/030-authenticated-document-peer.md b/docs/research/030-authenticated-document-peer.md new file mode 100644 index 0000000..1ff569d --- /dev/null +++ b/docs/research/030-authenticated-document-peer.md @@ -0,0 +1,85 @@ +# 030 — 같은 문서 작업을 인증된 Pure 피어로 연결 + +2026-09-24. Linux AArch64, C17/GCC 14.2, OpenSSL 3. 첫 공개 체크포인트의 중간 결과다. +기존 [`editor.pure`](../../prototypes/connections/examples/editor.pure)를 문서 CLI·HTTP·GTK 창과 +**같은** 준비·실행 코어에서 선택형 TLS 피어로 실행했다. 기본 빌드에는 TLS가 들어가지 않는다. + +## 실제 경로와 제거한 전송 + +`pure-document-peer`는 TLS 1.3/TCP의 상호 인증, 인증서 SAN 이름 확인, `pure-doc/1` +ALPN 뒤에 자체 이진 메시지를 사용한다. 이미 구현한 scalar 피어의 소켓·TLS 부분을 +[`tls-link.c`](../../prototypes/common/tls-link.c)로 분리해 두 피어가 재사용한다. +코어의 [`runtime.c`](../../prototypes/connections/runtime.c), 저장소, `editor.pure`는 +문서 CLI와 같다. 전송 코드는 선택 빌드의 호스트에만 있다. + +서버가 보내는 40 B hello는 `PDOC`, 버전 1, 예약 3 B, **원본 .pure 소스** SHA-256이다. +네이티브 클라이언트는 자신이 준비한 소스와 같은지 작업 전에 확인한다. 이는 현재의 +엄격한 호환성 검사이며 인증 수단은 아니다. 인증은 TLS 인증서가 담당한다. 같은 의미라도 +공백이 다른 소스는 불일치하며, 별도 외부 클라이언트는 인증을 통과하면 정의 검사 없이 +이 공개 형식을 직접 구현할 수 있다. + +요청은 14 B 고정 머리, 1~64 B 키, 선택한 본문 순서다. 머리는 작업 종류 1 B, +키 길이 1 B, 기대 버전 big-endian 8 B, 본문 길이 big-endian 4 B이다. 작업 종류의 +하위 7비트는 읽기 `1` 또는 쓰기 `2`, 최상위 비트는 **응답 본문을 소비함**을 뜻한다. +응답은 상태 1 B, 결과 버전 big-endian 8 B, **논리적 결과 길이** big-endian 4 B, +요청한 경우에만 결과 본문이다. 따라서 응답 본문을 원하지 않는 네이티브 편집 명령도 +프로그램이 산출한 길이를 받지만 그 바이트는 전송받지 않는다. 읽기 명령은 본문을 요청한다. + +쓰기 수신에서는 길이·예산·키를 확인해 실행 코어의 작업과 입력 영역을 **본문을 읽기 +전에** 예약한다. TLS 평문을 그 입력 영역에 직접 채운 뒤 한 번 공개한다. 송신에서는 +완료 결과가 대여한 범위를 TLS로 나눠 보낸 뒤 작업을 해제한다. 별도의 문서 전체 +프레임 버퍼는 없다. OpenSSL 내부 버퍼, 커널 버퍼와 저장소의 파일 재작성 비용까지 +없앤다는 뜻은 아니다. 본문이 잘리면 작업을 실행하지 않고 해제한다. + +1 MiB 삽입을 네이티브 피어에서 호출하면 프로그램의 논리적 응답 크기는 1 MiB지만 +그 호출은 응답 본문을 소비하지 않아 **TLS 앱 데이터에서 본문 1,048,576 B를 생략**한다. +같은 세션의 Python 클라이언트가 본문을 요청한 경우에는 전부 받았고, 다음 요청도 같은 +세션에서 해석됐다. 이 수치는 이 형식의 바이트 차이이며 지연·CPU 우위 측정은 아니다. + +## 재현과 비용 + +`make -C prototypes/connections document-peer`로 선택 빌드한다. 인증서 준비와 실제 +C↔C·Python↔C 시나리오는 `make -C prototypes/connections check-document-peer`로 +재현한다. [일반 빌드의 실제 TLS 결과](../../prototypes/connections/results/2026-09-24-document-peer/peer-flow.json)와 +[ASan/UBSan 결과](../../prototypes/connections/results/2026-09-24-document-peer/peer-sanitized.json)는 +상호 인증 거부, 서버 이름·ALPN 불일치, 정의 불일치, 지속 연결의 생성·읽기·충돌, +CLI와 교차 편집, 1 MiB 분할 수신·대여 응답, C 피어의 1 MiB 업로드, +잘린·잘못된·초과 본문, 유휴 기한, 재시작 후 전체 복원을 통과했다. +계측 빌드에서 OpenSSL 라이브러리 자체는 ASan으로 재빌드하지 않았다. +공통 TLS 코드로 전환한 기존 scalar 피어의 인증·기한·재접속 +[회귀](../../prototypes/connections/results/2026-09-24-document-peer/scalar-refactor.json)도 다시 통과했다. +기존 생성된 Zig 산출물에 새 C 코드를 링크해 확인했으며, 이 환경에는 Zig 0.15.2가 +없어 전체 scalar 앱 재생성 빌드는 이번 검증에서 실행하지 못했다. +문서 경로는 별도 빈 `build/checkpoint-clean` 디렉터리에서도 전체 기본 빌드와 피어를 +다시 만든 뒤 [기본 릴리스 검사](../../prototypes/connections/results/2026-09-24-document-peer/clean-release.log), +[선택형 피어 검사](../../prototypes/connections/results/2026-09-24-document-peer/clean-peer.log)를 +통과했다. + +같은 호스트의 [ELF 섹션](../../prototypes/connections/results/2026-09-24-document-peer/size.txt), +[파일 크기](../../prototypes/connections/results/2026-09-24-document-peer/file-sizes.txt), +[직접 동적 의존성](../../prototypes/connections/results/2026-09-24-document-peer/dynamic.txt), +[선택 빌드 예산 검사](../../prototypes/connections/results/2026-09-24-document-peer/peer-budget.json), +[소스·실행물 해시](../../prototypes/connections/results/2026-09-24-document-peer/hashes.txt)를 +따로 남겼다. + +| 선택 구성 | 코드 섹션 `text` | 확인한 실제 비용 | +|---|---:|---| +| 실행 코어 아카이브 | 5,932 B | 기존 6 KiB 예산 검사 그대로 통과 | +| 기본 문서 CLI | 35,807 B | libc 직접 의존 | +| 기본 HTTP 호환 서버 | 42,761 B | libc 직접 의존 | +| **선택형 문서 TLS 피어** | **46,878 B** | stripped 68,312 B; libssl·libcrypto·libc 직접 의존 | + +선택형 서버 프로세스의 `/proc` RSS는 연결 전 **7,840 KiB**, 서로 다른 두 1 MiB +문서를 처리한 뒤 **9,488 KiB**였다. 여기에는 선택한 TLS 라이브러리와 호스트 비용이 +포함된다. ASan 빌드의 22,592/31,632 KiB는 검증 전용이다. 기본 코어가 작다는 +사실을 이 전체 수치 대신 내세우지 않는다. UI는 별도 선택 구성이고 이전 GTK 문서 +창의 44 MiB RSS도 여전히 남아 있다. + +## 아직 해결하지 않은 계약 + +서버는 한 시점에 연결 하나와 작업 슬롯 하나만 처리한다. 호출 다중화, 느린 연결 사이의 +공정성, 재시도 식별자·중복 실행 억제, 키별 권한, 일반 앱 형식 협상, QUIC, 인터넷 환경의 +실패·부하·지연 검증은 없다. 각 요청의 TLS 읽기·쓰기에는 현재 5초 절대 기한이 있다. +응답이 끊긴 쓰기는 이미 저장됐을 수 있으므로 클라이언트는 재시도 전에 읽어 확인해야 한다. +저장소는 변경이 작아도 파일 전체를 다시 기록한다. 이 경로는 실용 문서 작업의 연결을 +검증하지만 Kitex급 성능이나 임베디드급 **전체 앱** 메모리를 입증하지 않는다. diff --git a/docs/research/031-relocatable-document-preview.md b/docs/research/031-relocatable-document-preview.md new file mode 100644 index 0000000..602f75e --- /dev/null +++ b/docs/research/031-relocatable-document-preview.md @@ -0,0 +1,52 @@ +# 031 — 독립된 문서 앱 묶음과 저장 비용 경계 + +2026-09-24. Linux AArch64 첫 공개 체크포인트의 중간 결과다. 기존 +[`editor.pure`](../../prototypes/connections/examples/editor.pure) 문서 앱을 새 디렉터리에서 +빌드하고, 실행물·정의 파일을 하나의 선택형 묶음으로 만들었다. + +## 실제로 옮겨 실행한 묶음 + +[`package_native.py`](../../prototypes/connections/package_native.py)는 기본 CLI·HTTP, +선택형 인증 문서 피어, 요청한 경우 GTK 창을 분리된 실행물로 담는다. 기본·피어의 +코드 섹션과 직접 동적 의존성 예산을 검사한 다음, 파일 SHA-256·크기·직접 공유 +라이브러리를 `manifest.json`에 기록한다. GTK·OpenSSL 라이브러리를 자체 코드의 +크기에 합쳐 숨기지 않고, **시스템 라이브러리를 묶음에 포함하지 않았음**을 명시한다. + +새 `build/checkpoint-clean` 디렉터리에서 `make all document-peer window`와 기본·피어 +검사를 통과한 후, [Linux AArch64 시험 묶음](../../prototypes/connections/results/2026-09-24-document-peer/pure-document-linux-aarch64.tar.gz)을 +생성했다. 압축 크기는 **316,927 B**다. 동일한 입력으로 두 번 생성한 파일과 결과 +디렉터리에 복사한 파일의 [SHA-256](../../prototypes/connections/results/2026-09-24-document-peer/package-hashes.txt)은 +모두 `dc3f7339bf5aa22b11eb5ac4c8b4a547168e0e06e65c8003a545f70a31500081`이다. +이는 같은 입력의 아카이브 재현성을 확인한 것이며, 다른 도구 버전에서 실행물까지 +같아진다는 뜻은 아니다. + +[`check_package.py`](../../prototypes/connections/check_package.py)는 압축을 별도 임시 +디렉터리에 풀어 [검증 결과](../../prototypes/connections/results/2026-09-24-document-peer/package-check.json)를 +남겼다. 파일 목록·해시를 확인하고 묶음 속 CLI로 문서를 생성·복원했다. 복사한 `.pure` +정의의 `reply(saved, edit.insert)`를 `reply(saved)`로 바꾸자 실제 결과 본문 길이가 +10 B에서 0 B로 바뀌었다. 묶음 속 인증 피어와 일반 Python TLS 클라이언트가 +1 MiB 문서를 교환했고, 묶음 속 GTK 창·CLI·HTTP가 같은 문서의 변경·충돌·저장을 +다시 통과했다. 처음 Xvfb 검사에서 창이 표시되기 전 포커스 시도가 실패해, 창 포커스를 +제한된 횟수로 다시 시도하도록 검사기를 수정한 뒤 통과했다. + +이는 **Linux AArch64 호스트에서 선택한 실용 앱이 이동해 실행된다**는 증거다. +정적 실행물, macOS 묶음, 시스템 공유 라이브러리의 재배포, 소스 설치형 패키지, +서명·릴리스 라이선스까지 해결됐다는 주장은 아니다. GTK 창의 44 MiB RSS도 압축 +파일 크기로 사라지지 않는다. + +## 작은 편집의 저장 비용은 그대로 남는다 + +현재 [`store.c`](../../prototypes/connections/store.c)의 `mutate`는 크기가 변하지 않는 +교체에도 새로운 파일에 24 B 헤더, 유지할 앞부분, 삽입 바이트, 유지할 뒷부분을 순서대로 +기록하고 `fsync`한 뒤 이름을 교체한다. 따라서 1 MiB 문서의 마지막 한 바이트를 바꿀 때 +이 경로의 **사용자 공간 파일 쓰기 요청 길이 합계는 1,048,600 B**다 +(1,048,576 B 문서 + 24 B 헤더). 이는 실제 저장 장치의 물리적 쓰기량과 같다는 +주장이 아니다. 결과 수명은 기존 파일의 읽기 전용 매핑과 새 파일 교체로 분리한다. + +이 비용을 `copy_file_range` 같은 복사 호출로 바꾸는 것만으로 새 파일의 전체 본문 +기록이 사라지지는 않는다. 또 현재 읽기 계약은 단일 연속 `PureSpan`이다. 델타나 +불변 조각으로 작은 변경만 기록하려면 읽을 때 다시 연속 버퍼를 조립하거나, 소비자가 +범위·조각을 직접 받도록 결과 표현을 바꾸어야 한다는 것이 **현재 코드에서의 추론**이다. +다음 저장 실험은 버전별 읽기 안정성, 공개 전후 실패, 충돌과 전체 앱 메모리를 그대로 +유지하면서 작은 편집의 실제 쓰기량을 줄여야 한다. 지금 구현을 이미 저쓰기량 저장소로 +소개하지 않는다. diff --git a/docs/research/032-micro-native-authoring-boundary.md b/docs/research/032-micro-native-authoring-boundary.md new file mode 100644 index 0000000..a2b8e3f --- /dev/null +++ b/docs/research/032-micro-native-authoring-boundary.md @@ -0,0 +1,82 @@ +# 032 — 마이크로 기본 비용과 선택형 네이티브 작성 창 + +## 이번 결정 + +기본 배포물에는 문서 CLI와 수정 가능한 `editor.pure`만 넣는다. HTTP, 인증 문서 피어, +GTK 작성 창은 명시적으로 선택한다. 컴파일 시 포함되지 않은 호스트의 라이브러리를 +기본 실행물로 끌어오지 않는 기존 경계를 패키지에도 적용했다. 배포물에서 DWARF +디버그 정보만 제거하며 검사를 통과한 빌드 실행물은 수정하지 않는다. + +| Linux AArch64/GCC 14 구성 | 선택 실행물 | gzip 묶음 | 이번 검증 | +|---|---|---:|---| +| 기본 | `pure-edit` | 22,482 B | 다른 위치에서 풀어 CLI 편집·복원·`.pure` 수정 효과 확인 | +| HTTP 선택 | CLI, HTTP | 47,527 B | 실제 HTTP 요청이 CLI와 같은 저장 상태를 변경 | +| TLS 선택 | CLI, 인증 피어 | 49,186 B | 1 MiB 문서 전송·오류·복원 | +| 창 선택 | CLI, GTK 창 | 48,934 B | HTTP 없이 CLI와 같은 문서의 충돌·정의 적용 확인 | +| 전체 선택 | CLI, HTTP, TLS 피어, GTK 창 | 100,900 B | 같은 검사에 1 MiB TLS 문서와 실제 GTK·CLI·HTTP 교차 편집 추가 | + +[기본 묶음](../../prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-micro-linux-aarch64.tar.gz)과 +[전체 선택 묶음](../../prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-authoring-linux-aarch64.tar.gz)은 +각각 같은 입력으로 두 번 생성했을 때 SHA-256이 일치했다. +[기본 추출 검사](../../prototypes/connections/results/2026-09-24-native-definition-authoring/micro-package-check.json), +[HTTP 선택 묶음](../../prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-http-only-linux-aarch64.tar.gz) +([결과](../../prototypes/connections/results/2026-09-24-native-definition-authoring/http-only-package-check.json)), +[TLS 선택 묶음](../../prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-peer-only-linux-aarch64.tar.gz) +([결과](../../prototypes/connections/results/2026-09-24-native-definition-authoring/peer-only-package-check.json)), +[창 단독 선택 묶음과 검사](../../prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-window-only-linux-aarch64.tar.gz) +([결과](../../prototypes/connections/results/2026-09-24-native-definition-authoring/window-only-package-check.json)), +[전체 추출 검사](../../prototypes/connections/results/2026-09-24-native-definition-authoring/full-package-check.json). +패키징 직전에도 CLI의 아래 메모리 상한을 재검사해 매니페스트에 예산 식별자를 남긴다. +이 숫자는 **압축 파일 크기**다. 실행 중 RAM이나 앱 전체 비용으로 해석하지 않는다. + +## 실제 비용 경계 + +기존 [코어 검사](../../prototypes/connections/core-budget.json)는 실행 코어 오브젝트 +`text` 5,932 B / 상한 6,144 B, data·BSS 0 B를 확인한다. 기본 CLI는 링크된 `text` +35,807 B이고 직접 동적 의존성은 libc 하나다. 코어 수치에는 파서·저장소·libc·스택이 +없으므로, 이번에는 [실제 작업 RSS 검사](../../prototypes/connections/check_micro_memory.py)를 +기본 `check-release`에 추가했다. 작은 C `fork`/`wait4` 도구로 자식을 실행해 Python +부모의 pre-exec 메모리가 자식 피크에 섞이지 않게 했다. + +| CLI 작업 | 이 장치의 피크 RSS | 회귀 상한 | +|---|---:|---:| +| 1 B 새 문서 편집 | 1,056 KiB | 4,096 KiB | +| 1 MiB 새 문서 편집 | 2,080 KiB | 4,096 KiB | +| 같은 1 MiB 문서 읽기 | 1,104 KiB | 4,096 KiB | + +[측정 결과와 정확한 범위](../../prototypes/connections/results/2026-09-24-native-definition-authoring/micro-memory.json). +이 상한은 Linux AArch64의 현재 CLI·문서 부하에만 적용된다. 캐시·커널 버퍼·다른 +작업은 포함하지 않는다. 코어 6 KiB 검사나 22 KiB 압축 묶음으로 임베디드급 +전체 앱을 입증했다고 주장하지 않는다. + +선택형 GTK 창은 1 MiB 문서를 연 상태에서 **43,792 KiB RSS**였다. 창의 링크된 +`text` 48,247 B와 직접 의존성 검사는 [별도 예산](../../prototypes/connections/window-budget.json)으로 +관리하지만, GTK 공유 라이브러리와 프로세스 전체 비용은 그 코드 수치에 포함되지 +않는다. 따라서 이 창은 현재 호스트 OS에서 사용성을 검증하는 도구이지 저메모리 UI +백엔드의 성과가 아니다. [실제 창 검사](../../prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow-final.json). +선택형 TLS 피어도 [전체 추출 검사](../../prototypes/connections/results/2026-09-24-native-definition-authoring/full-package-check.json)에서 +유휴 7,840 KiB, 1 MiB 작업 후 9,520 KiB RSS였다. 보안 전송의 이 비용을 기본 +런타임의 비용으로 숨기거나, 기본 CLI 크기로 피어 전체가 작다고 주장하지 않는다. + +## 작성 흐름과 안전성 + +문서 창의 **Definition** 패널에서 같은 `.pure` 소스를 편집하고 적용할 수 있다. 적용은 +새 정의의 구문과 문서 편집에 필요한 전체 읽기 레이아웃을 먼저 확인한다. 원본 파일이 +외부에서 바뀌었으면 덮어쓰지 않고, 저장 실패·잘못된 정의에서도 실행 중인 이전 +계획과 소스 초안을 유지한다. 정상 적용 시 파일을 일관성·내구성 옵션으로 저장하고 +현재 창의 실행 계획을 교체한다. 다른 호스트는 재시작 뒤 새 정의를 읽는다. + +실제 GTK 입력으로 응답 규칙을 바꾼 뒤 같은 창의 다음 편집 출력이 1 B에서 0 B로 +바뀌고, 새 프로세스에서도 복원됨을 확인했다. 부분 문서 읽기 정의, 잘못된 구문, +외부 수정 충돌, 쓰기 실패, 미저장 닫기를 각각 검증했다. ASan·UBSan에서도 같은 +흐름을 통과했다. [일반 실행 결과](../../prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow-final.json), +[계측 실행 결과](../../prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized-final.json), +[작성 창 화면](../../prototypes/connections/results/2026-09-24-native-definition-authoring/window-authoring.png). + +## 남은 판단 + +이 결과만으로 첫 공개 체크포인트는 끝나지 않았다. 현재 저장소는 1 B 편집에도 +문서 파일 전체를 다시 쓰므로 1 MiB 문서에서 쓰기 증폭이 크다. 큰 앱의 동시 작업, +메모리 상한, 네이티브 UI의 별도 저메모리 구현과 macOS 검증도 남았다. 다음 연구는 +코어에 서브시스템을 계속 추가하기보다 실제 문서 작업의 저장·전달 비용을 줄이고, +같은 정확성 조건에서 전체 앱 비용을 비교하는 데 집중한다. diff --git a/docs/research/033-paged-document-storage.md b/docs/research/033-paged-document-storage.md new file mode 100644 index 0000000..4e34b4d --- /dev/null +++ b/docs/research/033-paged-document-storage.md @@ -0,0 +1,92 @@ +# 033 — 연속 읽기를 유지하며 큰 문서의 쓰기량 줄이기 + +2026-09-24. 이전 [031](031-relocatable-document-preview.md)에서 확인한 문제는 +1 MiB 문서의 1 B 교체에도 현재 저장소가 새 파일에 1,048,600 B를 기록한다는 점이다. +이 결과는 Pure의 기본 저장소를 곧바로 교체한 것이 아니라, 같은 앱 인터페이스에 +붙인 **선택형 저장 경로**를 실제 CLI·HTTP·TLS·GTK 작업으로 검증한 중간 결과다. +이 보고서는 페이지 전용 바이너리와 결과 파일의 당시 상태를 기록한다. +`store-paged.c`는 이후 [034](034-hybrid-document-storage.md)의 인라인·페이지 +혼합형으로 확장돼, 현재 소스를 새로 빌드하면 이 보고서의 페이지 전용 +바이너리와 동일하지 않다. + +## 선택한 표현과 유지한 계약 + +[`store-paged.c`](../../prototypes/connections/store-paged.c)는 현재 Linux AArch64 호스트의 +16 KiB 페이지를 사용한다. 작은 불변 루트 파일에는 문서 길이·버전·페이지 참조를 +기록하고, 데이터 파일에는 새 페이지를 덧붙인다. 길이가 같은 교체에서는 영향을 +받은 페이지만 기록한 다음 새 루트를 `fsync`·`rename`·디렉터리 `fsync` 순서로 공개한다. +읽기는 필요한 페이지들을 하나의 예약된 가상 주소 범위에 매핑하므로 기존 +`PureSpan` 소비자는 여전히 연속 바이트를 본다. `MAP_FIXED` 매핑은 자신이 예약한 +범위 안에서만 사용한다. 이 호출의 정렬·교체 규칙은 [mmap(2)](https://man7.org/linux/man-pages/man2/mmap.2.html)에, +파일과 디렉터리 동기화의 구분은 [fsync(2)](https://man7.org/linux/man-pages/man2/fsync.2.html)에, +이름 교체의 원자성은 [rename(2)](https://man7.org/linux/man-pages/man2/rename.2.html)에 +기술되어 있다. + +읽기 시작 시 공유 잠금으로 루트와 데이터 파일을 함께 선택하고 매핑이 끝나면 잠금을 +놓는다. 오래된 매핑은 새 루트가 공개되고 이전 데이터 파일이 제거돼도 이전 바이트를 +유지한다. 쓰기는 기존과 같은 `.writer.lock`의 배타 잠금을 사용하므로 버전 충돌·동시 +작성 규칙을 유지한다. 큰 문서의 세그먼트가 살아 있는 데이터의 약 2배를 넘거나 +작은 문서의 세그먼트가 최소 8페이지 상한에 이르면 새 세대를 작성하고, +공개가 내구화된 뒤 낡은 세대와 크래시 후 남은 세그먼트·임시 루트를 수거한다. +삽입·삭제처럼 뒤쪽 바이트 위치가 바뀌는 작업은 아직 새 세대 전체를 기록한다. + +## 같은 실제 작업의 측정 + +[`check_write_cost.py`](../../prototypes/connections/check_write_cost.py)는 동일한 +`editor.pure` CLI로 문서를 만든 뒤 1 B를 교체하고 `strace`에서 **객체 파일로 성공한 +사용자 공간 `write` 요청의 바이트 수**를 합산한다. 저장 장치의 물리 쓰기량, 저널, +메타데이터·디렉터리 동기화 비용 또는 실행 시간을 뜻하지 않는다. + +| 작업 | 기존 불변 파일 | 선택형 페이지 저장소 | 판단 | +|---|---:|---:|---| +| 1 MiB 문서 마지막 1 B 교체 | 1,048,600 B / 258회 | 16,944 B / 3회 | 약 **61.9배 적은 요청 바이트** | +| 1 B 문서의 1 B 교체 | 25 B / 2회 | 16,440 B / 3회 | 약 **657.6배 많은 요청 바이트** | + +[원본 측정 결과](../../prototypes/connections/results/2026-09-24-paged-storage/write-cost.json). +큰 문서의 페이지 저장소 데이터 파일은 최초 1 MiB 작성 후 이 교체에서 16 KiB가 +추가됐고, 루트는 560 B였다. 작은 문서에서는 전체 페이지 기록이 압도적으로 +비싸다. **이 이유로 페이지 저장소는 기본값이 아니다.** 다음 설계는 작은 값의 +인라인 표현과 큰 값의 페이지 표현을 같은 의미론 아래에서 선택해야 한다. + +선택형 CLI의 링크된 `text`는 39,960 B로 기존 CLI의 35,807 B보다 4,153 B 늘었으나, +기존과 같은 40,960 B 상한을 통과했다. 기본 실행 코어는 바꾸지 않았다. +[코드·의존성 결과](../../prototypes/connections/results/2026-09-24-paged-storage/paged-profile.json). +1 MiB 새 문서 편집의 선택형 CLI 피크 RSS는 2,080 KiB이며 현재 4 MiB 회귀 상한을 +통과했다. 이 수치는 대규모 동시 작업이나 파일시스템 캐시를 포함하지 않는다. +[메모리 결과](../../prototypes/connections/results/2026-09-24-paged-storage/memory.json). + +## 앱과 실패 경로 검증 + +기존 문서 앱의 [CLI·HTTP 검사](../../prototypes/connections/results/2026-09-24-paged-storage/app-release.json), +[ASan·UBSan 검사](../../prototypes/connections/results/2026-09-24-paged-storage/app-asan.json), +[페이지 저장소 단위 검사](../../prototypes/connections/results/2026-09-24-paged-storage/unit.json)와 +[계측 단위 검사](../../prototypes/connections/results/2026-09-24-paged-storage/unit-asan.json)를 +통과했다. 단위 검사는 범위 읽기, 오래된 매핑, 같은 길이 교체, 세대 재작성·수거, +공개 전 크래시, 충돌, 공개 후 내구화 불확실성 및 길이 변경을 섞은 결정적 128회 +편집을 확인한다. +[GTK 문서·정의 편집](../../prototypes/connections/results/2026-09-24-paged-storage/gtk-flow.json)과 +[인증 TLS 문서 피어](../../prototypes/connections/results/2026-09-24-paged-storage/peer.json)도 +같은 저장 경로로 1 MiB 문서와 기존 충돌·복원 작업을 통과했다. GTK 1 MiB 문서 창은 +44,656 KiB RSS여서 이 저장 변경이 UI 전체 메모리 문제를 해결했다는 뜻은 아니다. + +페이지 저장 형식은 `PUREP001`이며 기존 `PUREB001`과 호환되지 않는다. 패키지는 +형식을 확인하고 별도 저장 디렉터리를 요구한다. [선택형 CLI 묶음](../../prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-micro-linux-aarch64.tar.gz) +(25,456 B)과 [전체 호스트 묶음](../../prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-authoring-linux-aarch64.tar.gz) +(111,765 B)을 각각 다른 위치에서 풀어 +[CLI](../../prototypes/connections/results/2026-09-24-paged-storage/package-micro.json)와 +[CLI·HTTP·TLS·GTK](../../prototypes/connections/results/2026-09-24-paged-storage/package-full.json)를 +재검증했다. 각 묶음은 같은 입력에서 두 번 생성해 해시 일치를 확인했다. + +## 공개 전 남은 조건 + +이 형식은 16 KiB OS 페이지를 요구하며 macOS 빌드, 기존 저장소 마이그레이션, +작은 값의 효율적인 인라인 표현이 없다. 읽기에서 페이지 참조가 흩어질수록 매핑 +호출이 늘어나는 비용과 큰 동시 작업의 최악 지연도 아직 검증하지 않았다. +쓰기 syscall 바이트 감소가 실제 장치 쓰기량·처리량 개선을 보장하지 않는다. +따라서 현재 기본 배포는 기존 저장소를 유지한다. 선택형 경로의 다음 검증은 +작은 값과 큰 값의 혼합 작업, 오래 살아 있는 읽기, 크래시 뒤 저장 공간 수거 및 +실제 앱 지연을 같은 계약 아래에서 확인하는 것이다. + +페이지 전용 결과의 바이너리 해시와 풀어서 실행 가능한 묶음은 위 결과 디렉터리에 +보존했다. 현재 소스의 후속 구현은 `make -C prototypes/connections check-hybrid`로 +검증한다. 페이지 전용 소스 빌드 명령은 현재 트리에 더 이상 제공하지 않는다. diff --git a/docs/research/034-hybrid-document-storage.md b/docs/research/034-hybrid-document-storage.md new file mode 100644 index 0000000..0dd9a5b --- /dev/null +++ b/docs/research/034-hybrid-document-storage.md @@ -0,0 +1,77 @@ +# 034 — 작은 값은 인라인, 큰 값은 페이지로 저장 + +2026-09-24. [033](033-paged-document-storage.md)의 페이지 전용 실험은 1 MiB 문서의 +1 B 수정을 줄였지만 1 B 문서의 수정 요청량을 25 B에서 16,440 B로 늘렸다. +이번에는 같은 문서 앱의 **선택형** 저장소에서 16 KiB 이하의 값은 기존 `PUREB001` +파일에 직접 담고, 그보다 큰 값만 `PUREP001` 페이지 루트로 표현한다. 실행 코어, +기본 `store.c`, 기본 패키지는 변경하지 않았다. + +[`store-paged.c`](../../prototypes/connections/store-paged.c)는 두 형식을 읽고, 결과 +길이에 따라 다음 루트 형식을 고른다. 크기 경계를 넘을 때는 이전 형식의 논리 +바이트를 읽어 새 파일을 완성한 후 공개한다. 페이지 저장 중 길이가 같은 부분 +수정은 기존처럼 변경된 페이지만 추가한다. 형식 전환과 인라인 수정에도 기존 +버전 충돌, 이전 읽기 스냅샷, 공개 전 실패와 공개 후 `PURE_UNCERTAIN` 계약을 +적용한다. [단위 검사](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit.json)는 +양방향 16 KiB 경계 전환, 기존 큰 `PUREB001` 파일의 첫 수정, 오래된 읽기, +크래시·충돌·혼합 편집을 포함한다. [ASan·UBSan 검사](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit-asan.json)도 통과했다. + +## 실제 비용과 마이크로 경계 + +같은 `editor.pure` CLI가 문서를 만들고 1 B를 바꿀 때, `strace`에서 객체 파일에 +성공한 사용자 공간 `write` 요청만 합산했다. 물리 디스크 쓰기량, 저널, +디렉터리 동기화와 지연 시간은 포함하지 않는다. + +| 작업 | 기본 불변 파일 | 선택형 하이브리드 | +|---|---:|---:| +| 1 MiB 마지막 1 B 교체 | 1,048,600 B / 258회 | 16,944 B / 3회 | +| 1 B 문서의 1 B 교체 | 25 B / 2회 | 25 B / 2회 | + +[원본 쓰기 추적](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/write-cost.json). +큰 문서의 약 61.9배 적은 요청 바이트와 작은 문서의 동일한 요청량을 확인했다. +256개의 다른 파일이 있는 저장 디렉터리에서도 작은 값의 수정은 디렉터리 +항목을 훑는 `getdents64` 호출이 없었다. +[작은 값 경로 추적](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/small-scan.json). +단, 기존 큰 `PUREB001` 문서를 처음 수정할 때는 전체를 페이지 파일로 옮겨 +써야 한다. 그 한 번의 전환 비용과 이후 수정의 총 지연·처리량은 아직 측정하지 +않았으므로 더 빠른 저장소라는 결론은 내리지 않는다. + +코드 비용도 늘었다. 같은 Linux AArch64/GCC 14 `-O2` 빌드에서 기본 CLI의 +링크된 `text`는 35,807 B, 하이브리드 CLI는 **41,064 B**로 5,257 B 더 크다. +페이지 전용 실험의 39,960 B보다도 1,104 B 늘었다. 기존 선택형 페이지 +예산인 40,960 B를 104 B 초과했다. 그래서 실패한 예산을 자동으로 통과시킨 +것이 아니라 [별도의 42 KiB 연구 상한](../../prototypes/connections/hybrid-budget.json)을 +명시했다. [코드·직접 의존성 결과](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/profile.json)는 +CLI와 HTTP 모두 libc만 직접 필요함을 확인한다. 1 MiB 새 문서 편집 CLI의 +피크 RSS는 **2,080 KiB**이며 기존 4 MiB 회귀 상한을 통과했다. +[프로세스 메모리 결과](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/memory.json). +이 상한은 파일시스템 캐시, 동시 작업, 선택형 UI와 TLS를 포함하지 않는다. + +## 앱과 배포 경계 + +[CLI·HTTP 전체 검사](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-release.json), +[앱 ASan·UBSan 검사](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-asan.json), +[인증 TLS 문서 피어](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/peer.json), +[GTK 문서·정의 편집](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/gtk-flow.json)을 +같은 저장 경로로 통과했다. GTK 큰 문서 창은 43,632 KiB RSS로, 이 실험이 +선택형 UI의 메모리 문제를 해결하지는 않는다. + +[CLI만 든 선택형 묶음](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-micro-linux-aarch64.tar.gz)은 +26,045 B이고, [HTTP·TLS·GTK까지 선택한 묶음](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-authoring-linux-aarch64.tar.gz)은 +114,025 B다. 각각 다른 디렉터리에 풀어 +[CLI와 양쪽 저장 형식](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-micro.json), +[전체 앱 경로](../../prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-full.json)를 +다시 확인했다. 시스템 공유 라이브러리는 묶음에 포함되지 않는다. + +작은 기존 `PUREB001` 파일은 그대로 사용할 수 있고, 큰 기존 파일도 읽고 +수정하면서 `PUREP001`로 전환할 수 있다. **기본 저장기는 전환된 큰 파일을 +읽을 수 없다.** 따라서 이 선택형 묶음에는 별도 저장 디렉터리를 사용해야 +한다. 16 KiB OS 페이지를 요구하며 macOS는 검증하지 않았다. 큰 동시 작업, +세대 수거의 최악 지연, 실제 장치 쓰기량 및 운영 중 되돌리기 절차는 남아 있다. +작은 값만 계속 수정하는 경로에서는 디렉터리 스캔을 피하므로, 크래시가 남긴 +임시 루트 파일도 다음 페이지 관련 작업 전까지 남을 수 있다. +이 비용과 제한 때문에 기본 마이크로 런타임에 포함하지 않는다. + +재현 명령은 `make -C prototypes/connections check-hybrid`와 +`python3 prototypes/connections/check_write_cost.py --flat +prototypes/connections/build/pure-edit --candidate +prototypes/connections/build/hybrid/pure-edit --candidate-mode hybrid`다. diff --git a/docs/research/035-native-multi-document-window.md b/docs/research/035-native-multi-document-window.md new file mode 100644 index 0000000..3aa910f --- /dev/null +++ b/docs/research/035-native-multi-document-window.md @@ -0,0 +1,66 @@ +# 035 — 한 네이티브 창에서 여러 문서를 선택하기 + +2026-09-24. 첫 공개 체크포인트의 문서 창은 CLI·HTTP·인증 피어와 같은 +`editor.pure`를 실행했지만, 시작 인자로 받은 문서 키 하나에 묶여 있었다. +다른 작업물을 보려면 창을 새로 시작해야 했으므로 “작업물을 선택하고 편집하는 +애플리케이션”의 사용 경험이 부족했다. + +선택형 [`edit-window.c`](../../prototypes/connections/edit-window.c)에 문서 키 입력과 +**Open · Ctrl+O**를 추가했다. 유효한 키를 열면 같은 `PureRuntime`과 작성된 +`editor.pure` 경로로 읽는다. 없는 키는 빈 새 문서로 열고, 있으면 해당 버전과 +본문을 채택한다. 읽기 결과를 확인하고 텍스트로 복사한 **뒤에만** 현재 키· +기준 본문·버전을 바꾼다. 따라서 잘못된 키, 읽기 실패, NUL/잘못된 UTF-8이 +든 객체는 현재 문서를 대체하지 않는다. 문서 또는 `.pure` 정의의 초안이 +수정된 상태라면 전환을 막아 둘 다 남긴다. 성공한 전환은 이전 문서의 출력 +표시를 지우고 새 문서의 제목·상태를 갱신한다. + +[실제 GTK 검사](../../prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-repeated.json)는 +한 창에서 `first → second → third`를 열고 각 문서를 저장·복원한다. +두 번째 문서의 미저장 본문, 수정 중인 정의 초안, 잘못된 키, 바이너리 객체에서 +현재 문서가 유지되는지 확인했다. 기존 외부 HTTP 충돌, 1 MiB 편집, 정의 적용· +실패·재시작 검사는 그대로 통과했다. +[ASan·UBSan 창 검사](../../prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-repeated.json)와 +[선택형 혼합 저장소의 같은 UI 경로](../../prototypes/connections/results/2026-09-24-multi-document-window/hybrid-gtk-flow.json)도 확인했다. + +## 비용과 배포 + +작업 접수·메모리 수명·효과 실행의 공통 코어는 바꾸지 않았다. 기본 CLI도 +그대로다. 선택형 GTK 창의 링크된 `text`는 앞선 48,247 B에서 **49,882 B**로 +1,635 B 증가했고, 기존 56 KiB 상한과 직접 의존성 검사를 통과했다. +[코드·의존성 결과](../../prototypes/connections/results/2026-09-24-multi-document-window/window-profile.json). +1 MiB 문서를 연 창의 단독 실행 RSS 표본은 **43,920 KiB**였다. 그러나 같은 +창에서 두 개의 1 MiB 문서를 64회 번갈아 열자 RSS가 +**58,752 KiB**까지 증가했다. +[8회 간격 원자료](../../prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-initial.json). +이 증가는 이 작업에서 측정한 프로세스 RSS다. 후속 +[메모리 분리 실험](036-gtk-memory-boundary.md)에서는 증가가 힙에 집중되고 +텍스트 뷰 갱신을 생략한 진단 빌드에서 사라졌다. 정확한 GTK 내부 객체와 +반환 시점은 아직 확인하지 못했다. GTK 문서에 따르면 `GtkTextBuffer.set_text()`는 +[실행 취소 이력에 남지 않는 교체](https://docs.gtk.org/gtk4/method.TextBuffer.set_text.html)다. +이전 버퍼를 매번 새 것으로 바꾸는 실험도 +[증가를 없애지 못해](../../prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-fresh-buffer.json) +되돌렸다. `GtkTextView.set_buffer()`는 [이전 버퍼의 참조를 놓는다](https://docs.gtk.org/gtk4/method.TextView.set_buffer.html). +따라서 이 창을 저메모리 UI의 근거로 삼지 않는다. 기본 코어·CLI의 기존 +크기·메모리·동작 검사 `make check-release`는 통과했다. + +[창 선택 묶음](../../prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-final-linux-aarch64.tar.gz)은 +49,746 B다. [다른 위치에 풀어 재검사한 결과](../../prototypes/connections/results/2026-09-24-multi-document-window/package-repeated.json)에는 +문서 전환 검사도 포함된다. 패키지 매니페스트는 이 창 기능을 명시한다. +예전 창 묶음에는 해당 표시가 없으므로 현재 검사기는 그 묶음에 새 기능을 +요구하지 않는다. [보관된 창 묶음 재검사](../../prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-repeated.json)가 +통과했다. + +이것은 문서 탐색의 첫 경로다. 아직 키를 직접 입력해야 하고, 목록·검색·메타데이터 +인덱스가 없다. 여러 문서의 원자적 변경도 제공하지 않는다. 현재 작성 언어는 +`get`/`put`의 단일 저장 효과에 국한되고 동기 저장 작업은 GTK 이벤트 루프를 +막을 수 있다. 이 상태를 큰 서비스용 애플리케이션 런타임이라고 부르지 않는다. +다음 구현은 위 실제 앱에서 필요한 다중 객체 탐색·갱신의 의미와 비용을 먼저 +정하고, 그 요구를 실행 모델에 통합해야 한다. 선택형 창을 확장하기 전에는 +[반복 텍스트 교체의 메모리 증가](036-gtk-memory-boundary.md)도 해결하거나 +더 작은 UI 어댑터에서 같은 사용 경험을 검증해야 한다. 단순히 버튼을 더 +붙이는 것은 그 빈칸을 채우지 못한다. + +재현 명령은 `make -C prototypes/connections window check-window-profile`과 +`xvfb-run -a python3 prototypes/connections/check_window.py --build +prototypes/connections/build --program prototypes/connections/examples/editor.pure`다. +64회 메모리 관찰에는 마지막 명령에 `--switch-rounds 64`를 추가한다. diff --git a/docs/research/036-gtk-memory-boundary.md b/docs/research/036-gtk-memory-boundary.md new file mode 100644 index 0000000..d90246d --- /dev/null +++ b/docs/research/036-gtk-memory-boundary.md @@ -0,0 +1,89 @@ +# 036 — 선택형 문서 창의 메모리 경계 + +2026-09-24. 사용자가 원하는 것은 작은 **애플리케이션 런타임**이다. GTK 창의 +프로세스 비용을 코어의 비용처럼 취급하거나, 작은 압축 묶음만으로 메모리 목표를 +달성했다고 말할 수 없다. 여기서는 [035](035-native-multi-document-window.md)의 +1 MiB 문서 반복 전환에서 나타난 증가를 분리했다. 앱 기능을 추가하지 않았다. + +Linux AArch64에서 실제 GTK 창을 Xvfb 아래 띄우고, 1 MiB 문서 두 개를 +64회 열었다. 8회마다 `/proc//status` RSS와 `smaps`의 영역별 RSS를 +기록했다. 각각의 대조 실험은 같은 문서 앱의 저장·편집·충돌·정의 적용 +시나리오도 통과했다. RSS는 단일 실행의 관찰값이며, 실험 간 절댓값 차이를 +성능 순위로 해석하지 않는다. + +| 조건 | 8회 후 RSS | 64회 후 RSS | 힙 RSS 증가 | +|---|---:|---:|---:| +| 두 키, 서로 다른 1 MiB 본문 | 47,472 KiB | 58,896 KiB | 11,296 KiB | +| 같은 키 다시 열기 | 47,056 KiB | 47,072 KiB | 16 KiB | +| 두 키, 동일한 본문 | 47,216 KiB | 59,680 KiB | 12,464 KiB | +| 두 키, 1 MiB 전환 시 텍스트 뷰 갱신만 생략한 **진단 빌드** | 47,216 KiB | 47,216 KiB | 0 KiB | +| 두 키, 문서 버퍼 실행 취소 기능 비활성화 | 47,440 KiB | 59,008 KiB | 11,440 KiB | +| 두 키, 줄바꿈 비활성화 | 45,888 KiB | 57,440 KiB | 11,424 KiB | + +원자료는 순서대로 +[기본](../../prototypes/connections/results/2026-09-24-window-memory-analysis/smaps.json), +[같은 키](../../prototypes/connections/results/2026-09-24-window-memory-analysis/same-key.json), +[같은 본문](../../prototypes/connections/results/2026-09-24-window-memory-analysis/same-body.json), +[텍스트 갱신 생략](../../prototypes/connections/results/2026-09-24-window-memory-analysis/skip-text.json), +[실행 취소 비활성화](../../prototypes/connections/results/2026-09-24-window-memory-analysis/no-document-undo.json), +[줄바꿈 비활성화](../../prototypes/connections/results/2026-09-24-window-memory-analysis/no-wrap.json)에 있다. +텍스트 갱신 생략 빌드는 **문서를 제대로 보여주지 않으므로 제품 수정안이 아니다.** +키·버전·본문 복사·정의·저장 경로는 그대로 실행하고, 1 MiB의 다른 키를 열 때 +`gtk_text_buffer_set_text()` 호출만 건너뛰어 원인 경계를 분리했다. + +`mallinfo2`를 추가한 별도 진단 빌드에서는 glibc 할당기의 `uordblks`가 +8회 후 10,897,312 B에서 64회 후 23,427,504 B로 증가했다 +([원자료](../../prototypes/connections/results/2026-09-24-window-memory-analysis/mallinfo.json)). +따라서 관찰된 증가는 단순히 해제된 블록을 할당기가 보유하는 현상만으로 +설명되지 않는다. 반면 이 수치는 어떤 GTK 객체가 데이터를 소유하는지, +그 객체가 언제 반환되는지까지 밝히지 않는다. 종료 후 누수량과 실행 중 +힙 사용량도 같은 지표가 아니므로, 이 결과만으로 누수를 단정하지 않는다. + +GTK 문서는 [`GtkTextBuffer.set_text()`가 이전 내용을 지우고 새 내용을 넣으며 +실행 취소 불가능한 작업으로 표시된다](https://docs.gtk.org/gtk4/method.TextBuffer.set_text.html)고 +설명한다. 실행 취소와 줄바꿈을 각각 꺼도 증가량이 유지된 점과 함께 보면, +현재 위치는 **GTK 텍스트 교체 경계**다. 이전 버퍼를 새 버퍼로 바꾸는 실험도 +[035의 원자료](../../prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-fresh-buffer.json)에서 +효과가 없었다. 특정 GTK 내부 캐시나 누수라는 결론은 내리지 않는다. + +재현 가능한 제품 빌드의 기준 경로는 +`make -C prototypes/connections window editor` 다음 +`xvfb-run -a python3 prototypes/connections/check_window.py --build prototypes/connections/build --program prototypes/connections/examples/editor.pure --switch-rounds 64 --diagnose-maps`다. +진단 변형은 제품 코드에서 제거했다. 텍스트 갱신 생략 변형은 +`open_document_key()`의 `gtk_text_buffer_set_text()` 앞에 +`if (!(strcmp(key, e->key) != 0 && e->baseline_size >= 1000000))`를 +놓되, 키 변경 전의 값을 비교해야 한다. 다른 변형은 창 생성 시 문서 버퍼에 +`gtk_text_buffer_set_enable_undo(buffer, FALSE)`를 호출하거나 문서 뷰의 +`GTK_WRAP_WORD_CHAR`를 `GTK_WRAP_NONE`으로 바꾼 것이다. `mallinfo2` +변형은 문서 열기 완료 직후 할당기 통계를 기록했다. + +현재 비용 경계는 분명하다. 기본 CLI의 1 MiB 작업에는 +[4 MiB 최대 RSS 검사](../../prototypes/connections/check_micro_memory.py)가 있고, +GTK·TLS는 기본 실행물에 링크되지 않는다. 선택형 GTK 창은 시작부터 약 +44 MiB RSS를 쓰고 위 반복 전환에서는 더 증가한다. 따라서 **이 창을 +마이크로 메모리 프로필이라고 부르지 않는다.** 선택형 창의 기능 검증은 +유효하지만 저메모리 UI를 증명하지 않는다. 큰 앱·서비스로 확장하기 전에 +실행 코어와 어댑터의 비용을 계속 따로 공개하고, UI에는 실제 화면·입력· +편집 기능을 유지한 상태에서 전환 후 메모리가 안정되는 구현이 필요하다. + +진단 코드를 제거한 최종 창도 [64회 전환 기능·RSS 검사](../../prototypes/connections/results/2026-09-24-window-memory-analysis/final-native-window.json)를 +통과했고, RSS는 8회 후 47,216 KiB에서 64회 후 58,528 KiB로 증가했다. +같은 상태의 `make check-release check-window-profile`에서는 코어 `text` +5,932 B, 기본 CLI의 1 MiB 편집 피크 RSS 2,080 KiB, 선택형 GTK 창의 +`text` 49,946 B가 각 회귀 상한을 통과했다. GTK 창의 코드 섹션 통과는 +전체 GTK 프로세스의 메모리 통과를 뜻하지 않는다. 이 결과를 반영해 다음 +GUI 기능을 추가하기 전에 창 메모리의 경계 또는 더 작은 네이티브 UI 어댑터를 +검증해야 한다. + +추가 대조에서 마지막 전환 후 10초간 창을 유휴 상태로 두어도 RSS는 +59,328→59,456 KiB로 줄지 않았다 +([원자료](../../prototypes/connections/results/2026-09-24-window-memory-analysis/settled-final.json)). +같은 1 MiB를 80 B 줄 대신 4,096 B 줄로 구성하면 8→64회 힙 RSS 증가가 +11,296 KiB에서 17,792 KiB로 오히려 커져, 단순한 **줄당 고정 비용** 가설도 +맞지 않았다 +([원자료](../../prototypes/connections/results/2026-09-24-window-memory-analysis/line-width-4096.json)). +전환마다 문서 `GtkTextView` 전체를 파괴·재생성한 진단 빌드 역시 힙 RSS가 +14,416→25,968 KiB로 증가했다 +([원자료](../../prototypes/connections/results/2026-09-24-window-memory-analysis/replace-view.json)). +이 세 실험은 GTK 내부 소유자를 특정하지 않는다. 뷰를 매번 재생성하는 +제품 코드는 채택하지 않았고, 작은 코어의 실행 모델 작업에 우선순위를 둔다. diff --git a/docs/research/037-build-time-prepared-runner.md b/docs/research/037-build-time-prepared-runner.md new file mode 100644 index 0000000..1a6fae4 --- /dev/null +++ b/docs/research/037-build-time-prepared-runner.md @@ -0,0 +1,59 @@ +# 037 — 작성 도구와 실행 전용 문서 앱 분리 + +2026-09-24. [036](036-gtk-memory-boundary.md)의 GTK 창은 44 MiB 이상의 +선택 비용을 지닌다. 마이크로 목표를 지키기 위해 이번에는 화면 기능을 늘리지 +않고, 기존 `.pure` 문서 앱의 **실행물에 매번 싣던 파서**를 분리했다. + +[`pure-compile`](../../prototypes/connections/compile-program.c)은 빌드 때 +기존 `pure_program_parse()`가 검증한 `PureProgram`을 C 초기화자로 출력한다. +[`pure-edit-fixed`](../../prototypes/connections/edit.c)는 생성된 상수를 +기존 `PureRuntime`과 기본 파일 저장소에 연결한다. 실행기 링크에는 +`program.c`와 컴파일 도구가 없다. 별도 `pure-edit`는 `.pure` 소스를 실행 때 +읽는 작성·실험 경로로 남긴다. 새 VM, 로더, 가변 명령 디스패치 또는 코어의 +항상 포함 기능을 추가하지 않았다. + +같은 Linux AArch64/GCC 14 `-O2` 빌드에서 기본 문서 CLI의 링크된 `text`는 +35,807 B, 고정 정의 실행기는 **19,244 B**다. 같은 `editor.pure`의 읽기, +조건부 수정, 응답과 기본 파일 저장을 수행하면서 **16,563 B(46.3%)** 줄었다. +직접 공유 라이브러리 의존성은 둘 다 libc뿐이다. +[20 KiB 실행기 코드 상한 결과](../../prototypes/connections/results/2026-09-24-fixed-program/profile-final.json). +컴파일 도구 자체의 `text`는 22,034 B이며 빌드 시 필요하다. 이를 프로젝트 +전체 비용에서 사라진 것으로 세지 않는다. + +1 MiB 문서 편집의 피크 RSS는 고정 실행기 **2,112 KiB**였다 +([4 MiB 회귀 상한 결과](../../prototypes/connections/results/2026-09-24-fixed-program/memory-verified.json)). +기존 소스 실행기의 같은 측정은 2,080 KiB였다. 따라서 이 결과는 **코드 +크기 감소**를 입증하지만 RAM 감소를 입증하지 않는다. 실행기 패키지는 +14,385 B 압축 파일이다 +([이동 후 실제 편집·복원 검사](../../prototypes/connections/results/2026-09-24-fixed-program/package-final.json)). +기존 22,482 B 소스 작성 패키지와는 포함 파일 및 실행 중 정의 변경 능력이 +다르므로 패키지 크기를 동등 기능의 성능 비교로 쓰지 않는다. 이 고정 패키지는 +`editor.pure` 파일과 파서·컴파일러를 포함하지 않는다. + +[동작 일치 검사](../../prototypes/connections/results/2026-09-24-fixed-program/parity-release.json)는 +소스 실행기와 고정 실행기를 서로 다른 저장 디렉터리에서 같은 작업으로 +비교했다. 문서 생성·수정·오래된 버전 충돌·잘못된 범위·1 MiB 복원뿐 아니라 +`selection.pure`의 동적 입력 범위, `excerpt.pure`의 중첩 읽기·필요 길이 +검사까지 결과 코드·본문·메타데이터가 같았다. 잘못된 소스의 컴파일은 기존 +생성 파일을 유지했고, 입력 파일을 출력 대상으로 지정하는 것도 거절했다. +같은 빌드 디렉터리에서 작성 정의를 +`editor → selection → editor`로 바꾸면 실행물이 각각 다시 빌드됐다. +`nm`에서도 고정 실행기의 파서 심볼이 없음을 확인했다. 기존 +`make check-release`, 새로운 `make check-fixed-profile`, 이동한 묶음의 +검사가 모두 통과했다. GTK 창의 기존 기능 검사도 통과했다. + +이 경로는 현재 **C 소스 초기화자**를 생성해 같은 대상의 C 컴파일러로 +빌드한다. 안정적인 이식 바이트코드 형식이나 대상 독립 ABI는 아니다. +사용자가 실행 중 `.pure` 정의를 바꾸려면 기존 소스 실행기·작성 창이 +필요하고, 고정 실행기는 다시 빌드해야 한다. 현재 작성 언어 자체도 +`get`/`put` 한 저장 효과 범위다. 따라서 코드 크기 절감이 대규모 앱 표현, +네이티브 UI 메모리, macOS 검증을 대신하지 않는다. 다음 구현은 더 큰 +앱을 작성 가능하게 하면서도 이 준비·실행 분리와 크기 상한이 유지되는지 +확인해야 한다. + +재현 명령: `make -C prototypes/connections fixed-editor check-fixed-profile`, +`make -C prototypes/connections BUILD=build/fixed-selection +FIXED_PROGRAM=examples/selection.pure fixed-editor`, +`make -C prototypes/connections BUILD=build/fixed-excerpt +FIXED_PROGRAM=examples/excerpt.pure fixed-editor`, +`python3 prototypes/connections/check_fixed_program.py`. diff --git a/docs/research/038-fixed-http-document-host.md b/docs/research/038-fixed-http-document-host.md new file mode 100644 index 0000000..5eeeaaf --- /dev/null +++ b/docs/research/038-fixed-http-document-host.md @@ -0,0 +1,51 @@ +# 038 — 준비된 한 앱을 CLI와 HTTP 경계에서 실행하기 + +2026-09-24. [037](037-build-time-prepared-runner.md)의 빌드 시 준비 경로를 +같은 문서 앱의 HTTP 호스트까지 확장했다. `pure-connections-fixed`는 +`editor.pure`에서 준비한 정적 `PureProgram`을 기존 실행 코어·파일 저장소와 +연결한다. 외부 HTTP 형식은 어댑터에서 처리하고, 실행 코어에 HTTP 타입이나 +파서를 넣지 않았다. 소스 작성용 `pure-connections`는 그대로 남는다. + +| Linux AArch64/GCC 14 `-O2` 구성 | 링크된 `text` | 직접 공유 의존성 | +|---|---:|---| +| 소스 정의 HTTP 호스트 | 43,009 B | libc | +| 고정 정의 HTTP 호스트 | **25,228 B** | libc | + +같은 문서 기능의 호스트 실행물에서 파서 분리로 **17,781 B(41.3%)**를 +줄였다. 실행기 구조체도 41,528→40,848 B로 680 B 작아졌는데, 이는 +소스 호스트에 있던 `PureProgram` 인스턴스를 정적 상수로 공유한 결과다. +[두 프로필의 코드·직접 의존성](../../prototypes/connections/results/2026-09-24-fixed-http/source-profile.json), +[고정 호스트의 26 KiB 회귀 상한](../../prototypes/connections/results/2026-09-24-fixed-http/fixed-profile.json). +압축 크기나 코드 섹션만으로 프로세스 메모리·네트워크 성능 향상을 주장하지 않는다. + +[실제 HTTP·CLI 교차 검사](../../prototypes/connections/results/2026-09-24-fixed-http/verified.json)에서는 +고정 HTTP로 만든 문서를 소스 HTTP가 읽고, 소스 HTTP의 수정을 고정 HTTP가 +읽었다. 오래된 버전과 잘못된 수정 범위를 양쪽에서 거절했고, 고정 HTTP에서 +1 MiB 문서를 생성한 뒤 소스 HTTP·고정 CLI가 같은 바이트를 읽었다. +CLI에서 끝 1 B를 수정한 뒤 양쪽 HTTP가 새 버전을 복원했다. 고정 HTTP +실행물에는 `pure_program_parse`·`pure_program_load` 심볼이 없다. + +같은 검사에서 느린 클라이언트가 1 MiB 응답을 한 바이트만 받은 동안, +1 MiB 보유 예산 아래의 다른 읽기는 거절됐다. 첫 응답을 끝까지 소비하자 +다음 요청이 성공했고, 종료 시 보유량과 활성 작업은 모두 0이었다. +이것은 응답 버퍼의 수명·예산·회수 검증이며 Kitex급 처리량을 증명하지 않는다. +한 번 실행의 프로세스 RSS 표본은 고정 HTTP 유휴 1,520 KiB, 소스 HTTP +유휴 1,552 KiB였다. 1 MiB 문서 작업 후 각각 1,648/1,680 KiB였다. +고정 HTTP의 이 작업 중 최고 RSS는 2,128 KiB로 **4 MiB 회귀 상한**을 +통과했다. 소스 HTTP와의 작은 RSS 차이를 안정적인 RAM 이점으로 단정하지 +않는다. 이 상한은 단일 1 MiB 작업과 위 느린 수신자 흐름에 적용되며, +동시 다중 클라이언트 전체 피크의 보증이 아니다. + +[CLI+HTTP 묶음](../../prototypes/connections/results/2026-09-24-fixed-http/pure-document-fixed-http-linux-aarch64.tar.gz)은 +31,733 B이고 libc 외 공유 라이브러리를 묶지 않는다. +[다른 디렉터리에서 해제·동작 검사](../../prototypes/connections/results/2026-09-24-fixed-http/package.json)는 +패키지의 두 호스트가 같은 저장 파일을 수정·복원함을 확인했다. 소스 정의나 +컴파일러가 들어 있지 않아 실행 중 앱을 다시 작성할 수 없다. 기존 소스·HTTP +패키지도 변경된 검사기로 재검증했다. 내부 Pure 피어의 TLS·정의 일치, +macOS 빌드, 다중 앱·대규모 서비스는 이 결과의 범위 밖이다. + +재현 명령은 `make -C prototypes/connections fixed-editor fixed-server +check-fixed-profile check-fixed-server`, +`python3 prototypes/connections/package_native.py --fixed --with-http --output +/tmp/pure-fixed-http.tar.gz`, `python3 prototypes/connections/check_package.py +/tmp/pure-fixed-http.tar.gz`다. diff --git a/docs/research/039-idle-http-wakeups.md b/docs/research/039-idle-http-wakeups.md new file mode 100644 index 0000000..f958aec --- /dev/null +++ b/docs/research/039-idle-http-wakeups.md @@ -0,0 +1,50 @@ +# 039 — 유휴 HTTP 호스트의 주기적 깨움 제거 + +후속 [041](041-posix-idle-signal-wakeup.md)에서 Linux 전용 `ppoll` 경로를 +POSIX 신호 알림 파이프로 교체했다. 아래 수치와 묶음은 그 전 단계의 기록이다. + +2026-09-24. [038](038-fixed-http-document-host.md)의 문서 HTTP 호스트는 +연결이 없어도 `poll(..., 50)`을 반복했다. 마이크로 런타임에서는 상시 실행 +서비스의 유휴 CPU 깨움도 비용이다. 이번 변경은 사용 중인 연결들의 가장 +이른 절대 기한까지 기다리고, 연결이 없으면 새 접속이나 종료 신호가 올 때까지 +블로킹한다. 전용 타이머 스레드, 실행 코어 타입, 추가 큐를 만들지 않았다. +종료 신호가 대기 직전에 도착하는 경합을 막기 위해 Linux 호스트는 +`SIGINT`·`SIGTERM`을 평소에 보류하고 `ppoll`이 대기할 때 원자적으로 +해제한다. 신호는 대기를 깨우며, 다음 루프에서 종료 상태를 확인한다. + +[시스템 호출 비교](../../prototypes/connections/results/2026-09-24-signal-safe-idle/reproducible-comparison.json)는 +같은 Linux AArch64 호스트에서 연결 없는 1.25초를 관찰했다. 보관된 +[이전 고정 HTTP 묶음](../../prototypes/connections/results/2026-09-24-fixed-http/pure-document-fixed-http-linux-aarch64.tar.gz)의 +호스트는 **50ms 제한의 `ppoll` 25회**, 새 호스트는 **제한 시간 없는 대기 +1회**였다. 비교기는 보관된 패키지의 실행물을 직접 추출해 추적하므로 +임시 빌드나 기억에 의존하지 않는다. 이는 시스템 호출·깨움 횟수에 대한 +실측이며 배터리 소모량이나 처리량 비교는 아니다. + +활성 연결이 있으면 각 연결의 절대 기한 중 가장 가까운 값으로 대기 시간을 +정한다. 이미 지난 기한은 즉시 처리한다. 기존 소스 정의 HTTP 검사 `make check-release`는 +1.2초 기한의 느린 수신자·여덟 개 불완전한 헤더 연결을 회수하는 경로를 +포함해 통과했다. [고정 정의 HTTP 회귀 검사](../../prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-server-regression.json)도 +1 MiB 문서 교차 송수신, 느린 수신자의 예산 점유·회수와 4 MiB 프로세스 +피크 상한을 통과했다. 유휴 블로킹 중 `SIGTERM`이 대기를 깨우고 정상 +종료하는 것도 비교 추적에서 확인했다. + +링크된 `text`는 소스 HTTP 43,009→**43,451 B**, 고정 HTTP +25,228→**25,790 B**로 증가했다. 앞선 대기 변경만으로는 각각 64 B가 +늘었지만, 종료 신호 경합까지 막은 최종 비용은 각각 442 B와 562 B다. +실행 코어 5,932 B와 기본 CLI는 그대로다. +[소스 호스트 크기](../../prototypes/connections/results/2026-09-24-signal-safe-idle/source-profile.json)와 +[고정 호스트 크기](../../prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-profile.json)는 +각각 기존 상한을 통과했다. [새 CLI+HTTP 묶음](../../prototypes/connections/results/2026-09-24-signal-safe-idle/pure-document-fixed-http-linux-aarch64.tar.gz)은 +32,023 B이고 [다른 위치에서의 편집·복원](../../prototypes/connections/results/2026-09-24-signal-safe-idle/package.json)을 +통과했다. 패키지에는 시스템 libc가 포함되지 않는다. + +이 결과는 **Linux 호스트의 유휴 폴링 제거**다. `ppoll` 신호 마스크는 +Linux 구현이므로 다른 플랫폼의 대기·종료 경로, 실제 전력, 높은 동시성의 지연·처리량과 큰 앱 표현은 +아직 검증하지 않았다. 서비스가 연결을 처리할 때 저장 효과는 여전히 +동기식이므로 이 변경으로 그것이 비동기가 되지는 않는다. + +재현 명령은 +`python3 prototypes/connections/check_idle_wakes.py --before +prototypes/connections/results/2026-09-24-fixed-http/pure-document-fixed-http-linux-aarch64.tar.gz +--after prototypes/connections/build/pure-connections-fixed`, +`make -C prototypes/connections check-release check-fixed-profile check-fixed-server`다. diff --git a/docs/research/040-document-service-capacity.md b/docs/research/040-document-service-capacity.md new file mode 100644 index 0000000..4016089 --- /dev/null +++ b/docs/research/040-document-service-capacity.md @@ -0,0 +1,48 @@ +# 040 — 한 문서 앱의 다중 객체·동시 요청 용량 확인 + +2026-09-24. 작은 코어와 한 문서 예제만으로 서비스 규모의 메모리 특성을 +판단할 수 없다. 기존 `editor.pure`를 그대로 사용하고, 소스 정의 HTTP 호스트와 +빌드 시 준비한 고정 정의 HTTP 호스트에 동일한 앱 작업을 실행했다. 새 런타임 +타입이나 전용 저장 형식을 추가하지 않았다. + +[재현 검사](../../prototypes/connections/check_service_scale.py)는 각 호스트에 +**8개 클라이언트**를 연결해 **1,024개 키**를 분담한다. 키마다 8,192 B 문서를 +생성하고, 읽고, 마지막 1 B를 조건부 수정하고, 새 버전을 다시 읽는다. +각 응답의 상태·버전·본문을 검증하고, 두 호스트가 만든 모든 최종 문서의 +해시를 비교한다. 호스트 종료 후에는 별도 CLI 프로세스로 네 문서를 읽어 +저장된 내용과 버전을 확인한다. 한 호스트당 **4,096 HTTP 작업**과 2,048개 저장 효과, +2,048개 읽기 효과다. 키별 작업은 순차적이며, 서로 다른 키 사이에 동시성이 +있다. 이전 검사에서 같은 키의 충돌과 느린 수신자는 별도로 검증했다. + +[현재 호스트의 실행 결과](../../prototypes/connections/results/2026-09-24-posix-idle/service-scale.json): + +| 한 호스트 기준 | 고정 정의 | 소스 정의 | +|---|---:|---:| +| 최종 저장 객체 | 1,024 | 1,024 | +| 저장 파일 총량 | 8,413,184 B | 8,413,184 B | +| 서버 유휴 RSS | 1,504 KiB | 1,552 KiB | +| 이 작업의 피크 RSS | **1,680 KiB** | **1,712 KiB** | +| 최대 동시 연결 | 8 | 8 | +| 최대 보유 payload | 49,185 B | 49,168 B | +| 종료 시 보유 payload | 0 B | 0 B | +| 거절·기한 만료 | 0 | 0 | + +두 호스트의 최종 문서 해시는 같다. 1 MiB 서버 payload 예산과 4 MiB +프로세스 피크 RSS 회귀 상한을 통과했다. 응답으로 빌려 보낸 논리적 본문은 +호스트당 25,166,848 B였고, 요청 헤더와 같은 첫 수신에 실린 본문을 입력 +버퍼로 옮긴 바이트는 이 실행에서 고정 정의 326,623 B, 소스 정의 +366,236 B였다. [앞선 동일 작업](../../prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale-restored.json)에서는 +각각 696,751 B와 410,074 B였다. +TCP 분할·스케줄링에 따라 달라지는 값이므로 두 호스트의 우열을 뜻하지 +않지만, 복사 제거가 아직 완전하지 않다는 근거다. 빌린 출력 바이트는 실제 +네트워크·커널 복사량이나 고유 물리 메모리량을 뜻하지 않는다. + +이 검사는 **실제 문서 앱의 다중 객체·8연결 정확성 및 자원 상한**을 보여준다. +Python 클라이언트가 만든 요청의 실행 시간을 서버 처리량으로 취급하지 +않는다. 저장 파일과 페이지 캐시, 파일 디스크립터, OS 네트워크 버퍼, +클라이언트 메모리는 서버 RSS에 포함되지 않는다. 1,024개 파일을 쓰는 +운영·복구 비용, 다수의 1 MiB 동시 응답, 장시간 부하, 여러 Pure 프로세스, +실서비스 동등 비교와 macOS는 아직 검증하지 않았다. 한 바이트 수정에도 +기본 저장소가 문서 전체를 다시 쓰는 비용 역시 남아 있다. + +재현 명령은 `make -C prototypes/connections check-service-scale`이다. diff --git a/docs/research/041-posix-idle-signal-wakeup.md b/docs/research/041-posix-idle-signal-wakeup.md new file mode 100644 index 0000000..5f7ba40 --- /dev/null +++ b/docs/research/041-posix-idle-signal-wakeup.md @@ -0,0 +1,37 @@ +# 041 — 유휴 HTTP 호스트의 POSIX 종료 경로 + +2026-09-24. [039](039-idle-http-wakeups.md)는 50ms 주기 깨움을 없앴지만 +Linux `ppoll`에 의존했다. `poll` 대기 직전에 종료 신호가 도착하면 신호 +플래그만으로는 무기한 대기 경합을 막을 수 없다. 이번 변경은 HTTP 호스트에 +작은 비차단 신호 알림 파이프를 둔다. `SIGINT`·`SIGTERM` 처리기는 종료 +플래그를 설정하고 파이프에 한 바이트를 쓴다. `poll`은 그 읽기 끝을 +클라이언트 소켓과 함께 감시한다. 신호가 `poll` 전후 어느 때 도착해도 +다음 대기가 깨어날 준비를 한다. 대기 전용 스레드나 실행 코어 타입은 없다. + +Apple의 [`poll(2)` 매뉴얼](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/poll.2.html)과 +[`sigaction(2)` 매뉴얼](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sigaction.2.html)은 +이 경계에서 쓰는 API를 문서화하고, 후자는 `write()`를 신호 처리기에서 +사용 가능한 함수로 열거한다. 여기서 검증한 것은 **Linux AArch64 실행**이다. +macOS SDK 컴파일·실행과 macOS 저장·패키지 경로는 아직 검증하지 않았다. + +[시스템 호출 추적](../../prototypes/connections/results/2026-09-24-posix-idle/reproducible-comparison.json)에서 +유휴 1.25초 동안 이전 호스트의 50ms 제한 대기 25회가 현재 호스트의 +무기한 대기 1회로 바뀌었다. `SIGTERM` 이후 프로세스는 정상 종료했다. +[HTTP 회귀 검사](../../prototypes/connections/results/2026-09-24-posix-idle/fixed-server-regression.json)는 +1 MiB 문서·느린 수신자·예산 회수를 통과했다. +[1,024개 문서·8개 클라이언트 검사](../../prototypes/connections/results/2026-09-24-posix-idle/service-scale.json)도 +고정·소스 호스트 각각 4,096 HTTP 작업, 같은 최종 문서 해시, 별도 CLI +복원, 종료 시 보유 0 B를 확인했다. 이 검사는 처리량 비교가 아니다. + +Linux AArch64/GCC 14에서 실행 코어 `text` 5,932 B와 고정 CLI 19,244 B는 +그대로다. [소스 HTTP 호스트](../../prototypes/connections/results/2026-09-24-posix-idle/source-profile.json)의 +`text`는 43,451→43,778 B, [고정 HTTP 호스트](../../prototypes/connections/results/2026-09-24-posix-idle/fixed-profile.json)는 +25,790→26,261 B다. 호스트마다 파이프 파일 디스크립터 2개를 더 쓴다. +[CLI+HTTP 묶음](../../prototypes/connections/results/2026-09-24-posix-idle/pure-document-fixed-http-linux-aarch64.tar.gz)은 +32,346 B이며 [다른 위치에서 실제 편집·복원](../../prototypes/connections/results/2026-09-24-posix-idle/package.json)을 +통과했다. 묶음 크기는 libc나 프로세스 메모리를 포함하지 않는다. + +`ppoll` 제거만으로 macOS 지원이 완성되지는 않는다. 현재 저장소, +컴파일러, 창, TLS 경계와 패키지도 macOS에서 각각 빌드·검증해야 한다. +마이크로 기준도 실행 코어만이 아니라 선택한 앱의 전체 메모리와 파일 +디스크립터를 포함해 판단해야 한다. diff --git a/docs/research/042-micro-native-terminal-editor.md b/docs/research/042-micro-native-terminal-editor.md new file mode 100644 index 0000000..5adb5a2 --- /dev/null +++ b/docs/research/042-micro-native-terminal-editor.md @@ -0,0 +1,44 @@ +# 042 — 같은 문서 앱의 저메모리 네이티브 터미널 편집기 + +2026-09-24. 기존 GTK 문서 창은 실제 편집 경로를 증명했지만 유휴부터 약 +44 MiB RSS를 사용한다. Pure의 작은 실행 구성을 실제 입력 인터페이스에서도 +확인하기 위해 **선택형 POSIX 터미널 편집기** `pure-edit-tui-fixed`를 만들었다. +기존 `editor.pure`를 빌드 시 준비한 실행 계획, `runtime.c`의 작업·예산·수명, +`store.c`의 조건부 저장 효과를 CLI·HTTP와 공유한다. 코어에 터미널 타입, +렌더러 또는 전용 UI 스레드를 넣지 않았다. + +편집기는 터미널의 크기 변경을 처리하고, 화살표로 커서를 이동하며, 한국어 +UTF-8 문서를 입력·저장한다. Ctrl+O로 키를 열고, Ctrl+S로 저장하며, +Ctrl+R은 다시 읽고 Ctrl+Q는 종료한다. 미저장 초안을 버리는 명령은 두 번 +입력해야 한다. 외부 실행기가 먼저 같은 문서를 수정하면 저장의 버전 충돌을 +보여주고 초안은 유지한다. 편집 중에는 갭 버퍼를 사용해 커서 주변 입력을 +파일 전체 이동 없이 처리한다. 저장 직전에 현재 파일 버전을 읽고 변경 +구간만 작성된 `splice` 입력에 담는다. 저장 대상 파일이 크면 기본 저장소가 +파일 전체를 다시 쓰는 비용은 여전히 남는다. + +[실제 PTY 검사](../../prototypes/connections/results/2026-09-24-fixed-tui-final/pty-flow.json)는 +커서 위치의 문서 생성·저장, 두 키 전환, 한국어 코드포인트 이동·입력, 화면 크기 변경, CLI가 먼저 수정한 +문서의 충돌, 1 MiB 문서 끝의 1 B 추가와 새 CLI 프로세스의 복원, 키 명령과 +`SIGTERM` 종료 후 터미널 설정 복구를 통과했다. +[ASan·UBSan 검사](../../prototypes/connections/results/2026-09-24-fixed-tui-final/pty-sanitized.json)도 +같은 흐름을 통과했다. 검사 환경에서는 LeakSanitizer가 ptrace와 함께 실행되지 +않아 누수 탐지만 껐다. 코어의 코드 크기와 기본 CLI는 바뀌지 않았다. + +Linux AArch64/GCC 14 `-O2`의 [선택형 TUI 크기 게이트](../../prototypes/connections/results/2026-09-24-fixed-tui-final/profile.json)에서 +링크된 `text`는 **28,226 B**, 직접 공유 의존성은 libc뿐이었다. +[PTY 검사](../../prototypes/connections/results/2026-09-24-fixed-tui-final/pty-flow.json)의 +1 MiB 편집 피크 프로세스 RSS는 **3,168 KiB**로 4 MiB 회귀 상한 아래다. +이는 한 번의 실행 값이며 터미널 에뮬레이터, +커널 TTY 버퍼, 저장 파일·페이지 캐시를 포함하지 않는다. +[CLI+TUI 묶음](../../prototypes/connections/results/2026-09-24-fixed-tui-final/pure-document-cli-tui-linux-aarch64.tar.gz)은 +**33,654 B**이며 [다른 위치에서 해제·편집](../../prototypes/connections/results/2026-09-24-fixed-tui-final/package.json)을 +검증했다. 시스템 libc는 묶지 않았다. + +이 경로는 **실용적인 저메모리 TUI의 첫 검증**이다. 현재 TUI는 빌드 시 고정한 +문서 정의만 실행하고, 앱 정의 자체를 편집하지 않는다. 코드포인트 단위 +이동은 복합 문자소·터미널별 폭 처리와 동일하지 않다. 마우스, 접근성 트리, +IME 통합, GUI 레이아웃, 문서 목록·검색, macOS 빌드·실행은 아직 검증하지 +않았다. 따라서 GTK GUI와 기능 동등·메모리 동등 비교를 주장하지 않는다. + +재현 명령은 `make -C prototypes/connections check-tui`, 실행은 +`prototypes/connections/build/pure-edit-tui-fixed --store STORE note`다. diff --git a/docs/research/043-micro-package-boundary.md b/docs/research/043-micro-package-boundary.md new file mode 100644 index 0000000..2bba83e --- /dev/null +++ b/docs/research/043-micro-package-boundary.md @@ -0,0 +1,29 @@ +# 043 — 고정 실행기 전체 묶음의 크기 경계 + +문서 편집기는 작은 실행 코어와 선택형 호스트로 분리되어 있지만, 코어만 재면 +배포물이 커져도 마이크로 목표를 지켰다고 착각할 수 있다. 고정 정의 앱의 압축 +배포물 전체에 선택별 상한을 추가했다. `package_native.py --fixed`는 실행물 섹션, +CLI 작업 RSS, 선택형 TUI의 1 MiB 작업 RSS를 확인한 다음 아카이브를 만들고, +상한을 넘으면 파일을 공개하지 않는다. 상한은 +[`fixed-package-budget.json`](../../prototypes/connections/fixed-package-budget.json)에 있다. + +| 선택 | 상한 도입 당시 아카이브 | 회귀 상한 | 이동 후 확인한 작업 | +|---|---:|---:|---| +| CLI | 14,692 B | 16,384 B | 생성·읽기·버전 확인 편집 | +| CLI + HTTP | 32,548 B | 34,816 B | 같은 문서를 loopback HTTP로 편집 | +| CLI + TUI | 33,761 B | 35,840 B | PTY의 UTF-8·1 MiB 문서 저장 | +| CLI + HTTP + TUI | 51,424 B | 53,248 B | 두 호스트를 포함한 위 작업 | + +Linux AArch64, GCC 14 및 현재 libc에서 확인했다. 각 묶음에는 선택한 실행 파일, +매니페스트와 사용 문서가 들어가며 디버그 정보는 빠진다. 크기는 기능별로 중복 +링크된 코드를 **모두 포함한 압축 배포량**이다. 공유 libc, 터미널 에뮬레이터, +커널·파일 캐시, 저장 데이터는 포함하지 않는다. 실행 중 RSS는 별도의 1 MiB 문서 +작업으로 확인하며, TUI는 이전에 기록한 3,168 KiB 피크와 4 MiB 상한을 유지한다. +네 묶음과 각 이동 실행 결과는 +[`results/2026-09-24-micro-package-boundary`](../../prototypes/connections/results/2026-09-24-micro-package-boundary)에 보관했다. + +이 경계에 따라 다음 단계에서 고정 TUI에 `.pure` 파서와 소스 편집을 넣지 않는다. +정의 작성은 기존 소스 CLI와 선택형 창이 맡고, 고정 TUI는 준비된 앱의 사용 경로로 +남긴다. 작성과 실행을 한 바이너리로 합쳐야 할 실제 사용 사례가 나오면, 기능· +배포 크기·작업 RSS를 함께 측정한 뒤 경계를 다시 결정한다. 현재 수치로 일반 앱, +macOS 실행, 보안 피어, 저메모리 GUI의 비용을 주장할 수는 없다. diff --git a/docs/research/044-terminal-viewport-cost.md b/docs/research/044-terminal-viewport-cost.md new file mode 100644 index 0000000..2ee73e0 --- /dev/null +++ b/docs/research/044-terminal-viewport-cost.md @@ -0,0 +1,49 @@ +# 044 — 1 MiB 문서의 화면 갱신 비용과 여러 행 복원 + +고정 정의 터미널 편집기는 1 MiB 문서를 저장했지만, 키를 누를 때마다 커서 위치와 +화면 출력을 계산하면서 문서 처음부터 끝까지 다시 순회했다. 같은 편집기를 실제 +PTY 24×80에서 재생하니 끝에서 한 글자를 입력한 뒤 화면이 완성되기까지 한국어 +1 MiB 문서는 10회 중앙값 37.244 ms가 걸렸다. 여러 행·탭이 들어 있는 정상 +문서를 다시 열지 못하는 문제도 확인했다. 입력 검사가 `wcwidth()`의 줄바꿈·탭 +반환값을 일반 문자와 같이 처리한 것이 원인이었다. + +터미널 호스트 안에 **한 개의 화면 위치 앵커**를 두었다. 바이트 위치·화면 행·열· +터미널 폭을 기억하고, 그 앞을 수정하거나 폭을 바꾸면 무효화한다. 평소 입력과 +화살표 이동은 이 위치부터 보이는 범위만 해석한다. 멀리 이동하면 다시 순회해 +앵커를 만든다. 출력 범위 앞의 연속된 출력 가능 ASCII는 문자마다 `glyph()`을 +호출하지 않고 셀 수를 계산한다. 문서 크기에 비례하는 색인이나 별도 복사본은 +만들지 않았다. 줄바꿈·탭은 텍스트 검사에서 허용하고, 화면 계산은 기존 처리에 +맡긴다. 실행 코어와 `.pure` 계획은 바뀌지 않았다. + +같은 Linux AArch64 장치·CLI·PTY에서 [이전 보관 실행물](../../prototypes/connections/results/2026-09-24-fixed-tui-final/pure-document-cli-tui-linux-aarch64.tar.gz)과 +[현재 실행물](../../prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-tui-linux-aarch64.tar.gz)을 +[`measure_tui_latency.py`](../../prototypes/connections/measure_tui_latency.py)로 비교했다. +각 문서를 열고 끝에 `Z`를 10회 순서대로 입력했다. 값은 **입력 전송부터 해당 +화면의 마지막 커서 표시까지**의 중앙값이며 문서 열기·저장·처리량은 포함하지 않는다. +두 실행물의 각 회차 PTY 화면 바이트 SHA-256은 모두 같았다. + +| 실제 문서 입력 | 이전 화면 갱신 중앙값 | 현재 | 비율 | +|---|---:|---:|---:| +| 256 B ASCII | 0.028 ms | 0.030 ms | 유사 | +| 1 MiB ASCII | 15.678 ms | 0.070 ms | 224× | +| 약 1 MiB 한국어 UTF-8 | 37.244 ms | 0.232 ms | 160× | + +[원자료 이전](../../prototypes/connections/results/2026-09-24-tui-viewport/latency-baseline.json), +[원자료 현재](../../prototypes/connections/results/2026-09-24-tui-viewport/latency-current.json). +이 수치는 이 장치·입력 위치·문서 구성에 한정된다. 처음 열기, 앵커보다 앞선 +편집, 화면 폭 변경, 먼 위치로 이동할 때는 전체 순회가 다시 필요할 수 있다. +완전한 문자소 단위 커서 이동·IME·다른 터미널 구현도 검증하지 않았다. + +[실제 PTY 검사](../../prototypes/connections/results/2026-09-24-tui-viewport/pty-flow.json)는 +여러 행·탭 문서의 CLI 생성→TUI 편집→재열기, 60행 위쪽 이동, 1 MiB 한국어 +문서의 앵커 앞 편집과 저장·복원을 확인한다. [ASan·UBSan 검사](../../prototypes/connections/results/2026-09-24-tui-viewport/pty-sanitized.json)도 +같은 흐름을 통과했다. LeakSanitizer는 실행 환경의 ptrace 제약으로 껐다. + +선택형 TUI `text`는 [28,226 B에서 28,762 B](../../prototypes/connections/results/2026-09-24-tui-viewport/profile.json)로 +536 B 증가했고 libc 외 직접 공유 의존성은 없다. 실제 1 MiB ASCII 편집의 +이번 프로세스 피크 RSS는 3,168 KiB였으며 4 MiB 회귀 상한을 통과했다. +이 RSS를 앞선 단일 실행 값보다 줄었다는 근거로 사용하지 않는다. CLI+TUI +[이동 묶음](../../prototypes/connections/results/2026-09-24-tui-viewport/package.json)은 +34,216 B로 선택 구성의 35,840 B 상한 안이다. HTTP까지 선택한 +[전체 묶음](../../prototypes/connections/results/2026-09-24-tui-viewport/package-http-tui.json)은 +51,844 B로 해당 53,248 B 상한을 통과했다. diff --git a/docs/research/045-owner-thread-async-effects.md b/docs/research/045-owner-thread-async-effects.md new file mode 100644 index 0000000..f866f5f --- /dev/null +++ b/docs/research/045-owner-thread-async-effects.md @@ -0,0 +1,51 @@ +# 045 — 코어가 효과의 완료를 기다리지 않는 선택형 경로 + +현재 문서 서비스의 `pure_runtime_step()`은 저장소 호스트 콜백이 반환할 때까지 +실행 영역을 붙잡는다. 큰 저장 작업이나 원격 효과가 생기면 요청 슬롯의 수명· +본문 예산을 유지하면서 다른 작업을 진행할 길이 필요하다. 이 단계에서는 +**선택형 비동기 효과 계약**을 실행 코어에 붙였다. 기본 CLI·HTTP·TUI는 기존 +동기 경로를 그대로 링크한다. + +`PURE_ASYNC_EFFECTS` 빌드에서 `pure_runtime_take()`는 준비된 READ·WRITE·SPLICE +효과와 세대가 있는 티켓을 호스트에 넘긴다. 효과에는 저장 키, 조건부 버전, +선택된 본문, 읽기 범위와 가용 예산이 포함된다. 호스트가 파일·네트워크 작업을 +마친 후 **소유 스레드에서** `pure_runtime_complete()`를 호출한다. 완료 순서는 +자유지만 저장 효과의 버전 조건은 여전히 해당 호스트에서 원자적으로 검사한다. +준비 중 범위가 유효하지 않은 입력은 호스트에 전달하기 전에 완료된다. + +호출자가 진행 중 티켓을 놓아도 입력은 작업이 완료될 때까지 남는다. 읽기 본문을 +다른 영역으로 전달하면 소비자가 완료·해제될 때까지 원본 매핑을 유지한다. +동시에 여러 READ가 완료되면 **완료 시점의 남은 예산**으로 다시 검사한다. +예산을 넘은 결과는 소비자에게 공개하지 않고 호스트 자원을 회수한다. 호스트가 +잘못된 소유 자원을 넘긴 완료 호출은 거절하고 자원과 작업을 그대로 둔다. + +[실제 저장소 검사](../../prototypes/connections/test-async.c)는 대기 중인 파일 쓰기의 +티켓을 먼저 놓고, 다른 문서를 먼저 저장한 뒤 첫 작업을 완료한다. 종료 요청 +중에는 재시작을 막고, 마지막 완료 후에만 허용한다. 저장된 값의 READ→다른 +영역 WRITE도 같은 매핑을 복사 없이 전달한다. 추가로 기존 +[`editor.pure`](../../prototypes/connections/examples/editor.pure)의 SPLICE 생성·추가· +낡은 버전 충돌·GET을 이 경로로 실행한다. 이 검사는 실제 파일과 한 작업자 +스레드를 사용하지만 HTTP 처리량을 측정하는 실험은 아니다. + +| 선택한 범위 | `text` | 영역 기록 | 작업 슬롯 | 효과 값 | +|---|---:|---:|---:|---:| +| 기본 실행 코어 | 5,932 B | 200 B | 224 B | 없음 | +| 비동기 완료 선택 코어 | 7,312 B | 200 B | 224 B | 호출자 저장 공간 128 B | + +[기본 코어 원자료](../../prototypes/connections/results/2026-09-24-async-effects/default-core.json), +[선택 코어 원자료](../../prototypes/connections/results/2026-09-24-async-effects/async-core.json). +두 아카이브 모두 파서·저장소·소켓·스레드 라이브러리를 포함하지 않는다. +선택 코어 증가량은 1,380 B이며 인스턴스·슬롯 반복 메모리는 증가하지 않았다. +실제 작업자 스택, 저장소, 호스트의 완료 큐는 이 수치에 **포함되지 않는다**. +[`make check-async`](../../prototypes/connections/Makefile)와 +[ASan·UBSan 실행](../../prototypes/connections/results/2026-09-24-async-effects/sanitized.json), +기존 `make check-release`가 통과했다. 검사 환경의 ptrace 제약 때문에 +LeakSanitizer는 껐다. + +이것은 **비동기 호스트를 위한 수명·완료 원시 기능**이다. 기본 HTTP·TLS 피어는 +동기 효과를 호출하며, 선택형 HTTP 호스트의 실제 연결은 [046](046-optional-async-document-http.md)에 기록했다. 시작한 효과의 취소도 호스트가 I/O 중단을 확인한 뒤 +완료로 돌려줘야 하며, 코어가 강제로 외부 효과를 되감지 않는다. 같은 저장 키의 +순서가 필요한 호스트는 키별 큐나 조건부 버전으로 이를 보장해야 한다. 실제 +HTTP 호스트의 완료 신호·연결 종료·기한·프로세스 종료와 **전체 앱** 메모리는 +[046](046-optional-async-document-http.md)에서 확인했다. 인증 피어와 동등 서버의 +지연 비교는 남아 있다. diff --git a/docs/research/046-optional-async-document-http.md b/docs/research/046-optional-async-document-http.md new file mode 100644 index 0000000..dd3c0a5 --- /dev/null +++ b/docs/research/046-optional-async-document-http.md @@ -0,0 +1,55 @@ +# 046 — 실제 HTTP 호스트에서 선택형 비동기 효과 실행 + +기존 HTTP 문서 호스트는 파일 저장 효과를 이벤트 루프 안에서 실행했다. 저장소의 +`fsync`가 지연되면 이미 연결된 다른 클라이언트의 헤더·기한·종료도 처리하지 +못한다. [045](045-owner-thread-async-effects.md)의 선택형 완료 계약을 **기존 +`editor.pure`와 기존 HTTP 호스트**에 연결했다. 기본 CLI·HTTP는 그대로 단일 +스레드와 동기 코어를 사용한다. + +`make async-server`는 파일 효과를 하나의 작업자에 넘긴다. 이벤트 루프는 +요청 입력·응답 출력·기한·시그널과 `pure_runtime_complete()`를 소유한다. +저장소 코드는 현재 프로세스 안에서 단일 스레드 사용을 전제로 하고 +임시 파일 번호를 공유하므로 작업자를 여러 개 띄우지 않았다. 최대 8개 작업 +슬롯과 작업자 스택 128 KiB가 이 선택 호스트의 정적 상한이다. 완료 신호에는 +이미 사용하던 깨움 파이프를 재사용한다. 연결이 기한 만료나 종료로 사라져도 +작업자가 빌린 입력은 완료할 때까지 보존하고, 완료를 소유 스레드에서 회수한다. + +[결정적 중단 검사](../../prototypes/connections/results/2026-09-24-async-http/gated.json)는 +테스트 전용 저장소 훅으로 파일 동기화 직전 작업자를 멈춘다. 그 동안 두 번째 +HTTP 연결의 잘못된 요청은 400으로 응답하고, 첫 연결은 기한에 닫힌다. 작업자를 +풀면 저장 완료와 본문 회수가 확인되며, 별도 시나리오에서 종료 신호가 진행 중인 +작업의 완료를 기다린다. 작업자가 멈춰 있는 동안 두 번째 **저장 효과**까지 +진행된다는 뜻은 아니다. 한 작업자가 효과를 순서대로 처리한다. 테스트 훅은 +배포 실행물에 링크되지 않는다. 같은 검사의 [ASan·UBSan 실행](../../prototypes/connections/results/2026-09-24-async-http/sanitized.json)도 통과했다. + +기존 [HTTP 정확성 검사](../../prototypes/connections/results/2026-09-24-async-http/full-check.json)를 +이 선택 호스트에 다시 적용했다. 이어서 +[서비스 작업 원자료](../../prototypes/connections/results/2026-09-24-async-http/service-scale-memory.json)에서 +고정 정의·소스 정의·선택형 비동기 소스 정의 호스트를 각각 실제 문서 +1,024개, 요청 4,096개, 동시 클라이언트 8개로 실행했다. 세 실행의 최종 문서 +해시와 저장 버전이 일치하고, 모두 종료 시 보유 payload 0 B·기한 만료 0건이다. + +| Linux AArch64 / GCC 14, 이 작업 | 기존 소스 HTTP | 선택형 비동기 소스 HTTP | +|---|---:|---:| +| 실행물 `text` | 43,778 B | 47,897 B | +| 실행물 `data` | 1,068 B | 1,180 B | +| 서버 구조체 | 41,528 B | 43,224 B | +| 유휴 프로세스 RSS | 1,536 KiB | 1,536 KiB | +| 이 작업의 피크 RSS | 1,712 KiB | 1,712 KiB | +| 유휴 가상주소 공간 | 2,464 KiB | 2,656 KiB | +| 이 작업의 피크 가상주소 공간 | 2,528 KiB | 2,736 KiB | +| 프로세스 스레드 | 1 | 2 | + +RSS의 동일한 숫자는 이 표본에서의 관측값이며 비동기 호스트의 일반적인 +메모리 우위를 뜻하지 않는다. 작업자 스택과 저장소 파일·페이지 캐시·커널 +소켓 버퍼는 실행물의 코드 섹션에 포함되지 않는다. [크기 상한 검사](../../prototypes/connections/results/2026-09-24-async-http/profile.json)는 +선택 호스트 `text` 48 KiB 이내와 직접 동적 의존성을 확인한다. 기본 HTTP의 +`text`는 여전히 43,778 B이고 `make check-release`가 통과했다. + +이 단계는 저장소의 지연이 **이벤트 루프 전체를 멈추지 않는 것**과 그 비용을 +입증한다. 단일 작업자가 모든 저장 효과를 직렬화하므로 효과 처리량이나 +RPC 지연이 빨라졌다는 주장은 하지 않는다. 기한이 지난 뒤 이미 접수된 쓰기는 +취소되지 않고 커밋될 수 있다. 이는 결과를 받지 못한 클라이언트가 같은 +조건부 버전으로 무조건 재시도하면 안 된다는 뜻이다. 이후 과제는 작업자 +단일성에 묶인 저장소 임시 이름과 직렬화를 재설계하고, 실제 인증 피어까지 +같은 완료·기한 모델을 적용한 뒤 동등한 서버와 지연 분포를 비교하는 것이다. diff --git a/docs/research/047-bounded-native-document-browser.md b/docs/research/047-bounded-native-document-browser.md new file mode 100644 index 0000000..ee6b45e --- /dev/null +++ b/docs/research/047-bounded-native-document-browser.md @@ -0,0 +1,46 @@ +# 047 — 저메모리 네이티브 편집기에서 문서 찾기 + +고정 정의 TUI는 키를 알고 있으면 다른 문서를 열 수 있었지만, 저장된 키를 +찾는 방법이 없었다. 그래서 실제 앱에서 이미 만든 문서를 다시 선택하려면 +외부 파일 목록을 봐야 했다. 편집기의 **Ctrl+L 문서 목록**을 저장소 호스트에 +추가했다. 화살표·Home·End로 이동하고 Enter로 연다. Ctrl+O는 새 키 입력을 +계속 담당한다. 미저장 초안이 있으면 목록으로 전환하지 않는다. + +목록은 저장소 디렉터리의 `obj-KEY` 이름을 한 번 읽어 정렬한 스냅샷이다. +한 키에 65 B를 사용하고 8,192개(최대 532,480 B)에서 멈춘다. 한도를 넘거나 +디렉터리 읽기에 실패하면 불완전한 목록을 보여주지 않고 기존 문서를 유지한다. +목록을 닫으면 배열을 해제한다. 삭제된 키를 선택해도 새 빈 문서로 조용히 +전환하지 않고 오류를 보여준다. 실제 본문을 열 때는 기존 런타임의 검증된 +READ 경로를 통과한다. 코어·저장 형식·작성 언어·고정 실행 계획은 바뀌지 않았다. + +[실제 PTY 검사](../../prototypes/connections/results/2026-09-24-terminal-document-list/tui.json)는 +문서 전환, 초안 보호, 목록 이후 삭제된 키, 1 MiB 편집을 확인한다. 별도 +저장소에 **1,024개 실제 문서**를 CLI로 만들고 TUI의 정렬된 목록에서 마지막 +키를 열었다. 같은 경로의 [ASan·UBSan 실행](../../prototypes/connections/results/2026-09-24-terminal-document-list/sanitized.json)도 +통과했다. LeakSanitizer는 검사 환경의 ptrace 제약으로 껐다. + +| Linux AArch64 / GCC 14 | 앞선 TUI | 문서 목록 포함 TUI | +|---|---:|---:| +| 링크된 `text` | 28,762 B | 31,303 B | +| 링크된 `data` | 988 B | 1,036 B | +| 1 MiB 문서 편집 피크 RSS | 3,168 KiB | 2,832 KiB | +| 1,024키 목록 피크 RSS | 미측정 | 1,856 KiB | +| CLI+TUI 압축 묶음 | 34,216 B | 35,642 B | +| CLI+HTTP+TUI 압축 묶음 | 51,844 B | 53,385 B | + +[TUI 코드 섹션 상한](../../prototypes/connections/results/2026-09-24-terminal-document-list/profile.json)은 +기능을 확인한 뒤 29,696→32,768 B로 검토해 높였다. 결합 묶음의 상한도 +53,248→54,272 B로 높였고 CLI 전용 16,384 B 상한은 유지했다. 이번 +[CLI 전용 묶음](../../prototypes/connections/results/2026-09-24-terminal-document-list/cli-check.json)은 +14,706 B다. 새 [CLI+TUI](../../prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui-check.json)와 +[CLI+HTTP+TUI](../../prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui-check.json)는 +모두 다른 위치에 풀어 실제 명령·PTY·HTTP를 실행했다. 이 압축 크기는 libc나 +실행 메모리가 아니다. RSS 값은 서로 다른 한 번의 실행에서 관찰한 값이며 +목록 추가로 1 MiB 편집 메모리가 줄었다는 증거가 아니다. 두 경우 모두 기존 +4 MiB 회귀 상한 안에 있다. + +목록은 편집 중 스토어 변화를 자동 추적하지 않는다. Ctrl+L로 다시 읽을 수 있고, +키를 여는 순간 본문과 버전을 다시 검증한다. 8,192개를 넘는 대형 저장소에는 +페이지 탐색이나 검색 색인이 필요하다. 이 선택 UI의 목록은 그런 범용 색인으로 +주장하지 않는다. `.pure` 정의 작성은 기존 별도 작성 호스트의 책임이므로 +고정 실행 TUI에 파서와 컴파일러를 링크하지 않았다. diff --git a/docs/research/048-immutable-read-write-overlap.md b/docs/research/048-immutable-read-write-overlap.md new file mode 100644 index 0000000..469ef0d --- /dev/null +++ b/docs/research/048-immutable-read-write-overlap.md @@ -0,0 +1,56 @@ +# 048 — 저장 쓰기 중에도 기존 문서를 읽는 선택형 호스트 + +[046](046-optional-async-document-http.md)의 선택형 HTTP 호스트는 파일 효과를 +작업자 하나에 넘겨 이벤트 루프를 보호했다. 다만 쓰기가 `fsync`에서 멈추면 다른 +문서 읽기도 그 작업자 뒤에서 대기했다. 저장소 코드를 확인하면 읽기는 파일 버전을 +열어 불변 `mmap`으로 유지하고, 쓰기만 임시 파일 번호와 `.writer.lock`을 사용한다. +따라서 **읽기 작업자 하나와 쓰기 작업자 하나**를 두되, 기존의 쓰기 직렬화는 +유지하는 경계가 가능하다. + +선택형 `PURE_HTTP_ASYNC` 호스트에서 두 작업자는 입력 종류에 따라 공유된 +최대 8개 작업 슬롯을 탐색한다. 각 작업자의 스택 크기는 128 KiB로 설정했다. +HTTP 연결과 `pure_runtime_complete()`는 기존처럼 소유 스레드에서 처리한다. +기본 코어·CLI·HTTP에는 이 작업자들이 링크되지 않는다. 새 앱 모델이나 공용 +코어 연산은 추가하지 않았다. + +[결정적 HTTP 검사](../../prototypes/connections/results/2026-09-24-async-read-write/gated.json)는 +실제 CLI로 `gated=old` 버전 1을 만든 뒤, HTTP 수정 `oldafter`를 파일 동기화 +직전에 멈춘다. 쓰기를 풀기 **전에** 같은 문서의 GET이 `old`, ETag `"1"`을 +반환한다. 이때 잘못된 다른 HTTP 요청도 400으로 답하고, 쓰기를 기다린 연결은 +기한에 닫힌다. 쓰기를 풀면 GET이 `oldafter`, ETag `"2"`를 반환한다. 종료 중인 +별도 실행에서도 진행 중인 쓰기 완료를 기다리고 종료 시 보유 본문은 0 B다. +이 검사는 한 작업자로 모든 파일 효과를 실행하던 호스트에서는 통과하지 않는다. +[ASan·UBSan 실행](../../prototypes/connections/results/2026-09-24-async-read-write/sanitized.json)과 +[기존 HTTP 정확성 전체 검사](../../prototypes/connections/results/2026-09-24-async-read-write/full-check.json)도 +통과했다. + +[서비스 작업 원자료](../../prototypes/connections/results/2026-09-24-async-read-write/service-scale.json)는 +고정 정의·소스 정의·선택형 비동기 소스 정의로 각각 8 KiB 문서 1,024개를 +만들고 8개 클라이언트가 4,096개 요청을 처리한 결과다. 세 호스트의 최종 문서 +해시와 저장 파일 수가 같고, 모두 보유 본문 0 B로 종료했다. 이 실행의 비용은 +다음과 같다. + +| Linux AArch64 / GCC 14 | 기본 소스 HTTP | 선택형 읽기+쓰기 HTTP | +|---|---:|---:| +| 링크된 `text` | 43,778 B | 48,401 B | +| 서버 구조체 | 41,528 B | 43,320 B | +| 유휴 RSS | 1,520 KiB | 1,552 KiB | +| 피크 RSS | 1,696 KiB | 1,728 KiB | +| 유휴 가상주소 공간 | 2,464 KiB | 2,848 KiB | +| 피크 가상주소 공간 | 2,528 KiB | 2,928 KiB | +| 프로세스 스레드 | 1 | 3 | + +선택형 호스트는 기본 호스트보다 코드 4,623 B, 구조체 1,792 B, 이 작업의 +피크 RSS 32 KiB가 더 든다. [이전 단일 작업자 기록](046-optional-async-document-http.md)과 +비교하면 `text` 504 B와 구조체 96 B가 늘었다. 두 128 KiB 스택은 코드 섹션 +크기에 포함되지 않는다. [크기 상한](../../prototypes/connections/results/2026-09-24-async-read-write/profile.json)은 +기존 선택형 48 KiB 상한을 올리지 않고 통과했다. `make check-release`에서도 +기본 코어 `text` 5,932 B, 기본 소스 HTTP `text` 43,778 B를 유지했다. + +이는 **느린 쓰기와 기존 버전 읽기의 독립성 및 스냅샷 정확성**에 대한 결과다. +읽기끼리, 쓰기끼리는 각각 직렬이며, 기한이 지난 쓰기도 취소되지 않고 커밋될 +수 있다. 매핑되지 않은 파일 캐시와 커널 소켓 버퍼는 프로세스 RSS 표에 포함되지 +않는다. 이 작업은 처리량이나 +Kitex급 RPC 성능을 측정하지 않는다. 일반 네트워크·UI 효과까지 같은 완료 모델에 +연결하려면 저장소형 `READ/WRITE/SPLICE` 계약에서 벗어나는 별도 설계와 앱 검증이 +필요하다. diff --git a/docs/research/049-idle-app-work-slots.md b/docs/research/049-idle-app-work-slots.md new file mode 100644 index 0000000..27c22d7 --- /dev/null +++ b/docs/research/049-idle-app-work-slots.md @@ -0,0 +1,101 @@ +# 049 — 앱이 쉴 때 작업 슬롯도 비운다 + +Pure 실행 영역은 호출자가 제공한 작업 슬롯을 보유한다. 기존 네이티브 작성 앱은 +입력 한 건에는 슬롯 하나, 저장·복원의 `READ → WRITE` 전달에는 둘이 필요해서 +**모든 유휴 인스턴스에 `PureWork[2]` 448 B를 계속 예약**했다. 이전에 계산 정의를 +공유해 scalar 인스턴스를 280 B까지 줄인 [028](028-shared-fixed-program.md) 이후에는 +이 448 B가 독립 앱의 반복 비용에서 더 큰 부분이었다. + +코어에 `pure_runtime_bind()`를 추가했다. 영역은 작업 슬롯 없이 시작할 수 있고, +호스트가 진행 중인 호출에 필요한 슬롯을 제공한다. 네이티브 앱은 입력에 1개, +저장·복원에 2개를 **소유 스레드의 호출 스택에만** 두고, 결과·대여가 모두 회수된 +뒤 분리한다. 슬롯을 바꿔도 영역의 예산·누적 계수·티켓 세대는 유지된다. 진행 중 +작업, 완료를 보유한 티켓, 다른 영역이 고정한 본문이 있으면 분리를 거절한다. +[코어의 실제 대여·세대 검사](../../prototypes/connections/test-runtime.c)는 수신 영역이 +원본 본문을 소비할 때까지 분리가 실패하고, 회수 뒤 성공하며, 오래된 티켓이 새 +슬롯을 가리키지 못함을 확인한다. 선택형 장시간 비동기 HTTP 호스트는 계속 고정 +슬롯을 사용한다. 이 변경은 동기 앱의 호출 수명에만 적용한다. + +[전체 앱 메모리 A/B 원자료](../../prototypes/workbench/results/2026-09-24-idle-work-binding/app-memory.json)는 +같은 준비된 `routed-layout.pure`와 같은 scalar 아카이브로 각각 **독립 앱 +1,000개**를 초기화한다. 각 앱이 코어를 통해 입력을 처리하고 서로 다른 문서를 +저장하며, 다섯 앱을 다시 복원한다. 실행마다 접수와 회수가 3,010건씩 일치하고 +저장 파일을 확인한다. 이전 앱 소스는 [동결된 작성 호스트 +소스](../../prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c)와 +[헤더](../../prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.h)를 +그대로 사용했다. 비교 하네스는 현재 소스에서 두 실행물을 다시 빌드한다. +Linux에서 Zig 0.15.2가 있다면 저장소 루트에서 +`python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure` +후 `(cd prototypes/workbench && python3 native/check_app_memory.py)`로 재현한다. + +| Linux AArch64 / GCC 14, 같은 실제 앱 1,000개 | 이전 고정 슬롯 | 호출 중 대여 | +|---|---:|---:| +| 앱 컨트롤러 / 인스턴스 | 928 B | 480 B | +| scalar 상태 / 인스턴스 | 280 B | 280 B | +| 초기화 후 RSS 증가, 3회 중앙값 | 1,264 KiB | 832 KiB | +| 저장 후 RSS 증가, 3회 중앙값 | 1,328 KiB | 896 KiB | +| A/B 하네스 실행물 `text` | 35,698 B | 35,962 B | + +컨트롤러에서는 인스턴스당 448 B, 실제 프로세스 RSS 증가량에서는 이 작업의 +**432 KiB**가 줄었다. A/B 둘 다 변경 후의 같은 코어를 링크했고, 앱 호스트만 +다른 실행물의 `text` 차이는 264 B다. 공통 코어의 +[최신 크기 검사](../../prototypes/connections/results/2026-09-24-idle-work-binding/core.json)는 +`text` 5,932→6,076 B, 영역 200 B, 작업 슬롯 224 B다. 코어의 기존 +6,144 B 상한은 올리지 않았다. 코드 증분을 숨기지 않으면서 유휴 앱 1,000개의 +반복 예약을 줄인 결과다. + +메모리가 사라진 것은 아니다. [GCC 정적 스택 추정](../../prototypes/workbench/results/2026-09-24-idle-work-binding/stack-baseline.su)을 +[새 실행](../../prototypes/workbench/results/2026-09-24-idle-work-binding/stack-elastic.su)과 +비교하면 `pure_native_app_action`은 208→432 B, 저장·복원 공통 `transfer`는 +288→736 B다. 각각 224 B와 448 B를 **호출 중에** 스택으로 옮겼다. 이 수치는 +함수 자신의 정적 스택 추정이며 호출 체인 전체의 피크가 아니다. 한 소유 스레드가 +동기 호출을 진행하는 현재 앱 계약에 맞는 선택이고, 작업을 반환 뒤에도 보유하는 +호스트에는 이 배치를 적용하지 않는다. + +## 선택한 배포 묶음의 비용 + +변경 후 기존 `--strip-debug` 방식의 CLI+TUI 압축 묶음은 +[35,847 B](../../prototypes/connections/results/2026-09-24-idle-work-binding/precompact-budget-failure.txt)가 되어 +35,840 B 상한을 7 B 넘었다. 상한을 올리는 대신 고정 실행 묶음에서는 실행에 +필요 없는 ELF 심볼도 제거하도록 했다. 디버깅용 빌드와 소스 작성 묶음은 그대로 +두며, `text` 코드 섹션이나 프로세스 RSS가 작아졌다고 주장하지 않는다. +패키지 매니페스트에는 실행마다 달라지는 RSS 표본을 넣지 않고, 크기 상한과 +실행물 식별자만 남겼다. 실제 RSS 표본은 별도 검사 JSON에 보관한다. +동일한 의존성 검사를 거쳐 새 위치에서 실제 실행한 결과는 다음과 같다. + +| 고정 정의 선택 | 압축 묶음 | 회귀 상한 | 이동 후 동작 | +|---|---:|---:|---| +| [CLI](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli.tar.gz) | 12,987 B | 16,384 B | [검증](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli-check.json) | +| [CLI+HTTP](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli-http.tar.gz) | 28,719 B | 34,816 B | [검증](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-check.json) | +| [CLI+TUI](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui.tar.gz) | 31,726 B | 35,840 B | [검증](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui-check.json) | +| [CLI+HTTP+TUI](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui.tar.gz) | 47,419 B | 54,272 B | [검증](../../prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui-check.json) | + +소스 `.pure`를 묶어 실행 시 정의를 읽는 [작성 CLI 묶음](../../prototypes/connections/results/2026-09-24-idle-work-binding/source-cli.tar.gz)은 +22,595 B이고 [이동 후 편집·복원](../../prototypes/connections/results/2026-09-24-idle-work-binding/source-cli-check.json)을 +확인했다. 고정 정의 묶음과 기능·크기를 동등한 것으로 취급하지 않는다. +앞선 [047의 CLI+TUI 묶음](047-bounded-native-document-browser.md)은 35,642 B였다. +현재 묶음은 코어 기능이 늘어난 뒤에도 3,916 B 작다. 이 차이는 패키지에서 +사용하지 않는 심볼을 제거한 효과다. 작성 CLI와 고정 정의 네 선택을 각각 다시 빌드한 +[아카이브 바이트 해시](../../prototypes/connections/results/2026-09-24-idle-work-binding/package-reproducibility.json)도 +일치했다. 프로그램 실행 메모리 절감은 위 1,000개 앱 A/B에서 별도로 확인했다. + +[기본 릴리스 검사](../../prototypes/connections/results/2026-09-24-idle-work-binding/release.log), +[선택형 비동기 코어](../../prototypes/connections/results/2026-09-24-idle-work-binding/async-core.json), +[실제 HTTP 쓰기 중 읽기](../../prototypes/connections/results/2026-09-24-idle-work-binding/async-gated.json), +[1,024개 문서 서비스](../../prototypes/connections/results/2026-09-24-idle-work-binding/service-scale.json), +[PTY 문서 편집기](../../prototypes/connections/results/2026-09-24-idle-work-binding/tui.json), +[인증 문서 피어](../../prototypes/connections/results/2026-09-24-idle-work-binding/document-peer.json)가 +통과했다. 작성 앱의 [독립 상태·충돌·정의 교체 +검사](../../prototypes/workbench/results/2026-09-24-idle-work-binding/native-unit.json), +[C 코드 ASan·UBSan](../../prototypes/workbench/results/2026-09-24-idle-work-binding/native-sanitized.json), +[인증 scalar 피어](../../prototypes/workbench/results/2026-09-24-idle-work-binding/scalar-peer.json)도 +통과했다. 공통 코어의 [ASan·UBSan 검사](../../prototypes/connections/results/2026-09-24-idle-work-binding/runtime-sanitized.json)는 +대여 중인 슬롯과 오래된 티켓을 포함한다. + +이 A/B는 같은 프로세스의 1,000개 **동기 네이티브 앱**에 대한 메모리 결과다. +실제 네트워크 서비스 1,000명 처리량, 불신 코드 격리, macOS RSS를 증명하지 않는다. +Zig 0.15.2 실행 파일이 현재 환경에 없어서 scalar 아카이브를 다시 빌드하지는 +못했다. 준비된 정의를 현재 소스로 재생성해 기존 아카이브 프로필과 일치함을 +검사했고 A/B 모두 동일한 아카이브를 링크했다. 코어·C 호스트는 현재 소스에서 +재빌드했다. 다음 연구에서는 유휴 영역 자체의 200 B와 일반 I/O 계약을 함께 +검토해야 하며, 슬롯 절감을 이유로 수명·예산의 귀속을 약화해서는 안 된다. diff --git a/docs/research/050-bounded-document-prefix-find.md b/docs/research/050-bounded-document-prefix-find.md new file mode 100644 index 0000000..a732be3 --- /dev/null +++ b/docs/research/050-bounded-document-prefix-find.md @@ -0,0 +1,33 @@ +# 050 — 문서 1,024개에서 추가 인덱스 없이 이름 찾기 + +선택형 네이티브 TUI는 저장소에서 문서 이름을 읽어 정렬하고 8,192개까지만 보유한다. +기존에는 화살표와 처음·끝 이동만 있어, 1,024개 실제 문서에서 중간 이름을 찾는 데 +목록을 길게 이동해야 했다. 목록을 연 상태에서 이름 접두어를 입력하면 정렬된 기존 +배열을 이진 탐색해 첫 일치 문서로 이동한다. Backspace는 접두어를 줄이고, +일치하지 않는 이름에는 Enter를 눌러도 문서를 열지 않는다. 화살표를 누르면 +접두어를 지우고 기존 목록 탐색으로 돌아간다. 검색용 배열·인덱스·상주 버퍼는 +만들지 않고 기존 65 B 키 입력 공간을 재사용했다. + +[실제 PTY 검사](../../prototypes/connections/results/2026-09-24-prefix-find/tui.json)는 +CLI로 문서 1,024개를 만든 뒤 `doc0919`를 찾아 열고, 없는 `zz`를 열지 않으며, +접두어를 지운 후 `doc1023`을 여는 작업을 검증한다. 저장·충돌·UTF-8·1 MiB 편집과 +터미널 복구 검사도 통과했다. [선택형 TUI 릴리스 프로필](../../prototypes/connections/results/2026-09-24-prefix-find/tui-profile.json)은 +Linux AArch64 / GCC 14에서 링크된 `text` **31,463→32,175 B(+712 B)**, +data 1,036 B, BSS 24 B로 기존 32,768 B 상한 안에 있다. 실행 코어에는 +이 검색 코드가 없으며 직전 [코어 검사](../../prototypes/connections/results/2026-09-24-idle-work-binding/core.json)의 +6,076 B 상한도 그대로다. + +[CLI+TUI 묶음](../../prototypes/connections/results/2026-09-24-prefix-find/cli-tui.tar.gz)은 +31,726→**32,233 B(+507 B)**로 기존 35,840 B 상한 안에 있다. [다른 위치에서 +풀어 실행한 검사](../../prototypes/connections/results/2026-09-24-prefix-find/package-check.json)는 +고정 정의 CLI의 조건부 편집과 선택형 TUI의 실제 작업을 확인했다. +이번 1 MiB 문서 PTY 실행의 피크 RSS는 3,200 KiB, 1,024개 목록에서는 +1,904 KiB였다. 이전 1 MiB 표본 2,832 KiB보다 높으므로 RAM이 줄었다고 +주장하지 않는다. 모두 기존 4,096 KiB 회귀 상한 안의 단일 호스트 표본이다. + +macOS의 엄격한 X/Open 네임스페이스는 TUI가 사용하는 `O_NOFOLLOW`를 숨길 수 있다. +[Apple의 fcntl.h](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/sys/fcntl.h)를 +근거로 해당 TUI 소스에서만 Darwin 확장을 노출하고, `check-portable`에서 TUI +실행물까지 컴파일하도록 했다. Linux AArch64에서는 C 검사·실제 PTY가 통과했다. +Apple Silicon SDK 컴파일·실행은 아직 확인하지 않았으며, 이름 접두어 이동은 +문서 본문 검색이 아니다. diff --git a/docs/research/051-optional-runtime-metrics.md b/docs/research/051-optional-runtime-metrics.md new file mode 100644 index 0000000..6e8ae1c --- /dev/null +++ b/docs/research/051-optional-runtime-metrics.md @@ -0,0 +1,59 @@ +# 051 — 실행 영역의 계측을 선택형으로 분리 + +마이크로 런타임의 영역마다 최고 사용량과 누적 작업 수를 항상 저장할 필요는 없다. +`retained`, `active`, 예산, 티켓 세대는 안전한 실행에 남기고, 최고치·누적 +접수·읽기·쓰기·충돌·회수·대여 출력 바이트는 `PURE_RUNTIME_METRICS` 빌드에서만 +보관한다. 기본 CLI·TUI·네이티브 앱·TLS 피어는 작은 구조체를 사용한다. 통계 +응답을 제공하는 HTTP 호스트와 계수 검증 테스트는 계측 빌드로 유지했다. +서로 다른 구조체 배치의 오브젝트가 섞이면 초기화 심볼 +`pure_runtime_init`/`pure_runtime_init_metrics`의 차이로 링크가 실패한다. +이 구조체는 외부 ABI가 아니며, 한 실행물의 모든 컴파일 단위에 같은 설정을 써야 한다. + +| Linux AArch64, GCC 14 `-O2` | 기본 | 계측 선택 | +|---|---:|---:| +| `PureRuntime` / 영역 | 128 B | 200 B | +| `runtime.o + evaluate.o`의 `text` | [5,908 B](../../prototypes/connections/results/2026-09-24-compact-runtime/core.json) | 6,076 B | +| 코어 `data` / `bss` | 0 / 0 B | 0 / 0 B | +| 유휴 네이티브 앱 컨트롤러 | 408 B | 480 B | + +[실제 앱 1,000개 세 방식 비교](../../prototypes/workbench/results/2026-09-24-compact-runtime/app-memory-three-way-final.json)는 +동일한 준비된 `routed-layout.pure`·scalar 아카이브·현재 C 코어로 실행했다. +각 앱이 입력 한 건을 처리하고 서로 다른 문서를 저장하며, 다섯 앱은 복원도 했다. +세 방식 모두 입력 1,000건과 저장·복원 1,005건이 성공했고, 종료 시 활성 작업과 +보관 바이트는 0이었다. 세 번 실행한 RSS 증가량의 중앙값은 다음과 같다. + +| 1,000개 실제 앱 | 컨트롤러 / 앱 | 초기화 후 RSS 증가 | 저장 후 RSS 증가 | +|---|---:|---:|---:| +| 이전 상시 작업 슬롯, 작은 코어 | 856 B | 1,200 KiB | 1,264 KiB | +| 호출 중 슬롯, 작은 코어 | 408 B | 752 KiB | 816 KiB | +| 호출 중 슬롯, 계측 코어 | 480 B | 832 KiB | 896 KiB | + +같은 앱과 슬롯 배치에서 계측을 제외하면 컨트롤러는 **72 B/앱**, +측정된 RSS 증가는 1,000개에서 **80 KiB** 낮다. 이전 [049](049-idle-app-work-slots.md)의 +슬롯 절감도 **448 B/앱**, 이번 세 방식 비교에서는 RSS **448 KiB**로 분리된다. +RSS는 페이지 단위의 프로세스 관측값이며 구조체 바이트 수와 정확히 일치하지 않는다. +코어의 `text` 168 B 감소는 오브젝트 측정이고, 전체 앱 실행물·공유 라이브러리· +스택·저장 파일의 크기를 뜻하지 않는다. 계측을 빼면 HTTP 통계와 같은 누적 진단 +정보를 잃으므로, 그 호스트는 계속 선택형 계측을 링크한다. + +현재 소스에서 만든 [소스 편집 CLI](../../prototypes/connections/results/2026-09-24-compact-runtime/source-cli.tar.gz)는 +22,454 B, [고정 정의 CLI](../../prototypes/connections/results/2026-09-24-compact-runtime/cli.tar.gz)는 +12,885 B, [CLI+TUI](../../prototypes/connections/results/2026-09-24-compact-runtime/cli-tui.tar.gz)는 +31,971 B로 압축됐다. 각각 [소스 묶음](../../prototypes/connections/results/2026-09-24-compact-runtime/source-cli-check.json), +[고정 CLI](../../prototypes/connections/results/2026-09-24-compact-runtime/cli-check.json), +[TUI](../../prototypes/connections/results/2026-09-24-compact-runtime/cli-tui-check.json)를 +다른 경로에 풀어 실제 문서 작업으로 확인했다. 압축 크기는 실행 중 메모리와 다르며, +고정 정의 묶음에는 실행 시 `.pure` 수정 기능이 없다. +[기본 호스트](../../prototypes/connections/results/2026-09-24-compact-runtime/default-profile.json), +[고정 정의 호스트](../../prototypes/connections/results/2026-09-24-compact-runtime/fixed-profile.json), +[TUI](../../prototypes/connections/results/2026-09-24-compact-runtime/tui-profile.json)의 +링크된 코드·직접 의존성 검사도 각각 통과했다. 별도 [PTY 재검사](../../prototypes/connections/results/2026-09-24-compact-runtime/tui.json)의 +1 MiB 작업 피크 RSS는 3,168 KiB로 4,096 KiB 상한 이내였다. + +`make -C prototypes/connections check-portable check-core check-async`, `check-release`, +`check-async-server`, `check-tui`, `check-document-peer`가 통과했다. 비동기 HTTP는 +계측된 응답과 작업 회수도 확인했다. 현재 C 소스로 다시 링크한 scalar TLS 피어도 +상호 인증·충돌·재시작 검사를 통과했다. Linux AArch64의 결과이며 macOS 빌드나 +다수 앱의 처리량, 격리 수준을 증명하지 않는다. 이번 환경에는 Zig 0.15.2가 +없어 scalar 아카이브를 재생성하지 못했다. 작성된 C 정의가 현재 소스로 다시 +생성해도 동일한지 검사했고, 세 비교 빌드에 동일한 기존 scalar 아카이브를 사용했다. diff --git a/docs/research/052-clean-native-source-preview.md b/docs/research/052-clean-native-source-preview.md new file mode 100644 index 0000000..c7ff7cd --- /dev/null +++ b/docs/research/052-clean-native-source-preview.md @@ -0,0 +1,27 @@ +# 052 — 빌드 결과 없이 다시 만드는 네이티브 소스 묶음 + +첫 공개 체크포인트에는 이미 빌드된 Linux 실행물만이 아니라, 소스를 받은 사람이 +같은 문서 앱을 깨끗한 디렉터리에서 다시 빌드할 경로가 필요하다. 이번 묶음은 +마이크로 실행기의 기능이나 상주 메모리를 늘리지 않고 배포 경계만 만든다. + +[소스 묶음](../../prototypes/connections/results/2026-09-24-source-preview/pure-source-preview.tar.gz)은 +316,748 B로 압축됐고, C 실행 코어·문서 CLI/TUI/HTTP/TLS 호스트·테스트와 +Workbench 작성 소스를 포함한다. 과거 결과, 빌드 산출물, 캐시와 symlink는 +제외한다. 단, Workbench의 웹 예제를 위한 기존 `core.wasm`은 포함된다. +묶음 안의 `RUN.md`가 사용 명령을, `manifest.json`이 161개 파일 각각의 +크기와 SHA-256을 기록한다. gzip·tar 헤더의 시간과 파일 순서를 고정했고, +같은 소스에서 독립 생성한 세 묶음의 SHA-256은 모두 +`a11a058fa0ba71878a0fed0ee63100566f580721fa5cb602ad25cd46e6b94f6f`였다. + +[압축 해제 후 검사](../../prototypes/connections/results/2026-09-24-source-preview/source-check.json)는 +원본 작업 디렉터리의 빌드 결과를 참조하지 않는다. 임시 디렉터리에서 각 파일 +해시를 검증하고 `make check-portable`과 Linux `make check-release`를 실행했다. +소스 CLI가 실제 문서를 만들고, 새로 빌드한 고정 실행기가 같은 문서를 읽었으며, +`.pure`의 응답 규칙을 바꾼 뒤 새 문서의 응답이 달라졌다. 릴리스 검사에는 +로컬 HTTP 동작·코어/호스트 코드 크기·직접 의존성·CLI RSS 상한이 포함된다. + +이것은 **소스 개발 묶음**이다. 316,748 B는 실행 코어나 설치된 앱의 메모리가 +아니다. 고정 CLI 압축 실행물 12,885 B와도 기능·내용이 다르다. Linux AArch64 +깨끗한 빌드만 확인했다. Apple Silicon macOS의 컴파일·PTY·저장 동작은 아직 +실행하지 못했고, 소스 재배포 라이선스·서명·플랫폼별 설치 절차도 정해지지 +않았다. 따라서 공개 준비의 한 단계를 통과한 것이지 최종 공개 완료는 아니다. diff --git a/docs/research/053-source-loaded-native-terminal-editor.md b/docs/research/053-source-loaded-native-terminal-editor.md new file mode 100644 index 0000000..b57dd79 --- /dev/null +++ b/docs/research/053-source-loaded-native-terminal-editor.md @@ -0,0 +1,40 @@ +# 053 — 같은 문서 앱의 소스 정의를 읽는 네이티브 TUI + +기존 저메모리 TUI는 빌드 시 고정된 `editor.pure`만 실행했다. 정의를 바꿔 +실제로 써 보려면 소스 CLI 또는 큰 GTK 작성 창으로 옮겨야 했다. 선택형 +`source-tui`는 기존 터미널 편집기·저장 어댑터·실행 코어를 그대로 사용하며, +시작할 때 `--program FILE`로 `.pure`를 준비한다. CLI·TUI가 같은 정의와 +저장 문서·버전을 사용한다. 파일을 고친 뒤 **새 TUI 세션**을 열어야 바뀐 +정의가 적용된다. TUI 안에서 `.pure` 텍스트를 작성하거나 실행 중 정의를 +교체하는 기능은 아니다. + +이 TUI가 지원하는 정의에는 전체 문서 읽기와 서로 다른 두 u32le splice +필드, 뒤따르는 삽입 바이트가 필요하다. 맞지 않는 정의는 터미널 모드나 +저장소를 변경하기 전에 거절한다. [실제 PTY 검사](../../prototypes/connections/results/2026-09-24-source-tui/tui.json)에서는 +필드 순서를 바꾼 새 `.pure`를 읽어 두 번째 편집의 바이트 범위를 올바르게 +인코딩했다. 일반 정의에서는 한글·여러 행·탭·충돌 시 초안·크기 변경· +1 MiB 편집·1,024개 저장 문서 탐색과 터미널 복구를 재검증했다. + +| Linux AArch64 / GCC 14 | 고정 TUI | 소스 TUI | +|---|---:|---:| +| 링크된 `text` | [31,999 B](../../prototypes/connections/results/2026-09-24-source-tui/fixed-tui-profile.json) | [48,604 B](../../prototypes/connections/results/2026-09-24-source-tui/tui-profile.json) | +| CLI+TUI 압축 묶음 | [31,929 B](../../prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui.tar.gz) | [50,942 B](../../prototypes/connections/results/2026-09-24-source-tui/source-cli-tui.tar.gz) | +| 실행 중 `.pure` 파서 | 없음 | 있음 | + +소스 TUI의 실제 PTY 1 MiB 작업 피크 RSS는 이번 반복에서 3,216~3,664 KiB로 +기존 4,096 KiB 상한 안에 있었다. RSS는 실행마다 변동하므로 두 TUI의 +메모리 우열로 해석하지 않는다. [기본 실행 코어](../../prototypes/connections/results/2026-09-24-source-tui/core.json)의 5,908 B 코드 섹션과 +128 B 영역 구조체는 그대로이며, 소스 파서는 선택한 TUI 실행물에만 포함된다. + +[소스 CLI+TUI 묶음 검증](../../prototypes/connections/results/2026-09-24-source-tui/source-cli-tui-check.json)은 +다른 경로에 풀어 CLI의 수정된 `.pure` 응답과 TUI의 실제 PTY 문서 작업을 +확인했다. [고정 CLI+TUI 묶음](../../prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui-check.json)도 +별도로 이동 후 확인했다. 현재 162개 파일의 [소스 개발 묶음](../../prototypes/connections/results/2026-09-24-source-tui/pure-source-preview.tar.gz)은 +318,619 B이고, [깨끗한 디렉터리 검증](../../prototypes/connections/results/2026-09-24-source-tui/source-check.json)에서 +C 테스트·릴리스 검사·소스 TUI PTY 작업을 통과했다. 독립 재생성 해시도 +`6134ff966d09c76134b921008d16b828a349b7031bdf029379de6eea9e745eaa`로 일치했다. + +이 결과는 Linux AArch64의 문서 앱과 호환되는 `.pure` 정의에 한정된다. +macOS의 터미널·입력기·파일 저장은 검증하지 못했다. 소스 TUI의 편집 경험은 +범용 UI 작성기나 실시간 정의 교체를 대신하지 않으며, 소스 묶음의 크기도 +런타임 상주 메모리를 뜻하지 않는다. diff --git a/docs/research/054-direct-document-path.md b/docs/research/054-direct-document-path.md new file mode 100644 index 0000000..a5f72de --- /dev/null +++ b/docs/research/054-direct-document-path.md @@ -0,0 +1,53 @@ +# 054 — 실제 문서 작업의 직접 저장 경로와 Pure 경로 + +첫 공개 체크포인트에는 작은 코어 수치뿐 아니라 **같은 앱 작업을 직접 작성했을 때의 +전체 비용**이 필요하다. [재현 가능한 비교 구현](../../research/experiments/004-direct-document-path/README.md)은 +기존 `editor.pure`의 문서 생성→전체 읽기→마지막 1바이트 조건부 교체→전체 읽기를 +문서마다 실행한다. 두 실행물은 동일한 `store.c`를 링크하고 리비전·본문·결과 +해시를 검사한다. 각 쌍의 최종 저장 파일 바이트도 일치한다. 직접 경로는 저장 +함수를 호출하고, Pure 경로는 빌드 시 준비한 정의와 실행 코어·저장 호스트를 지난다. + +Linux AArch64, GCC 14 `-O2`, ext4 `/dev/mmcblk0p2`에서 저장 디렉터리를 +매번 새로 만들고 4쌍을 직접→Pure / Pure→직접 순서로 번갈아 실행했다. 각 쓰기는 +교체 파일과 디렉터리를 동기화하는 **동일한 저장소 코드**를 지난다. 먼저 +문서 4개로 무결성을 검사한 뒤 본 측정을 했다. `text`는 GNU `size`의 링크된 +코드 섹션이며, RSS는 Python 부모가 아닌 작은 네이티브 실행 부모가 수집한 +자식 프로세스의 피크다. + +| 작업 / 실행물 | 직접 호출 | Pure 경로 | +|---|---:|---:| +| 링크된 `text` | 10,364 B | 18,640 B | +| 링크된 `bss` | 16 B | 376 B | +| 256×8 KiB 문서, 1,024개 작업: 벽시계 중앙값 | 7.870 s | 7.601 s | +| 위 작업: 피크 RSS 중앙값 | 1,072 KiB | 1,088 KiB | +| 32×1 MiB 문서, 128개 작업: 벽시계 중앙값 | 8.929 s | 9.840 s | +| 위 작업: 피크 RSS 중앙값 | 4,368 KiB | 4,136 KiB | + +[8 KiB 원자료](../../research/experiments/004-direct-document-path/results/2026-09-24-ext4-8k-linux-aarch64.json)에서 +각 쌍의 Pure−직접 시간 차이는 +0.408, −0.408, +0.565, −0.379초다. +[1 MiB 원자료](../../research/experiments/004-direct-document-path/results/2026-09-24-ext4-1m-linux-aarch64.json)에서는 ++2.522, −4.239, +1.475, +10.147초다. 저장소 지연의 흔들림이 커서 +이 네 쌍으로 Pure의 속도 우열이나 평균 추가 비용을 확정할 수 없다. +1 MiB RSS도 실행별 직접 4,128~4,592 KiB, Pure 4,128~4,176 KiB로 +겹친다. 다만 **이 비교 실행물에서** Pure 경로는 +링크 코드 8,276 B와 정적 `bss` 360 B가 더 크다. 여기에는 준비된 정의, +평가기, 실행 코어, 저장 호스트, 계측용 작업 코드가 포함되므로 8,276 B를 +독립 코어의 크기나 모든 앱의 고정 오버헤드라고 부를 수 없다. + +이 작업의 지배적인 비용은 저장소에 있다. 8 KiB 프로필의 중앙값에서 +생성·교체 단계의 합은 직접 약 7.69초, Pure 약 7.58초로 각 경로의 +전체 시간 대부분이다. 읽기에는 본문 전체 비교와 FNV 해시도 포함한다. +커널·파일시스템 동작과 해시 비용이 작은 실행 분배 비용을 가리므로, +이 실험은 순수 디스패치 지연의 정밀 측정이 아니다. 앞선 `/tmp`의 +[8 KiB](../../research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-8k-preliminary-linux-aarch64.json)· +[1 MiB](../../research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-1m-preliminary-linux-aarch64.json) +탐색 결과는 tmpfs라서 영속 저장소 비교에 사용하지 않는다. + +직접 경로는 이 성공 입력의 결과만 맞춘 좁은 기준선이다. Pure의 예산, +소유권, 작업 완료·취소, 실패 전파, 동시 작업이나 RPC 보안까지 구현하지 +않았다. 따라서 같은 기능·보안을 갖춘 서버 비교, 네트워크 지연 비교, +실서비스 처리량 주장으로 확장할 수 없다. 다음 비교는 동일한 외부 +프로토콜·보안·오류 의미를 갖춘 피어에 같은 문서 작업을 주고, 본문 +소유권과 복사량, 메모리·지연의 비용을 함께 공개해야 한다. 그전까지 +마이크로 목표는 **기본 코어의 작은 수치와 필수 앱 전체 비용을 별개로 +판정**한다. diff --git a/docs/research/055-authenticated-peer-transport-cost.md b/docs/research/055-authenticated-peer-transport-cost.md new file mode 100644 index 0000000..b060594 --- /dev/null +++ b/docs/research/055-authenticated-peer-transport-cost.md @@ -0,0 +1,70 @@ +# 055 — 인증 문서 피어의 공통 전송 지연과 서버 전용 비용 + +같은 문서 앱의 직접 저장 비교 [054](054-direct-document-path.md)에 이어, 실제 +인증 RPC 경계까지 직접 기준선을 만들었다. [실험 구현·재현 절차](../../research/experiments/005-direct-document-peer/README.md)의 +두 서버는 같은 `editor.pure`, 프로그램 파서·평가기, 파일 저장소, +OpenSSL TLS 1.3, 상호 인증, ALPN, 요청·응답 형식을 사용한다. Pure 서버는 +작업 접수·수명·결과 대여를 실행 코어에 맡긴다. 직접 서버는 이 성공 +문서 작업을 수동으로 처리하므로 **일반 런타임 기능까지 동등한 기준선은 아니다.** + +처음에는 양쪽에서 인증된 8 KiB 문서 읽기 **32회에 약 1.412초**가 걸렸다. +요청당 약 44ms였다. [변경 전 원자료](../../research/experiments/005-direct-document-peer/results/2026-09-24-before-nodelay-8k-linux-aarch64.json)의 +중앙값은 직접 1,411.866ms, Pure 1,411.853ms다. 응답의 13B 머리와 +본문을 별도 TLS 쓰기로 보내는 공통 전송 경로에 `TCP_NODELAY`를 적용한 뒤, +[같은 32회 작업](../../research/experiments/005-direct-document-peer/results/2026-09-24-after-nodelay-8k-linux-aarch64.json)은 +직접 3.024ms, Pure 2.790ms였다. 같은 루프백 조건의 이 짧은 작업에서 +각각 약 467배·506배의 시간 감소다. TLS/TCP 경로에 있던 지연이 +Pure 디스패치 비용을 가렸다는 해석과 맞다. Nagle·지연 ACK의 구체적인 +패킷 상호작용은 캡처하지 않았으므로 **원인 메커니즘은 추론**이다. +이 배수를 원격망 처리량이나 범용 RPC 성능으로 내세우지 않는다. +`TCP_NODELAY`는 작은 TCP 패킷을 늘릴 수 있다. 패킷 수·원격망 지연·전력 +비용은 측정하지 않았으므로 이 선택의 범용 최적성도 아직 주장하지 않는다. + +수정 뒤 더 긴 실제 문서 읽기를 새 저장소·인증 연결에서 번갈아 실행했다. +Linux AArch64, GCC 14 `-O2`, OpenSSL 3.6.4, ext4 저장소의 루프백 결과다. +TLS 핸드셰이크·문서 생성·첫 읽기는 측정 밖이고, 매 요청의 8 KiB 또는 +1 MiB 본문 전체를 Python 클라이언트가 받았으며 결과를 확인했다. + +| 인증된 읽기 배치 | 직접 서버 중앙값 | Pure 서버 중앙값 | +|---|---:|---:| +| 8 KiB × 1,024회, 6쌍 | 84.436ms | 91.455ms | +| 위 배치의 요청당 시간 | 82.46µs | 89.31µs | +| 1 MiB × 64회, 4쌍 | 149.964ms | 152.685ms | +| 위 배치의 요청당 시간 | 2.343ms | 2.386ms | +| 벤치마크의 링크된 `text` | 37,862B | 43,454B | + +[8 KiB 원자료](../../research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-8k-linux-aarch64.json)의 +쌍별 Pure−직접 시간은 +17.06, −2.19, +15.97, +3.42, −1.13, +−22.65ms다. [1 MiB 원자료](../../research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-1m-linux-aarch64.json)는 ++0.75, −1.22, +18.14, +0.08ms다. 중앙값만 보면 Pure가 느리지만 +쌍별 편차와 역전이 있어 추가 지연의 안정적인 크기를 판정하지 않는다. +두 서버의 최종 저장 파일 SHA-256은 매 쌍에서 같았다. 서버 RSS는 +반복 읽기 뒤 대체로 약 8.6 MiB로 겹쳤다. OpenSSL·libc의 공유 매핑과 +프로세스 메모리를 포함하므로 전체 TLS 서버를 6 KiB 런타임이라고 +부를 수 없다. + +서버만 설치하는 경우를 위해 제품 피어에서 클라이언트 명령을 제외하는 +`make -C prototypes/connections document-server` 선택 빌드를 추가했다. +기존 클라이언트 겸용 빌드의 `text`는 46,934B, 서버 전용은 +[43,462B](../../research/experiments/005-direct-document-peer/pure-server-after-nodelay-profile-linux-aarch64.json)로 +3,472B 작다. 이 서버만의 [크기·의존성 상한](../../prototypes/connections/peer-server-budget.json)은 +선택 빌드에만 적용한다. 실험기가 별도 컴파일 명령으로 만든 43,454B와 +8B 차이가 있으므로 둘을 같은 실행물의 수치로 합치지 않는다. +기본 실행 코어와 CLI에는 TLS가 추가되지 않았다. +[서버 전용 이동 묶음과 크기 상한](056-server-only-document-package.md)도 +추가해, 같은 문서·인증 시나리오를 추출한 아카이브에서 재검증했다. + +[기존 클라이언트 겸용](../../research/experiments/005-direct-document-peer/pure-peer-after-nodelay-parity-linux-aarch64.json), +[서버 전용](../../research/experiments/005-direct-document-peer/pure-server-after-nodelay-parity-linux-aarch64.json), +[직접 서버](../../research/experiments/005-direct-document-peer/direct-peer-parity-linux-aarch64.json)의 +실제 인증·충돌·1 MiB·오류·복원 시나리오가 통과했다. +[직접 서버 ASan/UBSan 검사](../../research/experiments/005-direct-document-peer/direct-peer-sanitized-parity-linux-aarch64.json)와 +[Pure 서버 전용 ASan/UBSan 검사](../../research/experiments/005-direct-document-peer/pure-server-sanitized-parity-linux-aarch64.json)도 +통과했다. 공통 TLS 코드를 쓰는 기존 scalar 피어도 +[회귀 검사](../../research/experiments/005-direct-document-peer/scalar-peer-after-nodelay-parity-linux-aarch64.json)를 +통과했다. 이 검사는 기존 생성된 Zig 아카이브와 새 C 전송 코드를 다시 +링크한 것이며 Zig 전체 재생성은 아니다. 이는 현재 시나리오의 기능·메모리 +오류 증거이며, 여러 연결의 +공정성이나 장애망·공격 부하를 입증하지 않는다. 서버는 여전히 한 번에 +연결 하나만 처리하고, 5초 절대 요청 기한을 쓴다. macOS 실행, +실제 네트워크와 Kitex/QUIC 비교는 남아 있다. diff --git a/docs/research/056-server-only-document-package.md b/docs/research/056-server-only-document-package.md new file mode 100644 index 0000000..51ed32f --- /dev/null +++ b/docs/research/056-server-only-document-package.md @@ -0,0 +1,54 @@ +# 056 — 서버 전용 문서 피어를 실제 이동 묶음으로 검증 + +[055](055-authenticated-peer-transport-cost.md)의 서버 전용 실행물은 빌드와 +로컬 테스트까지만 확인했다. 이번에는 `package_native.py --with-peer-server`로 +CLI·`editor.pure`·인증 TLS 서버를 하나의 Linux AArch64 아카이브에 넣고, +다른 디렉터리에 풀어 실제 인증 문서 작업을 실행했다. 서버만의 배포 +구성에서도 외부 통신을 유지한다. `--with-peer`는 기존의 C 클라이언트 +겸용 구성을 그대로 선택한다. 두 옵션은 동시에 사용할 수 없다. + +```sh +python3 prototypes/connections/package_native.py --with-peer-server \ + --output dist/pure-document-server-only-linux-aarch64.tar.gz +python3 prototypes/connections/check_package.py \ + dist/pure-document-server-only-linux-aarch64.tar.gz +``` + +| 같은 CLI·정의와 TLS 피어 하나를 포함한 구성 | 서버 전용 | 클라이언트 겸용 | +|---|---:|---:| +| 압축 아카이브 전체 | [47,686B](../../prototypes/connections/results/2026-09-24-server-only-package/pure-document-server-only-linux-aarch64.tar.gz) | [49,382B](../../prototypes/connections/results/2026-09-24-server-only-package/pure-document-combined-peer-linux-aarch64.tar.gz) | +| 선택 피어의 링크된 `text` | 43,462B | 46,934B | +| 검토한 아카이브 상한 | 51,200B | 53,248B | + +동일한 선택 범위에서 서버 전용 묶음은 **1,696B 작다**. 이 차이는 압축 +파일 비용이며 프로세스 RAM 절감량이 아니다. [새 아카이브 예산](../../prototypes/connections/peer-package-budget.json)은 +CLI와 TLS 피어만 담은 두 구성에 적용된다. HTTP·TUI·GTK 또는 연구용 +저장소까지 고른 조합에는 이 상한을 적용하지 않는다. 기존의 실행물 +`text`·data·BSS와 직접 동적 의존성 검사도 별도로 유지한다. +서버 전용 묶음을 다른 출력 경로에 다시 생성해 SHA-256 +`34fb507c160e4aa719cf44fb4f8bbcb422d2c0ee6dc3f206f6a5fe3bc4d089b6`가 +같음을 확인했다. 현재 TLS 피어와 연구용 페이지 저장소를 동시에 선택하면 +저장 형식이 다를 수 있으므로 패키저가 그 조합을 거절한다. + +[서버 전용 이동 검증](../../prototypes/connections/results/2026-09-24-server-only-package/server-only-package-check.json)은 +manifest와 모든 파일의 SHA-256을 확인하고, 추출한 CLI의 문서 생성·복원과 +수정한 `.pure`의 응답 변화를 실행했다. 같은 추출 경로의 서버에는 패키지 +밖의 Python TLS 클라이언트가 접속했다. 잘못된 클라이언트 인증서·서버 +호스트 이름·ALPN을 거부했고, 조건부 생성·충돌·1 MiB 분할 수신·응답 +본문 생략·마지막 1바이트 변경을 처리했다. 잘린 프레임과 잘못된 연산은 +저장값을 바꾸지 않았으며 서버 재시작 뒤에도 1 MiB 값을 읽었다. 패키지 +안 CLI도 같은 값을 읽었다. 이 검사의 서버 `/proc` RSS 관찰값은 연결 전 +7,968 KiB, 큰 작업 뒤 8,672 KiB다. 피크나 다른 장치의 메모리 상한은 +아니다. + +[클라이언트 겸용 이동 회귀](../../prototypes/connections/results/2026-09-24-server-only-package/combined-peer-package-check.json)는 +기존 C/Python TLS·1 MiB 작업과 재시작까지 통과했다. 두 아카이브의 +manifest에는 선택 호스트, 파일 해시, 시스템 공유 라이브러리의 직접 +의존성 및 검토한 이진·아카이브 상한이 들어 있다. OpenSSL과 libc를 +묶음 안에 넣지 않으며, 인증서도 사용자가 준비해야 한다. 서버 전용 +묶음에는 C `call` 명령이 없다. 외부 클라이언트는 `pure-doc/1` 형식을 +구현하거나 별도의 Pure 클라이언트를 사용한다. + +이 결과는 이동 가능한 Linux AArch64 패키지의 한 선택 구성을 증명한다. +macOS/Apple Silicon 실행, 서명·설치 경험, 다중 연결 공정성, 장시간 부하, +장애망 복구와 전체 공개 체크포인트는 아직 완료되지 않았다. diff --git a/docs/research/057-portable-native-terminal-check.md b/docs/research/057-portable-native-terminal-check.md new file mode 100644 index 0000000..116339e --- /dev/null +++ b/docs/research/057-portable-native-terminal-check.md @@ -0,0 +1,45 @@ +# 057 — 같은 네이티브 문서 TUI의 휴대형 PTY 검증 경로 + +첫 공개 체크포인트는 사용자가 주로 쓰는 Apple Silicon macOS에서 네이티브 +앱이 실행되어야 한다. 지금 작업 호스트는 Linux AArch64라서 macOS 실행을 +증명할 수 없다. 기존 `check-portable`은 C 단위 동작만 검사했고, 실제 +TUI 검사는 Linux `/proc` RSS를 반드시 읽었다. 이에 동일한 TUI 시나리오를 +유지하되 `/proc` 수치를 명시적으로 제외하는 `check-portable-tui` 경로를 +추가했다. 고정 정의와 소스 정의 TUI를 모두 실제 PTY에서 실행한다. + +```sh +make -C prototypes/connections check-portable +make -C prototypes/connections check-portable-tui +``` + +검사기는 설치된 UTF-8 로케일을 골라 자식 터미널에 지정한다. 휴대형 모드의 +RSS 필드는 `null`, 상한은 0이며 결과에 `portable: true`를 기록한다. +기존 Linux 검사에서는 `/proc` 피크와 4,096 KiB 상한을 그대로 확인한다. + +| Linux AArch64에서 실제 실행한 경로 | 확인한 내용 | +|---|---| +| [고정 TUI 휴대형 PTY](../../prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-aarch64.json) | 입력·다른 문서·한글 이동·크기 변경·충돌·1 MiB 편집·1,024개 목록·종료 후 터미널 복구 | +| [소스 TUI 휴대형 PTY](../../prototypes/connections/results/2026-09-24-portable-tui/source-linux-aarch64.json) | 위 작업과 잘못된 정의 거부, `.pure` 필드 배치를 바꾼 뒤의 실제 편집 | +| [고정 TUI Linux RSS 회귀](../../prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-rss-regression.json) | 1 MiB 작업 피크 3,168 KiB, 기존 코드·메모리 상한 통과 | +| [소스 TUI Linux RSS 회귀](../../prototypes/connections/results/2026-09-24-portable-tui/source-linux-rss-regression.json) | 1 MiB 작업 피크 3,664 KiB, 기존 코드·메모리 상한 통과 | + +새 [소스 묶음](../../prototypes/connections/results/2026-09-24-portable-source/pure-source-preview.tar.gz)은 +165개 파일, 압축 321,399B다. SHA-256은 +`5f1766fefe2c319d86c3ff57c881638a9d1ee24374dea33c614e25577d087814`이며 +다른 출력 경로에 다시 만든 묶음과 일치했다. [깨끗한 디렉터리 검사](../../prototypes/connections/results/2026-09-24-portable-source/source-check-linux-aarch64.json)는 +manifest의 모든 파일 해시, C17 검사, 고정·소스 TUI PTY 작업, 실제 문서 +작성·규칙 변경, Linux 릴리스 크기·RSS 검사를 통과했다. +321,399B는 개발용 소스 배포 크기이며 실행물 크기나 상주 메모리가 아니다. +이번 변경은 Python 검사기·Make·문서에 한정되어 C 런타임 실행 경로를 +키우지 않았다. 기본 코어와 선택형 TUI의 코드·RSS 상한은 각각 기존 +`check-release`, `check-tui`, `check-source-tui`로 분리해 검사한다. + +macOS에서 검증할 때는 이 소스 묶음을 풀어 위 두 `make` 명령을 실행해야 +한다. `check-portable-tui`가 macOS에서 통과했다는 결과는 **아직 없다**. +PTY 시나리오도 IME 조합 입력이나 GUI 접근성 검사가 아니다. macOS 저장소의 +실제 APFS 동작과 전원 장애 내구성 역시 별도로 검증해야 한다. 현재 저장 +어댑터는 파일·디렉터리에 `fsync`를 사용한다. Apple의 +[fsync(2) 설명](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fsync.2.html)은 +드라이브 캐시까지 포함한 강한 쓰기 순서를 보장하려면 `F_FULLFSYNC`를 +구분해야 한다고 설명한다. 따라서 이 Linux 검사를 macOS의 강한 영속성 +증거로 해석하지 않는다. diff --git a/docs/research/058-portable-authenticated-peer-check.md b/docs/research/058-portable-authenticated-peer-check.md new file mode 100644 index 0000000..0928468 --- /dev/null +++ b/docs/research/058-portable-authenticated-peer-check.md @@ -0,0 +1,38 @@ +# 058 — 선택형 인증 피어의 휴대형 실작업 검사 + +첫 공개 체크포인트에서는 Pure 간 연결이 같은 앱 정의와 문서 작업을 실제로 +교환해야 한다. 기존 인증 피어 검사는 서버의 `/proc` RSS를 읽어 Linux 외 +환경에서 시작할 수 없었다. `check-portable-peer`는 그 수집만 제외하고 +같은 TLS 클라이언트·서버 작업을 실행한다. 실행 코어와 C 피어 코드는 +변경하지 않았고, TLS는 여전히 선택한 빌드에만 링크된다. + +```sh +make -C prototypes/connections check-portable-peer +``` + +OpenSSL 헤더·라이브러리·명령행 도구와 Python 3가 필요하다. 검사기는 임시 +인증서를 만들고 실제 루프백 소켓에서 상호 인증, 서버 이름·ALPN·앱 소스 해시 +불일치, C 피어와 외부 Python 클라이언트의 문서 읽기·편집·충돌·복구, 조각난 +1 MiB 요청, 잘못된 프레임, 연결 기한, 서버 재시작 후 복원을 확인한다. + +| Linux AArch64 실제 실행 | 결과 | +|---|---| +| [범용 피어 휴대형 검사](../../prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-aarch64.json) | 9개 실작업 시나리오 통과, RSS 미수집 | +| [서버 전용 휴대형 검사](../../prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-aarch64.json) | 같은 9개 시나리오 통과, RSS 미수집 | +| [범용 피어 Linux 회귀](../../prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-rss-regression.json), [코드 경계](../../prototypes/connections/results/2026-09-24-portable-peer/client-server-profile.json) | `text` 46,934 B, 유휴 7,968 KiB·1 MiB 작업 후 9,712 KiB RSS | +| [서버 전용 Linux 회귀](../../prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-rss-regression.json), [코드 경계](../../prototypes/connections/results/2026-09-24-portable-peer/server-only-profile.json) | `text` 43,462 B, 유휴 7,968 KiB·1 MiB 작업 후 9,712 KiB RSS | + +새 [소스 묶음](../../prototypes/connections/results/2026-09-24-portable-peer-source/pure-source-preview.tar.gz)은 +165개 파일, 압축 321,958 B이며 SHA-256은 +`bf51f79c400f820b48dbf78522449cda6625563ce860809e86d76a8023377dbb`다. +[이동 후 검사](../../prototypes/connections/results/2026-09-24-portable-peer-source/source-check-linux-aarch64.json)는 +manifest 해시·C 동작·두 TUI의 실제 PTY 작업·변경된 `.pure` 결과·선택형 +TLS 두 서버·Linux 릴리스 크기와 RSS 상한을 깨끗한 디렉터리에서 통과했다. +압축 소스 묶음 크기는 실행 중 메모리가 아니다. TLS 서버의 위 RSS에는 +선택한 OpenSSL 공유 라이브러리와 실제 작업 비용이 포함되며, 기본 실행 +코어의 크기를 대신하지도 않는다. + +Apple Silicon macOS에서 이 명령을 컴파일하거나 실행한 증거는 아직 없다. +특히 macOS의 OpenSSL 헤더·라이브러리 경로와 저장소 디렉터리 동기화는 +별도 검증이 필요하다. 휴대형 결과의 RSS 필드는 `null`이며 Linux 메모리 +측정을 대체하지 않는다. diff --git a/docs/research/059-immutable-file-copy-path.md b/docs/research/059-immutable-file-copy-path.md new file mode 100644 index 0000000..42bff3a --- /dev/null +++ b/docs/research/059-immutable-file-copy-path.md @@ -0,0 +1,57 @@ +# 059 — 큰 불변 문서의 바뀌지 않은 구간을 복사하는 비용 + +기본 저장소는 문서 전체를 불변 파일로 다시 공개한다. 1 MiB 문서의 마지막 +1바이트를 바꿀 때 기존 바이트를 4 KiB 스택 버퍼로 읽고 새 파일에 쓴다. +[재현 검사](../../prototypes/connections/check_copy_path.py)는 같은 +`editor.pure` CLI로 문서를 만든 뒤 실제 마지막 바이트를 수정하고, 별도 +프로세스에서 최종 1 MiB 본문을 대조한다. 저장 디렉터리는 작업트리의 +ext4에 만들고 `findmnt`로 파일시스템을 기록한다. Linux `strace`로 객체 파일의 +성공한 호출을 세고 작은 네이티브 부모의 `wait4`로 자식 피크 RSS를 잰다. + +선택형 `PURE_STORE_CFR_COPY` 경로는 길이 16 KiB 이상의 바뀌지 않은 +구간에 Linux `copy_file_range()`를 먼저 시도한다. 짧은 복사는 반복하고, +지원되지 않는다고 보고되면 남은 오프셋부터 기존 읽기·쓰기로 이어간다. +다른 저장 형식을 만들지 않으며, 기본 빌드와 macOS 빌드에는 들어가지 않는다. +부분 복사 뒤 미지원 오류를 강제로 주입한 C 저장 검사도 통과했다. + +| 같은 1 MiB 문서의 마지막 1 B 수정 | 기본 | 선택형 Linux 복사 | +|---|---:|---:| +| 원본 객체 `pread64` 호출 | 257 | 1 | +| `copy_file_range` 호출·전달 바이트 | 0 | 1회·1,048,575 B | +| 임시 객체 `write` 호출·바이트 | 258회·1,048,600 B | 2회·25 B | +| 이 작업의 자식 피크 RSS | 1,056 KiB | 1,056 KiB | +| CLI 링크 `text` | 35,783 B | 36,052 B | +| HTTP 호스트 링크 `text` | 43,938 B | 44,207 B | + +[실제 복사 추적·본문 해시·RSS](../../prototypes/connections/results/2026-09-24-copy-range/copy-path.json), +[기본 실행물 비용](../../prototypes/connections/results/2026-09-24-copy-range/default-profile.json), +[선택 실행물 비용](../../prototypes/connections/results/2026-09-24-copy-range/profile.json). +선택에 따른 링크 `text` 증가는 CLI·HTTP에서 각각 **269 B**다. +[기본 코어](../../prototypes/connections/results/2026-09-24-copy-range/default-core.json)의 +`text` 5,908 B·영역 128 B는 바뀌지 않는다. 선택형 CLI의 1 MiB 문서 생성 +[RSS 회귀](../../prototypes/connections/results/2026-09-24-copy-range/memory.json)도 +4 MiB 상한 안이다. 위 1,056 KiB는 기존 문서 수정 한 번의 피크이며, +생성·출력을 합친 전체 앱 피크와 혼동하지 않는다. + +이 결과는 **사용자 공간 읽기·쓰기 호출 수**의 감소다. 전달된 1,048,575 B가 +사라진 것은 아니다. 파일시스템이 데이터를 실제로 복사할지 공유할지는 구현에 +달렸고, 물리 디스크 쓰기량·지연 시간의 개선은 증명하지 않았다. Linux +[copy_file_range(2) 설명](https://man7.org/linux/man-pages/man2/copy_file_range.2.html)은 +부분 복사와 파일시스템별 구현 및 오래된 커널의 일부 가상 파일시스템 결함을 +명시한다. 따라서 이 경로는 **현재 Linux 6.18/ext4에서 검증한 선택형 연구 +경로**로 남긴다. 다른 커널·파일시스템의 성공 반환이 실제 본문과 일치하는지는 +별도로 검증해야 한다. + +[실제 앱 기능 검사](../../prototypes/connections/results/2026-09-24-copy-range/functional.json), +[8클라이언트·1,024문서 검사](../../prototypes/connections/results/2026-09-24-copy-range/service-scale.json)는 +최종 문서와 프로세스 상한을 통과했다. 다만 규모 검사의 문서는 8 KiB이므로 +이 선택형 16 KiB 이상 복사 경로의 **성능 검사로 해석하지 않는다**. +기본 경로의 [전체 릴리스 검사](../../prototypes/connections/results/2026-09-24-copy-range/default-functional.json)도 +통과했다. 재현 명령은 `make -C prototypes/connections check-copy-range`다. + +최종 [소스 묶음](../../prototypes/connections/results/2026-09-24-copy-range-source/pure-source-preview.tar.gz)은 +166개 파일, 압축 324,261 B, SHA-256 +`ce48ef8e330cff470b78aec9c9e6c6d8877e4c8c371512f190b91b6036ddc8ba`다. +[깨끗한 디렉터리 검사](../../prototypes/connections/results/2026-09-24-copy-range-source/source-check-linux-aarch64.json)에서 +C 동작·두 TUI의 PTY 편집·선택형 TLS 피어·파일 간 복사·Linux 릴리스 +상한을 모두 통과했다. 이 압축량은 소스 배포 비용이지 상주 메모리가 아니다. diff --git a/docs/research/060-host-native-checkpoint.md b/docs/research/060-host-native-checkpoint.md new file mode 100644 index 0000000..88caf0f --- /dev/null +++ b/docs/research/060-host-native-checkpoint.md @@ -0,0 +1,24 @@ +# 호스트 네이티브 체크포인트: 실행 비용은 앱 작업에서 측정 + +기본 C 코어와 실행물에는 기능을 추가하지 않았다. 개발용 `check-native-checkpoint` +명령이 기존 C 동작 검사와 실제 PTY 편집 검사를 실행하고, 고정 정의 및 소스 정의 +CLI 각각에서 1 MiB 문서 생성·읽기·1바이트 교체·새 프로세스 복원을 검사한다. +`measure-peak-rss`는 `wait4`로 자식 프로세스의 최대 RSS를 수집한다. +현재 [Apple XNU `getrusage(2)` 설명](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/man/man2/getrusage.2)은 +`ru_maxrss`를 바이트로, Linux는 KiB로 정의한다. 도우미는 원시 값·단위를 함께 +남기고 KiB로 정규화한다. 플랫폼 사이에 같은 메모리 상한을 적용하지 않는다. + +2026-09-24 Linux AArch64의 새 빌드 `build/native-checkpoint`에서 C·고정/소스 +TUI PTY 검사가 통과했다. [소스 CLI 결과](../../prototypes/connections/results/2026-09-24-native-checkpoint/source-memory.json)는 +1 MiB 생성 2,080 KiB, 1바이트 수정 1,104 KiB 피크 RSS였다. +[고정 CLI 결과](../../prototypes/connections/results/2026-09-24-native-checkpoint/fixed-memory.json)는 +각각 2,128 KiB와 1,104 KiB였다. 두 결과 모두 디버그 심볼을 포함한 실행 파일 +크기와 SHA-256을 기록한다. 각 [고정 TUI](../../prototypes/connections/results/2026-09-24-native-checkpoint/fixed-tui.json)와 +[소스 TUI](../../prototypes/connections/results/2026-09-24-native-checkpoint/source-tui.json)의 +portable 검사는 실제 PTY 동작을 확인하지만 RSS는 기록하지 않는다. +기존 Linux 전용 `check-micro-memory` 상한도 재검사해 통과했다. + +이 수치는 프로세스 피크이며 파일 시스템 캐시·커널 버퍼·선택형 TLS/GUI 비용을 +포함하지 않는다. 지연이나 디스크 물리 쓰기 개선을 뜻하지 않는다. Apple Silicon +macOS에서는 아직 컴파일·실행하지 못했다. 소스 묶음에서 명령을 실행한 뒤 +그 호스트의 실제 결과를 받아야 네이티브 호환성과 메모리 비용을 판단할 수 있다. diff --git a/docs/research/061-durable-document-edit-latency.md b/docs/research/061-durable-document-edit-latency.md new file mode 100644 index 0000000..e458d46 --- /dev/null +++ b/docs/research/061-durable-document-edit-latency.md @@ -0,0 +1,54 @@ +# 061 — 같은 불변 문서 편집의 완료 시간과 페이지 재구성 비용 + +큰 문서의 1바이트 수정에서 기본 불변 파일, Linux `copy_file_range` 선택 경로, +16 KiB 페이지 선택 저장소를 **같은 `editor.pure` 소스 CLI**로 비교했다. +비교 코드는 [단일 수정](../../research/experiments/005-storage-edit-latency/measure.py)과 +[연속 수정](../../research/experiments/005-storage-edit-latency/sequence.py)에 있다. +각 저장소에서 1 MiB 문서를 만들고, 새 CLI 프로세스로 마지막 1바이트를 리비전 +검사와 함께 바꾼다. 생성·수정은 각각 `fsync`를 포함한다. 이후 다른 프로세스의 +`show`로 전체 본문과 리비전을 확인한다. 측정값은 CLI 시작·`.pure` 준비· +저장소 초기화까지 포함하고 검증용 `show` 시간은 제외한다. + +2026-09-24 Linux AArch64, ext4 `/dev/mmcblk0p2`에서 각 방식 18회씩, +실행 순서를 순환해 측정했다. [모든 원자료](../../research/experiments/005-storage-edit-latency/2026-09-24-ext4-linux-aarch64.json)에 +실행물 해시·매회 완료 시간·최종 본문 해시·파일 할당량이 있다. + +| 실제 CLI 작업의 중앙값 | 기본 불변 파일 | 선택형 파일 간 복사 | 선택형 페이지 | +|---|---:|---:|---:| +| 1 MiB 새 문서 생성 | 83.556 ms | 81.672 ms | 88.948 ms | +| 기존 문서 마지막 1 B 수정 | 78.805 ms | 77.908 ms | 14.473 ms | +| 수정 후 파일 할당량 | 1,052,672 B | 1,052,672 B | 1,069,056 B | +| CLI 링크 `text` | 35,783 B | 36,052 B | 41,040 B | + +별도 자식 프로세스 RSS 검사에서 1 MiB 생성 피크는 기본 2,080 KiB, +파일 간 복사 2,080 KiB, 페이지 2,112 KiB였다. 1바이트 수정 피크는 +각각 1,104·1,056·1,056 KiB였다. 이 한 번씩의 값은 작은 차이의 우열을 +판정하지 못하지만 세 방식 모두 같은 규모의 전체 CLI 메모리를 사용했다. +[기본](../../research/experiments/005-storage-edit-latency/flat-memory.json), +[파일 간 복사](../../research/experiments/005-storage-edit-latency/copy-memory.json), +[페이지](../../research/experiments/005-storage-edit-latency/paged-memory.json)의 +원시 단위·실행물 해시를 함께 남겼다. + +페이지 경로는 같은 회차의 기본 경로보다 16/18회, 파일 간 복사보다 17/18회 +짧았다. 그러나 한 번은 565.5 ms로 길었다. 파일 간 복사는 사용자 공간 호출을 +줄이지만, 이 환경의 완전한 내구성 편집 시간에서는 기본 경로와 구분되는 +중앙값 이득을 보이지 않았다. 이를 보편적인 처리량 결과로 확장하지 않는다. + +한 문서를 70번 연속 수정하는 [원자료](../../research/experiments/005-storage-edit-latency/2026-09-24-sequence-ext4-linux-aarch64.json)에서는 +기본 파일 83.176 ms, 페이지 저장소 18.858 ms가 중앙값이다. 페이지가 같은 +수정 회차에서 더 짧은 경우는 59/70회였다. 64회차까지 페이지 세그먼트의 +할당량이 2,101,248 B로 증가했고, **65회차에 재구성**되며 1,052,672 B로 +내려갔다. 이번 재구성은 35.067 ms였으나 다른 회차에 219.485 ms 지연이 +발생했다. 즉, 재구성만이 긴 지연의 원인은 아니다. 저장 장치의 캐시·동기화· +백그라운드 작업을 통제하지 않았으므로 꼬리 지연 상한은 제시할 수 없다. + +현재 선택형 페이지 저장소의 [전체 검사 기록](../../research/experiments/005-storage-edit-latency/2026-09-24-check-hybrid.log.gz)은 +앱 동작, 충돌·스냅샷·실패·재구성 C 검사와 Linux 코드·RSS 상한을 통과했다. +페이지 저장소의 링크 `text`는 기본 CLI보다 **5,257 B 크다**. 기본 코어는 +변경하지 않았다. 또한 기존 기본 저장소는 새 페이지 형식을 직접 읽지 못하고, +선택형 저장소는 OS의 16 KiB 페이지 크기를 요구한다. 이후 +[오프라인 형식 내보내기](062-paged-store-rollback.md)는 확인했지만 macOS 실행과 +운영 중 투명한 전환은 검증하지 못했다. 따라서 지금은 페이지 저장을 **선택형 연구 +프로필**로 유지하고, 이 실측 결과를 형식 전환·복구 설계의 우선순위 근거로 +사용한다. 작은 문서의 쓰기량, 충돌, 오래된 읽기와 실패 계약은 +[기존 하이브리드 기록](034-hybrid-document-storage.md)에 있다. diff --git a/docs/research/062-paged-store-rollback.md b/docs/research/062-paged-store-rollback.md new file mode 100644 index 0000000..1f734dc --- /dev/null +++ b/docs/research/062-paged-store-rollback.md @@ -0,0 +1,45 @@ +# 062 — 선택형 페이지 저장소를 기본 불변 형식으로 되돌리기 + +[061](061-durable-document-edit-latency.md)의 페이지 저장소는 실제 큰 문서 +수정에서 짧은 중앙값을 보였지만, 기본 저장기가 `PUREP001`을 읽지 못했다. +이 형식 경계를 줄이기 위해 [문서 앱 전용 내보내기](../../prototypes/connections/export_flat_store.py)를 +만들었다. 도구는 **새 디렉터리**에 현재 본문과 리비전을 `PUREB001`로 기록하고, +기본 고정 CLI로 전부 재읽은 후에 디렉터리를 공개한다. 원본을 변경하지 않는다. +런타임 코어와 기본 앱의 C 소스·링크 경로에는 코드가 추가되지 않았다. + +변환 동안 원본 `.writer.lock`의 공유 POSIX 잠금을 유지한다. 협력하는 쓰기 +프로세스는 `busy`를 받으며, 키 목록과 각 읽기는 한 시점의 일관된 상태를 본다. +각 대상 파일과 대상 디렉터리를 동기화하고, 임시 디렉터리를 최종 경로로 +이름 변경한 뒤 상위 디렉터리도 동기화한다. 중간 읽기가 실패하면 임시 +디렉터리를 지우고 최종 경로를 공개하지 않는다. 대상 경로는 실행 전에 +존재하지 않아야 하며, 그 경로를 다른 프로세스가 동시에 생성하지 않아야 한다. +서비스 쓰기가 `busy`를 받지 않도록 유지보수 중 실행하는 경로다. + +[실제 앱 검사](../../prototypes/connections/results/2026-09-24-flat-export/export-check.json)는 +빈 값·1바이트·16 KiB 경계·16 KiB 초과·1 MiB 문서를 생성하고, 큰 문서를 +65회 수정한 후 내보냈다. 모든 본문과 리비전이 새 기본 저장기에서 일치했고, +기본 저장기로 이어서 수정하자 리비전이 66→67로 증가했다. 원본은 66을 +유지했다. 변환 중 별도 쓰기는 `busy`, 종료 뒤 같은 쓰기는 성공했다. +기존 대상·심볼릭 링크 객체·주입한 중간 읽기 실패도 거절하고 미완성 +대상을 남기지 않았다. + +[1,024개 추가 문서 검사](../../prototypes/connections/results/2026-09-24-flat-export/export-scale-1024.json)는 +총 1,029개 객체, 1,212,418 논리 바이트를 변환하고 각 객체를 기본 CLI로 +재읽었다. 이는 `/tmp` 파일시스템의 기능 검사이며 처리량 자료가 아니다. +[ext4 경로 검사](../../prototypes/connections/results/2026-09-24-flat-export/export-ext4.json)는 +같은 다섯 문서와 동기화·공개·복원을 작업트리의 ext4에서 실행했다. +재현 명령은 `make -C prototypes/connections check-flat-export`다. 현재 +`check-hybrid`에도 이 검사가 포함된다. + +[새 소스 묶음](../../prototypes/connections/results/2026-09-24-flat-export-source/pure-source-preview.tar.gz)은 +168개 파일, 압축 329,118 B이며 [별도 디렉터리 검사](../../prototypes/connections/results/2026-09-24-flat-export-source/source-check-linux-aarch64.json)에서 +이 변환, C·TUI·CLI 동작과 Linux 릴리스 상한을 통과했다. 앞선 묶음과 +해시를 비교하면 실행용 C·헤더는 모두 같다. 새 코드는 선택형 Python +변환기·검사와 사용 안내에만 있다. + +도구는 번들된 `editor.pure` 문서 앱의 고정 CLI를 사용하며 범용 저장소 +마이그레이터는 아니다. 소스가 16 KiB 페이지를 요구하고 Apple Silicon에서는 +아직 컴파일·실행하지 않았다. 원본을 제자리에서 바꾸거나 실행 중 쓰기를 +투명하게 이전하지 않으며, 파일 손상 복구나 모든 전원 장애를 증명하지 않는다. +기본 저장기는 여전히 페이지 파일을 직접 읽지 못한다. 이 경로는 선택형 +페이지 실험을 되돌릴 실제 수단이지, 기본 저장 경로 승격의 근거 전체는 아니다. diff --git a/docs/research/063-real-in-process-routing.md b/docs/research/063-real-in-process-routing.md new file mode 100644 index 0000000..24296c5 --- /dev/null +++ b/docs/research/063-real-in-process-routing.md @@ -0,0 +1,47 @@ +# 063 — 실제 문서 하나를 같은 프로세스의 여러 실행 영역으로 전달 + +Pure의 출력이 다른 입력이 되는 코어 계약을 실제 앱 데이터로 연결했다. +선택형 [네이티브 `pure-mirror`](../../prototypes/connections/mirror.c)는 +빌드 시 준비한 [`storage.pure`](../../prototypes/connections/examples/storage.pure)를 +사용한다. 원본 저장소·각 대상 저장소는 같은 프로세스의 독립된 `PureRuntime` +영역이다. 원본 문서를 한 번 읽어 매핑하고 `pure_runtime_forward`로 최대 8개 +대상에 전달한다. 대상 호스트의 입력 할당기는 `NULL`로 두었다. 원본 호출자의 +티켓을 대상 실행 전에 해제해도 코어가 매핑을 유지하며, 마지막 대상 완료 후 +영역의 보유 바이트와 작업 슬롯이 0으로 돌아오는 것을 호스트가 확인한다. +대상 파일을 영속화하는 쓰기는 각각 수행한다. **절약한 것은 그 사이의 추가 +입력 버퍼·직렬화**, 전체 저장 바이트나 디스크 쓰기가 아니다. + +[실제 ext4 검사](../../prototypes/connections/results/2026-09-24-in-process-routing/mirror-ext4.json)는 +`editor.pure`로 만든 1 MiB 문서를 두 영역에 전달해 각 최종 본문·리비전을 +새 CLI에서 검사했다. 원본을 한 바이트 수정해도 두 대상은 이전 스냅샷을 +유지했다. 다음 전달에서 한 대상은 리비전 2로 성공하고 다른 대상은 잘못된 +예상 리비전으로 충돌했다. 성공을 되돌리지 않고 대상별 결과를 보고하며, +충돌 대상은 올바른 리비전으로 다시 전달해 복구했다. 두 번째 대상의 키가 +잘못되면 모든 대상의 접수가 끝나기 전에 문서 쓰기를 시작하지 않았다. 빈 문서와 +원본 없음도 확인했다. 최대 8개 대상에도 1 MiB 본문·리비전이 모두 일치했다. +대상 예산을 1,024 B로 제한하면 1 MiB 전달은 문서 쓰기 전에 거절됐고, +예산이 0인 대상에는 빈 문서가 전달됐다. +[같은 시나리오의 임시 저장소 검사](../../prototypes/connections/results/2026-09-24-in-process-routing/mirror-tmp.json)도 통과했다. + +Linux AArch64에서 이 선택형 명령의 링크된 `text`는 **18,538 B**였다. +ext4의 1 MiB 문서를 두 저장소로 전달한 한 실행의 **전체 프로세스 피크 RSS는 +2,144 KiB**였고, 같은 실행기에서 8개 저장소로 전달한 샘플도 2,144 KiB였다. +대상별 예산 장부는 각각 1 MiB를 기록하지만 같은 원본 매핑의 크기를 물리 +메모리로 8번 더하지 않는다. 이 수치에는 libc와 파일 쓰기가 포함되지만 페이지 캐시, +커널 버퍼와 다른 앱은 포함되지 않는다. 기본 실행 코어·기본 CLI 코드에는 +변경이 없다. [ASan·UBSan 실행](../../prototypes/connections/results/2026-09-24-in-process-routing/mirror-sanitized.json)도 +같은 기능 검사를 통과했다. 이 샌드박스에서 LeakSanitizer는 ptrace 때문에 +실행되지 않아 누수 탐지는 그 검사에 포함되지 않는다. + +재현 명령은 `make -C prototypes/connections check-mirror`다. 이 경로는 +**프로세스 내 전달과 수명**의 실제 앱 증거다. 대상 간 원자적 트랜잭션, +강제 격리, 장애 시 자동 재시도, 장기 실행 영역 관리자, 네트워크 성능, +macOS 실행을 증명하지 않는다. 부분 성공을 호출자에게 명확히 보여주는 +것이 현재 계약이다. 이 결과를 일반적인 Kitex 수준 RPC 성능 주장으로 +확장하지 않는다. + +[최신 소스 묶음](../../prototypes/connections/results/2026-09-24-mirror-source/pure-source-preview.tar.gz)은 +170개 파일, 압축 332,792 B다. [분리된 디렉터리의 검증](../../prototypes/connections/results/2026-09-24-mirror-source/source-check-linux-aarch64.json)에서 +C·PTY·실제 전달·저장 형식 내보내기·Linux 릴리스 상한을 다시 통과했고, +독립 생성한 압축 파일의 해시도 일치했다. 앞선 묶음과 비교하면 기존 C·헤더는 +동일하며 새 C 파일은 이 선택형 명령 하나다. diff --git a/docs/research/064-release-callback-reentrancy.md b/docs/research/064-release-callback-reentrancy.md new file mode 100644 index 0000000..68ce819 --- /dev/null +++ b/docs/research/064-release-callback-reentrancy.md @@ -0,0 +1,29 @@ +# 064 — 해제 콜백의 영역 재진입 차단 + +실행 코어는 읽기·쓰기 효과 중 같은 영역으로 다시 들어오는 호출을 거절한다. +반면 버퍼의 `release` 콜백은 취소와 티켓 회수 중에도 호출되는데, 그동안 +`running`이 꺼져 있었다. 콜백이 같은 영역의 티켓을 해제하거나 새 작업을 +받아들이면, 바깥쪽 회수 코드가 작업 슬롯과 예산 장부를 계속 수정할 수 있었다. + +[`runtime.c`](../../prototypes/connections/runtime.c)는 자원 해제 콜백 동안 해당 +영역을 실행 중으로 표시하고 이전 상태를 복원한다. 새 상태 필드나 작업별 +할당은 없다. [`test-runtime.c`](../../prototypes/connections/test-runtime.c)는 +취소, 대기 작업 해제, 유효하지 않은 호스트 할당 반환에서 콜백이 같은 티켓을 +해제하거나 새 작업을 접수하려는 시도가 거절되는지 검사한다. + +Linux AArch64의 [기본 코어 검사](../../prototypes/connections/results/2026-09-24-release-callback/core.json)는 +`text` **5,908→5,980 B** (+72 B), data/BSS 0 B, 영역 128 B, 작업 슬롯 224 B를 +기록했다. [선택형 비동기 코어](../../prototypes/connections/results/2026-09-24-release-callback/async-core.json)는 +7,372 B로 자체 8 KiB 상한을 통과했다. [수명 검사](../../prototypes/connections/results/2026-09-24-release-callback/runtime.json), +ASan·UBSan 실행 및 `make check-release`의 실제 문서·HTTP·코드·RSS 검사가 +통과했다. 이 검사는 해제 콜백의 **같은 영역** 재진입을 막는 것이며, +프로세스 내 메모리 격리나 다른 영역으로의 호출 권한을 제공하지 않는다. + +수정한 코드가 담긴 [기본 CLI 묶음](../../prototypes/connections/results/2026-09-24-release-callback/pure-document-micro-linux-aarch64.tar.gz)은 +22,758 B이며 [분리된 위치의 편집·복원 검사](../../prototypes/connections/results/2026-09-24-release-callback/micro-package-check.json)를 +통과했다. [소스 묶음](../../prototypes/connections/results/2026-09-24-release-callback/pure-source-preview.tar.gz)은 +170개 파일, 압축 333,037 B다. [깨끗한 추출·빌드 검사](../../prototypes/connections/results/2026-09-24-release-callback/source-package-check.json)는 +PTY·문서 RSS·프로세스 내 전달·저장 형식 내보내기·Linux 릴리스 상한을 다시 +실행했다. 이 소스 배포 크기는 런타임의 상주 메모리 수치가 아니다. +독립적으로 다시 생성한 소스 묶음도 SHA-256 +`cff657597c0cc33bd9185c8460e0d84f0843da20a786578324ac612fa255c2b4`로 일치했다. diff --git a/docs/research/065-native-save-latency.md b/docs/research/065-native-save-latency.md new file mode 100644 index 0000000..1789ee2 --- /dev/null +++ b/docs/research/065-native-save-latency.md @@ -0,0 +1,57 @@ +# 065 — 실제 네이티브 편집기의 1바이트 저장 완료 시간 + +큰 문서의 작은 수정에서 페이지 저장소가 CLI 실험 밖에서도 유용한지 확인했다. +같은 빌드 시 준비된 `editor.pure`와 네이티브 TUI를 기본 불변 파일 저장소, +선택형 16 KiB 하이브리드 페이지 저장소에 각각 연결했다. 입력·커서·화면은 +같은 코드다. TUI에서 마지막 문자를 바꾸고 Ctrl+S를 보낸 순간부터 +**내구성 저장 완료가 화면에 표시된 프레임**까지 측정했다. 이 시간에는 초안 +비교, Pure 실행, 파일 `fsync`, 화면 갱신이 포함된다. 최초 문서 생성과 +사후의 독립 CLI 읽기는 제외했다. 매 세션의 최종 본문과 리비전은 그 CLI로 확인했다. + +2026-09-24 Linux AArch64, ext4 `/dev/mmcblk0p2`에서 256 B와 1 MiB 문서별로 +각 저장소를 세 세션씩 열고 세션마다 8회 수정했다. 저장소 순서를 세션마다 +뒤집었다. 아래 수치는 저장소·문서 크기별 **24회**의 중앙값이다. +[측정 코드](../../research/experiments/006-native-save-latency/measure.py)와 +[매회 시간·실행물 해시·최종 본문 해시](../../research/experiments/006-native-save-latency/2026-09-24-ext4-linux-aarch64.json)를 +남겼다. + +| Ctrl+S → 저장 완료 표시 | 기본 파일 | 선택형 페이지 | +|---|---:|---:| +| 256 B 문서의 마지막 1 B 수정 | 8.54 ms | 9.23 ms | +| 1 MiB 문서의 마지막 1 B 수정 | 93.17 ms | 22.04 ms | + +1 MiB의 세 세션별 중앙값은 기본 93.19·93.83·92.17 ms, 페이지 +21.34·22.04·22.86 ms였다. 이 작업에서는 약 **4.2배** 차이가 났다. +256 B에는 페이지 경로의 이득이 없다. 최대 지연도 각각 349.60·97.70 ms로 +흔들렸으므로 꼬리 지연의 보편적 상한은 주장하지 않는다. 장치의 캐시·동시 +작업·온도는 통제하지 않았다. + +비용도 따로 기록했다. 같은 Linux 빌드의 고정 정의 TUI 링크 `text`는 +**32,079→37,021 B** (+4,942 B), 소스 정의 TUI는 **48,684→53,642 B** +(+4,958 B)다. 현재의 소스 CLI+TUI 압축 묶음은 +[기본 51,117 B](../../prototypes/connections/results/2026-09-24-native-save/pure-document-flat-cli-tui-linux-aarch64.tar.gz)와 +[선택형 페이지 57,788 B](../../prototypes/connections/results/2026-09-24-native-save/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz)다. +각각 [기본형](../../prototypes/connections/results/2026-09-24-native-save/flat-package-check.json)과 +[페이지형](../../prototypes/connections/results/2026-09-24-native-save/package-check.json)을 +분리된 디렉터리에서 검증했다. +선택형 CLI+TUI 묶음을 다시 생성해도 SHA-256 +`b3c156eb7880ee4cb91b3fe52854cb5069ef035d082b7d539fa2a79592247ec5`로 일치했다. +실제 [소스 TUI 검사](../../prototypes/connections/results/2026-09-24-native-save/hybrid-source-tui.json)는 +1 MiB 편집·UTF-8·충돌·1,024개 문서 조작을 통과했고, 한 실행의 피크 RSS는 +3,200 KiB였다. [기본형의 별도 실행](../../prototypes/connections/results/2026-09-24-native-save/flat-source-tui.json)은 +3,216 KiB였다. 이 한 번씩의 RSS로 미세한 메모리 우열을 판정하지 않는다. + +선택형 패키지는 `.pure`를 실행 때 읽어 정의를 바꿀 수 있다. 작은 문서는 +기존 `PUREB001`이고 큰 문서는 `PUREP001`이 된다. 기본 저장기는 큰 페이지 +문서를 직접 읽을 수 없으므로 별도 저장소에서 사용하고, 기본 형식으로 돌아갈 +때는 [오프라인 내보내기](062-paged-store-rollback.md)를 거쳐야 한다. +16 KiB OS 페이지가 필요한 이 실험은 Linux에서만 실행했고 macOS는 미검증이다. +기본 코어와 기본 TUI는 변경하지 않았다. 이 비용·형식 경계 때문에 페이지 +저장소는 **명시적 선택 기능**으로 유지한다. + +최신 [소스 배포본](../../prototypes/connections/results/2026-09-24-native-save/pure-source-preview.tar.gz)은 +171개 파일, 압축 333,925 B다. [다른 디렉터리의 깨끗한 빌드](../../prototypes/connections/results/2026-09-24-native-save/source-package-check.json)에서 +이 선택형 TUI의 빌드·실제 문서 작업·패키징·이동 후 검사를 기본 C·PTY·전달· +내보내기·Linux 릴리스 검사와 함께 통과했다. 별도 생성한 소스 배포본도 +SHA-256 `b258788cc9f90d8e6eccc964c8c9f4e2c5460df3dfbb9da07268da07799691e4`로 +일치했다. 소스 압축 크기는 상주 메모리와 다르다. diff --git a/docs/research/066-application-boundary.md b/docs/research/066-application-boundary.md new file mode 100644 index 0000000..44e6d86 --- /dev/null +++ b/docs/research/066-application-boundary.md @@ -0,0 +1,65 @@ +# 066 — 작은 코어에서 하나의 앱 경계 만들기 + +2026-09-24. 첫 공개 체크포인트를 위한 당시 코드 경계 조사. 아래 수치는 변경 전 +기준선이고, 문서 해석을 코어 밖으로 옮긴 결과는 [068](068-opaque-app-core.md)에 있다. + +## 현재 코드가 증명하는 것과 못 하는 것 + +`prototypes/connections/runtime.c`는 입력 접수, 바이트 예산, 작업 슬롯, 완료, +대여와 다른 영역으로의 무복사 전달을 소유한다. 하지만 실행 단계에서 +`PureProgram`의 저장 전용 `read`/`write`/`splice`를 직접 해석하고 +`PureHost.read`/`write`/`splice`를 호출한다. 따라서 현재 5,980 B 코어는 +**범용 앱 코어의 크기**가 아니라 **문서 저장 실행 프로필의 크기**다. + +`prototypes/workbench/native/app.c`는 별도의 `PureScalarPlan`을 실행한다. +터치·키·숫자 입력은 `write("event")`, 상태 스냅샷은 `read("snapshot")`, +복원은 `write("restore")`라는 호스트 키로 연결된다. 수명·예산·저장·복원은 +같은 `PureRuntime`을 통과하지만, 작성한 scalar 규칙이 문서의 바이트 효과를 +선택하거나 거절하지는 못한다. 두 문법을 한 앱 표현이라고 주장할 수 없다. + +이 차이를 없애려고 모든 입력을 범용 객체로 포장하거나 코어에 새 VM, +프로세스 관리자, 프로토콜 파서를 추가하면 마이크로 목표와 충돌한다. +기존 작업물에 **숫자 계산과 바이트 효과가 함께 필요한 한 작업**을 추가해 +필요한 경계만 추출하는 순서가 더 검증 가능하다. + +## 첫 통합 작업과 실패 조건 + +기존 문서 편집 앱의 부분 수정 경로에 작성 가능한 크기 정책을 연결한다. +입력은 현재 문서 길이·제거 길이·삽입 길이·설정된 한도이고, 결과는 +저장 효과의 허용 여부와 오류다. 허용되면 기존의 조건부 splice가 실제 +바이트를 처리하고, 거절되면 저장소와 리비전은 변하지 않는다. 정책은 +CLI·TUI·인증 피어가 같은 준비된 앱 정의에서 읽어야 한다. 단순히 각 +호스트에 같은 `if`를 복사하면 이 실험은 실패다. 정책만 있는 새 데모나 +별도 언어를 만드는 것도 실패다. + +필요한 입력·출력 계약은 작다: 검증된 정수 길이, 조건부 효과 결정, +대여한 바이트 범위, 버전, 결과 상태. 호스트가 소유한 파일·소켓·화면 +핸들은 이 계약의 일부가 아니다. 연결될 때만 작업 슬롯과 payload 예산을 +사용하고, 유휴 앱마다 작업자·큐·힙을 만들지 않는다. + +검증은 실제 문서에 대해 한도가 허용/거절하는 수정, 버전 충돌, 1 MiB +본문, CLI/TUI/피어 간 동일 결과, 실패 시 상태·리비전 불변, 대여 회수를 +포함한다. 정책을 수정한 뒤 동작이 바뀌는 작성 경로와, 파서를 제외한 +고정 실행 경로도 각각 확인한다. 비교 대상은 정책을 호스트 C 코드에 +직접 구현한 동등 기능이다. 지연·메모리 우위는 측정 전까지 주장하지 않는다. + +## 코어 경계와 크기 합격 조건 + +현재 저장 전용 `PureProgram` 해석과 `splice` 필드는 앱/효과 어댑터의 +후보이다. 우선 실제 문서 앱과 scalar 앱의 작업에서 공통인 접수·예산· +완료·대여·전달만 분리하고, 각 앱의 계산과 외부 I/O는 선택형 코드로 +유지한다. 이 과정에서 기존의 취소, 비동기 완료, 다중 영역 전달 계약을 +깨지 않는지 확인해야 한다. 이름만 `read/write`에서 `input/output`으로 +바꾸는 작업은 의미가 없다. + +현재 [기본 코어 예산](../../prototypes/connections/core-budget.json)은 Linux +AArch64/GCC의 `text` 6 KiB, 유휴 영역 128 B, 작업 슬롯 216 B 상한이다. +통합 뒤에도 기본 코어, 선택한 앱의 링크 코드·전체 RSS·유휴 인스턴스, +실제 작업당 payload/복사량을 **각각** 비교한다. 코어 밖으로 비용을 +옮겨서 전체 앱이 커졌다면 개선으로 취급하지 않는다. 문서 앱의 기존 +크기·내구성·복원 검사를 통과하지 못해도 채택하지 않는다. macOS arm64 +빌드·실행 증거는 별도로 필요하다. + +이 통합이 성공한 뒤에야 UI와 저장 외의 네트워크/RPC 입력에 같은 계약을 +적용한다. 이것은 OS 프로세스, pod 또는 VM을 상시 생성하겠다는 계획이 +아니다. 강제 격리가 필요한 배치는 호스트 경계의 선택으로 남긴다. diff --git a/docs/research/067-work-slot-layout.md b/docs/research/067-work-slot-layout.md new file mode 100644 index 0000000..55097be --- /dev/null +++ b/docs/research/067-work-slot-layout.md @@ -0,0 +1,46 @@ +# 067 — 작업 슬롯 8 B 절감, 실행 코드 증가 없음 + +2026-09-24. Linux AArch64, GCC 14.2, C17 `-O2`; 첫 공개 체크포인트의 +반복 메모리 경계 수정이다. 현재 `PureWork`는 내부 저장 구조이며 ABI나 +디스크·네트워크 형식이 아니다. + +64 B 이름과 종단 NUL, 수명 티켓, 자원·결과·예산 필드를 그대로 둔 채 +필드 순서를 바꿨다. 같은 64비트 호스트에서 `sizeof(PureWork)`는 +**224→216 B**다. 1,024개 동시 작업 슬롯의 정적 배열이라면 정확히 +8,192 B를 덜 예약한다. 실제 서비스 RSS가 8,192 B 줄었다고 주장하지 +않는다. 유휴 앱은 이미 작업 슬롯을 분리하므로 이득은 활성 슬롯에만 있다. +64비트 C 호스트에는 슬롯 216 B, 기본 영역 128 B, 계측 영역 200 B의 +컴파일 시 검사를 추가했다. 크기 상한 JSON도 216 B로 낮췄다. + +| Linux AArch64 결과 | 이전 | 현재 | +|---|---:|---:| +| 기본 코어 오브젝트 `text` | 5,980 B | 5,980 B | +| 선택형 비동기 코어 오브젝트 `text` | 7,372 B | 7,328 B | +| 기본 영역 / 활성 작업 슬롯 | 128 / 224 B | 128 / 216 B | +| 소스 CLI / HTTP 링크 `text` | 35,863 / 43,978 B | 35,855 / 43,826 B | +| 고정 CLI / HTTP 링크 `text` | 19,324 / 26,589 B | 19,316 / 26,437 B | +| 고정 / 소스 TUI 링크 `text` | 32,079 / 48,684 B | 32,071 / 48,676 B | + +계측 프로필은 이 필드 배치에서 코어 오브젝트 `text` 5,968 B, +영역 200 B다. 기본 코어보다 코드 섹션이 12 B 작게 나온 것은 이 +컴파일러의 코드 생성 결과일 뿐 계측 비용이 없다는 뜻이 아니다. +기본 앱의 실제 1 MiB 편집 피크 RSS는 이번 Linux 검사에서 소스 CLI +2,080 KiB, 고정 CLI 2,112 KiB, 고정/소스 TUI 3,168/3,200 KiB였다. +이전 측정과의 인과적 RSS 차이는 주장하지 않는다. + +`make check-release`, `check-async`, `check-async-server`, `check-mirror`, +`check-tui`, `check-source-tui`, `check-portable`을 통과했다. C 호스트를 +현재 소스와 기존 동일 해시의 Zig scalar 라이브러리로 다시 링크해 +작성 앱의 세 인스턴스 입력·상태·저장·복원·정의 교체도 통과했다. +이 검사는 Zig 라이브러리를 새로 빌드한 증거가 아니다. + +[새 소스 묶음](../../prototypes/connections/results/2026-09-24-work-slot/pure-source-preview.tar.gz)은 +171개 파일, 334,267 B이고, [깨끗한 추출 검사](../../prototypes/connections/results/2026-09-24-work-slot/source-package-check.json)는 +C·실제 TUI·문서 RSS·수정한 정의·영역 간 1 MiB 전달·페이지 형식 +내보내기·선택형 TUI 패키지·Linux 릴리스 경로를 재실행했다. +[소스 CLI](../../prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-linux-aarch64.tar.gz), +[고정 CLI](../../prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-linux-aarch64.tar.gz), +[고정 CLI+TUI](../../prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-tui-linux-aarch64.tar.gz), +[소스 CLI+TUI](../../prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-tui-linux-aarch64.tar.gz)는 +각각 이동 후 실제 문서 작업을 통과했다. 압축 크기는 차례로 22,784, +12,972, 32,119, 51,167 B다. macOS arm64 실행은 여전히 미검증이다. diff --git a/docs/research/068-opaque-app-core.md b/docs/research/068-opaque-app-core.md new file mode 100644 index 0000000..419338f --- /dev/null +++ b/docs/research/068-opaque-app-core.md @@ -0,0 +1,74 @@ +# 068 — 문서 해석을 떼어낸 마이크로 실행 코어 + +2026-09-24. Linux AArch64, GCC 14.2, C17 `-O2`. [066](066-application-boundary.md)의 +첫 경계 수정이다. 목적은 새 운영체제 기능을 늘리는 것이 아니라, 모든 앱이 +링크해야 하는 실행 코어에서 문서 전용 연산을 제거하는 것이다. + +`PureRuntime`은 이제 앱 정의를 불투명 포인터로 보관한다. 코어는 입력 접수, +작업 슬롯·바이트 예산, 완료, 결과 대여, 취소, 영역 간 전달을 관리한다. +문서의 필드 선택·응답 구간·splice 계산은 선택형 `store-port.c`가 해석한다. +코어 아카이브는 `runtime.o` 한 개이며 `evaluate.o`나 `PureProgram`을 +포함하지 않는다. scalar 앱은 정의 포인터 없이 같은 코어를 쓴다. +비동기 효과는 원시 입력을 빌려 주고, 선택한 호스트가 문서 규칙을 적용한 +뒤 입력 내부의 응답 구간을 돌려준다. 코어는 이 대여 범위를 검사한다. + +| 비용 | 이전 067 | 이번 결과 | 차이 | +|---|---:|---:|---:| +| 기본 코어 오브젝트 `text` | 5,980 B | **4,632 B** | -1,348 B | +| 비동기 코어 오브젝트 `text` | 7,328 B | **5,724 B** | -1,604 B | +| 기본 영역 / 작업 슬롯 | 128 / 216 B | **120 / 216 B** | -8 / 0 B | +| 계측 영역 | 200 B | **192 B** | -8 B | +| 소스 CLI / HTTP 링크 `text` | 35,855 / 43,826 B | **35,743 / 43,950 B** | -112 / +124 B | +| 고정 CLI / HTTP 링크 `text` | 19,316 / 26,437 B | **19,204 / 26,625 B** | -112 / +188 B | +| 고정 / 소스 TUI 링크 `text` | 32,071 / 48,676 B | **31,959 / 48,572 B** | -112 / -104 B | +| 비동기 소스 HTTP 링크 `text` | 48,537 B | **48,241 B** | -296 B | + +기본 코어의 `text`는 약 22.5% 줄었지만, **코어 절감량을 전체 앱 절감량으로 +말할 수 없다.** 문서 해석은 선택한 호스트에 필요하고, 특히 고정 HTTP 실행물은 +188 B 커졌다. 그래서 해당 호스트의 Linux/GCC 회귀 상한을 26,624→26,880 B로 +검토해 조정했다. 코어 상한은 6,144→4,864 B, 비동기 코어 상한은 +7,680→6,144 B로 낮췄다. 두 코어의 `data`/`bss`는 각각 0 B다. +이 수치는 오브젝트·링크 섹션이고 libc, 스택, 파일 매핑, payload와 +전체 프로세스 메모리를 포함하지 않는다. + +실제 1 MiB 문서 검사에서는 소스·고정 CLI 편집 피크가 모두 2,128 KiB, +고정/소스 TUI는 3,168/3,200 KiB였다. 고정 HTTP 호스트의 테스트 중 피크는 +2,112 KiB였다. 이전 런과의 RSS 인과 차이는 주장하지 않는다. 입력의 +할당·복사 계약도 이번 변경으로 자동 최적화된 것은 아니다. + +`check-release`, `check-async`, `check-async-server`, `check-fixed-server`, +`check-mirror`, `check-tui`, `check-source-tui`, `check-portable`을 통과했다. +동기 고정·소스 HTTP와 선택형 비동기 소스 HTTP는 각각 1,024개 실제 +8 KiB 문서, 8개 동시 클라이언트, 4,096개 작업에서 같은 최종 문서 해시를 +만들었다. 각 호스트는 종료 시 활성 작업·보존 payload가 0이었고, +측정 피크 RSS는 고정 1,680 KiB, 소스 1,712 KiB, 비동기 소스 1,760 KiB였다. +이는 기능·자원 회귀 검사이며 처리량 우위의 증거는 아니다. +선택형 TLS 피어와 서버 전용 피어도 인증·외부 클라이언트·1 MiB 문서· +재시작 검사를 통과했다. 링크 `text`는 각각 46,894 / 43,422 B이고, +OpenSSL 공유 라이브러리의 코드·메모리는 이 섹션 크기에 포함되지 않는다. +ASan/UBSan을 적용한 C 계약 검사도 통과했다. 이 환경의 ptrace 아래에서는 +LeakSanitizer가 실행되지 않아 해당 검사에서는 누수 탐지를 끄고 실행했다. +기존 해시의 Zig scalar 정적 라이브러리를 현재 C 호스트와 다시 링크한 +세 인스턴스 입력·저장·복원 검사와 고정 정의 두 인스턴스 검사도 통과했다. +Zig 자체를 새로 빌드한 +증거는 아니다. + +[새 소스 묶음](../../prototypes/connections/results/2026-09-24-opaque-core/pure-source-preview.tar.gz)은 +171개 파일, 334,682 B다. [깨끗한 추출 검사](../../prototypes/connections/results/2026-09-24-opaque-core/source-package-check.json)는 +C·TUI·문서 RSS·수정한 `.pure` 동작·1 MiB 영역 간 전달·선택형 저장 형식 +내보내기·Linux 릴리스 경로를 다시 실행했다. +[소스 CLI](../../prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz), +[고정 CLI](../../prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz), +[고정 CLI+TUI](../../prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz), +[소스 CLI+TUI](../../prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz)는 +각각 이동 후 실제 문서 작업을 통과했다. 압축 크기는 순서대로 +22,655 / 12,886 / 31,965 / 51,005 B다. 압축 크기는 RSS가 아니다. + +이 변경으로 하나의 작성 가능한 앱 모델이 생긴 것은 아니다. 코어에는 여전히 +문서에서 물려받은 64 B ASCII 키와 `READ`/`WRITE` 두 입력 종류가 있다. +scalar 규칙과 문서 효과는 서로 다른 작성 모델이며, 네트워크/RPC와 UI의 +일반 연결도 아직 합쳐지지 않았다. 다음 검증은 실제 앱의 숫자 정책이 문서 +수정을 허용·거절하는 한 경로를 동일한 작성 정의에서 실행하는 것이다. +그때도 문서·네트워크·UI 코드를 기본 코어에 넣지 않고, 선택한 앱의 +전체 링크 비용·프로세스 RSS·작업별 보존 바이트를 함께 비교해야 한다. +macOS arm64 빌드와 실행은 아직 수행하지 않았다. diff --git a/docs/research/069-atomic-authored-size-policy.md b/docs/research/069-atomic-authored-size-policy.md new file mode 100644 index 0000000..4718e89 --- /dev/null +++ b/docs/research/069-atomic-authored-size-policy.md @@ -0,0 +1,72 @@ +# 069 — 작성한 크기 정책을 실제 문서 효과에 연결 + +2026-09-24. Linux AArch64, GCC 14.2, C17 `-O2`. 첫 공개 체크포인트를 향한 +실제 앱 경계 검증이다. [068](068-opaque-app-core.md)의 불투명 실행 코어는 +변경하지 않았다. + +[`bounded-editor.pure`](../../prototypes/connections/examples/bounded-editor.pure)는 +`store.splice(..., edit.insert, 8)`로 수정 후 문서의 최대 바이트 수를 작성한다. +준비 단계에서 상수를 검사하고, 저장 어댑터가 그 값을 효과에 전달한다. +두 저장 구현은 쓰기 잠금 안에서 현재 버전의 크기, 제거 길이, 삽입 길이로 +최종 크기를 계산한다. 초과하면 새 파일·리비전을 공개하지 않는다. +정책 검사를 위한 별도 읽기나 전체 문서 버퍼는 만들지 않는다. 요청 본문이 +상한 값을 결정하지도 못한다. 예상 리비전 충돌을 먼저 판단하므로 낡은 +요청은 정책 결과로 충돌을 가리지 않는다. + +CLI에서 6 B 생성→2 B 추가는 허용됐고 1 B 추가는 거절됐으며, 기존 8 B +내용과 리비전 2가 유지됐다. 1 B 제자리 교체는 허용됐다. 정의의 상한을 +4 B로 바꾸고 새 소스 호스트를 시작하자 같은 길이의 교체도 거절되고, +4 B 삭제만 허용됐다. 고정 빌드는 처음 준비한 8 B 정책을 유지했다. +소스·고정 HTTP는 같은 저장 파일에서 동일한 결과를 냈고, 두 TUI는 +저장 거절 뒤 초안을 보존했다. 선택형 상호 인증 TLS 피어도 초과 수정은 +상태 5(`limit`)로 거절하고 허용한 삭제는 CLI에서 같은 버전으로 읽혔다. +[실행 증거](../../prototypes/connections/results/2026-09-24-atomic-policy/policy.json)와 +[인증 피어 증거](../../prototypes/connections/results/2026-09-24-atomic-policy/policy-peer.json)에 +재현된 시나리오를 기록했다. + +| Linux AArch64 `text` / 저장 비용 | 이전 068 | 현재 | 변화 | +|---|---:|---:|---:| +| 기본 실행 코어 | 4,632 B | **4,632 B** | 0 | +| 유휴 영역 / 활성 슬롯 | 120 / 216 B | **120 / 216 B** | 0 / 0 | +| 준비된 문서 정의 | 680 B | **680 B** | 0 | +| 소스 CLI / HTTP | 35,743 / 43,950 B | **36,135 / 44,342 B** | +392 / +392 B | +| 고정 CLI / HTTP | 19,204 / 26,625 B | **19,268 / 26,689 B** | +64 / +64 B | +| 고정 / 소스 TUI | 31,959 / 48,572 B | **32,023 / 48,972 B** | +64 / +400 B | +| 선택형 비동기 소스 HTTP | 48,241 B | **48,633 B** | +392 B | +| 선택형 TLS 겸용 피어 | 46,894 B | **47,286 B** | +392 B | +| 선택형 혼합 저장소 HTTP | 49,133 B | **49,589 B** | +456 B | + +`PureProgram`의 정책 값은 기존 정렬 패딩에 배치했다. 64비트 호스트의 +680 B 크기를 컴파일 시 검사한다. 기본 코어 아카이브에는 계속 `runtime.o` +하나만 있으며, 문서 평가기·정책·저장소는 포함되지 않는다. 선택형 혼합 +저장소 HTTP는 기존 49,152 B 상한을 437 B 초과했다. 이전 소스 묶음을 +같은 도구로 다시 빌드해 49,133 B 기준을 확인했고, 456 B 증가를 검토해 +선택형 상한만 50,176 B로 조정했다. 기본 코어 상한은 그대로 4,864 B다. +소스 TUI는 현재 49,152 B 상한까지 180 B만 남는다. 이후 기능은 이 +선택형 실행물의 코드 구조를 다시 검토해야 하며 상한을 자동으로 늘리지 않는다. +이 수치들은 코드 섹션이고 libc/OpenSSL, 스택, payload 및 저장 파일을 +포함하지 않는다. 기존 1 MiB 문서 검사의 피크 RSS는 소스 CLI 2,080 KiB, +고정 CLI 2,112 KiB였다. 런 간 차이를 정책의 RAM 효과라고 주장하지 않는다. + +`check-release`에는 실제 CLI·HTTP·TUI 정책 경로를 넣었다. +`check-policy-peer`, `check-hybrid`, `check-async`, `check-async-server`, +기존 TUI·인증 피어 검사와 ASan/UBSan C·정책 검사가 통과했다. 이 환경의 +ptrace 아래에서 LeakSanitizer는 실행되지 않아 누수 탐지는 끄고 실행했다. +현재 C 호스트를 기존 Zig scalar 정적 라이브러리와 다시 링크한 세 앱 +인스턴스 검사도 통과했지만 Zig를 새로 빌드한 증거는 아니다. + +[소스 묶음](../../prototypes/connections/results/2026-09-24-atomic-policy/pure-source-preview.tar.gz)은 +173개 파일이다. [깨끗한 추출 검사](../../prototypes/connections/results/2026-09-24-atomic-policy/source-package-check.json)는 +릴리스·TUI·TLS·문서 전달·저장 형식 변환 경로를 다시 실행했다. +[소스 CLI](../../prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz), +[고정 CLI](../../prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz), +[고정 CLI+TUI](../../prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz), +[소스 CLI+TUI](../../prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz)는 +각각 이동한 위치에서 실제 문서 작업을 통과했다. 압축 크기는 순서대로 +22,862 / 12,966 / 32,144 / 51,398 B다. + +이것은 **문서 수정 효과 안의 한 숫자 정책**이다. scalar 앱의 규칙 언어와 +문서 앱이 하나로 합쳐진 것은 아니며, 일반 산술·조건부 효과·여러 효과, +네트워크/RPC의 범용 연결도 아직 없다. 바이트 상한은 문자나 문자소 +수의 상한이 아니다. macOS arm64 CI에는 정책 검사를 추가했지만, 해당 +빌드·실행 결과는 아직 확인하지 못했다. diff --git a/docs/research/070-micro-default-build.md b/docs/research/070-micro-default-build.md new file mode 100644 index 0000000..33b8408 --- /dev/null +++ b/docs/research/070-micro-default-build.md @@ -0,0 +1,31 @@ +# 070 — 기본 빌드의 비용 경계 + +2026-09-24. Linux AArch64, GCC 14.2, C17 `-O2`. 마이크로 런타임의 기본 빌드가 +선택형 기능을 실수로 끌어오지 않도록 `make all`을 코어와 고정 정의 CLI로 제한했다. +`editor.pure`를 빌드할 때 준비하는 작은 컴파일러 실행물도 생성되지만, 완성된 +CLI에는 링크하거나 배포하지 않는다. 실행 중 `.pure`를 읽고 바꾸는 CLI는 +`make editor`, HTTP는 `make server`, TUI와 인증 피어도 각각 명시적 선택이다. + +깨끗한 소스에서 기본 `make`를 실행했을 때 빌드 디렉터리에는 +`runtime.o`, `libpure-execution.a`, `pure-compile`, `embedded-plan.h`, +`pure-edit-fixed`만 있었다. 새 [소스 묶음](../../prototypes/connections/results/2026-09-24-micro-default/pure-source-preview.tar.gz)을 +다른 디렉터리에 풀어 같은 목록을 검사하고, CLI·TUI의 실제 PTY 작업, +1 MiB 문서 RSS, 소스 정의 수정, Linux 릴리스 검사를 다시 실행했다. +[추출 검사 결과](../../prototypes/connections/results/2026-09-24-micro-default/source-package-check.json)에 +재현 경로를 기록했다. + +| 비용 | 기본 고정 실행 경로 | 소스 읽기 선택 경로 | +|---|---:|---:| +| 코어 `text` | 4,632 B | 동일 | +| 문서 CLI 링크 `text` | 19,268 B | 36,135 B | +| 1 MiB 문서 편집 피크 RSS | 2,112 KiB | 2,080 KiB | + +이 변경은 링크 코드와 기본 의존성 선택을 줄였으며, RSS 개선을 입증한 것은 아니다. +측정된 RSS는 오히려 고정 CLI 쪽이 32 KiB 높았고 실행·페이지 배치의 변동과 +분리할 근거가 없다. 코어 `text`와 120 B 유휴 영역, 216 B 작업 슬롯은 +변하지 않았다. 두 CLI의 직접 동적 라이브러리 의존성은 이 호스트에서 libc 하나다. +빌드용 컴파일러, libc, 스택과 저장 파일 비용을 코어 수치에 포함하지 않았다. + +이후 앱 기능은 기본 코어에 넣기 전에 필수 의미론인지 확인하고, 선택한 +실행물의 링크 코드·실제 전체 메모리·배포 묶음 비용을 별도로 검사한다. +선택형 어댑터의 기능을 기본 빌드에 추가할 때는 같은 검사를 다시 통과해야 한다. diff --git a/prototypes/common/tls-link.c b/prototypes/common/tls-link.c new file mode 100644 index 0000000..adfaac2 --- /dev/null +++ b/prototypes/common/tls-link.c @@ -0,0 +1,217 @@ +#define _POSIX_C_SOURCE 200809L +#include "tls-link.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +uint64_t pure_link_now_ms(void) { + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts)) abort(); + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; +} +static bool ready(int fd, short events, uint64_t deadline) { + for (;;) { + uint64_t now = pure_link_now_ms(); + if (now >= deadline) return false; + int remaining = (int)(deadline - now); + struct pollfd p = {.fd = fd, .events = events}; + int result = poll(&p, 1, remaining); + if (result > 0) return !(p.revents & (POLLERR | POLLNVAL)) && (p.revents & (events | POLLHUP)); + if (!result) return false; + if (errno != EINTR) return false; + } +} +bool pure_link_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0; +} +static bool tls_retry(PureLink *channel, int code, uint64_t deadline) { + if (code == SSL_ERROR_WANT_READ) return ready(channel->fd, POLLIN, deadline); + if (code == SSL_ERROR_WANT_WRITE) return ready(channel->fd, POLLOUT, deadline); + return false; +} +int pure_link_read(PureLink *channel, void *output, size_t size, uint64_t deadline) { + size_t at = 0; + while (at < size) { + if (pure_link_now_ms() >= deadline) return -1; + size_t received = 0; + ERR_clear_error(); + int result = SSL_read_ex(channel->ssl, (unsigned char *)output + at, size - at, &received); + if (result == 1) { if (!received) return -1; at += received; continue; } + int code = SSL_get_error(channel->ssl, result); + if (code == SSL_ERROR_ZERO_RETURN && !at) return 0; + if (!tls_retry(channel, code, deadline)) return -1; + } + return 1; +} +bool pure_link_write(PureLink *channel, const void *input, size_t size, uint64_t deadline) { + size_t at = 0; + while (at < size) { + if (pure_link_now_ms() >= deadline) return false; + size_t sent = 0; + size_t chunk = size - at; + if (chunk > 16384) chunk = 16384; + ERR_clear_error(); + int result = SSL_write_ex(channel->ssl, (const unsigned char *)input + at, chunk, &sent); + if (result == 1) { if (!sent) return false; at += sent; continue; } + int code = SSL_get_error(channel->ssl, result); + if (!tls_retry(channel, code, deadline)) return false; + } + return true; +} +int pure_link_read_frame(PureLink *channel, unsigned char *output, size_t capacity, + size_t *size, uint64_t deadline) { + unsigned char prefix[2]; + int first = pure_link_read(channel, prefix, 2, deadline); + if (first != 1) return first; + size_t length = ((size_t)prefix[0] << 8) | prefix[1]; + if (!length || length > capacity || pure_link_read(channel, output, length, deadline) != 1) return -1; + *size = length; + return 1; +} +bool pure_link_write_frame(PureLink *channel, const unsigned char *data, size_t size, + uint64_t deadline) { + if (!size || size > UINT16_MAX) return false; + unsigned char prefix[2] = {(unsigned char)(size >> 8), (unsigned char)size}; + return pure_link_write(channel, prefix, 2, deadline) && pure_link_write(channel, data, size, deadline); +} +void pure_link_close(PureLink *channel, bool healthy) { + if (channel->ssl) { + if (healthy) (void)SSL_shutdown(channel->ssl); + SSL_free(channel->ssl); + } + if (channel->fd >= 0) close(channel->fd); + *channel = (PureLink){.fd = -1}; +} +static int select_alpn(SSL *ssl, const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int length, void *arg) { + (void)ssl; + const unsigned char *alpn = arg; + for (unsigned int at = 0; at < length;) { + unsigned int n = in[at++]; + if (!n || n > length - at) return SSL_TLSEXT_ERR_ALERT_FATAL; + if (n == alpn[0] && !memcmp(in + at, alpn + 1, n)) { + *out = alpn + 1; *outlen = alpn[0]; return SSL_TLSEXT_ERR_OK; + } + at += n; + } + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +SSL_CTX *pure_link_context(bool server, const char *ca, const char *cert, + const char *key, const unsigned char *alpn) { + SSL_CTX *ctx = SSL_CTX_new(server ? TLS_server_method() : TLS_client_method()); + if (!ctx) return NULL; + if (!alpn || !alpn[0] || !SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION) || + !SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION) || + !SSL_CTX_set_max_early_data(ctx, 0) || + !SSL_CTX_load_verify_locations(ctx, ca, NULL) || + SSL_CTX_use_certificate_chain_file(ctx, cert) != 1 || + SSL_CTX_use_PrivateKey_file(ctx, key, SSL_FILETYPE_PEM) != 1 || + SSL_CTX_check_private_key(ctx) != 1) { + SSL_CTX_free(ctx); return NULL; + } + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | (server ? SSL_VERIFY_FAIL_IF_NO_PEER_CERT : 0), NULL); + if (server) SSL_CTX_set_alpn_select_cb(ctx, select_alpn, (void *)alpn); + return ctx; +} +bool pure_link_handshake(PureLink *channel, SSL_CTX *ctx, bool server, + const char *peer_name, const unsigned char *alpn) { + /* This protocol writes a small reply header followed by a separate TLS + * body record. Disable Nagle on the connected TCP socket so that the + * header cannot hold the body behind a delayed acknowledgement. */ + int enabled = 1; + if (setsockopt(channel->fd, IPPROTO_TCP, TCP_NODELAY, &enabled, sizeof(enabled))) return false; + channel->ssl = SSL_new(ctx); + if (!channel->ssl || SSL_set_fd(channel->ssl, channel->fd) != 1) return false; + if (server) SSL_set_accept_state(channel->ssl); + else { + SSL_set_connect_state(channel->ssl); + SSL_set_hostflags(channel->ssl, X509_CHECK_FLAG_NO_WILDCARDS | X509_CHECK_FLAG_NEVER_CHECK_SUBJECT); + if (SSL_set1_host(channel->ssl, peer_name) != 1 || + SSL_set_tlsext_host_name(channel->ssl, peer_name) != 1 || + SSL_set_alpn_protos(channel->ssl, alpn, alpn[0] + 1u) != 0) return false; + } + uint64_t deadline = pure_link_now_ms() + PURE_LINK_DEADLINE_MS; + for (;;) { + ERR_clear_error(); + int result = SSL_do_handshake(channel->ssl); + if (result == 1) break; + int code = SSL_get_error(channel->ssl, result); + if (!tls_retry(channel, code, deadline)) return false; + } + if (SSL_version(channel->ssl) != TLS1_3_VERSION || SSL_get_verify_result(channel->ssl) != X509_V_OK) + return false; + X509 *peer = SSL_get1_peer_certificate(channel->ssl); + if (!peer) return false; + bool verified = !server || X509_check_host(peer, peer_name, 0, + X509_CHECK_FLAG_NO_WILDCARDS | X509_CHECK_FLAG_NEVER_CHECK_SUBJECT, NULL) == 1; + X509_free(peer); + const unsigned char *chosen = NULL; + unsigned int length = 0; + SSL_get0_alpn_selected(channel->ssl, &chosen, &length); + return verified && length == alpn[0] && !memcmp(chosen, alpn + 1, length); +} +bool pure_link_port_number(const char *text, bool allow_zero) { + if (!text || !*text) return false; + unsigned long value = 0; + for (const char *p = text; *p; ++p) { + if (*p < '0' || *p > '9') return false; + unsigned digit = (unsigned)(*p - '0'); + if (value > (65535ul - digit) / 10) return false; + value = value * 10 + digit; + } + return allow_zero || value != 0; +} +int pure_link_listen_at(const char *host, const char *port) { + struct addrinfo hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, + .ai_flags = AI_PASSIVE | AI_NUMERICHOST}; + struct addrinfo *addresses = NULL; + if (getaddrinfo(host, port, &hints, &addresses)) return -1; + int listener = -1; + for (struct addrinfo *it = addresses; it; it = it->ai_next) { + int fd = socket(it->ai_family, it->ai_socktype, it->ai_protocol); + if (fd < 0) continue; + int enabled = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)); + if (bind(fd, it->ai_addr, it->ai_addrlen) == 0 && listen(fd, 32) == 0 && pure_link_nonblocking(fd)) { + listener = fd; break; + } + close(fd); + } + freeaddrinfo(addresses); + return listener; +} +int pure_link_connect_to(const char *host, const char *port) { + struct addrinfo hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM}; + struct addrinfo *addresses = NULL; + if (getaddrinfo(host, port, &hints, &addresses)) return -1; + int connected = -1; + uint64_t deadline = pure_link_now_ms() + PURE_LINK_DEADLINE_MS; + for (struct addrinfo *it = addresses; it; it = it->ai_next) { + int fd = socket(it->ai_family, it->ai_socktype, it->ai_protocol); + if (fd < 0) continue; + if (pure_link_nonblocking(fd)) { + int result = connect(fd, it->ai_addr, it->ai_addrlen); + if (result == 0 || (result < 0 && errno == EINPROGRESS && ready(fd, POLLOUT, deadline))) { + int error = 0; socklen_t size = sizeof(error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) == 0 && !error) { + connected = fd; break; + } + } + } + close(fd); + } + freeaddrinfo(addresses); + return connected; +} diff --git a/prototypes/common/tls-link.h b/prototypes/common/tls-link.h new file mode 100644 index 0000000..53e05ff --- /dev/null +++ b/prototypes/common/tls-link.h @@ -0,0 +1,32 @@ +#ifndef PURE_TLS_LINK_H +#define PURE_TLS_LINK_H +#include +#include +#include +#include + +enum { PURE_LINK_DEADLINE_MS = 5000 }; +typedef struct { int fd; SSL *ssl; } PureLink; + +uint64_t pure_link_now_ms(void); +bool pure_link_nonblocking(int fd); +/* read returns 1 for all bytes, 0 for clean EOF before any byte, -1 for an + * incomplete frame, TLS failure or deadline. Never treat -1 as an EOF. */ +int pure_link_read(PureLink *, void *output, size_t size, uint64_t deadline); +bool pure_link_write(PureLink *, const void *input, size_t size, uint64_t deadline); +int pure_link_read_frame(PureLink *, unsigned char *output, size_t capacity, + size_t *size, uint64_t deadline); +bool pure_link_write_frame(PureLink *, const unsigned char *data, size_t size, + uint64_t deadline); +void pure_link_close(PureLink *, bool healthy); + +/* alpn is a static length-prefixed protocol name, retained by SSL_CTX's + * callback until the context is freed. Its content has exactly alpn[0]+1 bytes. */ +SSL_CTX *pure_link_context(bool server, const char *ca, const char *cert, + const char *key, const unsigned char *alpn); +bool pure_link_handshake(PureLink *, SSL_CTX *, bool server, const char *peer_name, + const unsigned char *alpn); +bool pure_link_port_number(const char *text, bool allow_zero); +int pure_link_listen_at(const char *host, const char *port); +int pure_link_connect_to(const char *host, const char *port); +#endif diff --git a/prototypes/connections/.gitignore b/prototypes/connections/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/prototypes/connections/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/prototypes/connections/Makefile b/prototypes/connections/Makefile new file mode 100644 index 0000000..502d359 --- /dev/null +++ b/prototypes/connections/Makefile @@ -0,0 +1,251 @@ +CC ?= cc +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +BUILD ?= build + +.PHONY: all core async-core editor fixed-editor fixed-server fixed-tui source-tui compiler mirror window server async-server document-peer document-server check check-policy check-policy-peer check-portable check-portable-tui check-portable-peer check-native-checkpoint check-mirror check-core check-async check-async-server check-async-service-scale check-profile check-fixed-profile check-fixed-server check-service-scale check-tui-profile check-source-tui-profile check-tui check-source-tui check-release check-micro-memory check-hybrid check-flat-export check-copy-range check-peer-profile check-document-server-profile check-window-profile check-document-peer check-document-server FORCE +CORE = runtime.c +STORE ?= store.c +FIXED_PROGRAM ?= examples/editor.pure +HOST = $(STORE) store-port.c evaluate.c +HEADERS = runtime.h program.h store.h store-port.h flow.h result.h +all: core fixed-editor +core: $(BUILD)/libpure-execution.a +async-core: $(BUILD)/libpure-execution-async.a +editor: $(BUILD)/pure-edit +fixed-editor: $(BUILD)/pure-edit-fixed +fixed-server: $(BUILD)/pure-connections-fixed +fixed-tui: $(BUILD)/pure-edit-tui-fixed +source-tui: $(BUILD)/pure-edit-tui +compiler: $(BUILD)/pure-compile +mirror: $(BUILD)/pure-mirror +window: $(BUILD)/pure-edit-window +server: $(BUILD)/pure-connections +async-server: $(BUILD)/pure-connections-async +document-peer: $(BUILD)/pure-document-peer +document-server: $(BUILD)/pure-document-server + +$(BUILD): + mkdir -p $@ + +$(BUILD)/pure-connections: server.c $(HOST) flow.c program.c $(CORE) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS server.c $(HOST) flow.c program.c $(CORE) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/pure-connections-async: server.c $(HOST) flow.c program.c $(CORE) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS -DPURE_ASYNC_EFFECTS -DPURE_HTTP_ASYNC server.c $(HOST) flow.c program.c $(CORE) $(LDFLAGS) -pthread $(LDLIBS) -o $@ + +$(BUILD)/pure-connections-async-gated: server.c $(HOST) flow.c program.c $(CORE) test-async-server-gate.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS -DPURE_ASYNC_EFFECTS -DPURE_HTTP_ASYNC -DPURE_STORE_TEST server.c $(HOST) flow.c program.c $(CORE) test-async-server-gate.c $(LDFLAGS) -pthread $(LDLIBS) -o $@ + +$(BUILD)/pure-connections-fixed: server.c $(HOST) flow.c $(CORE) $(HEADERS) $(BUILD)/embedded-plan.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS -DPURE_EMBEDDED_PROGRAM -I$(BUILD) server.c $(HOST) flow.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)/pure-compile: compile-program.c program.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) compile-program.c program.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/embedded-plan.h: FORCE $(FIXED_PROGRAM) $(BUILD)/pure-compile + $(BUILD)/pure-compile $(FIXED_PROGRAM) $@ + +$(BUILD)/pure-edit-fixed: edit.c $(HOST) $(CORE) $(HEADERS) $(BUILD)/embedded-plan.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_EMBEDDED_PROGRAM -I$(BUILD) edit.c $(HOST) $(CORE) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/storage-plan.h: FORCE examples/storage.pure $(BUILD)/pure-compile + $(BUILD)/pure-compile examples/storage.pure $@ + +$(BUILD)/pure-mirror: mirror.c $(HOST) $(CORE) $(HEADERS) $(BUILD)/storage-plan.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -I$(BUILD) mirror.c $(HOST) $(CORE) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/pure-edit-tui-fixed: edit-tui.c $(HOST) $(CORE) $(HEADERS) $(BUILD)/embedded-plan.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_EMBEDDED_PROGRAM -I$(BUILD) edit-tui.c $(HOST) $(CORE) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/pure-edit-tui: edit-tui.c $(HOST) $(CORE) program.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) edit-tui.c $(HOST) $(CORE) program.c $(LDFLAGS) $(LDLIBS) -o $@ + +# TLS/socket code is linked only when a document peer is explicitly selected. +$(BUILD)/pure-document-peer: document-peer.c ../common/tls-link.c ../common/tls-link.h $(HOST) program.c $(CORE) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) document-peer.c ../common/tls-link.c $(HOST) program.c $(CORE) $(LDFLAGS) -lssl -lcrypto $(LDLIBS) -o $@ + +$(BUILD)/pure-document-server: document-peer.c ../common/tls-link.c ../common/tls-link.h $(HOST) program.c $(CORE) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_PEER_SERVER_ONLY document-peer.c ../common/tls-link.c $(HOST) program.c $(CORE) $(LDFLAGS) -lssl -lcrypto $(LDLIBS) -o $@ + +# GTK is selected only for the document window. The core, CLI and server do +# not need its headers, libraries, display server or runtime dependencies. +$(BUILD)/pure-edit-window: edit-window.c $(HOST) program.c $(CORE) $(HEADERS) | $(BUILD) + pkg-config --atleast-version=4.10 gtk4 + $(CC) $(CPPFLAGS) $(CFLAGS) $$(pkg-config --cflags gtk4) edit-window.c $(HOST) program.c $(CORE) $(LDFLAGS) $$(pkg-config --libs gtk4) $(LDLIBS) -o $@ + +$(BUILD)/runtime.o: runtime.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -c runtime.c -o $@ + +$(BUILD)/runtime-async.o: runtime.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_ASYNC_EFFECTS -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 + rm -f $@ + $(AR) rcs $@ $^ + +$(BUILD)/libpure-execution-async.a: $(BUILD)/runtime-async.o + rm -f $@ + $(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) -DPURE_RUNTIME_METRICS test-runtime.c program.c $(CORE) $(HOST) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-async: test-async.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS -DPURE_ASYNC_EFFECTS test-async.c program.c $(HOST) $(CORE) $(LDFLAGS) -pthread $(LDLIBS) -o $@ + +$(BUILD)/test-splice: test-splice.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS -DPURE_STORE_TEST test-splice.c program.c $(CORE) $(HOST) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-paged-store: test-paged-store.c store-paged.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_STORE_TEST test-paged-store.c store-paged.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/core-layout: core-layout.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) core-layout.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/core-layout-async: core-layout.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_ASYNC_EFFECTS core-layout.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/measure-peak-rss: measure-peak-rss.c | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) measure-peak-rss.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-async: async-core $(BUILD)/core-layout-async $(BUILD)/test-async + python3 check_core.py --archive $(BUILD)/libpure-execution-async.a --layout $(BUILD)/core-layout-async --budget async-core-budget.json $(if $(ASYNC_CORE_EVIDENCE),--output "$(ASYNC_CORE_EVIDENCE)") + $(BUILD)/test-async + +check-async-server: async-server fixed-editor $(BUILD)/pure-connections-async-gated + python3 check_profile.py --build $(BUILD) --budget async-server-budget.json $(if $(ASYNC_SERVER_PROFILE_EVIDENCE),--output "$(ASYNC_SERVER_PROFILE_EVIDENCE)") + python3 check_async_server.py --server $(BUILD)/pure-connections-async-gated --editor $(BUILD)/pure-edit-fixed $(if $(ASYNC_SERVER_EVIDENCE),--output "$(ASYNC_SERVER_EVIDENCE)") + +# Release-linked default hosts are reviewed separately from the execution core. +# Do not run this size gate on sanitizer-instrumented binaries. +check-profile: editor server + python3 check_profile.py --build $(BUILD) $(if $(PROFILE_EVIDENCE),--output "$(PROFILE_EVIDENCE)") + +check-fixed-profile: fixed-editor fixed-server $(BUILD)/measure-peak-rss + python3 check_profile.py --build $(BUILD) --budget fixed-budget.json $(if $(FIXED_PROFILE_EVIDENCE),--output "$(FIXED_PROFILE_EVIDENCE)") + python3 check_micro_memory.py --build $(BUILD) --fixed $(if $(FIXED_MEMORY_EVIDENCE),--output "$(FIXED_MEMORY_EVIDENCE)") + +check-fixed-server: fixed-editor fixed-server server + python3 check_fixed_server.py --build $(BUILD) $(if $(FIXED_SERVER_EVIDENCE),--output "$(FIXED_SERVER_EVIDENCE)") + +check-service-scale: fixed-editor fixed-server editor server + python3 check_service_scale.py --build $(BUILD) $(if $(SERVICE_SCALE_EVIDENCE),--output "$(SERVICE_SCALE_EVIDENCE)") + +check-async-service-scale: fixed-editor fixed-server editor server async-server + python3 check_service_scale.py --build $(BUILD) --async-source $(if $(SERVICE_SCALE_EVIDENCE),--output "$(SERVICE_SCALE_EVIDENCE)") + +check-tui-profile: fixed-tui + python3 check_profile.py --build $(BUILD) --budget tui-budget.json $(if $(TUI_PROFILE_EVIDENCE),--output "$(TUI_PROFILE_EVIDENCE)") + +check-source-tui-profile: source-tui + python3 check_profile.py --build $(BUILD) --budget source-tui-budget.json $(if $(TUI_PROFILE_EVIDENCE),--output "$(TUI_PROFILE_EVIDENCE)") + +check-tui: fixed-editor check-tui-profile + python3 check_tui.py --build $(BUILD) $(if $(TUI_EVIDENCE),--output "$(TUI_EVIDENCE)") + +check-source-tui: editor fixed-editor check-source-tui-profile + python3 check_tui.py --build $(BUILD) --source $(if $(TUI_EVIDENCE),--output "$(TUI_EVIDENCE)") + +check-release: check-core check-profile check-fixed-profile check check-policy check-micro-memory + +check-micro-memory: editor $(BUILD)/measure-peak-rss + python3 check_micro_memory.py --build $(BUILD) $(if $(MEMORY_EVIDENCE),--output "$(MEMORY_EVIDENCE)") + +check-hybrid: + $(MAKE) BUILD=build/hybrid STORE=store-paged.c check build/hybrid/test-paged-store + build/hybrid/test-paged-store + python3 check_profile.py --build build/hybrid --budget hybrid-budget.json + $(MAKE) BUILD=build/hybrid STORE=store-paged.c check-micro-memory + python3 check_small_scan.py --editor build/hybrid/pure-edit $(if $(SMALL_SCAN_EVIDENCE),--output "$(SMALL_SCAN_EVIDENCE)") + $(MAKE) check-flat-export + +# Selected page format can be exported to the unchanged default store format. +# This conversion tool and test are outside the runtime and default app builds. +check-flat-export: fixed-editor + $(MAKE) BUILD=build/hybrid STORE=store-paged.c fixed-editor + python3 check_flat_export.py --paged-editor build/hybrid/pure-edit-fixed --flat-editor $(BUILD)/pure-edit-fixed $(if $(EXPORT_EVIDENCE),--output "$(EXPORT_EVIDENCE)") + +# Real source document -> two same-process domains using pinned mapped input. +check-mirror: mirror fixed-editor $(BUILD)/measure-peak-rss + python3 check_mirror.py --mirror $(BUILD)/pure-mirror --editor $(BUILD)/pure-edit-fixed --rss-helper $(BUILD)/measure-peak-rss $(if $(MIRROR_SCRATCH_PARENT),--scratch-parent "$(MIRROR_SCRATCH_PARENT)") $(if $(MIRROR_EVIDENCE),--output "$(MIRROR_EVIDENCE)") + +# Selected Linux-only storage copy experiment; does not change the default build. +check-copy-range: editor $(BUILD)/measure-peak-rss + $(MAKE) BUILD=build/cfr CPPFLAGS="$(CPPFLAGS) -DPURE_STORE_CFR_COPY" check check-profile check-micro-memory check-service-scale + python3 check_copy_path.py --baseline $(BUILD)/pure-edit --candidate build/cfr/pure-edit --baseline-helper $(BUILD)/measure-peak-rss --candidate-helper build/cfr/measure-peak-rss $(if $(COPY_RANGE_EVIDENCE),--output "$(COPY_RANGE_EVIDENCE)") + +check-peer-profile: document-peer + python3 check_profile.py --build $(BUILD) --budget peer-budget.json $(if $(PEER_PROFILE_EVIDENCE),--output "$(PEER_PROFILE_EVIDENCE)") + +check-document-server-profile: document-server + python3 check_profile.py --build $(BUILD) --budget peer-server-budget.json $(if $(PEER_SERVER_PROFILE_EVIDENCE),--output "$(PEER_SERVER_PROFILE_EVIDENCE)") + +check-window-profile: window + python3 check_profile.py --build $(BUILD) --budget window-budget.json $(if $(WINDOW_PROFILE_EVIDENCE),--output "$(WINDOW_PROFILE_EVIDENCE)") + +check-document-peer: check-peer-profile editor + python3 check_document_peer.py --peer $(BUILD)/pure-document-peer --editor $(BUILD)/pure-edit $(if $(PEER_EVIDENCE),--output "$(PEER_EVIDENCE)") + +check-document-server: check-document-server-profile document-peer editor + python3 check_document_peer.py --peer $(BUILD)/pure-document-peer --server $(BUILD)/pure-document-server --editor $(BUILD)/pure-edit $(if $(PEER_SERVER_EVIDENCE),--output "$(PEER_SERVER_EVIDENCE)") + +# Opt-in TLS host checks: same authenticated work and failure scenarios without +# Linux /proc RSS or GNU binary-size tools. Requires OpenSSL headers and libs. +check-portable-peer: document-peer document-server editor + python3 check_document_peer.py --peer $(BUILD)/pure-document-peer --editor $(BUILD)/pure-edit --portable $(if $(PORTABLE_PEER_EVIDENCE),--output "$(PORTABLE_PEER_EVIDENCE)") + python3 check_document_peer.py --peer $(BUILD)/pure-document-peer --server $(BUILD)/pure-document-server --editor $(BUILD)/pure-edit --portable $(if $(PORTABLE_DOCUMENT_SERVER_EVIDENCE),--output "$(PORTABLE_DOCUMENT_SERVER_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 --store-source $(STORE) $(if $(EVIDENCE),--output "$(EVIDENCE)") + +check-policy: editor server source-tui + $(MAKE) BUILD=$(BUILD)/policy FIXED_PROGRAM=examples/bounded-editor.pure fixed-editor fixed-server fixed-tui + python3 check_policy.py --source $(BUILD)/pure-edit --fixed $(BUILD)/policy/pure-edit-fixed --source-server $(BUILD)/pure-connections --fixed-server $(BUILD)/policy/pure-connections-fixed --source-tui $(BUILD)/pure-edit-tui --fixed-tui $(BUILD)/policy/pure-edit-tui-fixed --definition examples/bounded-editor.pure $(if $(POLICY_PEER),--peer $(POLICY_PEER)) $(if $(POLICY_EVIDENCE),--output "$(POLICY_EVIDENCE)") + +check-policy-peer: document-peer + $(MAKE) BUILD=$(BUILD) POLICY_PEER=$(BUILD)/pure-document-peer check-policy + +# Host-neutral C smoke path; the release size/RSS gates above are calibrated +# only for Linux AArch64 and must not be reported as macOS measurements. +check-portable: $(BUILD)/test-flow $(BUILD)/test-program $(BUILD)/test-fields $(BUILD)/test-runtime $(BUILD)/test-splice $(BUILD)/test-async $(BUILD)/pure-edit $(BUILD)/pure-connections $(BUILD)/pure-edit-tui-fixed $(BUILD)/pure-edit-tui + $(BUILD)/test-flow + $(BUILD)/test-program + $(BUILD)/test-fields + $(BUILD)/test-runtime + $(BUILD)/test-splice + $(BUILD)/test-async + +# Same real PTY/CLI scenarios as the Linux TUI gates, with no /proc RSS claim. +check-portable-tui: fixed-editor fixed-tui editor source-tui + python3 check_tui.py --build $(BUILD) --portable $(if $(PORTABLE_FIXED_TUI_EVIDENCE),--output "$(PORTABLE_FIXED_TUI_EVIDENCE)") + python3 check_tui.py --build $(BUILD) --source --portable $(if $(PORTABLE_SOURCE_TUI_EVIDENCE),--output "$(PORTABLE_SOURCE_TUI_EVIDENCE)") + +# Host-native behavior plus the same real document workload on both CLI variants. +# RSS is recorded on macOS and Linux; only check-micro-memory enforces the +# reviewed Linux AArch64 ceiling. +check-native-checkpoint: check-portable check-portable-tui fixed-editor $(BUILD)/measure-peak-rss + python3 check_micro_memory.py --build $(BUILD) --portable $(if $(PORTABLE_SOURCE_MEMORY_EVIDENCE),--output "$(PORTABLE_SOURCE_MEMORY_EVIDENCE)") + python3 check_micro_memory.py --build $(BUILD) --fixed --portable $(if $(PORTABLE_FIXED_MEMORY_EVIDENCE),--output "$(PORTABLE_FIXED_MEMORY_EVIDENCE)") diff --git a/prototypes/connections/README.md b/prototypes/connections/README.md new file mode 100644 index 0000000..31b9c27 --- /dev/null +++ b/prototypes/connections/README.md @@ -0,0 +1,545 @@ +# Pure: 작성한 연결을 준비해 실행하는 네이티브 경로 + +기존 Workbench에서 만든 코드·배치를 실제 TCP 요청으로 저장하고 새 실행기에서 복원한다. +서버는 C17 네이티브 실행 파일이다. SQL·SQLite 의존성을 제거했고, 호출자의 바이트를 +그대로 파일에 기록한다. 저장할 구간과 응답할 구간은 `.pure` 파일로 작성하며 서버 시작 때 +실행 경로를 준비한다. 기존 Node/Wasm CLI는 그대로 사용한다. + +이 구현은 저장·전송의 수명을 확인하는 최소 경로다. 객체별 파일이나 전체 교체 방식을 +Pure의 최종 데이터 모델로 정한 것은 아니다. 필드 배치·상수 구간·별칭은 준비 단계에서 +처리하고, 입력 값에 의존하는 구간은 요청 때 계산한다. +[구현 결과와 한계](../../docs/research/021-field-connections.md). + +## 실행 코어의 현재 상태 + +요청 접수·본문 예산·작업 준비·완료·해제를 `runtime.c`가 관리한다. HTTP는 그 코어의 한 +입력/출력 어댑터다. 다른 실행 영역에 결과를 전달할 때 코어가 원본을 유지하며, 수신 영역의 +효과와 응답 소비가 끝나면 회수한다. `test-runtime.c`는 수명·예산 단위 계약을, +선택형 `make check-mirror`는 실제 1 MiB 문서가 한 프로세스의 두 저장 영역으로 +전달되는 것을 검사한다. [실제 연결 비용과 한계](../../docs/research/063-real-in-process-routing.md). + +`make all`은 파서 없는 고정 정의 CLI와 실행 전용 `build/libpure-execution.a`를 만든다. +빌드 시 `.pure` 정의를 준비하는 컴파일러도 생성하지만 CLI에는 링크되지 않는다. +실행 중 소스를 읽는 CLI는 `make editor`, HTTP 호스트는 `make server`로 선택한다. +아카이브에는 `runtime.c`만 +들어가며 문서 평가기·파서·HTTP·POSIX 저장 구현은 없다. 문서 평가는 선택한 +`store-port.c`에 연결된다. 서버는 소스 파일을 읽으므로 파서를 별도로 포함한다. +기본 호스트 효과는 동기식이고 작성 문법은 아직 get/put 저장 프로필이다. +전체 앱 런타임·자체 보안 RPC·스레드 간 안전성·강제 메모리 격리를 구현한 것은 아니다. +[구현·비용·검증 보고서](../../docs/research/022-execution-core.md), +[첫 공개 체크포인트](../../CHECKPOINT.md). + +인증 문서 피어가 필요한 경우 `make document-peer`는 서버·클라이언트 명령을 +한 실행물에 넣는다. 서버만 배치할 때는 `make document-server`를 선택할 수 있다. +`make check-document-server`는 서버 전용 코드·의존성 상한과 기존 C/Python TLS +문서 시나리오를 검사한다. 이 선택 빌드는 기본 코어·CLI에 TLS를 추가하지 않는다. +루프백의 [직접 서버 비교와 전송 지연 결과](../../docs/research/055-authenticated-peer-transport-cost.md)는 +전체 프로세스 비용과 아직 남은 다중 연결 한계를 함께 기록한다. +이동 가능한 서버 전용 묶음은 `package_native.py --with-peer-server --output FILE`로 +만들고 `check_package.py FILE`로 검증한다. CLI+TLS 아카이브 전체 상한과 +[이동 후 인증·문서 작업 결과](../../docs/research/056-server-only-document-package.md)를 +별도로 공개한다. + +## 필요한 구성만 빌드하기 + +`make core`, `make fixed-editor`, `make editor`, `make server`로 구성을 각각 선택한다. `make core`에는 +소스 파서·저장소·네트워크·UI가 들어가지 않는다. 영역마다 스레드·소켓·힙을 생성하지 않는다. +release 빌드의 `make check-core`는 코어 코드 섹션 4,864 B, data/BSS 0 B와 현재 구조체 비용의 +상한·아카이브 의존성을 검사한다. 현재 Linux AArch64의 기본 구성은 +`text` 4,632 B / 영역 120 B / 작업 216 B다. 누적 계수가 필요한 HTTP 호스트와 +테스트는 `PURE_RUNTIME_METRICS`를 선택한다. 이 프로필의 영역은 192 B다. +코어에서 문서 해석을 분리하기 전의 `text` 5,980 B / 영역 128 B와 구분한다. +[현재 경계와 전체 앱 비용](../../docs/research/068-opaque-app-core.md)에 기록했다. +[해제 콜백 재진입 방어 비용](../../docs/research/064-release-callback-reentrancy.md)을 +기본 코어 상한 안에서 검증했다. +[실제 앱 1,000개 비교](../../docs/research/051-optional-runtime-metrics.md)에서 두 비용을 +분리했다. 전체 앱의 RAM 수치는 아니다. GNU 호환 도구를 사용하는 현재 프로필의 회귀 검사다. +`make check-profile`은 기본 CLI·HTTP 실행물의 링크된 섹션과 직접 동적 의존성을 검사한다. +두 검사를 실제 동작·CLI 전체 프로세스 RSS 검사와 함께 실행할 때는 release 빌드에서 +`make check-release`를 사용한다. RSS 검사는 1 B/1 MiB 실제 문서 편집·읽기를 +[현재 장치의 4 MiB 회귀 상한](memory-budget.json)과 비교한다. +이 검사에는 파서 없는 고정 정의 CLI·HTTP 실행물의 크기·RSS 상한도 포함된다. +`make check-service-scale`은 같은 문서 앱의 1,024개 객체를 8개 클라이언트가 +생성·읽기·수정·복원하여 두 HTTP 호스트의 결과, 서버 RSS, 버퍼 회수를 확인한다. +[검사 범위와 결과](../../docs/research/040-document-service-capacity.md). +`make check-async`는 선택형 비동기 완료 코어와 실제 저장 효과를 검사한다. +유휴 동기 앱은 `pure_runtime_bind()`로 작업 슬롯을 분리할 수 있다. 진행 중인 +작업·대여가 남으면 분리를 거절하며, 슬롯 재결합에도 티켓 세대는 유지된다. +[실제 네이티브 앱 1,000개의 메모리 비용](../../docs/research/049-idle-app-work-slots.md). +기본 코어·CLI·HTTP에는 작업자 스레드나 비동기 완료 코드가 링크되지 않는다. +선택형 코어 자체는 작업자를 만들지 않으며, 효과를 가져온 호스트가 I/O를 실행하고 +같은 소유 스레드에서 완료해야 한다. 기본 서비스 호스트는 이 경로를 사용하지 +않는다. [계약·비용·한계](../../docs/research/045-owner-thread-async-effects.md). +`make async-server`는 이 계약을 기존 문서 HTTP 앱에 선택적으로 연결한다. +`make check-async-server`는 크기 상한과 쓰기가 멈춘 동안 기존 문서 읽기·기한· +종료를 검사하고, `make check-async-service-scale`은 기존 1,024개 문서 작업을 +같은 소스 정의 앱으로 다시 실행한다. 이 선택 호스트는 읽기 작업자 하나와 +쓰기 작업자 하나를 사용하고, 모든 쓰기는 한 작업자에서 직렬화한다. +[현재 동작과 비용](../../docs/research/048-immutable-read-write-overlap.md). +GTK·TLS를 선택하지 않아도 기본 실행물에 섞이면 검사가 실패한다. 예산은 +[`profile-budget.json`](profile-budget.json)에 기록했으며, 호스트·컴파일러가 바뀌면 +측정값을 다시 검토해야 한다. 이 검사만으로 전체 앱 RAM이 작다고 증명하지는 않는다. +[현재 비용과 제한](../../docs/research/023-document-edits.md). + +Linux AArch64의 기본 시험 묶음에는 CLI와 `.pure` 정의만 넣는다. `make check-micro-memory`로 +빌드한 뒤 첫 명령을 실행한다. 전체 교차 검증 묶음은 `make all server document-peer window` +와 `make check-micro-memory` 뒤에 HTTP·피어·창을 모두 명시적으로 선택한다. +패키징은 CLI 메모리 상한도 다시 확인한다. 소스 작성 묶음은 디버그 정보를, +고정 정의 묶음은 실행에 필요 없는 심볼까지 제거하며 빌드 실행물은 그대로 둔다. +둘 다 정적 실행물이 아니고 매니페스트에 시스템 공유 +라이브러리를 기록한다. `check_package.py`는 묶음을 다른 위치에 풀어 선택된 +경로의 실제 동작을 다시 확인한다. +고정 정의 묶음은 선택한 CLI·HTTP·TUI 조합의 압축 아카이브 전체 크기 상한도 +[`fixed-package-budget.json`](fixed-package-budget.json)으로 검사한다. 초과하면 패키지를 +만들지 않는다. 이 값은 libc·터미널·저장 파일을 포함한 실행 메모리 값은 아니다. +[현재 터미널 편집기](../../docs/research/044-terminal-viewport-cost.md)는 여러 행·탭 문서의 +재열기를 처리하며, 1 MiB 화면의 반복 입력은 문서 전체를 매번 스캔하지 않는다. +[문서 이름 접두어 탐색](../../docs/research/050-bounded-document-prefix-find.md)은 이미 정렬된 +목록을 사용하며, 목록을 연 상태에서 이름을 입력하고 Enter로 연다. Backspace는 +접두어를 줄이고 화살표는 검색어를 지운 뒤 목록을 이동한다. + +```sh +python3 prototypes/connections/package_native.py \ + --output dist/pure-document-micro-linux-aarch64.tar.gz +python3 prototypes/connections/check_package.py \ + dist/pure-document-micro-linux-aarch64.tar.gz + +python3 prototypes/connections/package_native.py \ + --with-http --with-peer --with-window \ + --output dist/pure-document-authoring-linux-aarch64.tar.gz +python3 prototypes/connections/check_package.py \ + dist/pure-document-authoring-linux-aarch64.tar.gz +``` + +필요한 호스트만 선택할 수도 있다. 예를 들어 `--with-window`만 주면 HTTP·TLS 없이 +CLI와 GTK 창만 포함하며, 창의 외부 변경·충돌은 CLI를 통해 검증한다. + +[기본·선택 묶음, 메모리 비용과 한계](../../docs/research/032-micro-native-authoring-boundary.md). + +동일한 문서 인터페이스에 인라인/16 KiB 페이지 혼합 저장소를 **선택형 연구 경로**로 붙여볼 수 있다. +작은 기존 `PUREB001` 값은 그대로 읽지만, 큰 값을 `PUREP001`로 바꾼 뒤에는 기본 저장기로 +직접 열 수 없다. 따라서 실험에 별도 `--store` 디렉터리를 쓴다. 아래 내보내기 +명령은 새 기본 형식 디렉터리를 만들어 본문·리비전을 보존하며, 기존 디렉터리를 +제자리에서 바꾸지 않는다. 기본 저장소를 교체하는 명령이 아니다. + +```sh +make -C prototypes/connections check-hybrid +printf 'hello Pure\n' > /tmp/pure-hybrid-insert.txt +prototypes/connections/build/hybrid/pure-edit \ + --program prototypes/connections/examples/editor.pure \ + --store /tmp/pure-hybrid-documents edit note 0 0 0 /tmp/pure-hybrid-insert.txt +prototypes/connections/build/hybrid/pure-edit \ + --program prototypes/connections/examples/editor.pure \ + --store /tmp/pure-hybrid-documents show note +python3 prototypes/connections/package_native.py \ + --build prototypes/connections/build/hybrid --hybrid-storage \ + --output dist/pure-document-hybrid-micro-linux-aarch64.tar.gz +make -C prototypes/connections check-flat-export +python3 prototypes/connections/export_flat_store.py \ + --source /tmp/pure-hybrid-documents --destination /tmp/pure-flat-documents \ + --paged-editor prototypes/connections/build/hybrid/pure-edit-fixed \ + --flat-editor prototypes/connections/build/pure-edit-fixed +``` + +[실제 큰·작은 문서 쓰기량과 전체 제한](../../docs/research/034-hybrid-document-storage.md), +[기본 형식으로 내보내는 검증](../../docs/research/062-paged-store-rollback.md). + +같은 `.pure` 문서 편집기를 선택형 페이지 저장소와 함께 터미널에서 사용하려면 +Linux의 16 KiB OS 페이지 환경에서 새 저장 디렉터리를 지정한다. CLI와 TUI를 +함께 담은 [선택형 묶음](results/2026-09-24-native-save/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz)은 +다음 빌드로 만들며, 기본 `make all`과 기본 저장 형식은 유지된다. + +```sh +make -C prototypes/connections BUILD=build/hybrid STORE=store-paged.c editor source-tui +python3 prototypes/connections/package_native.py \ + --build prototypes/connections/build/hybrid --hybrid-storage --with-tui \ + --output dist/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz +python3 prototypes/connections/check_package.py \ + dist/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz +``` + +[실제 Ctrl+S 완료 시간·코드·RSS 비용](../../docs/research/065-native-save-latency.md)을 +함께 검토해야 한다. 큰 문서의 `PUREP001` 형식은 기본 저장기가 직접 열 수 없다. + +기본 `PUREB001` 형식을 유지하며 큰 수정의 사용자 공간 복사 호출을 줄이는 +Linux 전용 선택형 실험은 `make -C prototypes/connections check-copy-range`로 +검사한다. `strace`·`findmnt`와 GNU 크기 도구가 필요하며, 기본 `make editor`에는 +이 경로가 들어가지 않는다. [같은 1 MiB 문서의 시스템 호출·전체 RSS·코드 비용과 +지원 범위](../../docs/research/059-immutable-file-copy-path.md)를 분리해 공개한다. + +## 네이티브 CLI로 문서 일부 바꾸기 + +저장소 루트에서 실행한다. 입력 파일에 삽입할 실제 내용만 둔다. + +```sh +make -C prototypes/connections editor +printf 'hello Pure\n' > /tmp/pure-insert.txt +prototypes/connections/build/pure-edit --program prototypes/connections/examples/editor.pure --store prototypes/connections/build/edited-objects edit note 0 0 0 /tmp/pure-insert.txt +printf 'world' > /tmp/pure-insert.txt +prototypes/connections/build/pure-edit --program prototypes/connections/examples/editor.pure --store prototypes/connections/build/edited-objects edit note 1 6 4 /tmp/pure-insert.txt +prototypes/connections/build/pure-edit --program prototypes/connections/examples/editor.pure --store prototypes/connections/build/edited-objects show note +``` + +새 저장소에서 마지막 출력은 `hello world`와 줄바꿈이다. `edit`의 숫자는 예상 버전·시작 바이트· +삭제 바이트 수다. 성공 시 JSON 버전을 출력하며, `show`는 stdout에 원문을, stderr에 버전을 +출력한다. 버전 충돌의 종료 코드는 3, 다른 오류는 2다. 변경이 없더라도 성공한 효과는 버전을 +증가시킨다. UTF-8 문자소 위치는 호출자가 바이트 범위로 변환해야 한다. + +편의 명령은 준비된 두 숫자 필드와 뒤따르는 삽입 바이트를 채운다. 다른 입력 구성은 +`apply KEY REVISION INPUT_FILE`로 실행한다. `--budget BYTES`를 명령 앞에 지정하면 입력/결과의 +본문 크레딧을 제한한다. 저장소가 파일을 다시 쓰는 4 KiB 스택 버퍼·커널 비용은 별도다. +서버에 같은 `editor.pure`·저장 경로를 지정하면 외부 HTTP 입력도 같은 효과를 실행한다. +이 CLI는 네이티브 자동화 경로다. 문서 창은 아래의 선택 빌드다. + +## 인증된 문서 피어 + +`make document-peer`는 같은 `editor.pure`·저장소·실행 코어를 사용하면서 TLS 1.3 +상호 인증과 `pure-doc/1` 자체 이진 형식을 선택 빌드한다. 기본 `make all`에는 +OpenSSL이 들어가지 않는다. 준비된 CA와 서버·클라이언트 인증서가 필요하며, +서버 인증서의 DNS SAN은 `localhost`, 클라이언트 인증서의 DNS SAN은 +아래 예시의 `pure-client`와 일치해야 한다. + +```sh +make -C prototypes/connections document-peer +prototypes/connections/build/pure-document-peer serve 127.0.0.1 9443 \ + prototypes/connections/examples/editor.pure /tmp/pure-documents \ + /path/to/ca.pem /path/to/server.pem /path/to/server.key pure-client +prototypes/connections/build/pure-document-peer call 127.0.0.1 9443 localhost \ + prototypes/connections/examples/editor.pure /path/to/ca.pem \ + /path/to/client.pem /path/to/client.key show note +``` + +`call`은 `show KEY`, `edit KEY REVISION OFFSET REMOVE INSERT_FILE`, +`apply KEY REVISION INPUT_FILE`을 받는다. 읽기는 원문을 stdout에 보낸다. +편집은 결과 본문을 소비하지 않으므로 논리적 결과 길이만 받고 본문 전송은 생략한다. +서버는 지속 세션을 처리하지만 예제 C 명령은 호출마다 연결한다. 인증서를 자동 생성해 +실제 C·Python 클라이언트, 1 MiB 문서, 실패·재시작을 검사하려면 +`make -C prototypes/connections check-document-peer`를 실행한다. 이 대상은 선택형 +실행물의 코드·직접 의존성 [예산](peer-budget.json)도 검사한다. +[형식·비용·한계](../../docs/research/030-authenticated-document-peer.md). + +## 네이티브 문서 창 + +Linux에서 GTK 4.10 이상 개발 헤더·라이브러리와 `pkg-config`가 있으면 같은 +[`editor.pure`](examples/editor.pure)를 실제 텍스트 창으로 실행할 수 있다. GTK는 이 +선택 빌드에만 들어가며 `make all`이나 코어·CLI·서버의 의존성이 아니다. + +```sh +make -C prototypes/connections window +prototypes/connections/build/pure-edit-window \ + --program prototypes/connections/examples/editor.pure \ + --store /tmp/pure-documents note +``` + +문서를 편집한 뒤 **Save** 또는 Ctrl+S를 누른다. **Definition** 버튼 또는 +Ctrl+Shift+D로 같은 `.pure` 소스를 편집하고 **Apply & save** 또는 Ctrl+Shift+S로 +현재 창에 적용한다. 잘못된 정의, 다른 실행기의 소스 변경, 저장 실패는 초안과 기존 +실행 계획을 보존한다. 다른 실행기는 재시작한 뒤 저장된 정의를 읽는다. +창 아래의 문서 키 입력과 **Open** 또는 Ctrl+O로 다른 문서를 연다. 없는 키는 새 +빈 문서로 열고, 미저장 문서·정의 초안이 있거나 키·본문이 유효하지 않으면 +현재 문서와 버전을 유지한다. 목록·검색 기능은 아직 없다. +변경한 바이트 구간만 작성된 splice 입력에 +넣고, 저장소는 이전 버전이 일치할 때만 공개한다. CLI·HTTP 서버도 같은 프로그램과 저장 +경로를 쓰므로 다른 실행기가 먼저 저장하면 창은 초안을 유지하며 충돌을 보여준다. Reload는 +미저장 변경을 보호하고 Discard & reload만 명시적으로 버린다. 닫기·Ctrl+W도 미저장 +초안을 확인한다. 창은 UTF-8 텍스트만 열며 이진 데이터는 CLI로 다룬다. + +실제 창·CLI·HTTP 교차 검증, 정의 적용·실패 복구, 1 MiB 문서 수정과 전체 메모리 비용은 +[032 보고서](../../docs/research/032-micro-native-authoring-boundary.md)에 있다. 화면 없이 +같은 시나리오를 재현하려면 Xvfb·xdotool·xclip을 설치하고 다음을 실행한다. + +```sh +make -C prototypes/connections window editor server +xvfb-run -a -s '-screen 0 1280x800x24' \ + python3 prototypes/connections/check_window.py +``` + +## 작은 네이티브 터미널 편집기 + +빌드 시 준비한 같은 문서 앱을 GTK 없이 터미널에서 편집하려면 `fixed-tui`를 +선택한다. Ctrl+S 저장, Ctrl+O 다른 키 열기, Ctrl+R 다시 읽기, Ctrl+Q 종료다. +미저장 초안을 버리려면 다시 읽기·종료 명령을 두 번 입력해야 한다. 이 실행물은 +`.pure` 정의를 실행 중 다시 작성하지 않는다. + +```sh +make -C prototypes/connections fixed-tui +prototypes/connections/build/pure-edit-tui-fixed --store /tmp/pure-documents note +make -C prototypes/connections check-tui +python3 prototypes/connections/package_native.py --fixed --with-tui \ + --output dist/pure-document-cli-tui-linux-aarch64.tar.gz +``` + +실제 PTY 조작·1 MiB 편집·프로세스 메모리·패키지의 범위는 +[042 보고서](../../docs/research/042-micro-native-terminal-editor.md)에 있다. + +실행 시 `.pure` 정의를 읽는 TUI가 필요하면 `source-tui`를 선택한다. 기존 고정 TUI와 +달리 파서가 선택한 실행물에 포함된다. 정의를 고친 뒤 새 세션을 열어야 적용되며, +편집 중인 문서 상태를 묵시적으로 변환하지 않는다. 기본 코어와 고정 TUI에는 영향이 없다. + +```sh +make -C prototypes/connections source-tui +prototypes/connections/build/pure-edit-tui --program \ + prototypes/connections/examples/editor.pure --store /tmp/pure-documents note +make -C prototypes/connections check-source-tui +python3 prototypes/connections/package_native.py --with-tui \ + --output dist/pure-document-source-cli-tui-linux-aarch64.tar.gz +``` + +구체적인 지원 범위와 전체 비용은 [소스 TUI 검증](../../docs/research/053-source-loaded-native-terminal-editor.md)에 기록한다. + +## 실행 + +POSIX 환경의 C17 컴파일러와 make가 필요하다. 검증 환경은 Linux AArch64다. +저장소 루트에서 실행한다. + +```sh +make -C prototypes/connections server +prototypes/connections/build/pure-connections --program prototypes/connections/examples/storage.pure --store prototypes/connections/build/objects --port 8767 +``` + +`--store`는 디렉터리다. 기존 SQLite 파일 등 일반 파일을 지정하면 거절하며 덮어쓰거나 +변환하지 않는다. 자동 마이그레이션은 제공하지 않는다. +`--program`은 필수다. 파일 수정은 다음 서버 시작에 반영되며 실행 중 교체는 지원하지 않는다. + +다른 터미널에서 기존 작업물을 연다. Node가 필요하다. + +```sh +node prototypes/workbench/cli.mjs prototypes/workbench/examples/direct.pure +``` + +CLI에서 실행할 예: + +```text +point drag down 90 170 +point drag move 360 220 +point drag up 360 220 +connect http://127.0.0.1:8767 my_work +push 0 +quit +``` + +빈 CLI를 다시 실행하고 `connect http://127.0.0.1:8767 my_work`, `pull`, `show`를 입력하면 +저장한 배치가 복원된다. 이후 변경은 `push`로 저장한다. `push 0`은 새 이름을 만들 때만 사용한다. +서버를 재시작해도 같은 저장 디렉터리를 지정하면 내용을 읽을 수 있다. 다른 실행기가 먼저 +갱신했다면 412 충돌로 거절하고 로컬 배치를 유지한다. `pull`은 로컬 배치를 원격 내용으로 +교체하므로, 보존할 작업은 먼저 `save-project PATH`로 저장한다. + +## 필드로 문서 선택·저장하기 + +[selection.pure](examples/selection.pure)는 선택 시작·길이와 문서를 입력받아 선택한 내용만 +저장하고 응답한다. 필드의 바이트 위치를 연산마다 직접 계산하지 않는다. + +```text +layout Selection { + start: u32le; + count: u32le; + document: bytes; +} + +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); +} +``` + +예제 파일에는 저장한 내용을 조회하는 get도 있다. 별도 저장 디렉터리로 실행한다. + +```sh +prototypes/connections/build/pure-connections --program prototypes/connections/examples/selection.pure --store prototypes/connections/build/selections --port 8768 +``` + +다른 터미널에서 UTF-8 텍스트 파일의 선택 구간을 저장한다. `--start`·`--end`는 0부터 시작하는 +유니코드 코드 포인트 위치이며 끝은 포함하지 않는다. 바이트 위치 변환과 입력 인코딩은 +예제 Python 클라이언트가 맡는다. 사용자에게 보이는 글자 덩어리(grapheme) 단위는 아니다. + +```sh +python3 prototypes/connections/examples/select-text.py /path/to/document.txt --key excerpt --start 0 --end 5 --url http://127.0.0.1:8768 +``` + +성공하면 저장 버전과 선택 내용이 출력된다. 이후 갱신에는 `--revision 1`처럼 현재 버전을 +지정한다. 서버의 숫자 필드는 바이트 단위다. 예제 클라이언트는 UTF-8 문자 경계에 맞추지만 +서버의 `bytes` 필드 자체가 텍스트 인코딩을 검증하는 것은 아니다. 클라이언트는 Python이며 +필드 해석·범위 계산·저장·응답을 하는 서버는 네이티브다. + +`layout`은 선택한 입출력 경계의 배치 선언이다. 패딩 없이 선언 순서로 필드를 배치하며, +`u32le`, `bytes[N]`, 마지막 가변 `bytes`를 지원한다. 고정 필드만 있는 선언은 접두부를 읽는다. +다른 입력·저장·RAM 표현을 이 형식으로 바꾸도록 강제하지 않는다. 송신자와 수신자의 선언이 +맞아야 하며 자동 형식 감지·버전 변환은 없다. 예제는 두 u32 뒤에 원본 UTF-8 바이트를 보낸다. + +필드 이름과 타입은 준비 단계에서 접근 위치로 바뀐다. 요청별 이름 검색이나 레코드 객체 +할당은 없다. 이 예제에는 요청 시 계산할 범위가 하나 남고 저장·응답이 그 결과를 함께 참조한다. +입력 헤더가 잘렸거나 범위가 틀리면 저장 전에 실패한다. +[필드 연결 계약](../../docs/design/005-field-connections.md). + +## 상수 구간 작성과 준비 결과 확인 + +[storage.pure](examples/storage.pure)는 전체 작업물을 저장·복원하는 기존 동작이다. +[excerpt.pure](examples/excerpt.pure)는 저장된 본문의 일부만 응답한다. 다음 get 정의에서 +원본은 최소 80 B여야 하며 실제 응답은 오프셋 32부터 16 B다. + +```text +on get { + let object = store.read(request.key); + let window = range(object.body, 16, 64); + let excerpt = range(window, 16, 16); + reply(object, excerpt); +} +``` + +준비 결과는 저장소·소켓을 만들지 않고 확인할 수 있다. 서버의 `--program`에 같은 파일을 +지정하면 해당 경로로 실행한다. 부분 응답 예제는 전체 JSON을 요구하는 Workbench `pull`용이 아니다. + +```sh +prototypes/connections/build/pure-connections --check-program prototypes/connections/examples/excerpt.pure +``` + +`on put`에서는 저장할 부분을 고르고 같은 입력을 응답에도 연결할 수 있다. +해제·복사 코드를 따로 쓰지 않아도 원본 수신 버퍼를 전송 완료까지 유지한다. + +```text +on put { + let window = range(request.body, 2, 10); + let value = range(window, 3, 4); + let saved = store.write(request.key, request.revision, value); + reply(saved, value); +} +``` + +두 핸들러가 모두 필요하고 각각 저장 효과는 한 번이다. `range`는 정확한 범위를 검사한다. +사용하지 않은 구간의 실패도 보존하며, 쓰기의 모든 구간 정의는 저장 호출보다 앞에 둔다. +작성 오류는 저장소 초기화 전에 거절한다. 소스는 16 KiB, 핸들러당 정의는 32개까지다. +배치 선언은 4개, 선언당 필드는 16개, 입력 값에 의존하는 구간 계산은 8단계까지다. +이 동적 범위는 PUT 입력에 한정하며 GET의 숫자 필드 값으로 결정하는 범위는 시작 때 거절한다. +GET의 이름 기반 고정 필드·가변 꼬리 구간은 지원한다. 숫자/화면 DSL과 파서는 아직 별개다. +조건 분기·반복·여러 외부 효과·일반 산술은 이번 프로파일에 없다. +[작성 계약](../../docs/design/004-prepared-connections.md). + +실제 문서 효과에는 선택형 최종 크기 정책을 쓸 수 있다. +[`bounded-editor.pure`](examples/bounded-editor.pure)의 +`store.splice(request.key, request.revision, edit.start, edit.remove, edit.insert, 8)`은 +수정 후 문서가 8 B를 넘으면 `limit`을 돌려준다. 현재 크기·제거 범위·삽입 길이는 +쓰기 잠금 안에서 같은 버전으로 계산한다. 상한은 작성자가 준비한 상수이며 요청 본문이 +정책을 바꿀 수 없다. 기본 `editor.pure`는 기존 객체 상한을 사용한다. +`make check-policy`는 소스·고정 CLI/HTTP/TUI의 허용·거절·리비전·초안 보존을, +`make check-policy-peer`는 선택형 인증 피어까지 같은 정의로 검사한다. +이 기능은 문서 어댑터와 저장소에만 있고 4,632 B 기본 코어와 작업 슬롯은 늘리지 않는다. +일반 숫자 규칙 언어가 완성됐다는 뜻은 아니다. + +`--check-program`의 `steps`가 비면 구간 연산은 준비 단계에서 끝난다. `write_step`·`reply_step`이 +0이면 정적인 범위 설명을 쓰고, 양수이면 `steps`의 해당 1-based 결과를 사용한다. +`to_end`는 offset부터 원본 끝까지를 뜻한다. `runtime_view_steps`는 준비된 동적 단계 수이며 +실행 속도나 CPU 명령 수 측정값이 아니다. 오류가 나면 뒤 단계와 저장 효과는 실행하지 않는다. + +## 연결과 소유권 + +| 경로 | 데이터와 완료의 의미 | +|---|---| +| 수신 → 저장 | 본문 크기만큼 한도를 예약하고 수신한다. 작성한 구간을 쓰기 호출에 전달한다. 응답도 입력을 사용하면 전송 종료까지 원본을 유지하며, 아니면 쓰기 호출 종료 후 반납한다. | +| 저장 → 공개 | 쓰기 잠금 안에서 예상 revision을 확인한다. 새 파일에 메타데이터와 본문을 기록·fsync하고 rename으로 공개한다. | +| 공개 → 응답 | 디렉터리 fsync까지 성공해야 저장 성공으로 응답한다. 공개 후 fsync 실패는 `durability_unknown`이며 롤백으로 표현하지 않는다. | +| 조회 → 전송 | 중간 구간들의 길이 조건과 최종 범위를 같은 파일 버전에서 검사한다. 응답에 필요한 부분만 읽기 전용으로 매핑한다. 별도의 응답 본문 배열을 만들지 않는다. | +| 부분 전송 → 회수 | 헤더·본문의 전송 위치만 전진시킨다. 마지막 바이트를 OS에 넘겼거나 전송이 중단되면 매핑을 해제한다. 상대 앱의 수신 확인은 별개다. | +| 복원 → 상태 공개 | 클라이언트가 UTF-8·기존 작업물 형식·상태를 검증한 뒤 복원한다. 실패하면 로컬 상태와 기억한 원격 revision을 유지한다. | + +저장된 버전은 제자리에서 수정하지 않는다. 새 파일로 교체하므로 기존 매핑은 전송이 끝날 +때까지 이전 버전을 참조한다. 이는 불변 파일 운용 규칙에 의한 보장이다. 외부 프로그램이 +파일을 직접 수정·잘라내는 경우까지 `mmap`이 스냅샷을 보장하는 것은 아니다. + +저장 C API의 `pure_store_select`는 준비된 구간과 최소 원본 길이를 받아 필요한 부분만 매핑한다. +기본 `storage.pure`와 Workbench 복원은 전체 작업물을 사용한다. HTTP Range나 저장 파일의 +제자리 부분 갱신은 구현한 것이 아니다. 본문 없는 응답도 존재·파일 형식 검사는 수행한다. + +파일은 식별자·버전·길이의 24 B 헤더 뒤에 원본 본문을 둔다. 테이블·행·쿼리 계획은 없다. +이 헤더는 공개·복원을 위한 프로토타입 메타데이터이며 새 범용 문서·미디어 형식이 아니다. +Workbench JSON의 의미 검증은 클라이언트가 맡는다. 이 JSON이나 파일 배치를 Pure의 RAM +표현으로 강제하지 않는다. 현재 작성 가능한 연결은 등록된 읽기·쓰기·응답에 한정되며 +[모듈 계약 초안](../../docs/design/003-module-contract.md) 전체의 구현은 아니다. + +`GET /objects/NAME`, `PUT /objects/NAME`의 제한된 HTTP/1.1 요청 경로다. +이름은 영문·숫자·`_`·`-` 1~64자다. 생성은 `If-None-Match: *`, 갱신은 `If-Match: "revision"`을 +요구한다. 성공한 조회·저장은 `ETag: "revision"`을 반환한다. revision은 64비트 양수이며 +클라이언트에서도 문자열로 유지한다. 404 없음, 412 충돌, 413 크기/버전 한도, +503 예산 부족/쓰기 잠금, 500 저장 오류를 구분한다. 500의 `X-Pure-Error: durability_unknown`은 +새 내용이 이미 공개됐지만 영속화 확인이 실패했다는 뜻이다. 재조회는 현재 내용을 확인할 +수 있을 뿐 전원 장애 내구성을 증명하지 않는다. 응답 유실도 저장 취소를 뜻하지 않는다. +요청마다 연결을 닫고 Content-Length만 지원한다. 범용 HTTP/RPC 서버는 아니다. + +HTTP를 Pure 간 기본 통신 모델로 확정한 것은 아니다. +[자체 통신 형식 설계](../../docs/design/006-native-protocol.md)는 작성된 연결에서 바이너리 +형식을 준비하고 보안 전송을 별도로 선택하는 방향이다. 같은 get/put 효과를 실행하는 +[선택형 인증 문서 피어](../../docs/research/030-authenticated-document-peer.md)가 별도로 있다. +이 평문·요청당 연결 종료 HTTP 경로로 QUIC과 성능을 비교하지 않는다. + +## 한도와 남은 비용 + +- 이 절의 HTTP 서버는 `127.0.0.1`에만 바인드하며 인증·TLS가 없다. 인증된 선택형 + 문서 피어는 별도 실행물이고 인터넷 공개 배포는 검증하지 않았다. +- 동시 연결은 8개, 헤더는 연결당 4 KiB, 객체는 Workbench 파일 상한인 1,449,984 B까지다. +- 현재 준비 구조체는 검증 환경에서 프로그램당 648 B다. 동적 구간 8개를 위한 공간도 + 고정 예약한다. 이름·필드 표는 준비 후 유지하지 않지만 전체 컴파일·실행 메모리의 상한을 뜻하지 않는다. +- `--budget` 기본값은 4 MiB다. 수신 본문 할당과 전송 중 참조하는 본문 길이를 합해 접수한다. + 이는 전체 RSS·매핑 페이지·페이지 캐시·디스크 사용량의 상한이 아니다. 매핑 정렬·페이지 + 단위 비용, OS 소켓 버퍼, 클라이언트 JSON·문자열·응답 버퍼도 별도다. + 입력의 일부만 응답해도 원본 수신 할당 전체를 붙잡으므로 해당 예산을 전부 유지한다. +- 헤더와 함께 받은 본문 접두부는 한 번 복사하며 `ingress_prefix_copy_bytes`에 기록한다. + `borrowed_output_bytes`는 참조한 구간의 누적 길이다. 절약한 RAM의 측정값이 아니다. + 커널의 파일 쓰기·소켓 전송, 페이지 폴트, 클라이언트의 직렬화 비용은 남는다. +- `--timeout-ms` 기본값은 접속부터 10초다. 느린 송수신자는 기한 뒤 종료한다. 파일 작업은 + 동기 실행하므로 디스크 호출 중에는 기한을 강제로 집행하거나 취소하지 못한다. +- 매핑을 유지하면 이전 파일의 저장 공간도 남을 수 있다. 작은 범위 참조도 해당 파일 버전의 + 디스크 수명을 연장한다. 본문 예산만으로 장기 운용의 디스크 사용량을 보장하지 않는다. +- 쓰기는 디렉터리별 잠금 하나로 직렬화한다. C API 제어 호출은 단일 스레드를 전제로 하며, + 이 잠금 규칙을 따르는 여러 프로세스의 충돌을 처리한다. 다중 객체 트랜잭션은 없다. +- 공개 전 프로세스가 종료되면 `.pending-*` 파일이 남을 수 있다. 읽기는 이를 무시하며 + 자동 정리·디스크 복구 정책은 아직 없다. 본문 손상 검출용 체크섬도 없다. +- macOS 실행, 실제 전원 장애·디스크 고장은 검증하지 않았다. 파일을 이 어댑터 밖에서 + 제자리 수정하지 않는 로컬 저장소를 전제로 한다. + +## 검증 + +Python 3와 Node가 필요하다. + +```sh +make -C prototypes/connections check +make -C prototypes/connections check-portable +make -C prototypes/connections check-portable-tui +make -C prototypes/connections BUILD=build/native-checkpoint check-native-checkpoint +make -C prototypes/connections check-portable-peer +make -C prototypes/connections check BUILD=build/sanitized CFLAGS='-std=c17 -O1 -g -Wall -Wextra -Wpedantic -Werror -fsanitize=address,undefined -fno-omit-frame-pointer' +``` + +`check-portable`은 C 단위 동작을 검사하며 Linux AArch64의 GNU `size`/`readelf`와 +RSS 상한 검사를 요구하지 않는다. `check-portable-tui`는 고정·소스 정의 편집기를 +실제 PTY에서 검사하며 `/proc` RSS를 읽지 않는다. `check-native-checkpoint`는 +둘을 실행하고 고정·소스 CLI의 실제 1 MiB 생성·읽기·1 B 수정 RSS를 호스트 +단위에 맞춰 기록한다. [기록된 Linux 결과](../../docs/research/060-host-native-checkpoint.md)는 +코어 코드 증가 없이 기존 RSS 상한 검사를 통과했다. 현재 통과 결과는 +Linux AArch64에만 해당한다. 기존 `check-tui`·`check-source-tui`의 Linux +4,096 KiB RSS 상한 검사는 그대로 유지한다. Apple Silicon 실행과 +디렉터리 동기화·전원 장애 내구성은 별도 검증이 필요하다. +`check-portable-peer`는 OpenSSL이 준비된 경우에만 선택해서 실행한다. C 피어 +클라이언트·두 서버를 실제 TLS 소켓으로 검증하지만 `/proc` RSS는 수집하지 않는다. +[휴대형 피어 검사와 Linux 비용](../../docs/research/058-portable-authenticated-peer-check.md)을 +별도로 기록했으며 macOS에서의 통과 결과는 아직 없다. + +임시 디렉터리와 실제 루프백 TCP 소켓을 사용한다. 단위 검사에는 범위 접근·페이지 경계· +이전 참조 유지·공개 전후 실패 주입·공개 전 자식 프로세스 종료가 포함된다. 주입 훅은 +단위 검사 실행 파일에만 들어간다. 실제 두 서버의 쓰기 경쟁과 Workbench 저장·복원 등 +작성에 따른 부분 읽기·쓰기·응답, 한글·이모지 문서 선택, 필드 재배치, 잘린 입력·동적 범위· +수명 보존을 포함한 28개 통합 시나리오를 일반 빌드와 ASan·UBSan 빌드에서 확인했다. +준비된 구간과 순차 범위 연산, 숫자 필드의 정렬·바이트 순서·경계 동작도 대조했다. + +[검증 자료](results/2026-09-23-field-connections/README.md). 시간·처리량 비교는 수행하지 않았다. +상수 구간 작성 자료는 [020](results/2026-09-23-prepared-connections/README.md)에 있다. +작성 기능 도입 전 자료는 [019](results/2026-09-23-byte-storage/README.md)에 있다. +이전 SQLite 구현의 결과는 [018 자료](results/2026-09-23-native-connections/README.md)에 +별도로 보존했다. + +## 이번 변경의 판단 기준 + +이름 기반 필드·데이터 의존 범위를 실제 저장·응답으로 연결하는 +[착수 기준](../../docs/design/005-field-connections.md)을 확인했다. 다음에는 앱의 읽기·변경·저장 +경로에서 필요한 연산과 버전 관계를 다룬다. 범용 그래프 실행기나 자동 재시도·피드백 루프를 +검증 없이 추가하지 않는다. diff --git a/prototypes/connections/SOURCE_PREVIEW.md b/prototypes/connections/SOURCE_PREVIEW.md new file mode 100644 index 0000000..9e0d78d --- /dev/null +++ b/prototypes/connections/SOURCE_PREVIEW.md @@ -0,0 +1,119 @@ +# Pure native source preview + +This bundle contains the C17 execution core, the authored document app, its +CLI and optional terminal/HTTP/TLS hosts, plus the Workbench sources used by +the integration checks. It excludes prior benchmark results and build outputs. +`manifest.json` records the exact files and their SHA-256 hashes. + +Build and run the small default path from the extracted directory: + +```sh +make -C prototypes/connections check-portable +make -C prototypes/connections check-portable-tui +make -C prototypes/connections check-policy +make -C prototypes/connections fixed-editor fixed-tui +make -C prototypes/connections source-tui +mkdir -p /tmp/pure-source-store +printf 'hello Pure\n' > /tmp/pure-source-insert.txt +prototypes/connections/build/pure-edit-fixed --store /tmp/pure-source-store \ + edit note 0 0 0 /tmp/pure-source-insert.txt +prototypes/connections/build/pure-edit-fixed --store /tmp/pure-source-store show note +prototypes/connections/build/pure-edit-tui --program \ + prototypes/connections/examples/editor.pure --store /tmp/pure-source-store note +``` + +`check-portable` builds the C hosts and runs the host-neutral C behavior tests. +`check-portable-tui` runs the fixed and source-loaded terminal editors through +real PTYs, including UTF-8 input, resize, 1 MiB edits, conflicts and document +browsing. It records no Linux `/proc` RSS number; on macOS it is a behavior +check, not a proof of the 4 MiB Linux memory ceiling. +For a single native checkpoint command, run from the extracted directory: + +```sh +make -C prototypes/connections BUILD=build/native-checkpoint check-native-checkpoint +``` + +This requires a C17 compiler, Make and Python 3. It builds the core, CLI and +TUI, exercises both terminal variants, then records whole-process peak RSS for +the fixed and source-loaded CLIs during a real 1 MiB document creation, read +and 1-byte revision edit. To save the two memory results, add +`PORTABLE_SOURCE_MEMORY_EVIDENCE=/path/source.json` and +`PORTABLE_FIXED_MEMORY_EVIDENCE=/path/fixed.json` to the `make` command. +The RSS helper reports the OS raw unit and normalized KiB. No macOS RSS budget +has been established; compare macOS results only with the same host and build. +The optional `make -C prototypes/connections check-portable-peer` builds the +TLS client and two server profiles, then checks mutual authentication, document +editing, invalid frames and restart over real sockets. It requires OpenSSL +headers, libraries and command-line tool, and records no RSS in portable mode. +On Linux AArch64, `make -C prototypes/connections check-release` additionally +checks the measured code, dependency and process-memory ceilings and the actual +document/HTTP paths; it needs Python 3, Node.js, and GNU `size`/`readelf`. +`check-policy` runs the authored final-size guard through the CLI, HTTP and +terminal hosts; `check-policy-peer` additionally requires OpenSSL and checks +the same policy over mutually authenticated TLS. +The source-loading CLI can run a changed `.pure` definition with +`make -C prototypes/connections editor` and `--program PATH`. + +The selected 16 KiB-page document store can be exported into a **new** default +store directory without linking it into the small default app: + +```sh +make -C prototypes/connections fixed-editor +make -C prototypes/connections BUILD=build/hybrid STORE=store-paged.c fixed-editor +python3 prototypes/connections/export_flat_store.py \ + --source /path/to/paged-store --destination /path/to/new-flat-store \ + --paged-editor prototypes/connections/build/hybrid/pure-edit-fixed \ + --flat-editor prototypes/connections/build/pure-edit-fixed +``` + +The source stays intact. The tool holds its writer lock during conversion and +checks every object with the default editor before publishing the destination. +Writers may receive `busy` until it finishes; schedule this as maintenance. +The destination must not exist. `make -C prototypes/connections check-flat-export` +exercises the conversion with real mixed-size and repeatedly edited documents. + +On a Linux host with 16 KiB OS pages, the selected hybrid store can also run +the source-loaded native terminal editor without adding it to the default app: + +```sh +make -C prototypes/connections BUILD=build/hybrid STORE=store-paged.c editor source-tui +python3 prototypes/connections/package_native.py \ + --build prototypes/connections/build/hybrid --hybrid-storage --with-tui \ + --output /tmp/pure-hybrid-tui.tar.gz +python3 prototypes/connections/check_package.py /tmp/pure-hybrid-tui.tar.gz +``` + +Use a new store directory for this profile; large documents use `PUREP001` +and need the offline exporter above before the default store can read them. + +For a same-process connection between real document stores, build the selected +`storage.pure` mirror and send one source snapshot to one or more destinations: + +```sh +make -C prototypes/connections mirror +prototypes/connections/build/pure-mirror \ + /path/to/source-store document /path/to/target-store document 0 +make -C prototypes/connections check-mirror +``` + +The final number is the expected target revision. Up to eight targets may be +specified as store/key/revision triples. Each target commits independently; +the JSON result reports conflicts and partial success. The mapped source body +stays alive until every forwarded target releases it. No socket or additional +target input buffer is involved, and this host is a one-shot command rather +than a long-running application supervisor. An optional leading +`--target-budget BYTES` bounds each destination's admitted body size; an +insufficient budget rejects the transfer before any document write. + +The fixed CLI and TUI contain one build-time prepared `editor.pure` and do not +reload a changed definition. The source TUI loads `.pure` at startup; edit the +file and start a new session to apply a different definition. The Workbench +scalar-native build additionally requires Zig 0.15.2; the GTK and TLS hosts +need their platform libraries. +On Linux, `make -C prototypes/connections check-copy-range` compares an +experimental large-extent file copy path with the ordinary bounded-scratch +build. It needs `strace`, `findmnt` and GNU size tools and has only been validated on the +recorded Linux kernel and filesystem. +macOS source compilation and execution are not yet verified. This preview is +not a signed release, does not bundle system libraries, and does not establish +a redistribution license. diff --git a/prototypes/connections/async-core-budget.json b/prototypes/connections/async-core-budget.json new file mode 100644 index 0000000..d2d4c0b --- /dev/null +++ b/prototypes/connections/async-core-budget.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-owner-thread-async-effects", + "max_text_bytes": 6144, + "max_data_bytes": 0, + "max_bss_bytes": 0, + "layout_max_bytes": { + "domain_bytes": 120, + "work_slot_bytes": 216, + "ticket_bytes": 24, + "pointer_bytes": 8, + "effect_bytes": 72 + }, + "archive_members": ["runtime-async.o"], + "external_symbols_allowed": ["memcpy", "memmove", "memset", "strlen"], + "scope": "Selected compact owner-thread completion extension without opt-in PURE_RUNTIME_METRICS. Host I/O and worker stacks are excluded. The default execution core and fixed document packages do not link this profile." +} diff --git a/prototypes/connections/async-server-budget.json b/prototypes/connections/async-server-budget.json new file mode 100644 index 0000000..b1664fc --- /dev/null +++ b/prototypes/connections/async-server-budget.json @@ -0,0 +1,12 @@ +{ + "profile": "linux-aarch64-gcc14-selected-read-write-document-http", + "binaries": { + "pure-connections-async": { + "max_text_bytes": 49152, + "max_data_bytes": 1200, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Optional one-read/one-write-worker HTTP host on the recorded Linux AArch64/GCC 14 toolchain. Section limits exclude the two 128 KiB worker stacks, libc, heap, mapped files and kernel buffers. Default CLI and HTTP do not link this option. Inspect process RSS and VM separately." +} diff --git a/prototypes/connections/check.py b/prototypes/connections/check.py new file mode 100644 index 0000000..7719537 --- /dev/null +++ b/prototypes/connections/check.py @@ -0,0 +1,552 @@ +#!/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('= 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('' in line] + copies = [line for line in lines if 'copy_file_range(' in line] + writes = [line for line in lines if 'write(' in line and '/.pending-' in line] + assert all(SUCCESS.search(line) for line in reads + copies + writes) + + measured_store = root / (binary.parent.name + '-rss') + measured = [binary, '--program', HERE / 'examples/editor.pure', + '--store', measured_store] + run(*measured, 'edit', 'large', 0, 0, 0, initial) + usage = json.loads(run(helper, *measured, 'edit', 'large', 1, 1_048_575, 1, patch)) + assert usage['exit_code'] == 0 + assert run(*measured, 'show', 'large') == expected + return {'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(), + 'result_sha256': hashlib.sha256(expected).hexdigest(), + 'object_pread_calls': len(reads), + 'copy_file_range_calls': len(copies), + 'copy_file_range_bytes': sum(int(SUCCESS.search(line)[1]) for line in copies), + 'pending_write_calls': len(writes), + 'pending_write_bytes': sum(int(SUCCESS.search(line)[1]) for line in writes), + 'one_mib_splice_peak_rss_kib': usage['max_rss_kib']} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--baseline', type=Path, required=True) + parser.add_argument('--candidate', type=Path, required=True) + parser.add_argument('--baseline-helper', type=Path, required=True) + parser.add_argument('--candidate-helper', type=Path, required=True) + parser.add_argument('--scratch-dir', type=Path, default=HERE / 'build', + help='parent filesystem for the actual document stores') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + if platform.system() != 'Linux': + parser.error('strace and wait4 RSS evidence require Linux') + args.scratch_dir.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='pure-copy-path-', + dir=args.scratch_dir.resolve()) as name: + root = Path(name) + filesystem = run('findmnt', '-T', root, '-no', 'FSTYPE').decode().strip().splitlines()[0] + initial, patch = root / 'initial', root / 'patch' + initial.write_bytes(b'A' * 1_048_576) + patch.write_bytes(b'Z') + baseline = inspect(args.baseline.resolve(), args.baseline_helper.resolve(), + root, initial, patch) + candidate = inspect(args.candidate.resolve(), args.candidate_helper.resolve(), + root, initial, patch) + assert baseline['result_sha256'] == candidate['result_sha256'] + assert baseline['copy_file_range_calls'] == 0 + assert candidate['copy_file_range_bytes'] == 1_048_575 + assert candidate['object_pread_calls'] < baseline['object_pread_calls'] + result = {'profile': 'linux-immutable-one-mib-last-byte-splice', + 'platform': platform.platform(), 'baseline': baseline, + 'candidate': candidate, + 'filesystem': filesystem, + 'scratch_parent': str(args.scratch_dir.resolve()), + 'scope': 'Successful object-file syscalls and child peak RSS; excludes physical device writes, page cache and proof of lower latency.'} + body = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(body) + print(body, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_core.py b/prototypes/connections/check_core.py new file mode 100644 index 0000000..321896e --- /dev/null +++ b/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/check_document_peer.py b/prototypes/connections/check_document_peer.py new file mode 100644 index 0000000..1472ed7 --- /dev/null +++ b/prototypes/connections/check_document_peer.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +"""Real TLS, document work, external client and CLI interop for the optional peer.""" +import argparse +import json +from pathlib import Path +import select +import socket +import ssl +import struct +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parent + + +def openssl(*args): + subprocess.run(['openssl', *map(str, args)], check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def certificates(root): + ca = root / 'ca.pem' + openssl('req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', + '-nodes', '-days', '1', '-subj', '/CN=Pure test CA', + '-addext', 'basicConstraints=critical,CA:TRUE', + '-addext', 'keyUsage=critical,keyCertSign,cRLSign', + '-keyout', root / 'ca.key', '-out', ca) + for role, name, eku in [('server', 'localhost', 'serverAuth'), + ('client', 'pure-client', 'clientAuth'), + ('other', 'wrong-client', 'clientAuth')]: + ext = root / (role + '.ext') + ext.write_text('basicConstraints=critical,CA:FALSE\n' + 'keyUsage=critical,digitalSignature\n' + f'subjectAltName=DNS:{name}\nextendedKeyUsage={eku}\n') + openssl('req', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', + '-nodes', '-subj', '/CN=' + name, '-keyout', root / (role + '.key'), + '-out', root / (role + '.csr')) + openssl('x509', '-req', '-in', root / (role + '.csr'), '-CA', ca, + '-CAkey', root / 'ca.key', '-CAcreateserial', '-days', '1', + '-extfile', ext, '-out', root / (role + '.pem')) + return ca + + +def start(binary, program, store, root, ca): + proc = subprocess.Popen([str(binary), 'serve', '127.0.0.1', '0', str(program), + str(store), str(ca), str(root / 'server.pem'), + str(root / 'server.key'), 'pure-client'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, bufsize=1) + try: + assert select.select([proc.stdout], [], [], 5)[0], 'server did not become ready' + line = proc.stdout.readline() + assert line.startswith('READY '), (line, proc.poll()) + return proc, int(line.split()[1]) + except BaseException as error: + proc.kill() + stdout, stderr = proc.communicate(timeout=3) + raise AssertionError(f'server start failed: {error}; stdout={stdout!r}, stderr={stderr!r}') from error + + +def stop(proc): + proc.terminate() + stdout, stderr = proc.communicate(timeout=5) + assert proc.returncode == 0, (proc.returncode, stdout, stderr) + + +def connect(root, ca, port, *, cert='client', alpn='pure-doc/1', name='localhost'): + ctx = ssl.create_default_context(cafile=str(ca)) + ctx.minimum_version = ssl.TLSVersion.TLSv1_3 + ctx.maximum_version = ssl.TLSVersion.TLSv1_3 + if cert: + ctx.load_cert_chain(str(root / (cert + '.pem')), str(root / (cert + '.key'))) + ctx.set_alpn_protocols([alpn]) + plain = socket.create_connection(('127.0.0.1', port), timeout=5) + try: + secure = ctx.wrap_socket(plain, server_hostname=name) + secure.settimeout(7) + if secure.selected_alpn_protocol() != alpn: + raise ssl.SSLError('ALPN mismatch') + return secure + except BaseException: + plain.close() + raise + + +def exact(sock, count): + parts = [] + while count: + part = sock.recv(count) + if not part: + raise EOFError('unexpected EOF') + parts.append(part) + count -= len(part) + return b''.join(parts) + + +def hello(sock, program): + import hashlib + assert exact(sock, 40) == b'PDOC\x01\0\0\0' + hashlib.sha256(program.read_bytes()).digest() + + +def request(sock, op, key, expected=0, body=b'', *, pieces=False, want_body=True): + key = key.encode('ascii') + wire = struct.pack('!BBQI', op | (0x80 if want_body else 0), len(key), + expected, len(body)) + key + body + if pieces: + for at in range(0, len(wire), 4093): + sock.sendall(wire[at:at + 4093]) + else: + sock.sendall(wire) + status, revision, length = struct.unpack('!BQI', exact(sock, 13)) + assert length <= 1450000, length + return status, revision, exact(sock, length if want_body else 0), length + + +def edit(offset, remove, inserted): + return struct.pack(' 0 + children = Path(f'/proc/{tracer.pid}/task/{tracer.pid}/children') + pids = children.read_text().split() + assert len(pids) == 1, pids + time.sleep(seconds) + os.kill(int(pids[0]), signal.SIGTERM) + _, error = tracer.communicate(timeout=10) + assert tracer.returncode == 0, (tracer.returncode, error[-1000:]) + captured = trace.read_text() + calls = [line for line in captured.splitlines() + if line.startswith(('poll(', 'ppoll('))] + values = [] + for line in calls: + if line.startswith('ppoll('): + if re.search(r'\],\s*\d+,\s*NULL,', line): + values.append(-1) + elif match := re.search(r'\{tv_sec=(\d+), tv_nsec=(\d+)\}', line): + values.append(int(match.group(1)) * 1000 + + int(match.group(2)) // 1_000_000) + elif match := re.search(r',\s*(-?\d+)\)\s+=', line): + values.append(int(match.group(1))) + return {'poll_calls': len(calls), 'timeout_values_ms': sorted(set(values)), + 'trace_excerpt': captured[:500], + 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest()} + finally: + if tracer.poll() is None: + os.killpg(tracer.pid, signal.SIGTERM) + try: + tracer.communicate(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(tracer.pid, signal.SIGKILL) + tracer.communicate(timeout=5) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--before', type=Path, required=True) + parser.add_argument('--after', type=Path, required=True) + parser.add_argument('--output', type=Path) + args = parser.parse_args() + seconds = 1.25 + with tempfile.TemporaryDirectory(prefix='pure-idle-before-') as directory: + baseline = args.before.resolve() + if baseline.name.endswith('.tar.gz'): + with tarfile.open(baseline, 'r:gz') as archive: + body = archive.extractfile('pure-document/bin/pure-connections-fixed').read() + baseline = Path(directory) / 'pure-connections-fixed' + baseline.write_bytes(body) + baseline.chmod(0o755) + before = observe(baseline, seconds) + after = observe(args.after.resolve(), seconds) + assert before['poll_calls'] >= 10, before + assert after['poll_calls'] <= 2, after + assert -1 in after['timeout_values_ms'], after + result = {'profile': 'idle-http-host-poll-syscalls', + 'observation_seconds': seconds, 'before': before, 'after': after, + 'deadline_driven_idle_wait': 'pass'} + body = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(body) + print(body, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_micro_memory.py b/prototypes/connections/check_micro_memory.py new file mode 100644 index 0000000..95fad1a --- /dev/null +++ b/prototypes/connections/check_micro_memory.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Check whole-process RSS for the selected CLI document workload.""" +import argparse +import hashlib +import json +from pathlib import Path +import platform +import subprocess +import tempfile + +HERE = Path(__file__).resolve().parent + + +def measure(helper, command): + result = subprocess.run([helper, *command], capture_output=True, text=True, + check=True, timeout=15) + sample = json.loads(result.stdout) + if sample['exit_code']: + raise AssertionError(f'workload failed: {command}: {sample}') + return sample + + +def check(build, fixed=False, portable=False): + budget_path = HERE / 'memory-budget.json' + budget = json.loads(budget_path.read_text()) + if not portable and (platform.system() != 'Linux' or platform.machine() != 'aarch64'): + raise RuntimeError('reviewed memory profile requires Linux AArch64') + binary = (build / ('pure-edit-fixed' if fixed else 'pure-edit')).resolve() + helper = (build / 'measure-peak-rss').resolve() + program = HERE / 'examples/editor.pure' + with tempfile.TemporaryDirectory(prefix='pure-micro-memory-') as name: + root = Path(name) + small, large = root / 'small', root / 'large' + small.write_bytes(b'A') + large.write_bytes(b'B' * 1_048_576) + store = root / 'store' + base = [binary] + if not fixed: + base += ['--program', program] + base += ['--store', store] + raw_samples = { + 'small_edit_rss_kib': measure(helper, [*base, 'edit', 'small', '0', '0', '0', small]), + 'one_mib_edit_rss_kib': measure(helper, [*base, 'edit', 'large', '0', '0', '0', large]), + 'one_mib_show_rss_kib': measure(helper, [*base, 'show', 'large']), + } + samples = {name: sample['max_rss_kib'] for name, sample in raw_samples.items()} + shown = subprocess.run([*map(str, base), 'show', 'large'], + capture_output=True, check=True, timeout=15) + if shown.stdout != large.read_bytes(): + raise AssertionError('measured document was not stored correctly') + if portable: + replacement = root / 'replacement' + replacement.write_bytes(b'Z') + raw_samples['one_mib_splice_rss_kib'] = measure( + helper, [*base, 'edit', 'large', '1', '524288', '1', replacement]) + samples['one_mib_splice_rss_kib'] = raw_samples['one_mib_splice_rss_kib']['max_rss_kib'] + edited = subprocess.run([*map(str, base), 'show', 'large'], + capture_output=True, check=True, timeout=15) + expected = large.read_bytes()[:524288] + b'Z' + large.read_bytes()[524289:] + if edited.stdout != expected: + raise AssertionError('one-byte revision edit did not survive a new process') + failures = [] if portable else [f'{name}: {value} > {budget["max_" + name]}' + for name, value in samples.items() if value > budget['max_' + name]] + if portable: + scope = ('Child-process peak RSS for native CLI document creation, read and ' + 'one-byte revision edit; includes libc and the selected program form, ' + 'excludes filesystem cache, kernel buffers, TLS, GUI and other apps. ' + 'No cross-platform memory ceiling is asserted.') + else: + scope = budget['scope'] + if fixed: + scope = scope.replace('program parsing', 'the embedded prepared program') + return {'passed': not failures, 'failures': failures, + 'profile': 'portable-measurement-no-budget' if portable else budget['profile'], + 'variant': 'fixed-program' if fixed else 'source-program', + 'scope': scope, 'samples': samples, + 'raw_samples': raw_samples if portable else None, + 'budget_sha256': None if portable else hashlib.sha256(budget_path.read_bytes()).hexdigest(), + 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(), + 'binary_bytes': binary.stat().st_size, + 'platform': platform.platform()} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--build', type=Path, required=True) + parser.add_argument('--fixed', action='store_true', + help='measure the parser-free fixed-program editor') + parser.add_argument('--portable', action='store_true', + help='measure native document work without a Linux-specific RSS ceiling') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + result = check(args.build, args.fixed, args.portable) + output = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output) + print(output, end='') + if not result['passed']: + raise SystemExit(1) + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_mirror.py b/prototypes/connections/check_mirror.py new file mode 100644 index 0000000..ed46fa8 --- /dev/null +++ b/prototypes/connections/check_mirror.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Run a real document through multiple in-process Pure domains and stores.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import tempfile + + +def run(*args, code=0): + result = subprocess.run([str(arg) for arg in args], capture_output=True, timeout=30) + if result.returncode != code: + raise AssertionError((result.args, result.returncode, code, result.stderr[-500:])) + return result + + +def edit(binary, store, key, revision, offset, remove, file): + result = run(binary, '--store', store, 'edit', key, revision, offset, remove, file) + return json.loads(result.stdout)['revision'] + + +def show(binary, store, key, revision, body): + result = run(binary, '--store', store, 'show', key) + assert result.stdout == body + assert json.loads(result.stderr)['revision'] == str(revision) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--mirror', type=Path, required=True) + parser.add_argument('--editor', type=Path, required=True) + parser.add_argument('--rss-helper', type=Path) + parser.add_argument('--scratch-parent', type=Path) + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + mirror, editor = args.mirror.resolve(), args.editor.resolve() + if args.scratch_parent: + args.scratch_parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='pure-mirror-', + dir=args.scratch_parent.resolve() if args.scratch_parent else None) as name: + root = Path(name) + source, left, right = root / 'source', root / 'left', root / 'right' + initial = b'A' * 1_048_576 + changed = initial[:-1] + b'Z' + input_file, patch, empty = root / 'initial', root / 'patch', root / 'empty' + input_file.write_bytes(initial) + patch.write_bytes(b'Z') + empty.write_bytes(b'') + assert edit(editor, source, 'document', 0, 0, 0, input_file) == '1' + first = json.loads(run(mirror, source, 'document', left, 'document', 0, + right, 'document', 0).stdout) + assert first['source_revision'] == '1' and first['bytes'] == len(initial) + assert [target['status'] for target in first['targets']] == ['ok', 'ok'] + show(editor, left, 'document', 1, initial) + show(editor, right, 'document', 1, initial) + + assert edit(editor, source, 'document', 1, len(initial) - 1, 1, patch) == '2' + show(editor, right, 'document', 1, initial) + partial = json.loads(run(mirror, source, 'document', left, 'document', 1, + right, 'document', 0, code=3).stdout) + assert [target['status'] for target in partial['targets']] == ['ok', 'conflict'] + show(editor, left, 'document', 2, changed) + show(editor, right, 'document', 1, initial) + recovered = json.loads(run(mirror, source, 'document', right, 'document', 1).stdout) + assert recovered['targets'][0]['revision'] == '2' + show(editor, right, 'document', 2, changed) + + assert edit(editor, source, 'empty', 0, 0, 0, empty) == '1' + assert json.loads(run(mirror, source, 'empty', left, 'empty', 0).stdout)['bytes'] == 0 + show(editor, left, 'empty', 1, b'') + zero_budget = root / 'zero-budget' + assert json.loads(run(mirror, '--target-budget', 0, source, 'empty', + zero_budget, 'empty', 0).stdout)['targets'][0]['status'] == 'ok' + show(editor, zero_budget, 'empty', 1, b'') + low_budget = root / 'low-budget' + refused = json.loads(run(mirror, '--target-budget', 1024, source, + 'document', low_budget, 'document', 0, code=2).stdout) + assert refused == {'admission_status': 'budget', 'target_index': 0} + assert not list(low_budget.glob('obj-*')) + + rejected_store = root / 'rejected' + rejected = json.loads(run(mirror, source, 'document', rejected_store, + 'document', 0, right, 'bad/key', 0, code=2).stdout) + assert rejected == {'admission_status': 'invalid', 'target_index': 1} + assert not list(rejected_store.glob('obj-*')) + show(editor, right, 'document', 2, changed) + missing_store = root / 'missing-target' + missing = json.loads(run(mirror, source, 'absent', missing_store, + 'document', 0, code=2).stdout) + assert missing == {'source_status': 'not_found'} and not missing_store.exists() + + measured = None + fanout = [root / f'fanout-{index}' for index in range(8)] + fanout_args = [item for store in fanout for item in (store, 'document', 0)] + if args.rss_helper: + third, fourth = root / 'third', root / 'fourth' + measured = json.loads(run(args.rss_helper.resolve(), mirror, source, + 'document', third, 'document', 0, + fourth, 'document', 0).stdout) + assert measured['exit_code'] == 0 + show(editor, third, 'document', 1, changed) + show(editor, fourth, 'document', 1, changed) + fanout_rss = json.loads(run(args.rss_helper.resolve(), mirror, source, + 'document', *fanout_args).stdout) + assert fanout_rss['exit_code'] == 0 + else: + fanout_result = json.loads(run(mirror, source, 'document', *fanout_args).stdout) + assert len(fanout_result['targets']) == 8 + assert all(target['status'] == 'ok' for target in fanout_result['targets']) + fanout_rss = None + for store in fanout: + show(editor, store, 'document', 1, changed) + result = {'passed': True, 'source_body_sha256': hashlib.sha256(changed).hexdigest(), + 'mirror_sha256': hashlib.sha256(mirror.read_bytes()).hexdigest(), + 'editor_sha256': hashlib.sha256(editor.read_bytes()).hexdigest(), + 'source_one_mib_to_two_domains': first, + 'independent_partial_conflict': partial, + 'conflict_recovered_revision': recovered['targets'][0]['revision'], + 'empty_body_forwarded': True, + 'target_budget_checked_before_document_write': True, + 'all_admissions_before_document_writes': True, + 'source_failure_before_target_creation': True, + 'one_mib_two_target_peak_rss': measured, + 'one_mib_eight_targets_verified': True, + 'one_mib_eight_target_peak_rss': fanout_rss} + body = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(body) + print(body, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_package.py b/prototypes/connections/check_package.py new file mode 100644 index 0000000..84adc05 --- /dev/null +++ b/prototypes/connections/check_package.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Extract a binary preview elsewhere and run its real document paths.""" +import argparse +import hashlib +import http.client +import json +from pathlib import Path +import select +import subprocess +import struct +import tarfile +import tempfile + +HERE = Path(__file__).resolve().parent + + +def run(*args): + result = subprocess.run([str(arg) for arg in args], capture_output=True, timeout=90) + if result.returncode: + raise AssertionError((result.args, result.returncode, result.stdout[-1000:], result.stderr[-1000:])) + return result + + +def check_http(server_binary, binary, program, root): + store = root / 'http-store' + insert = root / 'http-insert' + insert.write_bytes(b'hello Pure') + program_args = [] if program is None else ['--program', program] + run(binary, *program_args, '--store', store, + 'edit', 'note', '0', '0', '0', insert) + server = subprocess.Popen([str(server_binary), *map(str, program_args), + '--store', str(store), '--port', '0'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + ready, _, _ = select.select([server.stdout], [], [], 5) + assert ready, 'packaged HTTP host did not start' + port = json.loads(server.stdout.readline())['port'] + connection = http.client.HTTPConnection('127.0.0.1', port, timeout=5) + try: + body = struct.pack('= 7 + checks.append('relocated terminal editor saves UTF-8 and one MiB documents') + return {'archive_sha256': digest, 'archive_bytes': archive.stat().st_size, + 'selected_hosts': manifest['selected_hosts'], 'checks': checks} + + binary = package / 'bin/pure-edit' + program = package / 'share/editor.pure' + store = root / 'store' + insert = root / 'insert.bin' + insert.write_bytes(b'hello Pure') + written = run(binary, '--program', program, '--store', store, + 'edit', 'note', '0', '0', '0', insert) + assert json.loads(written.stdout) == {'revision': '1', 'reply_bytes': 10} + profile = manifest.get('storage_profile') + expected_magic = b'PUREP001' if profile == 'PUREP001-16k-page-research' else b'PUREB001' + assert (store / 'obj-note').read_bytes()[:8] == expected_magic + shown = run(binary, '--program', program, '--store', store, 'show', 'note') + assert shown.stdout == b'hello Pure' + checks.append('extracted native CLI creates and restores the real document') + if profile == 'PUREB001+PUREP001-hybrid-16k-research': + big = root / 'big.bin' + big.write_bytes(b'X' * (16 * 1024 + 1)) + run(binary, '--program', program, '--store', store, + 'edit', 'big', '0', '0', '0', big) + assert (store / 'obj-big').read_bytes()[:8] == b'PUREP001' + assert run(binary, '--program', program, '--store', store, + 'show', 'big').stdout == big.read_bytes() + checks.append('selected storage keeps small values inline and maps large values') + + changed = root / 'changed.pure' + source = program.read_text() + assert 'reply(saved, edit.insert);' in source + changed.write_text(source.replace('reply(saved, edit.insert);', 'reply(saved);')) + changed_store = root / 'changed-store' + changed_result = run(binary, '--program', changed, '--store', changed_store, + 'edit', 'note', '0', '0', '0', insert) + assert json.loads(changed_result.stdout) == {'revision': '1', 'reply_bytes': 0} + checks.append('editing the packaged .pure source changes the executed reply') + + if 'pure-connections' in manifest['selected_hosts']: + check_http(package / 'bin/pure-connections', binary, program, root) + checks.append('extracted HTTP host updates the same document as the native CLI') + + if 'pure-edit-tui' in manifest['selected_hosts']: + tui = json.loads(run('python3', HERE / 'check_tui.py', '--build', + package / 'bin', '--source', '--program', program).stdout) + assert 'source CLI and TUI read the same document' in tui['checks'] + checks.append('relocated source-loaded TUI and CLI share UTF-8, large and many-document paths') + + peer = None + assert not ({'pure-document-peer', 'pure-document-server'} <= + set(manifest['selected_hosts'])) + if 'pure-document-peer' in manifest['selected_hosts']: + peer = json.loads(run('python3', HERE / 'check_document_peer.py', '--peer', + package / 'bin/pure-document-peer', '--editor', binary, + '--program', program).stdout) + assert len(peer['checks']) >= 9 + checks.append('extracted C/Python TLS peers share a 1 MiB document and recover') + if 'pure-document-server' in manifest['selected_hosts']: + peer = json.loads(run('python3', HERE / 'check_document_server_package.py', + '--server', package / 'bin/pure-document-server', + '--editor', binary, '--program', program).stdout) + assert len(peer['checks']) >= 5 + checks.append('extracted server-only TLS peer and external client share a 1 MiB document and recover') + if 'pure-edit-window' in manifest['selected_hosts']: + key_switch = 'native-document-key-switch' in manifest.get('features', []) + window = json.loads(run('xvfb-run', '-a', '-s', '-screen 0 1280x800x24', + 'python3', HERE / 'check_window.py', '--build', + package / 'bin', '--program', program, + *([] if key_switch else ['--legacy-window'])).stdout) + assert window['one_mib_document_one_byte_edit'] == 'pass' + if key_switch: + assert window['multi_document_switch_preserves_draft'] == 'pass' + checks.append('extracted GTK window switches documents without losing drafts') + else: + checks.append('extracted archived GTK window edits the same document') + result = {'archive_sha256': digest, 'archive_bytes': archive.stat().st_size, + 'selected_hosts': manifest['selected_hosts'], 'checks': checks} + if peer is not None: + result['peer_checks'] = peer['checks'] + result['peer_idle_rss_kib'] = peer['server_idle_rss_kib'] + result['peer_after_large_work_rss_kib'] = peer['server_after_large_work_rss_kib'] + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('archive', type=Path) + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + result = check(args.archive) + output = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output) + print(output, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_policy.py b/prototypes/connections/check_policy.py new file mode 100644 index 0000000..5fce560 --- /dev/null +++ b/prototypes/connections/check_policy.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Exercise an authored final-size guard through native source and fixed apps.""" +import argparse +import http.client +import json +from pathlib import Path +import select +import struct +import subprocess +import tempfile +from check_tui import Terminal +from check_document_peer import (certificates, connect, edit as peer_edit, + hello, request as peer_request, start, stop) + + +def run(binary, program, store, *args): + command = [str(binary)] + if program is not None: + command += ['--program', str(program)] + command += ['--store', str(store), *map(str, args)] + return subprocess.run(command, capture_output=True, timeout=15) + + +def edit(binary, program, store, payload, key, revision, offset, remove, body): + payload.write_bytes(body) + return run(binary, program, store, 'edit', key, revision, offset, remove, payload) + + +def result(process, status, revision): + assert process.returncode == status, (process.returncode, process.stderr) + channel = process.stdout if status == 0 else process.stderr + value = json.loads(channel) + assert value['revision'] == str(revision), value + if status: + assert value['error'] == 'limit' if status == 2 else value['error'] == 'conflict' + + +def body(binary, program, store, expected): + shown = run(binary, program, store, 'show', 'note') + assert shown.returncode == 0 and shown.stdout == expected + return int(json.loads(shown.stderr)['revision']) + + +class Server: + def __init__(self, binary, program, store): + command = [str(binary), '--store', str(store), '--port', '0'] + if program is not None: + command += ['--program', str(program)] + self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + ready, _, _ = select.select([self.process.stdout], [], [], 10) + if not ready: + self.process.kill() + raise AssertionError('document server did not start') + line = self.process.stdout.readline() + if not line: + raise AssertionError(self.process.communicate(timeout=5)) + self.port = json.loads(line)['port'] + + def request(self, method, data=b'', expected=None): + headers = {'Content-Length': str(len(data))} + if expected is not None: + headers['If-None-Match' if expected == 0 else 'If-Match'] = ( + '*' if expected == 0 else f'"{expected}"') + connection = http.client.HTTPConnection('127.0.0.1', self.port, timeout=15) + try: + connection.request(method, '/objects/note', data, headers) + response = connection.getresponse() + return response.status, response.getheader('ETag'), response.read() + finally: + connection.close() + + def close(self): + self.process.terminate() + _, error = self.process.communicate(timeout=10) + assert self.process.returncode == 0, error[-1000:] + stats = json.loads(error) + assert stats['active'] == stats['retained_payload_bytes'] == 0 + + +def splice(offset, remove, insert): + return struct.pack(' limit: + failures.append(f'{name} {section}: {value} > {limit}') + if observed['needed'] != sorted(limits['needed']): + failures.append(f'{name} dependencies: {observed["needed"]} ' + f'!= {sorted(limits["needed"])}') + result = {'passed': not failures, 'failures': failures, + 'profile': budget['profile'], 'scope': budget['scope'], + 'budget_sha256': hashlib.sha256(args.budget.read_bytes()).hexdigest(), + 'platform': platform.platform(), 'binaries': binaries} + 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/check_service_scale.py b/prototypes/connections/check_service_scale.py new file mode 100644 index 0000000..fb04baf --- /dev/null +++ b/prototypes/connections/check_service_scale.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Exercise the real document app across many objects and concurrent clients.""" +import argparse +from concurrent.futures import ThreadPoolExecutor +import hashlib +import json +from pathlib import Path +import subprocess +import tempfile + +from check_fixed_server import Host, memory_kib, request, splice + +HERE = Path(__file__).resolve().parent +DOCUMENTS = 1024 +CLIENTS = 8 +BODY_BYTES = 8192 +SERVER_BUDGET = 1_048_576 +PEAK_RSS_LIMIT_KIB = 4096 + + +def document(index): + seed = f'document:{index:04d}:'.encode() + return (seed * ((BODY_BYTES + len(seed) - 1) // len(seed)))[:BODY_BYTES] + + +def client(host, client_number): + digests = [] + for index in range(client_number, DOCUMENTS, CLIENTS): + key = f'doc{index:04d}' + original = document(index) + created = request(host, 'PUT', key, splice(0, 0, original), 0) + assert created == (200, '"1"', original), (index, 'create', created[:2]) + opened = request(host, 'GET', key) + assert opened == (200, '"1"', original), (index, 'open', opened[:2]) + replacement = bytes((65 + index % 26,)) + edited = request(host, 'PUT', key, + splice(BODY_BYTES - 1, 1, replacement), 1) + assert edited == (200, '"2"', replacement), (index, 'edit', edited[:2]) + final = original[:-1] + replacement + restored = request(host, 'GET', key) + assert restored == (200, '"2"', final), (index, 'restore', restored[:2]) + digests.append((index, hashlib.sha256(final).digest())) + return digests + + +def run_variant(build, root, fixed, async_source=False): + assert not (fixed and async_source) + store = root / ('fixed' if fixed else 'source-async' if async_source else 'source') + binary = build / ('pure-connections-fixed' if fixed else + 'pure-connections-async' if async_source else 'pure-connections') + host = Host(binary, store, fixed, budget=SERVER_BUDGET) + idle_rss = memory_kib(host.process.pid) + idle_vm = memory_kib(host.process.pid, 'VmSize:') + idle_threads = memory_kib(host.process.pid, 'Threads:') + try: + with ThreadPoolExecutor(max_workers=CLIENTS) as pool: + batches = list(pool.map(lambda number: client(host, number), + range(CLIENTS))) + all_digests = sorted(item for batch in batches for item in batch) + assert len(all_digests) == DOCUMENTS + digest = hashlib.sha256(b''.join(value for _, value in all_digests)).hexdigest() + peak_rss = memory_kib(host.process.pid, 'VmHWM:') + peak_vm = memory_kib(host.process.pid, 'VmPeak:') + peak_threads = memory_kib(host.process.pid, 'Threads:') + finally: + stats = host.close() + editor = build / ('pure-edit-fixed' if fixed else 'pure-edit') + for index in (0, 127, 512, DOCUMENTS - 1): + key = f'doc{index:04d}' + args = [str(editor), '--store', str(store)] + if not fixed: + args += ['--program', str(HERE / 'examples/editor.pure')] + shown = subprocess.run([*args, 'show', key], capture_output=True, timeout=15) + replacement = bytes((65 + index % 26,)) + assert shown.returncode == 0, (index, shown.stderr) + assert shown.stdout == document(index)[:-1] + replacement, index + assert json.loads(shown.stderr)['revision'] == '2', index + objects = list(store.glob('obj-*')) + assert len(objects) == DOCUMENTS, len(objects) + assert stats['accepted'] == DOCUMENTS * 4, stats + assert stats['commits'] == DOCUMENTS * 2, stats + assert stats['gets'] == DOCUMENTS * 2, stats + assert stats['releases'] == DOCUMENTS * 4, stats + assert stats['budget_rejections'] == stats['expired'] == 0, stats + assert stats['peak_connections'] <= CLIENTS, stats + assert stats['peak_retained_payload_bytes'] <= SERVER_BUDGET, stats + assert peak_rss <= PEAK_RSS_LIMIT_KIB, peak_rss + return {'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(), + 'document_digest': digest, 'stored_objects': len(objects), + 'stored_file_bytes': sum(path.stat().st_size for path in objects), + 'fresh_cli_restore_samples': 4, 'idle_rss_kib': idle_rss, + 'peak_rss_kib': peak_rss, 'idle_vm_kib': idle_vm, + 'peak_vm_kib': peak_vm, 'idle_threads': idle_threads, + 'peak_threads': peak_threads, 'stats': stats} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--build', type=Path, default=HERE / 'build') + parser.add_argument('--output', type=Path) + parser.add_argument('--async-source', action='store_true', + help='also run the optional one-worker HTTP host') + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + build = args.build.resolve() + with tempfile.TemporaryDirectory(prefix='pure-document-scale-') as directory: + root = Path(directory) + fixed = run_variant(build, root, True) + source = run_variant(build, root, False) + async_source = run_variant(build, root, False, True) if args.async_source else None + assert fixed['document_digest'] == source['document_digest'] + if async_source: + assert source['document_digest'] == async_source['document_digest'] + result = {'profile': 'document-app-many-objects-concurrent-clients', + 'documents_per_variant': DOCUMENTS, 'clients': CLIENTS, + 'body_bytes': BODY_BYTES, 'operations_per_variant': DOCUMENTS * 4, + 'server_budget_bytes': SERVER_BUDGET, + 'peak_rss_limit_kib': PEAK_RSS_LIMIT_KIB, + 'same_final_documents': 'pass', 'fixed': fixed, 'source': source} + if async_source: + result['source_async'] = async_source + body = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(body) + print(body, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_small_scan.py b/prototypes/connections/check_small_scan.py new file mode 100644 index 0000000..e5f3223 --- /dev/null +++ b/prototypes/connections/check_small_scan.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Reject directory-wide segment scans on the selected small-object edit path.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import tempfile + +HERE = Path(__file__).resolve().parent + + +def run(*args): + result = subprocess.run([str(value) for value in args], capture_output=True, timeout=20) + if result.returncode: + raise AssertionError((result.args, result.returncode, result.stderr[-500:])) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--editor', required=True, type=Path) + parser.add_argument('--output', type=Path) + args = parser.parse_args() + editor = args.editor.resolve() + with tempfile.TemporaryDirectory(prefix='pure-small-scan-') as name: + root = Path(name) + store = root / 'store' + one = root / 'one' + one.write_bytes(b'A') + base = (editor, '--program', HERE / 'examples/editor.pure', '--store', store) + run(*base, 'edit', 'tiny', 0, 0, 0, one) + for index in range(256): + (store / f'unrelated-{index:03d}').write_bytes(b'') + trace = root / 'trace' + run('strace', '-qq', '-e', 'getdents64', '-yy', '-o', trace, + *base, 'edit', 'tiny', 1, 0, 1, one) + scans = [line for line in trace.read_text().splitlines() + if 'getdents64(' in line and str(store) in line] + assert not scans, scans + result = {'small_edit_store_directory_scans': 0, + 'unrelated_directory_entries': 256, + 'binary_sha256': hashlib.sha256(editor.read_bytes()).hexdigest()} + output = json.dumps(result, indent=2) + '\n' + if args.output: + if args.output.exists(): + parser.error('evidence file already exists') + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output) + print(output, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_source_package.py b/prototypes/connections/check_source_package.py new file mode 100644 index 0000000..5bc2f15 --- /dev/null +++ b/prototypes/connections/check_source_package.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Verify a source snapshot after safe extraction and a clean native build.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import tarfile +import tempfile + +PREFIX = 'pure-source-preview/' + + +def run(*args, timeout=120): + result = subprocess.run([str(arg) for arg in args], capture_output=True, timeout=timeout) + if result.returncode: + raise AssertionError((result.args, result.returncode, + result.stdout[-2000:], result.stderr[-2000:])) + return result + + +def check(archive, release, peer, copy_range, flat_export, mirror, hybrid_tui): + archive = archive.resolve() + checks = [] + with tempfile.TemporaryDirectory(prefix='pure-source-extracted-') as directory: + root = Path(directory) + package = root / PREFIX[:-1] + with tarfile.open(archive, 'r:gz') as bundle: + members = bundle.getmembers() + names = [member.name for member in members] + assert len(names) == len(set(names)) + assert all(member.isfile() and member.name.startswith(PREFIX) and + '..' not in Path(member.name).parts for member in members) + for member in members: + destination = root / member.name + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(bundle.extractfile(member).read()) + manifest = json.loads((package / 'manifest.json').read_text()) + assert manifest['format'] == 'pure-source-preview-1' + assert set(names) == {PREFIX + name for name in manifest['files']} | {PREFIX + 'manifest.json'} + for name, metadata in manifest['files'].items(): + content = (package / name).read_bytes() + assert len(content) == metadata['bytes'] + assert hashlib.sha256(content).hexdigest() == metadata['sha256'] + checks.append('every extracted source file matches the manifest') + + connections = package / 'prototypes/connections' + run('make', '-C', connections, timeout=120) + default_outputs = {path.name for path in (connections / 'build').iterdir()} + assert default_outputs == {'runtime.o', 'libpure-execution.a', + 'pure-compile', 'embedded-plan.h', + 'pure-edit-fixed'}, default_outputs + checks.append('clean default build contains only the core, parser-free CLI and its build-time compiler') + run('make', '-C', connections, 'check-native-checkpoint', timeout=300) + checks.append('clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass') + run('make', '-C', connections, 'fixed-editor') + editor = connections / 'build/pure-edit' + fixed = connections / 'build/pure-edit-fixed' + program = connections / 'examples/editor.pure' + store = root / 'document-store' + insert = root / 'insert.bin' + insert.write_bytes(b'hello Pure') + written = run(editor, '--program', program, '--store', store, + 'edit', 'note', '0', '0', '0', insert) + assert json.loads(written.stdout) == {'revision': '1', 'reply_bytes': 10} + assert run(fixed, '--store', store, 'show', 'note').stdout == b'hello Pure' + checks.append('source-loaded CLI and parser-free CLI share the real stored document') + + changed = root / 'changed.pure' + source = program.read_text() + assert 'reply(saved, edit.insert);' in source + changed.write_text(source.replace('reply(saved, edit.insert);', 'reply(saved);')) + changed_store = root / 'changed-store' + changed_result = run(editor, '--program', changed, '--store', changed_store, + 'edit', 'note', '0', '0', '0', insert) + assert json.loads(changed_result.stdout) == {'revision': '1', 'reply_bytes': 0} + checks.append('editing .pure changes the native source-loaded reply') + + if peer: + run('make', '-C', connections, 'check-portable-peer', timeout=180) + checks.append('clean native TLS client and both server profiles pass authenticated document work') + + if copy_range: + run('make', '-C', connections, 'check-copy-range', timeout=240) + checks.append('clean selected Linux copy path passes app, scale, size, RSS and traced one-MiB splice') + + if flat_export: + run('make', '-C', connections, 'check-flat-export', timeout=180) + checks.append('clean selected page store exports mixed revisions to the default flat store') + + if mirror: + run('make', '-C', connections, 'check-mirror', timeout=180) + checks.append('clean one-MiB document fans out to two same-process Pure domains with independent conflict and release') + + if hybrid_tui: + run('make', '-C', connections, 'BUILD=build/hybrid', 'STORE=store-paged.c', + 'editor', 'source-tui', timeout=120) + run('python3', connections / 'check_tui.py', '--build', + connections / 'build/hybrid', '--source', timeout=120) + selected = root / 'hybrid-tui.tar.gz' + run('python3', connections / 'package_native.py', '--build', + connections / 'build/hybrid', '--hybrid-storage', '--with-tui', + '--output', selected, timeout=120) + run('python3', connections / 'check_package.py', selected, timeout=120) + checks.append('clean selected page store runs the source-loaded TUI and relocatable CLI+TUI package') + + if release: + run('make', '-C', connections, 'check-release', timeout=180) + checks.append('clean Linux release behavior, code, dependency and RSS gates pass') + run('make', '-C', connections, 'check-source-tui', timeout=120) + checks.append('clean source-loaded terminal editor PTY scenarios pass') + return {'archive_sha256': hashlib.sha256(archive.read_bytes()).hexdigest(), + 'archive_bytes': archive.stat().st_size, 'source_files': len(manifest['files']), + 'checks': checks} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('archive', type=Path) + parser.add_argument('--release', action='store_true', help='also run Linux release gates') + parser.add_argument('--peer', action='store_true', + help='also build and exercise the optional TLS client and servers') + parser.add_argument('--copy-range', action='store_true', + help='also check the opt-in Linux file copy path with strace') + parser.add_argument('--flat-export', action='store_true', + help='also check selected paged-to-default document store export') + parser.add_argument('--mirror', action='store_true', + help='also check one-process document fan-out through two Pure domains') + parser.add_argument('--hybrid-tui', action='store_true', + help='also build and relocate the selected 16 KiB-page native terminal app') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + result = check(args.archive, args.release, args.peer, args.copy_range, + args.flat_export, args.mirror, args.hybrid_tui) + text = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text) + print(text, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_tui.py b/prototypes/connections/check_tui.py new file mode 100644 index 0000000..5328d02 --- /dev/null +++ b/prototypes/connections/check_tui.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +"""Exercise the real terminal editor through a PTY and the same document CLI.""" +import argparse +import fcntl +import hashlib +import json +import locale +import os +from pathlib import Path +import pty +import select +import signal +import struct +import subprocess +import tempfile +import termios +import time + +HERE = Path(__file__).resolve().parent + + +def command(*args): + return subprocess.run([str(arg) for arg in args], capture_output=True, + timeout=20, check=True) + + +def utf8_locale(): + original = locale.setlocale(locale.LC_CTYPE) + candidates = [os.environ.get('LC_ALL'), os.environ.get('LC_CTYPE'), + os.environ.get('LANG'), 'C.UTF-8', 'en_US.UTF-8'] + try: + for candidate in candidates: + if not candidate: + continue + try: + locale.setlocale(locale.LC_CTYPE, candidate) + except locale.Error: + continue + if locale.nl_langinfo(locale.CODESET).upper().replace('-', '') == 'UTF8': + return candidate + finally: + locale.setlocale(locale.LC_CTYPE, original) + raise AssertionError('no UTF-8 locale available for terminal test') + + +def memory_kib(pid, field, portable=False): + if portable: + return None + for line in Path(f'/proc/{pid}/status').read_text().splitlines(): + if line.startswith(field): + return int(line.split()[1]) + raise AssertionError(field) + + +class Terminal: + def __init__(self, binary, program, store, key): + self.master, self.slave = pty.openpty() + fcntl.ioctl(self.slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0)) + self.original = termios.tcgetattr(self.slave) + language = utf8_locale() + self.process = subprocess.Popen([str(binary), + *(['--program', str(program)] if program else []), + '--store', str(store), key], + stdin=self.slave, stdout=self.slave, stderr=self.slave, + env={**os.environ, 'LANG': language, + 'LC_ALL': language}) + self.wait_for(f'Pure | {key}'.encode()) + + def wait_for(self, marker, timeout=8): + seen = b'' + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + ready, _, _ = select.select([self.master], [], [], 0.1) + if ready: + part = os.read(self.master, 65536) + seen = (seen + part)[-65536:] + if marker in seen: + return seen + if self.process.poll() is not None: + break + raise AssertionError((marker, self.process.poll(), seen[-2000:])) + + def type(self, data): + os.write(self.master, data) + + def resize(self, rows, columns): + fcntl.ioctl(self.slave, termios.TIOCSWINSZ, + struct.pack('HHHH', rows, columns, 0, 0)) + os.kill(self.process.pid, signal.SIGWINCH) + + def close(self, discard=False): + if self.process.poll() is None: + self.type(b'\x11') + if discard: + self.wait_for(b'Unsaved draft') + self.type(b'\x11') + self.process.wait(timeout=8) + assert self.process.returncode == 0, self.process.returncode + restored = termios.tcgetattr(self.slave) + assert restored == self.original, (restored, self.original) + os.close(self.master) + os.close(self.slave) + + def signal_close(self): + os.kill(self.process.pid, signal.SIGTERM) + self.process.wait(timeout=8) + assert self.process.returncode == 0, self.process.returncode + assert termios.tcgetattr(self.slave) == self.original + os.close(self.master) + os.close(self.slave) + + +def shown(editor, store, key): + result = command(*editor, '--store', store, 'show', key) + return result.stdout, json.loads(result.stderr)['revision'] + + +def wait_stored(editor, store, key, expected, revision): + deadline = time.monotonic() + 8 + while time.monotonic() < deadline: + try: + body, found_revision = shown(editor, store, key) + if body == expected and found_revision == str(revision): + return + except subprocess.CalledProcessError: + pass + time.sleep(0.05) + raise AssertionError((key, expected[:100], shown(editor, store, key))) + + +def check(build, rss_limit_kib, source, program_path, portable=False): + binary = (build / ('pure-edit-tui' if source else 'pure-edit-tui-fixed')).resolve() + program = (program_path or HERE / 'examples/editor.pure').resolve() if source else None + editor = [str((build / ('pure-edit' if source else 'pure-edit-fixed')).resolve())] + if source: + editor += ['--program', str(program)] + with tempfile.TemporaryDirectory(prefix='pure-tui-') as name: + root = Path(name) + store = root / 'store' + if source: + invalid = root / 'invalid.pure' + invalid.write_text('not a pure program\n') + for candidate, marker in ((invalid, b'Program:'), + (HERE / 'examples/storage.pure', b'Program needs')): + denied = subprocess.run([str(binary), '--program', str(candidate), + '--store', str(store), 'note'], + capture_output=True, timeout=10) + assert denied.returncode == 2 and marker in denied.stderr + assert not store.exists() + terminal = Terminal(binary, program, store, 'note') + try: + terminal.type(b'hello Pure\x1b[D\x1b[D!\x13') + wait_stored(editor, store, 'note', b'hello Pu!re', 1) + if source: + assert command(*editor, '--store', store, 'show', 'note').stdout == b'hello Pu!re' + terminal.type(b'\x0fsecond\r') + terminal.wait_for(b'Pure | second') + terminal.type('안녕'.encode() + b'\x1b[D' + '하'.encode() + b'\x13') + terminal.wait_for('안하녕'.encode()) + wait_stored(editor, store, 'second', '안하녕'.encode(), 1) + terminal.resize(32, 100) + terminal.wait_for(b'Pure | second') + terminal.type(b'\x0fnote\r') + terminal.wait_for(b'Pure | note') + terminal.type(b'\x0c') + listing = terminal.wait_for(b'Pure | documents') + assert b'note' in listing and b'second' in listing, listing[-1000:] + terminal.type(b'\x1b[B\r') + terminal.wait_for(b'Pure | second') + terminal.type(b'\x0c\x1b[A\r') + terminal.wait_for(b'Pure | note') + terminal.type(b'\x0c') + terminal.wait_for(b'Pure | documents') + (store / 'obj-second').unlink() + terminal.type(b'\x1b[B\r') + terminal.wait_for(b'Document disappeared; refresh the list') + terminal.type(b'\x1b') + terminal.wait_for(b'Pure | note') + terminal.type(b'!') + terminal.type(b'\x0c') + terminal.wait_for(b'Save or reload the draft before browsing') + external = root / 'external' + external.write_bytes(b' remote') + command(*editor, '--store', store, 'edit', 'note', '1', '11', '0', external) + terminal.type(b'\x13') + terminal.wait_for(b'Version conflict') + assert shown(editor, store, 'note') == (b'hello Pu!re remote', '2') + small_peak = memory_kib(terminal.process.pid, 'VmHWM:', portable) + finally: + terminal.close(discard=True) + + multiline = b'first\n\tsecond\nthird' + insert = root / 'multiline-insert' + insert.write_bytes(multiline) + command(*editor, '--store', store, 'edit', 'multiline', '0', '0', '0', insert) + terminal = Terminal(binary, program, store, 'multiline') + try: + terminal.resize(25, 80) + terminal.wait_for(b'third') + terminal.type(b'!\x13') + wait_stored(editor, store, 'multiline', multiline + b'!', 2) + finally: + terminal.close() + terminal = Terminal(binary, program, store, 'multiline') + terminal.resize(25, 80) + terminal.wait_for(b'third!') + terminal.close() + + rows = [f'line-{index:02}'.encode() for index in range(60)] + insert.write_bytes(b'\n'.join(rows)) + command(*editor, '--store', store, 'edit', 'rows', '0', '0', '0', insert) + terminal = Terminal(binary, program, store, 'rows') + try: + terminal.type(b'\x1b[A' * 3 + b'X\x13') + rows[56] += b'X' + wait_stored(editor, store, 'rows', b'\n'.join(rows), 2) + terminal.wait_for(b'line-56X') + finally: + terminal.close() + + large = b'Q' * 1_048_576 + insert = root / 'large-insert' + insert.write_bytes(large) + command(*editor, '--store', store, 'edit', 'large', '0', '0', '0', insert) + terminal = Terminal(binary, program, store, 'large') + try: + terminal.type(b'Z') + terminal.wait_for(b'Z') + terminal.type(b'\x13') + wait_stored(editor, store, 'large', large + b'Z', 2) + large_peak = memory_kib(terminal.process.pid, 'VmHWM:', portable) + finally: + terminal.close() + if rss_limit_kib: + assert large_peak <= rss_limit_kib, (large_peak, rss_limit_kib) + + unicode_body = '안녕'.encode() * 174_762 + insert.write_bytes(unicode_body) + command(*editor, '--store', store, 'edit', 'unicode', '0', '0', '0', insert) + terminal = Terminal(binary, program, store, 'unicode') + try: + terminal.type(b'K') + terminal.wait_for(b'K') + terminal.type(b'\x1b[HX') + terminal.wait_for(b'X' + '안녕'.encode()) + terminal.type(b'\x13') + wait_stored(editor, store, 'unicode', b'X' + unicode_body + b'K', 2) + finally: + terminal.close() + + listed = root / 'listed-store' + insert.write_bytes(b'X') + for index in range(1024): + command(*editor, '--store', listed, 'edit', f'doc{index:04}', '0', '0', '0', insert) + terminal = Terminal(binary, program, listed, 'doc0000') + try: + terminal.type(b'\x0c') + terminal.wait_for(b'Pure | documents | 1024 keys') + terminal.resize(8, 12) + terminal.wait_for(b'\033[8;1H') + terminal.resize(24, 80) + terminal.wait_for(b'Pure | documents | 1024 keys') + terminal.type(b'\x1b[F\r') + terminal.wait_for(b'Pure | doc1023') + assert shown(editor, listed, 'doc1023') == (b'X', '1') + terminal.type(b'\x0c') + terminal.wait_for(b'Pure | documents | 1024 keys') + terminal.type(b'doc0919') + terminal.wait_for(b'Find: doc0919_') + terminal.type(b'\r') + terminal.wait_for(b'Pure | doc0919') + assert shown(editor, listed, 'doc0919') == (b'X', '1') + terminal.type(b'\x0czz') + terminal.wait_for(b'No matching key') + terminal.type(b'\r') + terminal.wait_for(b'No matching key') + terminal.type(b'\x7f\x7fdoc1023\r') + terminal.wait_for(b'Pure | doc1023') + list_peak = memory_kib(terminal.process.pid, 'VmHWM:', portable) + finally: + terminal.close() + if rss_limit_kib: + assert list_peak <= rss_limit_kib, (list_peak, rss_limit_kib) + + if source: + alternate = root / 'alternate.pure' + definition = program.read_text() + fields = ' start: u32le;\n remove: u32le;' + assert fields in definition + alternate.write_text(definition.replace(fields, + ' remove: u32le;\n start: u32le;')) + alternate_store = root / 'alternate-store' + terminal = Terminal(binary, alternate, alternate_store, 'different_layout') + try: + terminal.type(b'abcdef\x13') + wait_stored(editor, alternate_store, 'different_layout', b'abcdef', 1) + terminal.type(b'\x1b[D\x1b[D\x1b[D!\x13') + wait_stored(editor, alternate_store, 'different_layout', b'abc!def', 2) + finally: + terminal.close() + + terminal = Terminal(binary, program, store, 'note') + terminal.signal_close() + return {'profile': 'native-source-document-terminal-editor' if source else + 'native-fixed-document-terminal-editor', + 'checks': (['source parse and incompatible definitions fail before store creation', + 'source CLI and TUI read the same document', + 'changed source field layout drives TUI input encoding'] if source else []) + + ['cursor insertion and save through terminal', 'open another key', + 'bounded document browser selects keys and protects unsaved draft', + 'UTF-8 codepoint movement, visible redraw and edit', 'resize redraw', + 'external revision conflict preserves draft', + 'stored multiline and tabbed document survives edit and reopen', + 'cached viewport preserves vertical movement across many rows', + 'one MiB document viewport, edit and fresh CLI restore', + 'one MiB UTF-8 viewport and edit before cached anchor', + '1,024 real documents listed, searched, resized and opened' + + (' within RSS gate' if not portable else ''), + 'terminal mode restored after key and signal exit'], + 'small_peak_rss_kib': small_peak, + 'one_mib_peak_rss_kib': large_peak, + 'one_thousand_key_list_peak_rss_kib': list_peak, + 'one_mib_peak_rss_limit_kib': rss_limit_kib, + 'portable': portable, + 'terminal_locale': utf8_locale(), + 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest()} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--build', type=Path, default=HERE / 'build') + parser.add_argument('--rss-limit-kib', type=int, default=4096, + help='0 skips the release RSS gate for sanitizer builds') + parser.add_argument('--source', action='store_true', help='check the source-loaded TUI') + parser.add_argument('--portable', action='store_true', + help='run all PTY behavior without Linux /proc RSS measurement') + parser.add_argument('--program', type=Path, help='definition for --source (including relocated bundles)') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + result = check(args.build, 0 if args.portable else args.rss_limit_kib, + args.source, args.program, args.portable) + body = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(body) + print(body, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/check_window.py b/prototypes/connections/check_window.py new file mode 100644 index 0000000..57c96ef --- /dev/null +++ b/prototypes/connections/check_window.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python3 +"""Real GTK/X11 document flow. Run under xvfb-run after `make window editor`.""" +import argparse +import http.client +import json +import os +from pathlib import Path +import re +import select +import struct +import subprocess +import tempfile +import time + +HERE = Path(__file__).resolve().parent +BUILD = HERE / "build" +PROGRAM = HERE / "examples/editor.pure" + + +def command(*args, input=None): + result = subprocess.run(args, input=input, capture_output=True, timeout=15) + if result.returncode: + raise AssertionError((args, result.returncode, result.stdout, result.stderr)) + return result.stdout + + +def xdo(*args): + return command("xdotool", *args).decode().strip() + + +def switch_document(window, key): + xdo("key", "ctrl+o") + xdo("key", "ctrl+a") + xdo("type", "--clearmodifiers", key) + xdo("key", "Return") + for _ in range(50): + title = xdo("getwindowname", window) + if f"{key} · Pure document" in title: + return + time.sleep(0.05) + raise AssertionError((key, title)) + + +def store_cli(directory, *args): + return subprocess.run( + [str(BUILD / "pure-edit"), "--program", str(PROGRAM), + "--store", str(directory), *args], capture_output=True, timeout=15 + ) + + +def read_document(directory, key, revision): + for _ in range(40): + result = store_cli(directory, "show", key) + if result.returncode == 0: + metadata = json.loads(result.stderr) + if metadata["revision"] == str(revision): + return result.stdout + time.sleep(0.1) + raise AssertionError((key, revision, result.returncode, result.stdout, result.stderr)) + + +def remote_edit(directory, key, revision, offset, inserted): + server = subprocess.Popen( + [str(BUILD / "pure-connections"), "--program", str(PROGRAM), + "--store", str(directory), "--port", "0"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + try: + ready, _, _ = select.select([server.stdout], [], [], 5) + assert ready, "document server did not start" + port = json.loads(server.stdout.readline())["port"] + connection = http.client.HTTPConnection("127.0.0.1", port, timeout=5) + try: + body = struct.pack(" 5 else '' + category = ('heap' if name == '[heap]' else + 'stack' if name.startswith('[stack') else + 'anonymous' if not name or name.startswith('[anon:') else + 'file' if name.startswith('/') else 'other') + elif line.startswith('Rss:'): + groups[category] += int(line.split()[1]) + return groups + + +def check(check_key_switch=True, switch_rounds=16, diagnose_maps=False, + switch_mode='alternate', graceful_large_close=False, settle_seconds=0, + large_line_width=80): + global PROGRAM + assert os.environ.get("DISPLAY"), "run under xvfb-run or another X11 display" + with tempfile.TemporaryDirectory(prefix="pure-document-window-") as name: + directory = Path(name) + source_definition = PROGRAM.read_bytes() + PROGRAM = directory / "editable.pure" + PROGRAM.write_bytes(source_definition) + app, _ = start_window(directory, "note") + try: + xdo("type", "--clearmodifiers", "--delay", "15", "hello Pure") + xdo("key", "ctrl+s") + assert read_document(directory, "note", 1) == b"hello Pure" + xdo("type", "--clearmodifiers", "!") + if (BUILD / 'pure-connections').exists(): + remote_edit(directory, "note", 1, 10, b" remote") + external_host = 'http' + else: + cli_external_edit(directory, "note", 1, 10, b" remote") + external_host = 'cli' + xdo("key", "ctrl+s") + assert read_document(directory, "note", 2) == b"hello Pure remote" + assert clipboard() == b"hello Pure!", "conflict replaced the local draft" + xdo("key", "ctrl+w") + time.sleep(0.15) + assert app.poll() is None, "unsaved draft closed without confirmation" + xdo("key", "Escape") + time.sleep(0.1) + assert app.poll() is None and clipboard() == b"hello Pure!" + finally: + stop_window(app) + + if check_key_switch: + # One native window must switch among stored objects without dropping + # a draft or installing a non-text object as the active document. + app, window = start_window(directory, "first") + try: + xdo("type", "--clearmodifiers", "alpha") + xdo("key", "ctrl+s") + assert read_document(directory, "first", 1) == b"alpha" + xdo("key", "ctrl+o") + xdo("key", "ctrl+a") + xdo("type", "--clearmodifiers", "second") + xdo("key", "Return") + assert "second · Pure document" in xdo("getwindowname", window) + xdo("type", "--clearmodifiers", "beta") + xdo("key", "ctrl+s") + assert read_document(directory, "second", 1) == b"beta" + xdo("type", "--clearmodifiers", "!") + xdo("key", "ctrl+o") + xdo("key", "ctrl+a") + xdo("type", "--clearmodifiers", "third") + xdo("key", "Return") + assert "second · Pure document" in xdo("getwindowname", window) + xdo("mousemove", "--window", window, "120", "150") + xdo("click", "1") + assert clipboard() == b"beta!", "switch discarded the document draft" + xdo("key", "ctrl+s") + assert read_document(directory, "second", 2) == b"beta!" + xdo("key", "ctrl+o") + xdo("key", "ctrl+a") + xdo("type", "--clearmodifiers", "third") + xdo("key", "Return") + assert "third · Pure document" in xdo("getwindowname", window) + xdo("type", "--clearmodifiers", "gamma") + xdo("key", "ctrl+s") + assert read_document(directory, "third", 1) == b"gamma" + + xdo("key", "ctrl+o") + xdo("key", "ctrl+a") + xdo("type", "--clearmodifiers", "bad/key") + xdo("key", "Return") + assert "third · Pure document" in xdo("getwindowname", window) + payload = directory / "binary-payload" + payload.write_bytes(b"\x00") + assert store_cli(directory, "edit", "binary", "0", "0", "0", str(payload)).returncode == 0 + xdo("key", "ctrl+o") + xdo("key", "ctrl+a") + xdo("type", "--clearmodifiers", "binary") + xdo("key", "Return") + assert "third · Pure document" in xdo("getwindowname", window) + assert read_document(directory, "third", 1) == b"gamma" + + xdo("key", "ctrl+shift+d") + xdo("key", "ctrl+End") + xdo("type", "--clearmodifiers", " draft") + xdo("key", "ctrl+o") + xdo("key", "ctrl+a") + xdo("type", "--clearmodifiers", "fourth") + xdo("key", "Return") + assert "third · Pure document" in xdo("getwindowname", window) + xdo("key", "ctrl+shift+d") + xdo("key", "ctrl+shift+d") + assert b" draft" in clipboard(), "switch discarded the definition draft" + finally: + stop_window(app) + + app, _ = start_window(directory, "empty") + try: + xdo("key", "ctrl+s") + assert read_document(directory, "empty", 1) == b"" + finally: + stop_window(app) + app, _ = start_window(directory, "empty") + try: + xdo("key", "ctrl+s") + assert read_document(directory, "empty", 1) == b"" + finally: + stop_window(app) + + insertion = directory / "remote-insert" + insertion.write_bytes("가나다".encode()) + initial = store_cli(directory, "edit", "unicode", "0", "0", "0", str(insertion)) + assert initial.returncode == 0, initial.stderr + app, _ = start_window(directory, "unicode") + try: + assert clipboard() == "가나다".encode() + # xclip forks an owner process which keeps inherited pipe handles. + pasted = subprocess.run(["xclip", "-selection", "clipboard", "-i", + "-t", "UTF8_STRING"], input="각나다".encode(), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=5) + assert pasted.returncode == 0 + xdo("key", "ctrl+v") + time.sleep(0.15) + xdo("key", "ctrl+s") + assert read_document(directory, "unicode", 2) == "각나다".encode() + xdo("key", "ctrl+s") + assert read_document(directory, "unicode", 2) == "각나다".encode(), "no-op save changed revision" + finally: + stop_window(app) + app, _ = start_window(directory, "unicode") + try: + assert clipboard() == "각나다".encode(), "new process did not restore the edit" + finally: + stop_window(app) + + # A real 1 MiB document crosses the GTK boundary, but an edit still + # carries only the prepared two u32 fields and one changed byte. + large = ((b"A" * (large_line_width - 1) + b"\n") * + (1_048_576 // large_line_width) + + b"A" * (1_048_576 % large_line_width)) + assert len(large) == 1_048_576 + insertion.write_bytes(large) + initial = store_cli(directory, "edit", "large", "0", "0", "0", str(insertion)) + assert initial.returncode == 0, initial.stderr + if check_key_switch: + other_large = b"B" + large[1:] + other_revision = 1 + insertion.write_bytes(other_large) + assert store_cli(directory, "edit", "large_other", "0", "0", "0", + str(insertion)).returncode == 0 + switch_rss_samples = [] + switch_map_samples = [] + app, large_window = start_window(directory, "large") + try: + time.sleep(0.25) + resident = rss_kib(app.pid) + xdo("key", "ctrl+End") + xdo("key", "BackSpace") + xdo("type", "--clearmodifiers", "Z") + xdo("key", "ctrl+s") + stored = read_document(directory, "large", 2) + assert stored == large[:-1] + b"Z" + if check_key_switch: + if switch_mode == 'same-body': + insertion.write_bytes(stored) + assert store_cli(directory, "edit", "large_other", "1", "0", + str(len(other_large)), str(insertion)).returncode == 0 + other_large = stored + other_revision = 2 + for step in range(switch_rounds): + key = "large" if switch_mode == 'same' else ( + "large_other" if step % 2 == 0 else "large") + switch_document(large_window, key) + if (step + 1) % 8 == 0: + switch_rss_samples.append(rss_kib(app.pid)) + if diagnose_maps: + switch_map_samples.append(rss_breakdown_kib(app.pid)) + assert read_document(directory, "large", 2) == stored + assert read_document(directory, "large_other", other_revision) == other_large + if settle_seconds: + time.sleep(settle_seconds) + settled_rss = rss_kib(app.pid) + settled_maps = rss_breakdown_kib(app.pid) if diagnose_maps else None + if graceful_large_close: + xdo("key", "ctrl+w") + _, error = app.communicate(timeout=15) + assert app.returncode == 0, (app.returncode, error[-4000:]) + finally: + large_stderr = stop_window(app) + + # Author the same running document app in the native window. Failed + # preparation and an external source conflict must preserve the draft. + changed_definition = source_definition.replace(b"reply(saved, edit.insert);", + b"reply(saved);") + assert changed_definition != source_definition + app, window = start_window(directory, "authored") + try: + xdo("key", "ctrl+shift+d") + time.sleep(0.25) + pasted = subprocess.run(["xclip", "-selection", "clipboard", "-i", + "-t", "UTF8_STRING"], input=changed_definition, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=5) + assert pasted.returncode == 0 + xdo("key", "ctrl+a") + xdo("key", "ctrl+v") + xdo("key", "ctrl+shift+s") + for _ in range(30): + if PROGRAM.read_bytes() == changed_definition: + break + time.sleep(0.1) + assert PROGRAM.read_bytes() == changed_definition, "definition was not saved" + assert "definition 2" in xdo("getwindowname", window) + xdo("key", "ctrl+shift+d") + xdo("type", "--clearmodifiers", "Z") + xdo("key", "ctrl+s") + assert read_document(directory, "authored", 1) == b"Z" + assert "output 0 B" in xdo("getwindowname", window), "old reply plan stayed active" + + xdo("key", "ctrl+shift+d") + cannot_save = changed_definition + b"\n// source write must fail\n" + pasted = subprocess.run(["xclip", "-selection", "clipboard", "-i", + "-t", "UTF8_STRING"], input=cannot_save, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=5) + assert pasted.returncode == 0 + xdo("key", "ctrl+a") + xdo("key", "ctrl+v") + prior_mode = directory.stat().st_mode & 0o777 + os.chmod(directory, 0o500) + try: + xdo("key", "ctrl+shift+s") + finally: + os.chmod(directory, prior_mode) + assert PROGRAM.read_bytes() == changed_definition, "failed source save changed the file" + assert "definition 2" in xdo("getwindowname", window) + assert clipboard() == cannot_save, "failed source save dropped the draft" + + partial_read = source_definition.replace( + b"reply(document, document.body);", + b"reply(document, range(document.body, 0, 1));") + assert partial_read != source_definition + pasted = subprocess.run(["xclip", "-selection", "clipboard", "-i", + "-t", "UTF8_STRING"], input=partial_read, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=5) + assert pasted.returncode == 0 + xdo("key", "ctrl+a") + xdo("key", "ctrl+v") + xdo("key", "ctrl+shift+s") + assert PROGRAM.read_bytes() == changed_definition, "partial read made document editing unsafe" + assert "definition 2" in xdo("getwindowname", window) + + invalid = b"not a Pure definition\n" + pasted = subprocess.run(["xclip", "-selection", "clipboard", "-i", + "-t", "UTF8_STRING"], input=invalid, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=5) + assert pasted.returncode == 0 + xdo("key", "ctrl+a") + xdo("key", "ctrl+v") + xdo("key", "ctrl+shift+s") + assert PROGRAM.read_bytes() == changed_definition + assert "definition 2" in xdo("getwindowname", window) + xdo("key", "ctrl+a") + xdo("key", "ctrl+c") + assert clipboard() == invalid, "invalid definition draft was discarded" + + local_draft = changed_definition + b"\n// local draft\n" + external = source_definition + b"\n// external change\n" + pasted = subprocess.run(["xclip", "-selection", "clipboard", "-i", + "-t", "UTF8_STRING"], input=local_draft, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=5) + assert pasted.returncode == 0 + xdo("key", "ctrl+a") + xdo("key", "ctrl+v") + PROGRAM.write_bytes(external) + xdo("key", "ctrl+shift+s") + assert PROGRAM.read_bytes() == external, "external definition was overwritten" + assert "definition 2" in xdo("getwindowname", window) + xdo("key", "ctrl+w") + time.sleep(0.15) + assert app.poll() is None, "unsaved definition draft closed without confirmation" + xdo("key", "Escape") + time.sleep(0.1) + assert app.poll() is None + xdo("mousemove", "--window", window, "680", "326") + xdo("click", "1") # Explicitly discard the source draft and reload disk. + xdo("key", "ctrl+shift+s") + assert PROGRAM.read_bytes() == external + assert "definition 3" in xdo("getwindowname", window) + xdo("key", "ctrl+shift+d") + xdo("type", "--clearmodifiers", "Y") + xdo("key", "ctrl+s") + assert len(read_document(directory, "authored", 2)) == 2 + assert "output 1 B" in xdo("getwindowname", window), "reloaded definition did not run" + finally: + stop_window(app) + + app, window = start_window(directory, "author-restored") + try: + xdo("type", "--clearmodifiers", "R") + xdo("key", "ctrl+s") + assert read_document(directory, "author-restored", 1) == b"R" + assert "output 1 B" in xdo("getwindowname", window), "saved definition did not restore" + finally: + stop_window(app) + + result = {"gtk_cli_write": "pass", f"{external_host}_conflict_preserves_draft": "pass", + "unsaved_close_guard": "pass", + "empty_document_restore": "pass", + "utf8_boundary_and_restart": "pass", "unchanged_save_keeps_revision": "pass", + "one_mib_document_one_byte_edit": "pass", "large_window_rss_kib": resident, + "native_definition_apply_and_running_output": "pass", + "definition_restores_in_new_window": "pass", + "partial_read_definition_rejected": "pass", + "source_write_failure_preserves_active_definition": "pass", + "invalid_and_external_definition_preserve_draft": "pass", + "reloaded_definition_installs_without_rewriting_source": "pass", + "profile": "optional-native-document-window"} + if check_key_switch: + result["multi_document_switch_preserves_draft"] = "pass" + result["large_switch_rss_samples_kib"] = switch_rss_samples + result["large_switch_mode"] = switch_mode + result["large_line_width"] = large_line_width + if settle_seconds: + result["large_switch_settled_rss_kib"] = settled_rss + if diagnose_maps: + result["large_switch_settled_map_kib"] = settled_maps + if diagnose_maps: + result["large_switch_map_samples_kib"] = switch_map_samples + allocations = re.findall( + rb'PURE_MALLOC key=([^ ]+) arena=(\d+) used=(\d+) free=(\d+) mapped=(\d+)', + large_stderr) + if allocations: + result['large_malloc_samples_bytes'] = [ + {'key': key.decode(), 'arena': int(arena), 'used': int(used), + 'free': int(free), 'mapped': int(mapped)} + for key, arena, used, free, mapped in allocations] + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path) + parser.add_argument("--build", type=Path, default=BUILD) + parser.add_argument("--program", type=Path, default=PROGRAM) + parser.add_argument("--legacy-window", action="store_true", + help="check an archived window built before document switching existed") + parser.add_argument("--switch-rounds", type=int, default=16, + help="number of alternating 1 MiB document opens, in groups of eight") + parser.add_argument("--diagnose-maps", action="store_true", + help="record RSS by Linux mapping category after each eight switches") + parser.add_argument("--switch-mode", choices=('alternate', 'same', 'same-body'), + default='alternate', + help="alternate two different or identical documents, or reopen one") + parser.add_argument("--graceful-large-close", action="store_true", + help="exit the large-text window normally so leak detectors can run") + parser.add_argument("--settle-seconds", type=float, default=0, + help="wait after the final switch, then sample RSS again") + parser.add_argument("--large-line-width", type=int, default=80, + help="bytes per line in the 1 MiB fixture (default: 80)") + args = parser.parse_args() + if args.switch_rounds < 8 or args.switch_rounds > 128 or args.switch_rounds % 8: + parser.error('--switch-rounds must be a multiple of eight between 8 and 128') + if args.settle_seconds < 0 or args.settle_seconds > 30: + parser.error('--settle-seconds must be between zero and 30') + if args.large_line_width < 2 or args.large_line_width > 1_048_576: + parser.error('--large-line-width must be between 2 and 1048576') + BUILD = args.build.resolve() + PROGRAM = args.program.resolve() + result = check(not args.legacy_window, args.switch_rounds, + args.diagnose_maps, args.switch_mode, args.graceful_large_close, + args.settle_seconds, args.large_line_width) + text = json.dumps(result, sort_keys=True) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text) + print(text, end="") diff --git a/prototypes/connections/check_write_cost.py b/prototypes/connections/check_write_cost.py new file mode 100644 index 0000000..4dcfe03 --- /dev/null +++ b/prototypes/connections/check_write_cost.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Trace one real document replacement through each selected storage binary.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess +import tempfile + +HERE = Path(__file__).resolve().parent +WRITE = re.compile(r'\bwrite\(\d+<([^>]+)>, .*?, (\d+)\) = (\d+)$') + + +def command(*args): + result = subprocess.run([str(arg) for arg in args], capture_output=True, timeout=20) + if result.returncode: + raise AssertionError((result.args, result.returncode, result.stderr[-1000:])) + return result + + +def inspect(binary, root, key, initial, patch, offset): + program = HERE / 'examples/editor.pure' + store = root / (binary.parent.name + '-' + key) + base = [binary, '--program', program, '--store', store] + command(*base, 'edit', key, 0, 0, 0, initial) + trace = root / (binary.parent.name + '-' + key + '.trace') + command('strace', '-qq', '-e', 'trace=write,pwrite64,writev,pwritev,fsync', + '-yy', '-o', trace, *base, 'edit', key, 1, offset, 1, patch) + expected = initial.read_bytes()[:offset] + patch.read_bytes() + initial.read_bytes()[offset + 1:] + assert command(*base, 'show', key).stdout == expected + writes = [] + for line in trace.read_text().splitlines(): + if '/.pending-' not in line and '/dat-' + key + '-' not in line: + continue + if not any(word in line for word in ('write(', 'pwrite64(', 'writev(', 'pwritev(')): + continue + match = WRITE.search(line) + if not match: + raise AssertionError(f'unaccounted object write: {line}') + requested, completed = int(match[2]), int(match[3]) + if requested != completed: + raise AssertionError(f'partial object write: {line}') + writes.append(completed) + return {'object_write_syscall_bytes': sum(writes), 'object_write_calls': len(writes), + 'object_files': {item.name: item.stat().st_size for item in store.iterdir() + if item.name.startswith(('obj-', 'dat-'))}, + 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest()} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--flat', type=Path, required=True) + parser.add_argument('--paged', '--candidate', dest='candidate', type=Path, required=True) + parser.add_argument('--candidate-mode', choices=('paged', 'hybrid'), default='paged') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + with tempfile.TemporaryDirectory(prefix='pure-write-cost-') as name: + root = Path(name) + initial, tiny, patch = root / 'initial', root / 'tiny', root / 'patch' + initial.write_bytes(b'A' * 1_048_576) + tiny.write_bytes(b'A') + patch.write_bytes(b'Z') + large_flat = inspect(args.flat.resolve(), root, 'large', initial, patch, 1_048_575) + large_paged = inspect(args.candidate.resolve(), root, 'large', initial, patch, 1_048_575) + tiny_flat = inspect(args.flat.resolve(), root, 'tiny', tiny, patch, 0) + tiny_paged = inspect(args.candidate.resolve(), root, 'tiny', tiny, patch, 0) + if [large_flat['object_write_syscall_bytes'], large_paged['object_write_syscall_bytes'], + tiny_flat['object_write_syscall_bytes'], tiny_paged['object_write_syscall_bytes']] != \ + [1_048_600, 16_944, 25, 25 if args.candidate_mode == 'hybrid' else 16_440]: + raise AssertionError((large_flat, large_paged, tiny_flat, tiny_paged)) + result = {'profile': 'linux-aarch64-16k-page-document-replacement', + 'candidate_mode': args.candidate_mode, + 'workloads': ['replace last byte of 1 MiB document', 'replace one-byte document'], + 'unit': 'successful user-space write syscall bytes targeting object files', + 'excludes': 'physical device writes, filesystem journal, metadata and directory sync cost', + 'one_mib': {'flat': large_flat, args.candidate_mode: large_paged, + f'flat_to_{args.candidate_mode}_ratio': large_flat['object_write_syscall_bytes'] / + large_paged['object_write_syscall_bytes']}, + 'one_byte': {'flat': tiny_flat, args.candidate_mode: tiny_paged, + f'{args.candidate_mode}_to_flat_ratio': tiny_paged['object_write_syscall_bytes'] / + tiny_flat['object_write_syscall_bytes']}} + output = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output) + print(output, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/compile-program.c b/prototypes/connections/compile-program.c new file mode 100644 index 0000000..7e291f3 --- /dev/null +++ b/prototypes/connections/compile-program.c @@ -0,0 +1,116 @@ +#include "program.h" +#include +#include +#include +#include + +/* Build-time preparation. The generated initializer is compiled with the + * target, so the run-only host contains neither this tool nor the parser. */ +static void region(FILE *out, PureRegion value) { + fprintf(out, "{.offset=%zu,.length=%zu,.to_end=%s}", value.offset, + value.length, value.to_end ? "true" : "false"); +} + +static void index_value(FILE *out, PureIndex value) { + fprintf(out, "{.value=%zu,.from_u32le=%s}", value.value, + value.from_u32le ? "true" : "false"); +} + +static void emit(FILE *out, const PureProgram *p) { + fputs("/* Generated from a validated .pure definition. Do not edit. */\n" + "static const PureProgram pure_embedded_program = {\n.read=", out); + region(out, p->read); + fputs(",\n.write=", out); + region(out, p->write); + fputs(",\n.reply=", out); + region(out, p->reply); + fprintf(out, ",\n.read_required=%zu,.input_required=%zu,\n" + ".view_definitions=%zu,.range_definitions=%zu,\n" + ".write_step=%zu,.reply_step=%zu,.input_steps=%zu,\n", + p->read_required, p->input_required, p->view_definitions, + p->range_definitions, p->write_step, p->reply_step, p->input_steps); + if (p->input_steps) fputs(".steps={\n", out); + for (size_t i = 0; i < p->input_steps; ++i) { + const PureRangeStep *step = &p->steps[i]; + fputs("{.source=", out); + region(out, step->source); + fprintf(out, ",.source_step=%zu,.offset=", step->source_step); + index_value(out, step->offset); + fputs(",.length=", out); + index_value(out, step->length); + fputs("},\n", out); + } + if (p->input_steps) fputs("},\n", out); + fprintf(out, ".reply_input=%s,.splice=%s,\n.splice_offset=", + p->reply_input ? "true" : "false", p->splice ? "true" : "false"); + index_value(out, p->splice_offset); + fputs(",.splice_remove=", out); + index_value(out, p->splice_remove); + fprintf(out, ",.splice_limit=%u\n};\n", p->splice_limit); +} + +static bool same_file(const char *left_path, const char *right_path) { + FILE *left = fopen(left_path, "rb"); + FILE *right = fopen(right_path, "rb"); + if (!left || !right) { + if (left) fclose(left); + if (right) fclose(right); + return false; + } + bool same = true; + for (;;) { + int a = fgetc(left), b = fgetc(right); + if (a != b) { same = false; break; } + if (a == EOF) break; + } + same = same && !ferror(left) && !ferror(right); + int left_status = fclose(left), right_status = fclose(right); + if (left_status || right_status) same = false; + return same; +} + +int main(int argc, char **argv) { + if (argc != 3) { + fputs("usage: pure-compile SOURCE.pure OUTPUT.h\n", stderr); + return 2; + } + struct stat source_info, output_info; + if (!stat(argv[1], &source_info) && !stat(argv[2], &output_info) && + source_info.st_dev == output_info.st_dev && + source_info.st_ino == output_info.st_ino) { + fputs("source and output must be different files\n", stderr); + return 2; + } + PureProgram program; + char error[160]; + if (!pure_program_load(argv[1], &program, error, sizeof(error))) { + fprintf(stderr, "Program: %s\n", error); + return 2; + } + size_t path_size = strlen(argv[2]) + 5; + char *temporary = malloc(path_size); + if (!temporary) { fputs("output path allocation failed\n", stderr); return 2; } + snprintf(temporary, path_size, "%s.tmp", argv[2]); + FILE *out = fopen(temporary, "wb"); + if (!out) { perror("output"); free(temporary); return 2; } + emit(out, &program); + if (fclose(out)) { + perror("output"); + remove(temporary); + free(temporary); + return 2; + } + if (same_file(temporary, argv[2])) { + remove(temporary); + free(temporary); + return 0; + } + if (rename(temporary, argv[2])) { + perror("output"); + remove(temporary); + free(temporary); + return 2; + } + free(temporary); + return 0; +} diff --git a/prototypes/connections/core-budget.json b/prototypes/connections/core-budget.json new file mode 100644 index 0000000..ebe4675 --- /dev/null +++ b/prototypes/connections/core-budget.json @@ -0,0 +1,15 @@ +{ + "profile": "opaque-definition-execution-core", + "max_text_bytes": 4864, + "max_data_bytes": 0, + "max_bss_bytes": 0, + "layout_max_bytes": { + "domain_bytes": 120, + "work_slot_bytes": 216, + "ticket_bytes": 24, + "pointer_bytes": 8 + }, + "archive_members": ["runtime.o"], + "external_symbols_allowed": ["memcpy", "memmove", "memset", "strlen"], + "scope": "Opaque-definition execution, resource credits, completion and loans only. Document view/splice evaluation is linked into the selected adapter. Without opt-in PURE_RUNTIME_METRICS, validated with GNU tools on Linux AArch64. Not total RAM, process RSS, stack, linked libraries, parser, storage, network or UI. This ceiling is a regression guard for this profile, not a universal runtime size promise." +} diff --git a/prototypes/connections/core-layout.c b/prototypes/connections/core-layout.c new file mode 100644 index 0000000..f037c76 --- /dev/null +++ b/prototypes/connections/core-layout.c @@ -0,0 +1,14 @@ +#include "runtime.h" +#include + +/* Host-side measurement tool; never linked into libpure-execution.a. */ +int main(void) { + printf("{\"domain_bytes\":%zu,\"work_slot_bytes\":%zu,\"ticket_bytes\":%zu," + "\"pointer_bytes\":%zu", + sizeof(PureRuntime), sizeof(PureWork), sizeof(PureTicket), sizeof(void *)); +#ifdef PURE_ASYNC_EFFECTS + printf(",\"effect_bytes\":%zu", sizeof(PureEffect)); +#endif + puts("}"); + return 0; +} diff --git a/prototypes/connections/document-peer.c b/prototypes/connections/document-peer.c new file mode 100644 index 0000000..9cbc64f --- /dev/null +++ b/prototypes/connections/document-peer.c @@ -0,0 +1,330 @@ +#define _POSIX_C_SOURCE 200809L +/* Selected document peer: same prepared program, store and execution core as + * pure-edit. TLS and the wire protocol remain outside libpure-execution.a. */ +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include "../common/tls-link.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { REQUEST_BYTES = 14, REPLY_BYTES = 13, HELLO_BYTES = 40, + OP_READ = 1, OP_WRITE = 2, WANT_BODY = 0x80 }; +static const unsigned char alpn[] = {10, 'p','u','r','e','-','d','o','c','/','1'}; +static volatile sig_atomic_t stopping; +static void signal_stop(int signum) { (void)signum; stopping = 1; } + +static uint32_t get32(const unsigned char *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; +} +static uint64_t get64(const unsigned char *p) { + uint64_t value = 0; + for (unsigned i = 0; i < 8; ++i) value = (value << 8) | p[i]; + return value; +} +static void put32(unsigned char *p, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(value >> (24 - 8 * i)); +} +static void put64(unsigned char *p, uint64_t value) { + for (unsigned i = 0; i < 8; ++i) p[i] = (unsigned char)(value >> (56 - 8 * i)); +} +#ifndef PURE_PEER_SERVER_ONLY +static void put32le(unsigned char *p, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(value >> (8 * i)); +} +#endif +static bool number(const char *text, uint64_t limit, uint64_t *out) { + if (!text || !*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 bool load_program(const char *path, PureProgram *program, unsigned char digest[32]) { + FILE *file = fopen(path, "rb"); + if (!file) 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; + char error[160]; + if (bad || !pure_program_parse(source, length, program, error, sizeof(error))) { + if (!bad) fprintf(stderr, "Program: %s\n", error); + return false; + } + return SHA256((const unsigned char *)source, length, digest) != NULL; +} +#ifndef PURE_PEER_SERVER_ONLY +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; + /* The convenience command is deliberately the eight-byte editor profile. + * Other authored layouts use apply, with no layout-specific wire fields. */ + return *prefix == 8 && p->input_required <= 8 && + ((start == 0 && remove == 4) || (start == 4 && remove == 0)); +} +#endif +static bool write_hello(PureLink *link, const unsigned char digest[32], uint64_t deadline) { + unsigned char hello[HELLO_BYTES] = {'P','D','O','C',1,0,0,0}; + memcpy(hello + 8, digest, 32); + return pure_link_write(link, hello, sizeof(hello), deadline); +} +#ifndef PURE_PEER_SERVER_ONLY +static bool read_hello(PureLink *link, const unsigned char digest[32], uint64_t deadline) { + unsigned char hello[HELLO_BYTES]; + const unsigned char header[] = {'P','D','O','C',1,0,0,0}; + return pure_link_read(link, hello, sizeof(hello), deadline) == 1 && + !memcmp(hello, header, sizeof(header)) && !memcmp(hello + 8, digest, 32); +} +#endif +/* The header always reports the logical output length. The requester chooses + * whether that borrowed output must cross this boundary at all. */ +static bool send_result(PureLink *link, PureOutcome outcome, bool want_body, uint64_t deadline) { + if (outcome.status > PURE_ALLOCATION || outcome.revision < 0 || outcome.body.size > PURE_OBJECT_LIMIT || + (outcome.body.size && !outcome.body.data)) return false; + unsigned char header[REPLY_BYTES]; + header[0] = (unsigned char)outcome.status; + put64(header + 1, (uint64_t)outcome.revision); + put32(header + 9, (uint32_t)outcome.body.size); + return pure_link_write(link, header, sizeof(header), deadline) && + (!want_body || pure_link_write(link, outcome.body.data, outcome.body.size, deadline)); +} +static bool serve_request(PureLink *link, PureRuntime *runtime, int *end) { + uint64_t deadline = pure_link_now_ms() + PURE_LINK_DEADLINE_MS; + unsigned char header[REQUEST_BYTES]; + int got = pure_link_read(link, header, sizeof(header), deadline); + if (!got) { *end = 1; return true; } + if (got != 1) return false; + unsigned op = header[0] & ~WANT_BODY, key_size = header[1]; + bool want_body = (header[0] & WANT_BODY) != 0; + uint64_t expected = get64(header + 2); + uint32_t body_size = get32(header + 10); + if ((op != OP_READ && op != OP_WRITE) || !key_size || key_size > 64 || + expected > INT64_MAX || body_size > PURE_OBJECT_LIMIT || + (op == OP_READ && (expected || body_size))) return false; + char key[65]; + if (pure_link_read(link, key, key_size, deadline) != 1) return false; + if (memchr(key, 0, key_size)) return false; + key[key_size] = 0; + PureTicket ticket; + PureResult accepted = pure_runtime_accept(runtime, op == OP_READ ? PURE_INPUT_READ : PURE_INPUT_WRITE, + key, (int64_t)expected, body_size, &ticket); + if (accepted != PURE_OK) { + /* The body is intentionally not consumed after failed admission. */ + PureOutcome error = {.status = accepted}; + (void)send_result(link, error, want_body, deadline); + return false; + } + if (op == OP_WRITE) { + size_t size = 0; + unsigned char *ingress = pure_runtime_ingress(ticket, &size); + if (size != body_size || (size && (!ingress || pure_link_read(link, ingress, size, deadline) != 1)) || + !pure_runtime_ready(ticket)) { + (void)pure_runtime_release(ticket); + return false; + } + } + PureOutcome outcome; + while (!pure_runtime_result(ticket, &outcome)) { + if (!pure_runtime_step(runtime)) { (void)pure_runtime_release(ticket); return false; } + } + bool sent = send_result(link, outcome, want_body, deadline); + (void)pure_runtime_release(ticket); + return sent; +} +static bool serve_connection(PureLink *link, PureRuntime *runtime, const unsigned char digest[32]) { + if (!write_hello(link, digest, pure_link_now_ms() + PURE_LINK_DEADLINE_MS)) return false; + int end = 0; + while (!end) if (!serve_request(link, runtime, &end)) return false; + return true; +} +static int serve(const char *bind_address, const char *port, const char *program_path, + const char *store_path, const char *ca, const char *cert, + const char *key, const char *client_name, unsigned max_connections) { + if (!pure_link_port_number(port, true) || !client_name || !*client_name) return 2; + PureProgram program; + unsigned char digest[32]; + if (!load_program(program_path, &program, digest) || pure_store_init(store_path) != PURE_OK) return 2; + SSL_CTX *ctx = pure_link_context(true, ca, cert, key, alpn); + if (!ctx) { ERR_print_errors_fp(stderr); return 2; } + PureRuntime runtime; + PureWork work[1]; + if (!pure_runtime_init(&runtime, work, 1, &program, pure_store_host(store_path), + 4u * 1024u * 1024u)) { SSL_CTX_free(ctx); return 2; } + int listener = pure_link_listen_at(bind_address, port); + if (listener < 0) { perror("listen"); SSL_CTX_free(ctx); return 2; } + struct sockaddr_storage address; + socklen_t address_size = sizeof(address); + if (getsockname(listener, (struct sockaddr *)&address, &address_size)) abort(); + unsigned actual_port = address.ss_family == AF_INET + ? ntohs(((struct sockaddr_in *)&address)->sin_port) + : ntohs(((struct sockaddr_in6 *)&address)->sin6_port); + printf("READY %u\n", actual_port); fflush(stdout); + signal(SIGTERM, signal_stop); signal(SIGINT, signal_stop); + unsigned accepted = 0; + while (!stopping && (!max_connections || accepted < max_connections)) { + struct pollfd p = {.fd = listener, .events = POLLIN}; + int result = poll(&p, 1, 1000); + if (result < 0) { if (errno == EINTR) continue; break; } + if (!result) continue; + int fd = accept(listener, NULL, NULL); + if (fd < 0) { if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; break; } + ++accepted; + PureLink link = {.fd = fd}; + bool healthy = pure_link_nonblocking(fd) && + pure_link_handshake(&link, ctx, true, client_name, alpn); + if (healthy) healthy = serve_connection(&link, &runtime, digest); + pure_link_close(&link, healthy); + } + close(listener); + pure_runtime_stop(&runtime); + int status = runtime.active || runtime.retained ? 1 : 0; + SSL_CTX_free(ctx); + return status; +} +#ifndef PURE_PEER_SERVER_ONLY +static int receive_result(PureLink *link, bool show, uint64_t deadline) { + unsigned char header[REPLY_BYTES]; + if (pure_link_read(link, header, sizeof(header), deadline) != 1 || + header[0] > PURE_ALLOCATION || get64(header + 1) > INT64_MAX || + get32(header + 9) > PURE_OBJECT_LIMIT) return -1; + PureResult result = (PureResult)header[0]; + int64_t revision = (int64_t)get64(header + 1); + uint32_t length = get32(header + 9); + if (result != PURE_OK && length) return -1; + unsigned char buffer[4096]; + size_t left = show ? length : 0; + int failed = 0; + while (left) { + size_t count = left < sizeof(buffer) ? left : sizeof(buffer); + if (pure_link_read(link, buffer, count, deadline) != 1) return -1; + if (show && result == PURE_OK && fwrite(buffer, 1, count, stdout) != count) failed = 1; + left -= count; + } + if (result != PURE_OK) { + fprintf(stderr, "{\"status\":%u,\"revision\":\"%" PRId64 "\"}\n", result, revision); + return result == PURE_CONFLICT ? 3 : 2; + } + if (show) fprintf(stderr, "{\"revision\":\"%" PRId64 "\",\"bytes\":%u}\n", revision, length); + else if (printf("{\"revision\":\"%" PRId64 "\",\"reply_bytes\":%u}\n", revision, length) < 0) failed = 1; + if (fflush(stdout)) failed = 1; + return failed ? 2 : 0; +} +static int call(const char *host, const char *port, const char *server_name, + const char *program_path, const char *ca, const char *cert, + const char *key, int argc, char **argv) { + if (!pure_link_port_number(port, false) || !server_name || !*server_name || argc < 2) return 2; + bool show = !strcmp(argv[0], "show"), edit = !strcmp(argv[0], "edit"); + if ((!show && !edit && strcmp(argv[0], "apply")) || argc != (show ? 2 : edit ? 6 : 4)) return 2; + PureProgram program; + unsigned char digest[32]; + if (!load_program(program_path, &program, digest)) return 2; + size_t prefix = 0; + if (edit && !edit_layout(&program, &prefix)) return 2; + uint64_t expected = 0, offset = 0, remove = 0; + if (!show && !number(argv[2], INT64_MAX, &expected)) return 2; + if (edit && (!number(argv[3], UINT32_MAX, &offset) || + !number(argv[4], UINT32_MAX, &remove))) return 2; + const char *document = argv[1]; + size_t key_size = strlen(document); + if (!key_size || key_size > 64) return 2; + FILE *file = NULL; + size_t body_size = 0; + if (!show) { + file = fopen(argv[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)) { + fclose(file); return 2; + } + body_size = prefix + (size_t)bytes; + } + SSL_CTX *ctx = pure_link_context(false, ca, cert, key, alpn); + if (!ctx) { if (file) fclose(file); ERR_print_errors_fp(stderr); return 2; } + PureLink link = {.fd = pure_link_connect_to(host, port)}; + bool ready = link.fd >= 0 && pure_link_handshake(&link, ctx, false, server_name, alpn); + bool matched = ready && read_hello(&link, digest, pure_link_now_ms() + PURE_LINK_DEADLINE_MS); + if (!matched) { + fputs(ready ? "peer application definition mismatch\n" : + "peer authentication or connection failed\n", stderr); + pure_link_close(&link, false); + SSL_CTX_free(ctx); + if (file) fclose(file); + return 2; + } + int status = 2; + { + uint64_t deadline = pure_link_now_ms() + PURE_LINK_DEADLINE_MS; + unsigned char header[REQUEST_BYTES] = {0}; + header[0] = (show ? OP_READ : OP_WRITE) | (show ? WANT_BODY : 0); + header[1] = (unsigned char)key_size; + put64(header + 2, expected); + put32(header + 10, (uint32_t)body_size); + ready = pure_link_write(&link, header, sizeof(header), deadline) && + pure_link_write(&link, document, key_size, deadline); + if (ready && prefix) { + unsigned char fields[8] = {0}; + put32le(fields + program.splice_offset.value, (uint32_t)offset); + put32le(fields + program.splice_remove.value, (uint32_t)remove); + ready = pure_link_write(&link, fields, prefix, deadline); + } + unsigned char buffer[4096]; + size_t left = body_size - prefix; + while (ready && left) { + size_t count = left < sizeof(buffer) ? left : sizeof(buffer); + if (fread(buffer, 1, count, file) != count) { ready = false; break; } + ready = pure_link_write(&link, buffer, count, deadline); + left -= count; + } + if (file && (fgetc(file) != EOF || ferror(file))) ready = false; + if (ready) { + status = receive_result(&link, show, deadline); + if (status < 0) { status = 2; ready = false; } + } + } + if (!ready) fputs("peer failed or timed out; an interrupted write may have committed; query before retry\n", stderr); + pure_link_close(&link, ready); + SSL_CTX_free(ctx); + if (file && fclose(file)) status = 2; + return status; +} +#endif +int main(int argc, char **argv) { + signal(SIGPIPE, SIG_IGN); +#ifndef PURE_PEER_SERVER_ONLY + if (argc >= 11 && !strcmp(argv[1], "call")) + return call(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argc - 9, argv + 9); +#endif + if ((argc == 10 || argc == 11) && !strcmp(argv[1], "serve")) { + uint64_t limit = 0; + if (argc == 11 && (!number(argv[10], UINT32_MAX, &limit) || !limit)) goto usage; + return serve(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], + (unsigned)limit); + } +usage: + fputs("pure-document-peer serve BIND_IP PORT PROGRAM STORE_DIR CA.pem SERVER_CERT.pem SERVER_KEY.pem CLIENT_DNS [MAX_CONNECTIONS]\n", stderr); +#ifndef PURE_PEER_SERVER_ONLY + fputs("pure-document-peer call HOST PORT SERVER_DNS PROGRAM CA.pem CLIENT_CERT.pem CLIENT_KEY.pem show KEY\n" + "pure-document-peer call HOST PORT SERVER_DNS PROGRAM CA.pem CLIENT_CERT.pem CLIENT_KEY.pem edit KEY REV OFFSET REMOVE INSERT_FILE\n" + "pure-document-peer call HOST PORT SERVER_DNS PROGRAM CA.pem CLIENT_CERT.pem CLIENT_KEY.pem apply KEY REV INPUT_FILE\n", stderr); +#endif + return 2; +} diff --git a/prototypes/connections/edit-tui.c b/prototypes/connections/edit-tui.c new file mode 100644 index 0000000..fbd73c1 --- /dev/null +++ b/prototypes/connections/edit-tui.c @@ -0,0 +1,945 @@ +#define _XOPEN_SOURCE 700 +/* O_NOFOLLOW is hidden by Darwin's strict X/Open namespace. */ +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE +#endif +/* Optional terminal document editor. The program and store effect are the + * same ones used by the CLI and HTTP hosts; terminal state stays here. */ +#include "runtime.h" +#include "store.h" +#include "store-port.h" +#ifdef PURE_EMBEDDED_PROGRAM +#include "embedded-plan.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + char name[65]; +} DocumentKey; + +typedef struct { + PureRuntime runtime; + PureWork work[2]; + const PureProgram *program; + const char *store; + char key[65], prompt[65], message[160]; + unsigned char *text; + size_t capacity, gap_start, gap_end, scroll_row, prefix, prompt_size; + /* Logical byte position and its visual coordinate in anchor_columns. */ + size_t anchor_position, anchor_row, anchor_column, anchor_columns; + DocumentKey *documents; + size_t document_count, document_selected, document_scroll; + int64_t revision; + bool exists, dirty, prompt_open, list_open, confirm_quit, confirm_reload, anchor_valid; + struct termios original_term; +} Editor; + +static volatile sig_atomic_t stopping, resized, wake_write = -1; + +static void wake_signal(int signal_number) { + int saved_errno = errno; + if (signal_number == SIGWINCH) resized = 1; + else stopping = 1; + if (wake_write >= 0) { + const char byte = 1; + (void)write((int)wake_write, &byte, 1); + } + errno = saved_errno; +} + +static bool valid_key(const char *key) { + size_t length = strlen(key); + if (!length || length > 64) return false; + for (size_t i = 0; i < length; ++i) { + unsigned char c = (unsigned char)key[i]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-')) return false; + } + return true; +} + +static int compare_document_key(const void *left, const void *right) { + return strcmp(((const DocumentKey *)left)->name, ((const DocumentKey *)right)->name); +} + +static void close_list(Editor *editor) { + free(editor->documents); + editor->documents = NULL; + editor->document_count = editor->document_selected = editor->document_scroll = 0; + editor->prompt_size = 0; + editor->prompt[0] = 0; + editor->list_open = false; +} + +/* Names are already sorted; find the first key with this prefix in-place. */ +static bool find_list_prefix(Editor *editor) { + if (!editor->prompt_size) return true; + size_t first = 0, end = editor->document_count; + while (first < end) { + size_t middle = first + (end - first) / 2; + if (strcmp(editor->documents[middle].name, editor->prompt) < 0) first = middle + 1; + else end = middle; + } + if (first == editor->document_count || + strncmp(editor->documents[first].name, editor->prompt, editor->prompt_size)) return false; + editor->document_selected = first; + return true; +} + +/* List names from the selected byte store, not from the execution core. + * Opening a name still goes through the store's checked READ path. Bound the + * optional browser to 8,192 names (520 KiB of key storage). */ +static bool open_list(Editor *editor) { + int descriptor = open(editor->store, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + DIR *directory = descriptor >= 0 ? fdopendir(descriptor) : NULL; + if (!directory) { + if (descriptor >= 0) close(descriptor); + snprintf(editor->message, sizeof(editor->message), "Could not read document directory"); + return false; + } + DocumentKey *items = NULL; + size_t count = 0, capacity = 0; + bool complete = true; + for (;;) { + errno = 0; + struct dirent *entry = readdir(directory); + if (!entry) { if (errno) complete = false; break; } + if (strncmp(entry->d_name, "obj-", 4) || !valid_key(entry->d_name + 4)) continue; + if (count == 8192) { complete = false; break; } + if (count == capacity) { + size_t next = capacity ? capacity * 2 : 32; + DocumentKey *grown = realloc(items, next * sizeof(*grown)); + if (!grown) { complete = false; break; } + items = grown; + capacity = next; + } + memcpy(items[count++].name, entry->d_name + 4, strlen(entry->d_name + 4) + 1); + } + if (closedir(directory)) complete = false; + if (!complete) { + free(items); + snprintf(editor->message, sizeof(editor->message), "Document list exceeds 8,192 keys or cannot be read"); + return false; + } + if (count > 1) qsort(items, count, sizeof(*items), compare_document_key); + close_list(editor); + editor->documents = items; + editor->document_count = count; + for (size_t i = 0; i < count; ++i) + if (!strcmp(items[i].name, editor->key)) { editor->document_selected = i; break; } + editor->list_open = true; + return true; +} + +static const char *result_name(PureResult result) { + switch (result) { + case PURE_NOT_FOUND: return "not found"; + case PURE_CONFLICT: return "version conflict"; + case PURE_BUSY: return "busy"; + case PURE_BUDGET: return "request budget exceeded"; + case PURE_LIMIT: return "document limit or invalid range"; + case PURE_UNCERTAIN: return "durability unknown; inspect the stored revision before retrying"; + case PURE_DENIED: return "operation denied"; + case PURE_ALLOCATION: return "allocation failed"; + default: return "operation failed"; + } +} + +static bool edit_layout(const PureProgram *program, size_t *prefix) { + if (!program->splice || !program->write.to_end || program->write_step || + program->input_steps || !program->splice_offset.from_u32le || + !program->splice_remove.from_u32le || program->read.offset || + !program->read.to_end || program->read_required) return false; + size_t start = program->splice_offset.value, remove = program->splice_remove.value; + *prefix = program->write.offset; + if (*prefix > PURE_OBJECT_LIMIT || program->input_required > *prefix || + start > *prefix || remove > *prefix || *prefix - start < 4 || + *prefix - remove < 4) return false; + return start < remove ? remove - start >= 4 : start - remove >= 4; +} + +static void put32(unsigned char *to, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) to[i] = (unsigned char)(value >> (8 * i)); +} + +static PureOutcome finish(Editor *editor, PureTicket ticket) { + PureOutcome result; + while (!pure_runtime_result(ticket, &result)) + if (!pure_runtime_step(&editor->runtime)) return (PureOutcome){.status = PURE_INVALID}; + return result; +} + +static size_t length(const Editor *editor) { + return editor->capacity - (editor->gap_end - editor->gap_start); +} + +static unsigned char byte_at(const Editor *editor, size_t index) { + return editor->text[index < editor->gap_start ? index : + index + editor->gap_end - editor->gap_start]; +} + +/* Skip printable ASCII directly within one side of the gap. Its terminal + * width is always one cell; UTF-8, controls and tabs use the glyph path. */ +static size_t plain_run(const Editor *editor, size_t position, size_t maximum) { + size_t available = position < editor->gap_start ? editor->gap_start - position + : length(editor) - position; + if (available > maximum) available = maximum; + const unsigned char *bytes = editor->text + (position < editor->gap_start + ? position : position + editor->gap_end - editor->gap_start); + size_t count = 0; + while (count < available && bytes[count] >= 32 && bytes[count] < 127) ++count; + return count; +} + +static void advance_plain(size_t count, size_t columns, size_t *row, size_t *column) { + size_t cells = *column + count; + *row += cells / columns; + *column = cells % columns; +} + +static void changed_at(Editor *editor, size_t position) { + if (editor->anchor_valid && position < editor->anchor_position) + editor->anchor_valid = false; +} + +static void move_gap(Editor *editor, size_t position) { + if (position < editor->gap_start) { + size_t count = editor->gap_start - position; + memmove(editor->text + editor->gap_end - count, editor->text + position, count); + editor->gap_start -= count; + editor->gap_end -= count; + } else if (position > editor->gap_start) { + size_t count = position - editor->gap_start; + memmove(editor->text + editor->gap_start, editor->text + editor->gap_end, count); + editor->gap_start += count; + editor->gap_end += count; + } +} + +static bool reserve_gap(Editor *editor, size_t needed) { + if (editor->gap_end - editor->gap_start >= needed) return true; + size_t used = length(editor); + if (needed > PURE_OBJECT_LIMIT - used) return false; + size_t target = editor->capacity * 2; + if (target < used + needed + 256) target = used + needed + 256; + if (target > PURE_OBJECT_LIMIT + 256) target = PURE_OBJECT_LIMIT + 256; + unsigned char *fresh = malloc(target); + if (!fresh) return false; + size_t right = editor->capacity - editor->gap_end; + memcpy(fresh, editor->text, editor->gap_start); + memcpy(fresh + target - right, editor->text + editor->gap_end, right); + free(editor->text); + editor->text = fresh; + editor->gap_end = target - right; + editor->capacity = target; + return true; +} + +static bool insert_byte(Editor *editor, unsigned char byte) { + if (length(editor) >= PURE_OBJECT_LIMIT || !reserve_gap(editor, 1)) return false; + changed_at(editor, editor->gap_start); + editor->text[editor->gap_start++] = byte; + editor->dirty = true; + return true; +} + +static size_t previous_codepoint(const Editor *editor, size_t position) { + if (!position) return 0; + --position; + while (position && (byte_at(editor, position) & 0xc0) == 0x80) --position; + return position; +} + +static size_t next_codepoint(const Editor *editor, size_t position) { + size_t count = length(editor); + if (position == count) return count; + ++position; + while (position < count && (byte_at(editor, position) & 0xc0) == 0x80) ++position; + return position; +} + +static bool valid_text_span(PureSpan span) { + for (size_t position = 0; position < span.size;) { + mbstate_t state = {0}; + wchar_t character; + size_t used = mbrtowc(&character, (const char *)span.data + position, + span.size - position, &state); + if (used == (size_t)-1 || used == (size_t)-2 || !used || + (character < 32 && character != L'\n' && character != L'\t') || + character == 127 || + (character != L'\n' && character != L'\t' && wcwidth(character) < 0)) return false; + position += used; + } + return true; +} + +static bool read_document(Editor *editor, const char *key, bool require_existing) { + PureTicket ticket; + PureResult admitted = pure_runtime_accept(&editor->runtime, PURE_INPUT_READ, + key, 0, 0, &ticket); + if (admitted != PURE_OK) { + snprintf(editor->message, sizeof(editor->message), "Read rejected: %s", result_name(admitted)); + return false; + } + PureOutcome result = finish(editor, ticket); + if (require_existing && result.status == PURE_NOT_FOUND) { + snprintf(editor->message, sizeof(editor->message), "Document disappeared; refresh the list"); + pure_runtime_release(ticket); + return false; + } + if (result.status != PURE_OK && result.status != PURE_NOT_FOUND) { + snprintf(editor->message, sizeof(editor->message), "Read failed: %s", result_name(result.status)); + pure_runtime_release(ticket); + return false; + } + size_t size = result.status == PURE_OK ? result.body.size : 0; + if (size > PURE_OBJECT_LIMIT || (size && !result.body.data) || + !valid_text_span(result.body)) { + snprintf(editor->message, sizeof(editor->message), "Document is too large or is not plain UTF-8 text"); + pure_runtime_release(ticket); + return false; + } + size_t capacity = size + 256; + unsigned char *fresh = malloc(capacity); + if (!fresh) { + snprintf(editor->message, sizeof(editor->message), "Could not allocate document buffer"); + pure_runtime_release(ticket); + return false; + } + if (size) memcpy(fresh, result.body.data, size); + pure_runtime_release(ticket); + free(editor->text); + editor->text = fresh; + editor->capacity = capacity; + editor->gap_start = size; + editor->gap_end = capacity; + editor->scroll_row = 0; + editor->anchor_valid = false; + editor->revision = result.status == PURE_OK ? result.revision : 0; + editor->exists = result.status == PURE_OK; + editor->dirty = false; + editor->confirm_reload = false; + memcpy(editor->key, key, strlen(key) + 1); + snprintf(editor->message, sizeof(editor->message), "%s %zu bytes · revision %" PRId64, + editor->exists ? "Opened" : "New document", size, editor->revision); + return true; +} + +static void save_document(Editor *editor) { + if (!editor->dirty && editor->exists) { + snprintf(editor->message, sizeof(editor->message), "No changes to save"); + return; + } + PureTicket baseline; + PureResult admitted = pure_runtime_accept(&editor->runtime, PURE_INPUT_READ, + editor->key, 0, 0, &baseline); + if (admitted != PURE_OK) { + snprintf(editor->message, sizeof(editor->message), "Read rejected: %s", result_name(admitted)); + return; + } + PureOutcome old = finish(editor, baseline); + if (old.status != PURE_OK && old.status != PURE_NOT_FOUND) { + snprintf(editor->message, sizeof(editor->message), "Could not inspect stored version: %s", result_name(old.status)); + pure_runtime_release(baseline); + return; + } + int64_t stored_revision = old.status == PURE_OK ? old.revision : 0; + if (stored_revision != editor->revision) { + snprintf(editor->message, sizeof(editor->message), "Version conflict: stored %" PRId64 "; draft preserved", + stored_revision); + pure_runtime_release(baseline); + return; + } + size_t old_size = old.status == PURE_OK ? old.body.size : 0; + size_t draft_size = length(editor); + size_t prefix = 0; + while (prefix < old_size && prefix < draft_size && + old.body.data[prefix] == byte_at(editor, prefix)) ++prefix; + while (prefix && prefix < old_size && (old.body.data[prefix] & 0xc0) == 0x80) --prefix; + size_t suffix = 0; + while (suffix < old_size - prefix && suffix < draft_size - prefix && + old.body.data[old_size - suffix - 1] == byte_at(editor, draft_size - suffix - 1)) ++suffix; + while (suffix && (((old.body.data[old_size - suffix] & 0xc0) == 0x80) || + ((byte_at(editor, draft_size - suffix) & 0xc0) == 0x80))) --suffix; + size_t remove = old_size - prefix - suffix; + size_t insert = draft_size - prefix - suffix; + if (!remove && !insert && editor->exists) { + editor->dirty = false; + snprintf(editor->message, sizeof(editor->message), "No changes to save"); + pure_runtime_release(baseline); + return; + } + if (prefix > UINT32_MAX || remove > UINT32_MAX || insert > PURE_OBJECT_LIMIT - editor->prefix) { + snprintf(editor->message, sizeof(editor->message), "Edit exceeds selected program limit"); + pure_runtime_release(baseline); + return; + } + PureTicket write_ticket; + admitted = pure_runtime_accept(&editor->runtime, PURE_INPUT_WRITE, editor->key, + editor->revision, editor->prefix + insert, &write_ticket); + if (admitted != PURE_OK) { + snprintf(editor->message, sizeof(editor->message), "Write rejected: %s", result_name(admitted)); + pure_runtime_release(baseline); + return; + } + unsigned char *input = pure_runtime_ingress(write_ticket, NULL); + memset(input, 0, editor->prefix); + put32(input + editor->program->splice_offset.value, (uint32_t)prefix); + put32(input + editor->program->splice_remove.value, (uint32_t)remove); + for (size_t i = 0; i < insert; ++i) input[editor->prefix + i] = byte_at(editor, prefix + i); + pure_runtime_release(baseline); + if (!pure_runtime_ready(write_ticket)) { + pure_runtime_release(write_ticket); + snprintf(editor->message, sizeof(editor->message), "Could not publish edit"); + return; + } + PureOutcome result = finish(editor, write_ticket); + pure_runtime_release(write_ticket); + if (result.status != PURE_OK) { + snprintf(editor->message, sizeof(editor->message), "Save failed: %s; draft preserved", result_name(result.status)); + return; + } + editor->revision = result.revision; + editor->exists = true; + editor->dirty = false; + editor->confirm_quit = editor->confirm_reload = false; + snprintf(editor->message, sizeof(editor->message), "Saved %zu-byte edit at byte %zu · revision %" PRId64, + insert, prefix, result.revision); +} + +static size_t glyph(const Editor *editor, size_t position, wchar_t *character, + int *width, char bytes[4], bool *valid) { + unsigned char first = byte_at(editor, position); + if (first < 128) { + bytes[0] = (char)first; + *character = (wchar_t)first; + *width = 1; + if ((first < 32 && first != '\n' && first != '\t') || first == 127 || !first) { + *valid = false; + bytes[0] = '?'; + *character = L'?'; + } + return 1; + } + size_t available = length(editor) - position; + if (available > 4) available = 4; + for (size_t i = 0; i < available; ++i) bytes[i] = (char)byte_at(editor, position + i); + mbstate_t state = {0}; + size_t used = mbrtowc(character, bytes, available, &state); + if (used == (size_t)-1 || used == (size_t)-2 || !used || + (*character < 32 && *character != L'\n' && *character != L'\t') || + *character == 127) { + *valid = false; + *character = L'?'; + *width = 1; + bytes[0] = '?'; + return 1; + } + *width = wcwidth(*character); + if (*width < 0) { + *valid = false; + *character = L'?'; + *width = 1; + bytes[0] = '?'; + return 1; + } + return used; +} + +static bool text_valid(const Editor *editor) { + for (size_t position = 0; position < length(editor);) { + wchar_t character; + int width; + char bytes[4]; + bool valid = true; + size_t used = glyph(editor, position, &character, &width, bytes, &valid); + if (!valid) return false; + position += used; + } + return true; +} + +static void advance(wchar_t character, int width, size_t columns, + size_t *row, size_t *column) { + if (character == L'\n') { ++*row; *column = 0; return; } + if (character == L'\t') width = 4 - (int)(*column % 4); + if (*column && *column + (size_t)width > columns) { ++*row; *column = 0; } + *column += (size_t)width; + if (*column >= columns) { ++*row; *column = 0; } +} + +static void position_of(const Editor *editor, size_t target, size_t columns, + size_t *row, size_t *column) { + size_t position = 0; + *row = *column = 0; + if (editor->anchor_valid && editor->anchor_columns == columns && + target >= editor->anchor_position) { + position = editor->anchor_position; + *row = editor->anchor_row; + *column = editor->anchor_column; + } + for (; position < target;) { + size_t run = plain_run(editor, position, target - position); + if (run) { + advance_plain(run, columns, row, column); + position += run; + continue; + } + wchar_t character; + int width; + char bytes[4]; + bool valid = true; + size_t used = glyph(editor, position, &character, &width, bytes, &valid); + advance(character, width, columns, row, column); + position += used; + } +} + +static size_t closest_position(const Editor *editor, size_t target_row, + size_t target_column, size_t columns) { + size_t row = 0, column = 0, position = 0, best = 0; + bool found = false; + if (editor->anchor_valid && editor->anchor_columns == columns && + target_row > editor->anchor_row) { + row = editor->anchor_row; + column = editor->anchor_column; + position = editor->anchor_position; + } + for (; position < length(editor);) { + if (row == target_row && column <= target_column) { best = position; found = true; } + if (row > target_row || (row == target_row && column > target_column)) + return found ? best : position; + wchar_t character; + int width; + char bytes[4]; + bool valid = true; + size_t used = glyph(editor, position, &character, &width, bytes, &valid); + advance(character, width, columns, &row, &column); + position += used; + if (row == target_row && column <= target_column) { best = position; found = true; } + } + return found ? best : length(editor); +} + +static void dimensions(size_t *rows, size_t *columns) { + struct winsize window = {0}; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) || !window.ws_row || !window.ws_col) { + *rows = 24; *columns = 80; + } else { *rows = window.ws_row; *columns = window.ws_col; } + if (*rows < 4) *rows = 4; + if (*columns < 8) *columns = 8; +} + +static void draw_list(Editor *editor, size_t rows, size_t columns) { + size_t visible = rows - 3; + bool matching = editor->document_count && (!editor->prompt_size || + !strncmp(editor->documents[editor->document_selected].name, + editor->prompt, editor->prompt_size)); + if (editor->document_selected < editor->document_scroll) + editor->document_scroll = editor->document_selected; + if (editor->document_selected >= editor->document_scroll + visible) + editor->document_scroll = editor->document_selected - visible + 1; + fputs("\033[?25l\033[H\033[2J", stdout); + char header[80]; + snprintf(header, sizeof(header), " Pure | documents | %zu keys", editor->document_count); + printf("%.*s", (int)columns, header); + for (size_t row = 0; row < visible && editor->document_scroll + row < editor->document_count; ++row) { + size_t index = editor->document_scroll + row; + printf("\033[%zu;1H%c %.*s", row + 2, + matching && index == editor->document_selected ? '>' : ' ', + (int)(columns - 2), editor->documents[index].name); + } + printf("\033[%zu;1H%.*s", rows - 1, (int)columns, + " Enter open Type to find Backspace clear Esc back"); + char footer[230]; + if (editor->prompt_size) + snprintf(footer, sizeof(footer), " Find: %.64s_ %.140s", editor->prompt, + matching ? editor->message : "No matching key"); + else if (editor->document_count) + snprintf(footer, sizeof(footer), " %zu/%zu %s", + editor->document_selected + 1, editor->document_count, editor->message); + else snprintf(footer, sizeof(footer), " No stored documents; Ctrl+O creates a new key"); + printf("\033[%zu;1H%.*s", rows, (int)columns, footer); + fflush(stdout); +} + +static void draw(Editor *editor) { + size_t rows, columns; + dimensions(&rows, &columns); + if (editor->list_open) { draw_list(editor, rows, columns); return; } + if (editor->anchor_columns != columns) editor->anchor_valid = false; + size_t content_rows = rows - 3; + size_t cursor_row, cursor_column; + position_of(editor, editor->gap_start, columns, &cursor_row, &cursor_column); + if (cursor_row < editor->scroll_row) editor->scroll_row = cursor_row; + if (cursor_row >= editor->scroll_row + content_rows) + editor->scroll_row = cursor_row - content_rows + 1; + fputs("\033[?25l\033[H\033[2J", stdout); + char header[160]; + snprintf(header, sizeof(header), " Pure | %s | revision %" PRId64 "%s | %zu bytes", + editor->key, editor->revision, editor->dirty ? " *" : "", length(editor)); + printf("%.*s", (int)columns, header); + size_t row = 0, column = 0, position = 0, drawn_row = SIZE_MAX; + size_t anchor_row = editor->scroll_row > content_rows + ? editor->scroll_row - content_rows : 0; + bool keep_anchor = editor->anchor_valid && editor->anchor_row <= editor->scroll_row && + editor->scroll_row - editor->anchor_row <= 2 * content_rows; + if (editor->anchor_valid && editor->anchor_row <= editor->scroll_row) { + row = editor->anchor_row; + column = editor->anchor_column; + position = editor->anchor_position; + } + if (!keep_anchor) editor->anchor_valid = false; + for (; position < length(editor);) { + if (!editor->anchor_valid && row >= anchor_row) { + editor->anchor_position = position; + editor->anchor_row = row; + editor->anchor_column = column; + editor->anchor_columns = columns; + editor->anchor_valid = true; + } + if (row >= editor->scroll_row + content_rows) break; + if (row < editor->scroll_row) { + size_t remaining = length(editor) - position; + size_t rows_to_view = editor->scroll_row - row; + size_t cells_to_view = rows_to_view > (remaining + column) / columns + ? remaining : rows_to_view * columns - column; + size_t run = plain_run(editor, position, cells_to_view); + if (run) { + advance_plain(run, columns, &row, &column); + position += run; + continue; + } + } + wchar_t character; + int width; + char bytes[4]; + bool valid = true; + size_t used = glyph(editor, position, &character, &width, bytes, &valid); + size_t next_row = row, next_column = column; + size_t visible_width = character == L'\t' ? 4 - column % 4 : (size_t)width; + if (character != L'\n' && column && column + visible_width > columns) { + ++row; + column = 0; + } + if (row >= editor->scroll_row && row < editor->scroll_row + content_rows && + character != L'\n') { + if (drawn_row != row) { + printf("\033[%zu;1H", row - editor->scroll_row + 2); + drawn_row = row; + } + if (character == L'\t') { + int spaces = 4 - (int)(column % 4); + while (spaces--) putchar(' '); + } else fwrite(bytes, 1, used, stdout); + } + row = next_row; + column = next_column; + advance(character, width, columns, &row, &column); + position += used; + } + printf("\033[%zu;1H%.*s", rows - 1, (int)columns, + " Ctrl+S save Ctrl+O open Ctrl+R reload Ctrl+Q quit arrows move"); + char footer[230]; + if (editor->prompt_open) + snprintf(footer, sizeof(footer), " Open key: %s_ (Enter to open, Esc to cancel)", editor->prompt); + else snprintf(footer, sizeof(footer), " %s", editor->message); + printf("\033[%zu;1H%.*s", rows, (int)columns, footer); + if (editor->prompt_open) { + size_t prompt_column = editor->prompt_size + strlen(" Open key: ") + 1; + if (prompt_column > columns) prompt_column = columns; + printf("\033[%zu;%zuH", rows, prompt_column); + } else printf("\033[%zu;%zuH", cursor_row - editor->scroll_row + 2, + cursor_column + 1); + fputs("\033[?25h", stdout); + fflush(stdout); +} + +static int read_byte(int wake_read, int timeout_ms) { + struct pollfd fds[2] = {{.fd = STDIN_FILENO, .events = POLLIN}, + {.fd = wake_read, .events = POLLIN}}; + int ready = poll(fds, 2, timeout_ms); + if (ready < 0) return errno == EINTR ? -2 : -1; + if (!ready) return -3; + if (fds[1].revents) { + char buffer[32]; + while (read(wake_read, buffer, sizeof(buffer)) > 0) { } + return -2; + } + unsigned char byte; + return read(STDIN_FILENO, &byte, 1) == 1 ? byte : -1; +} + +static void move_vertical(Editor *editor, int delta) { + size_t rows, columns, row, column; + dimensions(&rows, &columns); + (void)rows; + position_of(editor, editor->gap_start, columns, &row, &column); + if (delta < 0 && !row) return; + move_gap(editor, closest_position(editor, delta < 0 ? row - 1 : row + 1, + column, columns)); +} + +static void handle_escape(Editor *editor, int wake_read) { + int a = read_byte(wake_read, 30); + if (a != '[') { + if (editor->list_open) close_list(editor); + editor->prompt_open = false; + return; + } + int b = read_byte(wake_read, 30); + if (editor->list_open) { + if (b == 'A' || b == 'B' || b == 'H' || b == 'F') { + editor->prompt_size = 0; + editor->prompt[0] = 0; + } + if (b == 'A' && editor->document_selected) --editor->document_selected; + else if (b == 'B' && editor->document_selected + 1 < editor->document_count) + ++editor->document_selected; + else if (b == 'H') editor->document_selected = 0; + else if (b == 'F' && editor->document_count) + editor->document_selected = editor->document_count - 1; + return; + } + if (b == 'A') move_vertical(editor, -1); + else if (b == 'B') move_vertical(editor, 1); + else if (b == 'C') move_gap(editor, next_codepoint(editor, editor->gap_start)); + else if (b == 'D') move_gap(editor, previous_codepoint(editor, editor->gap_start)); + else if (b == 'H') { + size_t position = editor->gap_start; + while (position && byte_at(editor, position - 1) != '\n') --position; + move_gap(editor, position); + } else if (b == 'F') { + size_t position = editor->gap_start; + while (position < length(editor) && byte_at(editor, position) != '\n') ++position; + move_gap(editor, position); + } else if (b == '3' && read_byte(wake_read, 30) == '~') { + size_t next = next_codepoint(editor, editor->gap_start); + if (next > editor->gap_start) { + changed_at(editor, editor->gap_start); + editor->gap_end += next - editor->gap_start; + editor->dirty = true; + } + } +} + +static void handle_key(Editor *editor, int key, int wake_read) { + if (key < 0) return; + if (editor->prompt_open) { + if (key == 27) { + if (read_byte(wake_read, 30) == '[') { + int next = read_byte(wake_read, 30); + if (next >= '0' && next <= '9') (void)read_byte(wake_read, 30); + } else editor->prompt_open = false; + return; + } + if (key == 127 || key == 8) { + if (editor->prompt_size) editor->prompt[--editor->prompt_size] = 0; + return; + } + if (key == 13 || key == 10) { + if (valid_key(editor->prompt)) (void)read_document(editor, editor->prompt, false); + else snprintf(editor->message, sizeof(editor->message), "Key must be 1–64 ASCII letters, digits, _ or -"); + editor->prompt_open = false; + return; + } + if (editor->prompt_size < 64 && + ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || + (key >= '0' && key <= '9') || key == '_' || key == '-')) { + editor->prompt[editor->prompt_size++] = (char)key; + editor->prompt[editor->prompt_size] = 0; + } + return; + } + if (editor->list_open) { + if (key == 27) { handle_escape(editor, wake_read); return; } + if ((key == 127 || key == 8) && editor->prompt_size) { + editor->prompt[--editor->prompt_size] = 0; + (void)find_list_prefix(editor); + return; + } + if (editor->prompt_size < 64 && + ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || + (key >= '0' && key <= '9') || key == '_' || key == '-')) { + editor->prompt[editor->prompt_size++] = (char)key; + editor->prompt[editor->prompt_size] = 0; + (void)find_list_prefix(editor); + return; + } + if ((key == 13 || key == 10) && editor->document_count && + (!editor->prompt_size || !strncmp(editor->documents[editor->document_selected].name, + editor->prompt, editor->prompt_size))) { + if (read_document(editor, editor->documents[editor->document_selected].name, true)) + close_list(editor); + return; + } + if (key == 12) { (void)open_list(editor); return; } + if (key == 15) close_list(editor); + else if (key != 17 && key != 3) return; + } + if (key == 17 || key == 3) { + if (editor->dirty && !editor->confirm_quit) { + editor->confirm_quit = true; + snprintf(editor->message, sizeof(editor->message), "Unsaved draft; press Ctrl+Q again to discard and quit"); + } else stopping = 1; + return; + } + if (key != 18) editor->confirm_reload = false; + if (key != 17) editor->confirm_quit = false; + if (key == 19) { if (text_valid(editor)) save_document(editor); + else snprintf(editor->message, sizeof(editor->message), "Document contains invalid UTF-8 or control bytes"); + return; } + if (key == 18) { + if (editor->dirty && !editor->confirm_reload) { + editor->confirm_reload = true; + snprintf(editor->message, sizeof(editor->message), "Unsaved draft; press Ctrl+R again to discard and reload"); + } else (void)read_document(editor, editor->key, false); + return; + } + if (key == 15) { + if (editor->dirty) snprintf(editor->message, sizeof(editor->message), "Save or reload the draft before opening another key"); + else { editor->prompt_open = true; editor->prompt_size = 0; editor->prompt[0] = 0; } + return; + } + if (key == 12) { + if (editor->dirty) + snprintf(editor->message, sizeof(editor->message), "Save or reload the draft before browsing documents"); + else (void)open_list(editor); + return; + } + if (key == 27) { handle_escape(editor, wake_read); return; } + if (key == 127 || key == 8) { + size_t before = previous_codepoint(editor, editor->gap_start); + if (before < editor->gap_start) { + changed_at(editor, before); + editor->gap_start = before; + editor->dirty = true; + } + return; + } + if (key == 13) key = '\n'; + if (key == '\n' || key == '\t' || key >= 32) { + if (!insert_byte(editor, (unsigned char)key)) + snprintf(editor->message, sizeof(editor->message), "Document reached the byte limit"); + } +} + +static bool nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0; +} + +static bool raw_terminal(const struct termios *original) { + struct termios raw = *original; + raw.c_iflag &= (tcflag_t)~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= (tcflag_t)~OPOST; + raw.c_cflag |= CS8; + raw.c_lflag &= (tcflag_t)~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + return tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == 0; +} + +int main(int argc, char **argv) { +#ifdef PURE_EMBEDDED_PROGRAM + if (argc != 4 || strcmp(argv[1], "--store") || !valid_key(argv[3])) { + fputs("usage: pure-edit-tui-fixed --store DIRECTORY KEY\n", stderr); + return 2; + } + const char *store_path = argv[2], *key = argv[3]; + const PureProgram *program = &pure_embedded_program; +#else + if (argc != 6 || strcmp(argv[1], "--program") || strcmp(argv[3], "--store") || + !valid_key(argv[5])) { + fputs("usage: pure-edit-tui --program FILE --store DIRECTORY KEY\n", stderr); + return 2; + } + const char *store_path = argv[4], *key = argv[5]; + PureProgram loaded; + char error[160]; + if (!pure_program_load(argv[2], &loaded, error, sizeof(error))) { + fprintf(stderr, "Program: %s\n", error); + return 2; + } + const PureProgram *program = &loaded; +#endif + size_t prefix; + if (!edit_layout(program, &prefix)) { + fputs("Program needs a complete-document read and two u32le splice fields with trailing insertion bytes\n", stderr); + return 2; + } + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) { + fputs("terminal editor requires a TTY\n", stderr); + return 2; + } + setlocale(LC_CTYPE, ""); + Editor editor = {.store = store_path, .program = program, .prefix = prefix}; + if (pure_store_init(editor.store) != PURE_OK || + !pure_runtime_init(&editor.runtime, editor.work, 2, program, + pure_store_host(editor.store), 4u * 1024u * 1024u) || + !read_document(&editor, key, false)) { + fprintf(stderr, "Could not open document: %s\n", editor.message); + free(editor.text); + return 2; + } + int wake[2]; + if (pipe(wake) || !nonblocking(wake[0]) || !nonblocking(wake[1]) || + tcgetattr(STDIN_FILENO, &editor.original_term)) { + perror("terminal setup"); + free(editor.text); + return 2; + } + wake_write = wake[1]; + struct sigaction action = {.sa_handler = wake_signal}; + sigemptyset(&action.sa_mask); + if (sigaction(SIGINT, &action, NULL) || sigaction(SIGTERM, &action, NULL) || + sigaction(SIGWINCH, &action, NULL)) { + perror("sigaction"); + wake_write = -1; + close(wake[0]); + close(wake[1]); + free(editor.text); + return 2; + } + if (!raw_terminal(&editor.original_term)) { + perror("raw terminal"); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &editor.original_term); + wake_write = -1; + close(wake[0]); + close(wake[1]); + free(editor.text); + return 2; + } + fputs("\033[?1049h", stdout); + while (!stopping) { + draw(&editor); + int key = read_byte(wake[0], -1); + if (resized) resized = 0; + if (key == -1) { stopping = 1; break; } + handle_key(&editor, key, wake[0]); + } + fputs("\033[?25h\033[?1049l", stdout); + fflush(stdout); + wake_write = -1; + close(wake[0]); + close(wake[1]); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &editor.original_term); + pure_runtime_stop(&editor.runtime); + close_list(&editor); + free(editor.text); + return 0; +} diff --git a/prototypes/connections/edit-window.c b/prototypes/connections/edit-window.c new file mode 100644 index 0000000..303b2bf --- /dev/null +++ b/prototypes/connections/edit-window.c @@ -0,0 +1,638 @@ +#define _POSIX_C_SOURCE 200809L +/* Optional GTK document adapter for the same prepared edit program used by + * pure-edit and pure-connections. GTK text lives at the application boundary; + * Pure's execution core remains independent of the UI and document encoding. */ +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include +#include +#include +#include +#include + +typedef struct { + PureProgram program; + PureRuntime runtime; + PureWork work[1]; + const char *directory, *program_path; + char key[65]; + size_t prefix, baseline_size; + char *baseline; + char *definition_baseline; + size_t definition_baseline_size; + uint64_t definition_generation; + size_t last_output_bytes; + bool has_edit_output; + bool definition_needs_apply; + int64_t revision; + bool exists; + bool closing, asking_to_close; + GtkWidget *window, *text, *status, *version, *heading, *key_entry; + GtkWidget *source, *source_revealer, *source_status; +} Editor; + +static const char *result_name(PureResult result) { + switch (result) { + case PURE_OK: return "ok"; + case PURE_NOT_FOUND: return "not found"; + case PURE_CONFLICT: return "version conflict"; + case PURE_BUSY: return "busy"; + case PURE_BUDGET: return "request budget exceeded"; + case PURE_LIMIT: return "document limit or invalid range"; + case PURE_UNCERTAIN: return "durability unknown; inspect the stored revision before retrying"; + case PURE_DENIED: return "operation denied"; + case PURE_ALLOCATION: return "allocation failed"; + default: return "operation failed"; + } +} + +static bool valid_key(const char *key) { + size_t length = strlen(key); + if (!length || length > 64) return false; + for (size_t i = 0; i < length; ++i) { + char c = key[i]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-')) return false; + } + return true; +} + +/* The window supplies the convenience layout accepted by pure-edit. The + * prepared program, not the window, determines the splice field locations. */ +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 || + p->read.offset || !p->read.to_end || p->read_required) 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; +} + +static void put32(unsigned char *to, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) to[i] = (unsigned char)(value >> (8 * i)); +} + +static void status(Editor *e, const char *text) { + gtk_label_set_text(GTK_LABEL(e->status), text); + char revision[64]; + snprintf(revision, sizeof(revision), "%s · revision %" PRId64, + e->exists ? "Stored" : "New", e->revision); + gtk_label_set_text(GTK_LABEL(e->version), revision); +} + +static void definition_status(Editor *e, const char *text) { + gtk_label_set_text(GTK_LABEL(e->source_status), text); +} + +static void definition_title(Editor *e) { + char title[128]; + if (e->has_edit_output) + snprintf(title, sizeof(title), "%s · Pure document · definition %" PRIu64 + " · output %zu B", e->key, e->definition_generation, e->last_output_bytes); + else snprintf(title, sizeof(title), "%s · Pure document · definition %" PRIu64, + e->key, e->definition_generation); + gtk_window_set_title(GTK_WINDOW(e->window), title); +} + +static void apply_definition(Editor *e) { + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->source)); + GtkTextIter first, last; + gtk_text_buffer_get_bounds(buffer, &first, &last); + char *draft = gtk_text_buffer_get_text(buffer, &first, &last, FALSE); + size_t length = strlen(draft); + bool changed = length != e->definition_baseline_size || + memcmp(draft, e->definition_baseline, length) != 0; + if (!changed && !e->definition_needs_apply) { + gtk_text_buffer_set_modified(buffer, FALSE); + definition_status(e, "No definition changes to apply"); + g_free(draft); + return; + } + PureProgram candidate; + size_t prefix = 0; + char error[160] = {0}; + if (length > PURE_PROGRAM_LIMIT || + !pure_program_parse(draft, length, &candidate, error, sizeof(error)) || + !edit_layout(&candidate, &prefix)) { + char message[240]; + snprintf(message, sizeof(message), "Definition rejected: %s; draft preserved", + error[0] ? error : "document edit/read contract required"); + definition_status(e, message); + g_free(draft); + return; + } + char *on_disk = NULL; + gsize on_disk_size = 0; + GError *file_error = NULL; + bool read = g_file_get_contents(e->program_path, &on_disk, &on_disk_size, &file_error); + bool same = read && on_disk_size == e->definition_baseline_size && + !memcmp(on_disk, e->definition_baseline, on_disk_size); + g_free(on_disk); + if (file_error) g_error_free(file_error); + if (!same) { + definition_status(e, "Definition changed on disk; draft preserved. Reload or resolve it first"); + g_free(draft); + return; + } + if (e->runtime.active || e->runtime.running) { + definition_status(e, "A document operation is active; definition draft preserved"); + g_free(draft); + return; + } + if (changed && !g_file_set_contents_full(e->program_path, draft, (gssize)length, + G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_DURABLE, 0600, &file_error)) { + char message[240]; + snprintf(message, sizeof(message), "Could not save definition: %s; draft preserved", + file_error ? file_error->message : "unknown error"); + definition_status(e, message); + if (file_error) g_error_free(file_error); + g_free(draft); + return; + } + PureProgram previous = e->program; + size_t previous_prefix = e->prefix; + e->program = candidate; + e->prefix = prefix; + if (!pure_runtime_restart(&e->runtime, &e->program, + pure_store_host(e->directory), 4u * 1024u * 1024u)) { + e->program = previous; + e->prefix = previous_prefix; + bool restored = !changed || g_file_set_contents_full(e->program_path, e->definition_baseline, + (gssize)e->definition_baseline_size, + G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_DURABLE, 0600, NULL); + definition_status(e, restored ? "Could not install definition; previous source restored" : + "Definition installation failed; inspect source file before retrying"); + g_free(draft); + return; + } + g_free(e->definition_baseline); + e->definition_baseline = draft; + e->definition_baseline_size = length; + gtk_text_buffer_set_modified(buffer, FALSE); + e->definition_needs_apply = false; + ++e->definition_generation; + e->has_edit_output = false; + definition_title(e); + definition_status(e, "Applied locally & saved; other hosts load it after restart"); +} + +static void apply_definition_clicked(GtkButton *button, gpointer data) { + (void)button; + apply_definition(data); +} + +static void reload_definition(Editor *e) { + char *source = NULL; + gsize length = 0; + GError *error = NULL; + if (!g_file_get_contents(e->program_path, &source, &length, &error) || + length > PURE_PROGRAM_LIMIT || memchr(source, 0, length) || + !g_utf8_validate(source, (gssize)length, NULL)) { + definition_status(e, "Could not load a UTF-8 definition within 16 KiB; draft preserved"); + if (error) g_error_free(error); + g_free(source); + return; + } + g_free(e->definition_baseline); + e->definition_baseline = source; + e->definition_baseline_size = length; + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->source)); + gtk_text_buffer_set_text(buffer, source, (int)length); + gtk_text_buffer_set_modified(buffer, FALSE); + e->definition_needs_apply = true; + definition_status(e, "Definition reloaded from disk; Apply to run it"); +} + +static void reload_definition_clicked(GtkButton *button, gpointer data) { + (void)button; + Editor *e = data; + if (gtk_text_buffer_get_modified(gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->source)))) { + definition_status(e, "Definition draft has changes; use Discard & reload"); + return; + } + reload_definition(e); +} + +static void discard_definition_clicked(GtkButton *button, gpointer data) { + (void)button; + reload_definition(data); +} + +static void toggle_definition(Editor *e) { + bool shown = gtk_revealer_get_reveal_child(GTK_REVEALER(e->source_revealer)); + gtk_revealer_set_reveal_child(GTK_REVEALER(e->source_revealer), !shown); + gtk_widget_grab_focus(shown ? e->text : e->source); +} + +static void toggle_definition_clicked(GtkButton *button, gpointer data) { + (void)button; + toggle_definition(data); +} + +static PureOutcome finish(Editor *e, PureTicket ticket) { + PureOutcome result; + while (!pure_runtime_result(ticket, &result)) + if (!pure_runtime_step(&e->runtime)) return (PureOutcome){.status = PURE_INVALID}; + return result; +} + +static bool open_document_key(Editor *e, const char *key) { + PureTicket ticket; + PureResult accepted = pure_runtime_accept(&e->runtime, PURE_INPUT_READ, + key, 0, 0, &ticket); + if (accepted != PURE_OK) { status(e, result_name(accepted)); return false; } + PureOutcome result = finish(e, ticket); + char *copy = NULL; + if (result.status == PURE_OK) { + if (result.body.size > G_MAXINT || + (result.body.size && (!result.body.data || + memchr(result.body.data, 0, result.body.size) || + !g_utf8_validate((const char *)result.body.data, result.body.size, NULL)))) { + pure_runtime_release(ticket); + status(e, "Stored bytes are not plain UTF-8 text; use pure-edit for binary data"); + return false; + } + copy = g_malloc(result.body.size + 1); + if (result.body.size) memcpy(copy, result.body.data, result.body.size); + copy[result.body.size] = 0; + } + pure_runtime_release(ticket); + if (result.status != PURE_OK && result.status != PURE_NOT_FOUND) { + status(e, result_name(result.status)); + return false; + } + if (strcmp(key, e->key) != 0) memcpy(e->key, key, strlen(key) + 1); + g_free(e->baseline); + e->baseline = copy ? copy : g_strdup(""); + e->baseline_size = result.status == PURE_OK ? result.body.size : 0; + e->revision = result.status == PURE_OK ? result.revision : 0; + e->exists = result.status == PURE_OK; + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->text)); + gtk_text_buffer_set_text(buffer, e->baseline, (int)e->baseline_size); + gtk_text_buffer_set_modified(buffer, FALSE); + e->has_edit_output = false; + gtk_label_set_text(GTK_LABEL(e->heading), e->key); + definition_title(e); + char message[128]; + snprintf(message, sizeof(message), "%s · %zu bytes", e->exists ? "Loaded" : "New document", + e->baseline_size); + status(e, message); + return true; +} + +static void open_document(Editor *e) { + (void)open_document_key(e, e->key); +} + +static void open_key(Editor *e) { + GtkTextBuffer *document = gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->text)); + GtkTextBuffer *definition = gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->source)); + if (gtk_text_buffer_get_modified(document) || gtk_text_buffer_get_modified(definition)) { + status(e, "Save or discard the document and definition drafts before opening another key"); + return; + } + const char *key = gtk_editable_get_text(GTK_EDITABLE(e->key_entry)); + if (!valid_key(key)) { status(e, "Key must be 1–64 ASCII letters, digits, _ or -"); return; } + if (open_document_key(e, key)) gtk_widget_grab_focus(e->text); +} + +static void open_key_clicked(GtkButton *button, gpointer data) { + (void)button; + open_key(data); +} + +static void open_key_activated(GtkEntry *entry, gpointer data) { + (void)entry; + open_key(data); +} + +static void reload_clicked(GtkButton *button, gpointer data) { + (void)button; + Editor *e = data; + if (gtk_text_buffer_get_modified(gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->text)))) { + status(e, "Draft has changes; use Discard & reload to replace it"); + return; + } + open_document(e); +} + +static void discard_clicked(GtkButton *button, gpointer data) { + (void)button; + open_document(data); +} + +static bool continuation(unsigned char c) { return (c & 0xc0u) == 0x80u; } + +/* Find the smallest changed byte interval with valid UTF-8 boundaries. The + * storage effect still sees bytes; this only keeps the UI's edit interval + * meaningful when two code points happen to share leading/trailing bytes. */ +static void change_span(const char *old, size_t old_size, const char *draft, size_t draft_size, + size_t *start, size_t *remove, size_t *insert) { + size_t prefix = 0; + while (prefix < old_size && prefix < draft_size && old[prefix] == draft[prefix]) ++prefix; + while (prefix && prefix < old_size && continuation((unsigned char)old[prefix])) --prefix; + size_t suffix = 0; + while (suffix < old_size - prefix && suffix < draft_size - prefix && + old[old_size - suffix - 1] == draft[draft_size - suffix - 1]) ++suffix; + while (suffix && (continuation((unsigned char)old[old_size - suffix]) || + continuation((unsigned char)draft[draft_size - suffix]))) --suffix; + *start = prefix; + *remove = old_size - prefix - suffix; + *insert = draft_size - prefix - suffix; +} + +static void save_document(Editor *e) { + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->text)); + GtkTextIter first, last; + gtk_text_buffer_get_bounds(buffer, &first, &last); + char *draft = gtk_text_buffer_get_text(buffer, &first, &last, FALSE); + size_t draft_size = strlen(draft); + size_t offset, remove, insert; + change_span(e->baseline, e->baseline_size, draft, draft_size, &offset, &remove, &insert); + if (e->exists && !remove && !insert) { + gtk_text_buffer_set_modified(buffer, FALSE); + status(e, "No changes to save"); + g_free(draft); + return; + } + if (draft_size > PURE_OBJECT_LIMIT || offset > UINT32_MAX || remove > UINT32_MAX || + insert > PURE_OBJECT_LIMIT - e->prefix) { + status(e, "Document or edit exceeds the selected byte limit"); + g_free(draft); + return; + } + PureTicket ticket; + PureResult accepted = pure_runtime_accept(&e->runtime, PURE_INPUT_WRITE, e->key, + e->revision, e->prefix + insert, &ticket); + if (accepted != PURE_OK) { + status(e, result_name(accepted)); + g_free(draft); + return; + } + unsigned char *input = pure_runtime_ingress(ticket, NULL); + memset(input, 0, e->prefix); + put32(input + e->program.splice_offset.value, (uint32_t)offset); + put32(input + e->program.splice_remove.value, (uint32_t)remove); + if (insert) memcpy(input + e->prefix, draft + offset, insert); + if (!pure_runtime_ready(ticket)) { + pure_runtime_release(ticket); + status(e, "Could not publish edit"); + g_free(draft); + return; + } + PureOutcome result = finish(e, ticket); + size_t reply_size = result.body.size; + pure_runtime_release(ticket); + if (result.status != PURE_OK) { + char message[160]; + if (result.status == PURE_CONFLICT) + snprintf(message, sizeof(message), "Version conflict: stored revision %" PRId64 + "; draft preserved", result.revision); + else snprintf(message, sizeof(message), "%s; draft preserved", result_name(result.status)); + status(e, message); + g_free(draft); + return; + } + g_free(e->baseline); + e->baseline = draft; + e->baseline_size = draft_size; + e->revision = result.revision; + e->exists = true; + e->last_output_bytes = reply_size; + e->has_edit_output = true; + definition_title(e); + gtk_text_buffer_set_modified(buffer, FALSE); + char message[128]; + snprintf(message, sizeof(message), "Saved %zu-byte edit at byte %zu · output %zu bytes", + insert, offset, reply_size); + status(e, message); +} + +static void save_clicked(GtkButton *button, gpointer data) { + (void)button; + save_document(data); +} + +static gboolean key_pressed(GtkEventControllerKey *controller, guint keyval, guint keycode, + GdkModifierType modifiers, gpointer data) { + (void)controller; (void)keycode; + Editor *e = data; + if ((modifiers & GDK_CONTROL_MASK) && (modifiers & GDK_SHIFT_MASK) && + (keyval == GDK_KEY_d || keyval == GDK_KEY_D)) { + toggle_definition(e); + return TRUE; + } + if ((modifiers & GDK_CONTROL_MASK) && + (keyval == GDK_KEY_o || keyval == GDK_KEY_O)) { + gtk_widget_grab_focus(e->key_entry); + return TRUE; + } + if ((modifiers & GDK_CONTROL_MASK) && + (keyval == GDK_KEY_s || keyval == GDK_KEY_S)) { + if ((modifiers & GDK_SHIFT_MASK) || + gtk_window_get_focus(GTK_WINDOW(e->window)) == e->source) + apply_definition(e); + else save_document(e); + return TRUE; + } + if ((modifiers & GDK_CONTROL_MASK) && + (keyval == GDK_KEY_w || keyval == GDK_KEY_W)) { + gtk_window_close(GTK_WINDOW(e->window)); + return TRUE; + } + return FALSE; +} + +static void close_decision(GObject *source, GAsyncResult *response, gpointer data) { + Editor *e = data; + GError *error = NULL; + int selected = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), response, &error); + if (error) g_error_free(error); + e->asking_to_close = false; + if (selected == 1) { + e->closing = true; + gtk_window_destroy(GTK_WINDOW(e->window)); + } +} + +static gboolean close_requested(GtkWindow *window, gpointer data) { + Editor *e = data; + bool document_changed = gtk_text_buffer_get_modified(gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->text))); + bool definition_changed = gtk_text_buffer_get_modified(gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->source))); + if (e->closing || (!document_changed && !definition_changed)) + return FALSE; + if (e->asking_to_close) return TRUE; + e->asking_to_close = true; + GtkAlertDialog *dialog = gtk_alert_dialog_new("Discard unsaved document or definition changes?"); + gtk_alert_dialog_set_detail(dialog, "The current draft has not been saved or applied."); + const char *buttons[] = {"Keep editing", "Discard changes", NULL}; + gtk_alert_dialog_set_buttons(dialog, buttons); + gtk_alert_dialog_set_cancel_button(dialog, 0); + gtk_alert_dialog_set_default_button(dialog, 0); + gtk_alert_dialog_choose(dialog, window, NULL, close_decision, e); + g_object_unref(dialog); + return TRUE; +} + +static void activate(GtkApplication *application, gpointer data) { + Editor *e = data; + if (e->window) { gtk_window_present(GTK_WINDOW(e->window)); return; } + e->window = gtk_application_window_new(application); + g_signal_connect(e->window, "close-request", G_CALLBACK(close_requested), e); + gtk_window_set_default_size(GTK_WINDOW(e->window), 960, 680); + definition_title(e); + GtkWidget *column = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_window_set_child(GTK_WINDOW(e->window), column); + GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_widget_set_margin_start(bar, 14); + gtk_widget_set_margin_end(bar, 14); + gtk_widget_set_margin_top(bar, 12); + gtk_widget_set_margin_bottom(bar, 12); + gtk_box_append(GTK_BOX(column), bar); + e->heading = gtk_label_new(e->key); + gtk_widget_add_css_class(e->heading, "title-3"); + gtk_box_append(GTK_BOX(bar), e->heading); + e->version = gtk_label_new(""); + gtk_widget_add_css_class(e->version, "dim-label"); + gtk_box_append(GTK_BOX(bar), e->version); + GtkWidget *spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_hexpand(spacer, TRUE); + gtk_box_append(GTK_BOX(bar), spacer); + GtkWidget *definition = gtk_button_new_with_label("Definition · Ctrl+Shift+D"); + g_signal_connect(definition, "clicked", G_CALLBACK(toggle_definition_clicked), e); + gtk_box_append(GTK_BOX(bar), definition); + GtkWidget *reload = gtk_button_new_with_label("Reload"); + g_signal_connect(reload, "clicked", G_CALLBACK(reload_clicked), e); + gtk_box_append(GTK_BOX(bar), reload); + GtkWidget *discard = gtk_button_new_with_label("Discard & reload"); + g_signal_connect(discard, "clicked", G_CALLBACK(discard_clicked), e); + gtk_box_append(GTK_BOX(bar), discard); + GtkWidget *save = gtk_button_new_with_label("Save · Ctrl+S"); + gtk_widget_add_css_class(save, "suggested-action"); + g_signal_connect(save, "clicked", G_CALLBACK(save_clicked), e); + gtk_box_append(GTK_BOX(bar), save); + GtkWidget *scroll = gtk_scrolled_window_new(); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_box_append(GTK_BOX(column), scroll); + e->text = gtk_text_view_new(); + gtk_accessible_update_property(GTK_ACCESSIBLE(e->text), + GTK_ACCESSIBLE_PROPERTY_LABEL, "Document text", -1); + gtk_text_view_set_monospace(GTK_TEXT_VIEW(e->text), TRUE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(e->text), GTK_WRAP_WORD_CHAR); + gtk_widget_set_margin_start(e->text, 18); + gtk_widget_set_margin_end(e->text, 18); + gtk_widget_set_margin_top(e->text, 12); + gtk_widget_set_margin_bottom(e->text, 12); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), e->text); + e->source_revealer = gtk_revealer_new(); + gtk_box_append(GTK_BOX(column), e->source_revealer); + GtkWidget *source_panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_margin_start(source_panel, 14); + gtk_widget_set_margin_end(source_panel, 14); + gtk_widget_set_margin_bottom(source_panel, 8); + gtk_revealer_set_child(GTK_REVEALER(e->source_revealer), source_panel); + GtkWidget *source_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_append(GTK_BOX(source_panel), source_bar); + GtkWidget *source_heading = gtk_label_new("Application definition · editor.pure"); + gtk_widget_add_css_class(source_heading, "title-4"); + gtk_box_append(GTK_BOX(source_bar), source_heading); + GtkWidget *source_spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_hexpand(source_spacer, TRUE); + gtk_box_append(GTK_BOX(source_bar), source_spacer); + GtkWidget *source_reload = gtk_button_new_with_label("Reload definition"); + g_signal_connect(source_reload, "clicked", G_CALLBACK(reload_definition_clicked), e); + gtk_box_append(GTK_BOX(source_bar), source_reload); + GtkWidget *source_discard = gtk_button_new_with_label("Discard & reload"); + g_signal_connect(source_discard, "clicked", G_CALLBACK(discard_definition_clicked), e); + gtk_box_append(GTK_BOX(source_bar), source_discard); + GtkWidget *source_apply = gtk_button_new_with_label("Apply & save · Ctrl+S"); + gtk_widget_add_css_class(source_apply, "suggested-action"); + g_signal_connect(source_apply, "clicked", G_CALLBACK(apply_definition_clicked), e); + gtk_box_append(GTK_BOX(source_bar), source_apply); + GtkWidget *source_scroll = gtk_scrolled_window_new(); + gtk_widget_set_size_request(source_scroll, -1, 260); + gtk_box_append(GTK_BOX(source_panel), source_scroll); + e->source = gtk_text_view_new(); + gtk_accessible_update_property(GTK_ACCESSIBLE(e->source), + GTK_ACCESSIBLE_PROPERTY_LABEL, "Application definition source", -1); + gtk_text_view_set_monospace(GTK_TEXT_VIEW(e->source), TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(source_scroll), e->source); + GtkTextBuffer *source_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(e->source)); + gtk_text_buffer_set_text(source_buffer, e->definition_baseline, + (int)e->definition_baseline_size); + gtk_text_buffer_set_modified(source_buffer, FALSE); + e->source_status = gtk_label_new("Definition is active; edit and apply to change it"); + gtk_label_set_xalign(GTK_LABEL(e->source_status), 0); + gtk_box_append(GTK_BOX(source_panel), e->source_status); + GtkWidget *footer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_start(footer, 14); + gtk_widget_set_margin_end(footer, 14); + gtk_widget_set_margin_top(footer, 9); + gtk_widget_set_margin_bottom(footer, 11); + gtk_box_append(GTK_BOX(column), footer); + e->status = gtk_label_new(""); + gtk_label_set_xalign(GTK_LABEL(e->status), 0); + gtk_widget_set_hexpand(e->status, TRUE); + gtk_box_append(GTK_BOX(footer), e->status); + e->key_entry = gtk_entry_new(); + gtk_entry_set_max_length(GTK_ENTRY(e->key_entry), 64); + gtk_editable_set_text(GTK_EDITABLE(e->key_entry), e->key); + gtk_entry_set_placeholder_text(GTK_ENTRY(e->key_entry), "Document key"); + gtk_editable_set_width_chars(GTK_EDITABLE(e->key_entry), 15); + gtk_accessible_update_property(GTK_ACCESSIBLE(e->key_entry), + GTK_ACCESSIBLE_PROPERTY_LABEL, "Document key", -1); + g_signal_connect(e->key_entry, "activate", G_CALLBACK(open_key_activated), e); + gtk_box_append(GTK_BOX(footer), e->key_entry); + GtkWidget *open = gtk_button_new_with_label("Open · Ctrl+O"); + g_signal_connect(open, "clicked", G_CALLBACK(open_key_clicked), e); + gtk_box_append(GTK_BOX(footer), open); + GtkEventController *keys = gtk_event_controller_key_new(); + g_signal_connect(keys, "key-pressed", G_CALLBACK(key_pressed), e); + gtk_widget_add_controller(e->window, keys); + open_document(e); + gtk_window_present(GTK_WINDOW(e->window)); +} + +int main(int argc, char **argv) { + if (argc != 6 || strcmp(argv[1], "--program") || strcmp(argv[3], "--store") || + !valid_key(argv[5])) { + fputs("pure-edit-window --program FILE --store DIRECTORY KEY\n", stderr); + return 2; + } + Editor e = {.directory = argv[4], .program_path = argv[2], .definition_generation = 1}; + memcpy(e.key, argv[5], strlen(argv[5]) + 1); + char error[160] = {0}; + gsize source_size = 0; + GError *source_error = NULL; + if (!g_file_get_contents(e.program_path, &e.definition_baseline, &source_size, &source_error) || + source_size > PURE_PROGRAM_LIMIT || + memchr(e.definition_baseline, 0, source_size) || + !g_utf8_validate(e.definition_baseline, (gssize)source_size, NULL) || + !pure_program_parse(e.definition_baseline, source_size, &e.program, error, sizeof(error)) || + !edit_layout(&e.program, &e.prefix)) { + fprintf(stderr, "Program must define the simple splice edit layout: %s\n", + error[0] ? error : source_error ? source_error->message : "unsupported layout"); + if (source_error) g_error_free(source_error); + g_free(e.definition_baseline); + return 2; + } + e.definition_baseline_size = source_size; + if (pure_store_init(e.directory) != PURE_OK || + !pure_runtime_init(&e.runtime, e.work, 1, &e.program, + pure_store_host(e.directory), 4u * 1024u * 1024u)) { + fputs("Cannot initialize document store or execution domain\n", stderr); + return 2; + } + e.baseline = g_strdup(""); + GtkApplication *application = gtk_application_new("org.pure.DocumentEditor", G_APPLICATION_NON_UNIQUE); + g_signal_connect(application, "activate", G_CALLBACK(activate), &e); + char *app_argv[] = {argv[0], NULL}; + int result = g_application_run(G_APPLICATION(application), 1, app_argv); + g_object_unref(application); + g_free(e.baseline); + g_free(e.definition_baseline); + return result; +} diff --git a/prototypes/connections/edit.c b/prototypes/connections/edit.c new file mode 100644 index 0000000..7c070e4 --- /dev/null +++ b/prototypes/connections/edit.c @@ -0,0 +1,177 @@ +#define _POSIX_C_SOURCE 200809L +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include +#include +#include +#ifdef PURE_EMBEDDED_PROGRAM +#include "embedded-plan.h" +#endif + +/* 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) { +#ifndef PURE_EMBEDDED_PROGRAM + const char *program_path = NULL, *store_path = NULL; +#else + const char *store_path = NULL; +#endif + size_t budget = 4u * 1024u * 1024u; + int at = 1; + for (; at + 1 < argc && !strncmp(argv[at], "--", 2); at += 2) { + uint64_t value; + if (!strcmp(argv[at], "--store")) store_path = argv[at + 1]; +#ifndef PURE_EMBEDDED_PROGRAM + else if (!strcmp(argv[at], "--program")) program_path = argv[at + 1]; +#endif + else if (!strcmp(argv[at], "--budget") && number(argv[at + 1], SIZE_MAX, &value)) budget = (size_t)value; + else goto usage; + } +#ifndef PURE_EMBEDDED_PROGRAM + if (!program_path) goto usage; +#endif + if (!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; +#ifndef PURE_EMBEDDED_PROGRAM + PureProgram loaded; + char error[160]; + if (!pure_program_load(program_path, &loaded, error, sizeof(error))) { + fprintf(stderr, "Program: %s\n", error); return 2; + } + const PureProgram *program = &loaded; +#else + const PureProgram *program = &pure_embedded_program; +#endif + 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( +#ifdef PURE_EMBEDDED_PROGRAM + "pure-edit-fixed --store DIRECTORY [--budget BYTES] show KEY\n" + "pure-edit-fixed --store DIRECTORY [--budget BYTES] edit KEY REVISION BYTE_OFFSET REMOVE_BYTES INSERT_FILE\n" + "pure-edit-fixed --store DIRECTORY [--budget BYTES] apply KEY REVISION INPUT_FILE\n" +#else + "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" +#endif + "edit uses the prepared program's field locations; apply accepts its raw input bytes.\n", stderr); + return 2; +} diff --git a/prototypes/connections/evaluate.c b/prototypes/connections/evaluate.c new file mode 100644 index 0000000..88ffae7 --- /dev/null +++ b/prototypes/connections/evaluate.c @@ -0,0 +1,53 @@ +#include "program.h" +#include + +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/examples/bounded-editor.pure b/prototypes/connections/examples/bounded-editor.pure new file mode 100644 index 0000000..e9f2634 --- /dev/null +++ b/prototypes/connections/examples/bounded-editor.pure @@ -0,0 +1,19 @@ +// Same document edit as editor.pure, with an authored final-size policy. +// The store checks the limit against the current version under its writer lock. +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, 8); + reply(saved, edit.insert); +} diff --git a/prototypes/connections/examples/editor.pure b/prototypes/connections/examples/editor.pure new file mode 100644 index 0000000..4de78bc --- /dev/null +++ b/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/examples/excerpt.pure b/prototypes/connections/examples/excerpt.pure new file mode 100644 index 0000000..622f9bb --- /dev/null +++ b/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/examples/select-text.py b/prototypes/connections/examples/select-text.py new file mode 100644 index 0000000..ffaf80b --- /dev/null +++ b/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(' 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/examples/selection.pure b/prototypes/connections/examples/selection.pure new file mode 100644 index 0000000..7d9d4ec --- /dev/null +++ b/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/examples/storage.pure b/prototypes/connections/examples/storage.pure new file mode 100644 index 0000000..ff54ea3 --- /dev/null +++ b/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/export_flat_store.py b/prototypes/connections/export_flat_store.py new file mode 100644 index 0000000..9469d5a --- /dev/null +++ b/prototypes/connections/export_flat_store.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Export the selected paged document store to a new default-format store.""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +KEY = re.compile(r'[A-Za-z0-9_-]{1,64}\Z') +LIMIT = (2 * 6 + 32) * 32768 + 8192 +FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC + + +def show(binary, store, key): + result = subprocess.run([str(binary), '--store', str(store), 'show', key], + capture_output=True, timeout=30) + if result.returncode: + raise RuntimeError(f'{binary.name} could not read {key}: {result.stderr[-400:]!r}') + details = json.loads(result.stderr) + revision = int(details['revision']) + body = result.stdout + if not 1 <= revision <= (1 << 63) - 1 or len(body) != details['bytes'] or len(body) > LIMIT: + raise RuntimeError(f'invalid stored document: {key}') + return revision, body + + +def write_all(fd, content): + content = memoryview(content) + while content: + written = os.write(fd, content) + if written <= 0: + raise OSError('short store write') + content = content[written:] + + +def export(source, destination, paged_editor, flat_editor): + source = source.resolve(strict=True) + if os.path.lexists(destination): + raise ValueError('destination must not exist, including a symlink') + destination = destination.resolve(strict=False) + if (not source.is_dir() or source == destination or source in destination.parents or + os.path.lexists(destination)): + raise ValueError('source must be a directory and destination must be a new separate path') + parent = destination.parent.resolve(strict=True) + if not parent.is_dir(): + raise ValueError('destination parent must already exist') + lock = os.open(source / '.writer.lock', os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC) + stage = None + try: + # A separate CLI process reads each object; its lock-file close cannot + # release this parent's POSIX read lock. Cooperating writers get BUSY. + try: + fcntl.lockf(lock, fcntl.LOCK_SH | fcntl.LOCK_NB) + except BlockingIOError as error: + raise RuntimeError('source has an active writer; retry export') from error + keys = [] + with os.scandir(source) as entries: + for entry in entries: + if not entry.name.startswith('obj-'): + continue + key = entry.name[4:] + if not KEY.fullmatch(key) or not entry.is_file(follow_symlinks=False): + raise RuntimeError(f'invalid object entry: {entry.name}') + keys.append(key) + stage = Path(tempfile.mkdtemp(prefix=f'.{destination.name}.export-', dir=parent)) + stage_fd = os.open(stage, FLAGS) + digest = hashlib.sha256() + total = 0 + try: + marker = os.open('.writer.lock', os.O_CREAT | os.O_EXCL | os.O_WRONLY | + os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=stage_fd) + try: + os.fsync(marker) + finally: + os.close(marker) + for key in sorted(keys): + revision, body = show(paged_editor, source, key) + name = 'obj-' + key + fd = os.open(name, os.O_CREAT | os.O_EXCL | os.O_WRONLY | + os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=stage_fd) + try: + header = b'PUREB001' + revision.to_bytes(8, 'little') + len(body).to_bytes(8, 'little') + write_all(fd, header) + write_all(fd, body) + os.fsync(fd) + finally: + os.close(fd) + if show(flat_editor, stage, key) != (revision, body): + raise RuntimeError(f'flat readback differs: {key}') + digest.update(key.encode('ascii') + b'\0') + digest.update(header[8:]) + digest.update(body) + total += len(body) + os.fsync(stage_fd) + finally: + os.close(stage_fd) + if os.path.lexists(destination): + raise FileExistsError(destination) + os.rename(stage, destination) + stage = None + parent_fd = os.open(parent, FLAGS) + try: + os.fsync(parent_fd) + finally: + os.close(parent_fd) + return {'objects': len(keys), 'logical_bytes': total, + 'document_digest': digest.hexdigest(), + 'paged_reader_sha256': hashlib.sha256(paged_editor.read_bytes()).hexdigest(), + 'flat_reader_sha256': hashlib.sha256(flat_editor.read_bytes()).hexdigest()} + finally: + if stage is not None: + shutil.rmtree(stage) + os.close(lock) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, required=True) + parser.add_argument('--destination', type=Path, required=True) + parser.add_argument('--paged-editor', type=Path, required=True, + help='fixed editor linked with store-paged.c') + parser.add_argument('--flat-editor', type=Path, required=True, + help='fixed editor linked with the default store.c') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + result = export(args.source, args.destination, + args.paged_editor.resolve(strict=True), args.flat_editor.resolve(strict=True)) + body = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(body) + print(body, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/fixed-budget.json b/prototypes/connections/fixed-budget.json new file mode 100644 index 0000000..35439be --- /dev/null +++ b/prototypes/connections/fixed-budget.json @@ -0,0 +1,18 @@ +{ + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "binaries": { + "pure-edit-fixed": { + "max_text_bytes": 20480, + "max_data_bytes": 1000, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + }, + "pure-connections-fixed": { + "max_text_bytes": 26880, + "max_data_bytes": 1100, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The HTTP ceiling includes the reviewed 188-byte cost of moving document interpretation from the reusable core into the selected document adapter and the later 64-byte linked increase for an authored final-size guard. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files." +} diff --git a/prototypes/connections/fixed-package-budget.json b/prototypes/connections/fixed-package-budget.json new file mode 100644 index 0000000..2ae8671 --- /dev/null +++ b/prototypes/connections/fixed-package-budget.json @@ -0,0 +1,10 @@ +{ + "profile": "linux-aarch64-gcc14-fixed-document-package", + "max_archive_bytes": { + "cli": 16384, + "cli_http": 34816, + "cli_tui": 35840, + "cli_http_tui": 54272 + }, + "scope": "Deterministic gzip archive containing selected stripped AArch64 binaries, manifest and usage text. The combined CLI+HTTP+TUI ceiling was reviewed upward by 1 KiB for the bounded document browser; the CLI-only ceiling is unchanged. These are regression ceilings for the current document app and toolchain; they exclude host libc, terminal emulator, kernel buffers and stored documents. Optional hosts are charged to their selected package, not the CLI-only package." +} diff --git a/prototypes/connections/flow.c b/prototypes/connections/flow.c new file mode 100644 index 0000000..1070c70 --- /dev/null +++ b/prototypes/connections/flow.c @@ -0,0 +1,41 @@ +#include "flow.h" +#include + +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/flow.h b/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include +#include + +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/hybrid-budget.json b/prototypes/connections/hybrid-budget.json new file mode 100644 index 0000000..e36b9e7 --- /dev/null +++ b/prototypes/connections/hybrid-budget.json @@ -0,0 +1,18 @@ +{ + "profile": "linux-aarch64-gcc14-selected-hybrid-document-storage", + "binaries": { + "pure-edit": { + "max_text_bytes": 43008, + "max_data_bytes": 1200, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + }, + "pure-connections": { + "max_text_bytes": 50176, + "max_data_bytes": 1300, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Opt-in inline/PUREP001 document store on Linux AArch64/GCC 14. The CLI text ceiling is 42 KiB. The HTTP ceiling is 49 KiB after reviewing its 49133-to-49589-byte growth for the authored final-size guard, which checks the locked document version. This is a selected research budget, not the default micro-runtime budget. Direct dependencies only; excludes libc, process RAM, filesystem cache and journal, optional TLS/GTK." +} diff --git a/prototypes/connections/hybrid-tui-budget.json b/prototypes/connections/hybrid-tui-budget.json new file mode 100644 index 0000000..a5d37f2 --- /dev/null +++ b/prototypes/connections/hybrid-tui-budget.json @@ -0,0 +1,12 @@ +{ + "profile": "linux-aarch64-gcc14-hybrid-source-document-terminal-editor", + "binaries": { + "pure-edit-tui": { + "max_text_bytes": 55296, + "max_data_bytes": 1152, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Selected source-loaded native document terminal editor with the existing 16 KiB hybrid page store. This ceiling reviews the measured 53,642-byte linked text build on Linux AArch64/GCC 14. It excludes libc, process RSS, mapped document pages and filesystem cache; check_tui.py separately gates actual one-MiB and 1,024-document work. The default core, CLI and TUI do not select this store." +} diff --git a/prototypes/connections/measure-peak-rss.c b/prototypes/connections/measure-peak-rss.c new file mode 100644 index 0000000..1fe2e0b --- /dev/null +++ b/prototypes/connections/measure-peak-rss.c @@ -0,0 +1,43 @@ +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE +#else +#define _DEFAULT_SOURCE +#endif +#include +#include +#include +#include +#include + +/* Test helper: fork from a small native process so the interpreter's pre-exec + * RSS cannot become the measured child's peak. Not shipped. */ +int main(int argc, char **argv) { + if (argc < 2) return 2; + pid_t child = fork(); + if (child < 0) return 2; + if (!child) { + int null = open("/dev/null", O_WRONLY); + if (null < 0 || dup2(null, STDOUT_FILENO) < 0 || + dup2(null, STDERR_FILENO) < 0) _exit(127); + close(null); + execvp(argv[1], argv + 1); + _exit(127); + } + struct rusage usage; + int status; + if (wait4(child, &status, 0, &usage) < 0) return 2; + if (!WIFEXITED(status)) return 2; + if (usage.ru_maxrss < 0) return 2; +#ifdef __APPLE__ + /* XNU reports ru_maxrss in bytes; Linux reports KiB. */ + long peak_kib = usage.ru_maxrss / 1024 + (usage.ru_maxrss % 1024 != 0); + const char *raw_unit = "bytes"; +#else + long peak_kib = usage.ru_maxrss; + const char *raw_unit = "KiB"; +#endif + printf("{\"exit_code\":%d,\"max_rss_kib\":%ld," + "\"raw_max_rss\":%ld,\"raw_unit\":\"%s\"}\n", + WEXITSTATUS(status), peak_kib, usage.ru_maxrss, raw_unit); + return 0; +} diff --git a/prototypes/connections/measure_tui_latency.py b/prototypes/connections/measure_tui_latency.py new file mode 100644 index 0000000..569284b --- /dev/null +++ b/prototypes/connections/measure_tui_latency.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Measure one visible PTY redraw per edit in the real fixed document app.""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import platform +import pty +import select +import statistics +import struct +import subprocess +import tempfile +import termios +import time + +HERE = Path(__file__).resolve().parent +WORKLOADS = { + '256-byte-ascii': b'Q' * 256, + 'one-mib-ascii': b'Q' * 1_048_576, + 'one-mib-utf8': '안녕'.encode() * 174_762, +} + + +def frame(fd, process): + data = bytearray() + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + if select.select([fd], [], [], 0.5)[0]: + data.extend(os.read(fd, 65536)) + if b'\x1b[?25h' in data: + return bytes(data) + if process.poll() is not None: + break + raise RuntimeError(f'editor did not finish a frame: exit={process.poll()}, output={data[-300:]!r}') + + +def measure(tui, cli, store, root, name, body): + insert = root / f'{name}.insert' + insert.write_bytes(body) + subprocess.run([cli, '--store', store, 'edit', name, '0', '0', '0', insert], + check=True, capture_output=True, timeout=15) + master, slave = pty.openpty() + fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0)) + process = subprocess.Popen([tui, '--store', store, name], stdin=slave, + stdout=slave, stderr=slave, + env={**os.environ, 'LANG': 'C.UTF-8'}) + try: + frame(master, process) + samples = [] + for _ in range(10): + start = time.perf_counter_ns() + os.write(master, b'Z') + shown = frame(master, process) + samples.append({'nanoseconds': time.perf_counter_ns() - start, + 'frame_bytes': len(shown), + 'frame_sha256': hashlib.sha256(shown).hexdigest()}) + os.write(master, b'\x11') + frame(master, process) + os.write(master, b'\x11') + process.wait(timeout=5) + if process.returncode: + raise RuntimeError(f'editor exit={process.returncode}') + return {'document_bytes': len(body), 'samples': samples, + 'median_nanoseconds': int(statistics.median(s['nanoseconds'] for s in samples))} + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + os.close(master) + os.close(slave) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--tui', type=Path, required=True) + parser.add_argument('--cli', type=Path, default=HERE / 'build/pure-edit-fixed') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + tui, cli = args.tui.resolve(), args.cli.resolve() + with tempfile.TemporaryDirectory(prefix='pure-tui-latency-') as directory: + root = Path(directory) + workloads = {name: measure(tui, cli, root / 'store', root, name, body) + for name, body in WORKLOADS.items()} + result = {'platform': platform.platform(), 'terminal': 'PTY 24x80, C.UTF-8', + 'tui_sha256': hashlib.sha256(tui.read_bytes()).hexdigest(), + 'cli_sha256': hashlib.sha256(cli.read_bytes()).hexdigest(), + 'workloads': workloads} + text = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text) + print(text, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/memory-budget.json b/prototypes/connections/memory-budget.json new file mode 100644 index 0000000..320e011 --- /dev/null +++ b/prototypes/connections/memory-budget.json @@ -0,0 +1,7 @@ +{ + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "max_small_edit_rss_kib": 4096, + "max_one_mib_edit_rss_kib": 4096, + "max_one_mib_show_rss_kib": 4096, + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee." +} diff --git a/prototypes/connections/mirror.c b/prototypes/connections/mirror.c new file mode 100644 index 0000000..34ff1bf --- /dev/null +++ b/prototypes/connections/mirror.c @@ -0,0 +1,145 @@ +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include +#include +#include +#include "storage-plan.h" + +enum { MAX_TARGETS = 8 }; +typedef struct { + PureRuntime runtime; + PureWork work[1]; + PureTicket ticket; + PureOutcome outcome; +} Target; + +static const char *result_name(PureResult result) { + static const char *const names[] = { + "ok", "not_found", "conflict", "busy", "budget", "limit", + "store_error", "durability_unknown", "invalid", "denied", + "closed", "cancelled", "allocation" + }; + return (unsigned)result < sizeof(names) / sizeof(names[0]) ? names[result] : "unknown"; +} + +static bool number(const char *text, uint64_t limit, uint64_t *out) { + uint64_t value = 0; + if (!*text) return false; + 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; +} + +int main(int argc, char **argv) { + int at = 1; + size_t target_budget = PURE_OBJECT_LIMIT; + uint64_t parsed; + if (argc > 2 && !strcmp(argv[1], "--target-budget")) { + if (!number(argv[2], SIZE_MAX, &parsed)) return 2; + target_budget = (size_t)parsed; + at += 2; + } + if (argc - at < 5 || (argc - at - 2) % 3 || + (argc - at - 2) / 3 > MAX_TARGETS) { + fputs("usage: pure-mirror [--target-budget BYTES] SOURCE_STORE SOURCE_KEY TARGET_STORE TARGET_KEY EXPECTED_REVISION [TARGET_STORE TARGET_KEY EXPECTED_REVISION ...]\n", stderr); + return 2; + } + size_t count = (size_t)(argc - at - 2) / 3; + int64_t expected[MAX_TARGETS]; + for (size_t i = 0; i < count; ++i) { + if (!number(argv[at + 4 + i * 3], INT64_MAX, &parsed)) return 2; + expected[i] = (int64_t)parsed; + } + if (pure_store_init(argv[at]) != PURE_OK) return 2; + PureRuntime source; + PureWork source_work[1]; + if (!pure_runtime_init(&source, source_work, 1, &pure_embedded_program, + pure_store_host(argv[at]), PURE_OBJECT_LIMIT)) return 2; + PureTicket read; + PureResult status = pure_runtime_accept(&source, PURE_INPUT_READ, argv[at + 1], 0, 0, &read); + if (status != PURE_OK) { + printf("{\"source_status\":\"%s\"}\n", result_name(status)); + return 2; + } + PureOutcome snapshot; + if (!pure_runtime_step(&source) || !pure_runtime_result(read, &snapshot)) { + pure_runtime_release(read); + return 2; + } + if (snapshot.status != PURE_OK) { + printf("{\"source_status\":\"%s\"}\n", result_name(snapshot.status)); + pure_runtime_release(read); + return 2; + } + Target targets[MAX_TARGETS] = {0}; + size_t forwarded = 0; + for (size_t i = 0; i < count; ++i) { + const char *store = argv[at + 2 + i * 3], *key = argv[at + 3 + i * 3]; + PureHost host = pure_store_host(store); + host.allocate = NULL; /* Forwarded ingress uses the source mapping. */ + if (pure_store_init(store) != PURE_OK || + !pure_runtime_init(&targets[i].runtime, targets[i].work, 1, + &pure_embedded_program, host, + target_budget)) status = PURE_STORE_ERROR; + else status = pure_runtime_forward(read, &targets[i].runtime, key, + expected[i], &targets[i].ticket); + if (status != PURE_OK) { + for (size_t j = 0; j < forwarded; ++j) { + pure_runtime_cancel(targets[j].ticket); + pure_runtime_release(targets[j].ticket); + } + pure_runtime_release(read); + printf("{\"admission_status\":\"%s\",\"target_index\":%zu}\n", + result_name(status), i); + return 2; + } + ++forwarded; + } + /* Each queued target pins the mapped source snapshot independently. */ + pure_runtime_release(read); + if (snapshot.body.size && source.retained != snapshot.body.size) { + for (size_t i = 0; i < count; ++i) { + pure_runtime_cancel(targets[i].ticket); + pure_runtime_release(targets[i].ticket); + } + return 2; + } + int exit_status = 0; + for (size_t i = 0; i < count; ++i) { + if (!pure_runtime_step(&targets[i].runtime) || + !pure_runtime_result(targets[i].ticket, &targets[i].outcome)) { + for (size_t j = i; j < count; ++j) { + pure_runtime_cancel(targets[j].ticket); + pure_runtime_release(targets[j].ticket); + } + return 2; + } + if (targets[i].outcome.status == PURE_CONFLICT && !exit_status) exit_status = 3; + else if (targets[i].outcome.status != PURE_OK && + targets[i].outcome.status != PURE_CONFLICT) exit_status = 2; + pure_runtime_release(targets[i].ticket); + if (snapshot.body.size && i + 1 < count && + source.retained != snapshot.body.size) { + for (size_t j = i + 1; j < count; ++j) { + pure_runtime_cancel(targets[j].ticket); + pure_runtime_release(targets[j].ticket); + } + return 2; + } + } + if (source.active || source.retained) return 2; + printf("{\"source_revision\":\"%" PRId64 "\",\"bytes\":%zu,\"targets\":[", + snapshot.revision, snapshot.body.size); + for (size_t i = 0; i < count; ++i) + printf("%s{\"index\":%zu,\"status\":\"%s\",\"revision\":\"%" PRId64 "\"}", + i ? "," : "", i, result_name(targets[i].outcome.status), + targets[i].outcome.revision); + puts("]}"); + return fflush(stdout) ? 2 : exit_status; +} diff --git a/prototypes/connections/package_native.py b/prototypes/connections/package_native.py new file mode 100644 index 0000000..7ee9c4f --- /dev/null +++ b/prototypes/connections/package_native.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +"""Bundle the selected Linux document app without folding hosts into the core.""" +import argparse +import gzip +import hashlib +import io +import json +from pathlib import Path +import platform +import re +import subprocess +import tarfile +import tempfile + +HERE = Path(__file__).resolve().parent +NEEDED = re.compile(r'\(NEEDED\).*Shared library: \[([^]]+)\]') +ROOT_NAME = 'pure-document' +PAGED_STORAGE = """# Selected page storage format + +This preview uses the PUREP001 16 KiB-page store. It cannot open PUREB001 +flat-store objects. Use a new --store directory; this is a research format, +not a migration or a default storage backend. Small objects currently pay a +full-page write cost. See docs/research/033-paged-document-storage.md in the +source repository for measured boundaries and limitations. +""" +HYBRID_STORAGE = """# Selected inline/page storage experiment + +Objects up to 16 KiB use the existing PUREB001 file format. Larger objects +use PUREP001 page roots and append-only data generations. The current flat +binary cannot read PUREP001 objects, so use a new --store directory for this +preview. This is a selected research build, not the default backend or a +bidirectional migration path. It requires a Linux host with 16 KiB OS pages. +See docs/research/034-hybrid-document-storage.md in the source repository. +""" + +RUN = """# Pure document preview — Linux native build + +This archive includes the CLI and editable editor.pure program. HTTP, TLS peer, +and GTK hosts are included only when listed in manifest.json. Shared system libraries are not bundled; +manifest.json records every binary's direct dependencies. + +From this directory, try a local edit: + + printf 'hello Pure\\n' > /tmp/pure-insert.txt + bin/pure-edit --program share/editor.pure --store /tmp/pure-preview-store \\ + edit note 0 0 0 /tmp/pure-insert.txt + bin/pure-edit --program share/editor.pure --store /tmp/pure-preview-store show note + +When selected, the same program and store can be opened with the GTK window: + + bin/pure-edit-window --program share/editor.pure \\ + --store /tmp/pure-preview-store note + +When selected, the source-loaded terminal editor uses the same program and +store without GTK. Edit share/editor.pure, then start a new TUI session to +load the changed definition: + + bin/pure-edit-tui --program share/editor.pure \\ + --store /tmp/pure-preview-store note + +When selected, the TLS peer needs your own CA and server/client certificates. The server +certificate needs a DNS SAN matching SERVER_DNS; the client certificate needs +a DNS SAN matching CLIENT_DNS. This example binds only to loopback: + + bin/pure-document-peer serve 127.0.0.1 9443 share/editor.pure \\ + /tmp/pure-preview-store CA.pem SERVER.pem SERVER.key CLIENT_DNS + bin/pure-document-peer call 127.0.0.1 9443 SERVER_DNS share/editor.pure \\ + CA.pem CLIENT.pem CLIENT.key show note + +If manifest.json lists pure-document-server instead, run: + + bin/pure-document-server serve 127.0.0.1 9443 share/editor.pure \\ + /tmp/pure-preview-store CA.pem SERVER.pem SERVER.key CLIENT_DNS + +That selection omits the native `call` command; connect with an authenticated +external client using the pure-doc/1 binary protocol. + +When selected, the HTTP host is a plaintext compatibility adapter; bind it only to loopback. +This preview was checked on Linux AArch64. It is not a static or macOS package. +""" +RUN_FIXED = """# Pure fixed document runner — Linux AArch64 + +This artifact runs one build-time prepared editor.pure definition. It has no +source parser, compiler, GTK or TLS host. Editing a .pure file does not +change this binary; rebuild it with `make fixed-editor` in the source tree. + + bin/pure-edit-fixed --store STORE edit KEY 0 0 0 INSERT_FILE + bin/pure-edit-fixed --store STORE show KEY + +When manifest.json lists pure-connections-fixed, it serves the same definition +and store through a loopback HTTP compatibility boundary: + + bin/pure-connections-fixed --store STORE --port 8767 + +When manifest.json lists pure-edit-tui-fixed, it opens a native terminal +editor for the same stored document. Ctrl+S saves, Ctrl+O enters another key, +Ctrl+L browses stored keys (type a prefix to jump), Ctrl+R reloads, and +Ctrl+Q quits: + + bin/pure-edit-tui-fixed --store STORE note + +The external storage files use the default PUREB001 format. The ELF binary +needs the host's libc and is not a static or macOS package. +""" + + +def dependencies(path): + header = subprocess.check_output(['readelf', '-h', str(path)], text=True) + if not re.search(r'Machine:\s+AArch64\s*$', header, re.MULTILINE): + raise ValueError(f'{path} is not an AArch64 ELF') + output = subprocess.check_output(['readelf', '-d', str(path)], text=True) + return sorted(NEEDED.findall(output)) + + +def reviewed_budget(build, name, selected=()): + command = ['python3', str(HERE / 'check_profile.py'), '--build', str(build), + '--budget', str(HERE / name)] + for binary in selected: + command.extend(['--only', binary]) + result = subprocess.run(command, capture_output=True, text=True) + if result.returncode: + raise ValueError(f'{name} failed: {result.stdout or result.stderr}') + checked = json.loads(result.stdout) + return {'profile': checked['profile'], 'budget_sha256': checked['budget_sha256'], + 'checked_binaries': sorted(checked['binaries'])} + + +def reviewed_memory(build, fixed=False): + result = subprocess.run(['python3', str(HERE / 'check_micro_memory.py'), + '--build', str(build), *(['--fixed'] if fixed else [])], + capture_output=True, text=True) + if result.returncode: + raise ValueError(f'memory-budget.json failed: {result.stdout or result.stderr}') + checked = json.loads(result.stdout) + return {'profile': checked['profile'], 'budget_sha256': checked['budget_sha256']} + + +def reviewed_tui_memory(build, source=False): + result = subprocess.run(['python3', str(HERE / 'check_tui.py'), '--build', str(build), + *(['--source'] if source else [])], + capture_output=True, text=True) + if result.returncode: + raise ValueError(f'terminal editor check failed: {result.stdout or result.stderr}') + checked = json.loads(result.stdout) + return {'profile': checked['profile'], + 'one_mib_peak_rss_limit_kib': checked['one_mib_peak_rss_limit_kib'], + 'binary_sha256': checked['binary_sha256']} + + +def add_bytes(archive, name, content, mode): + info = tarfile.TarInfo(ROOT_NAME + '/' + name) + info.size = len(content) + info.mode = mode + info.mtime = 0 + info.uid = info.gid = 0 + info.uname = info.gname = '' + archive.addfile(info, io.BytesIO(content)) + + +def without_debug(source, compact=False): + # The release build retains symbols for local debugging; the runnable + # fixed package needs neither DWARF nor unused symbols. Never mutate the + # build used by size checks or source/authoring package symbols. + with tempfile.TemporaryDirectory(prefix='pure-package-strip-') as directory: + destination = Path(directory) / source.name + destination.write_bytes(source.read_bytes()) + subprocess.run(['strip', '--strip-unneeded' if compact else '--strip-debug', + str(destination)], check=True) + if dependencies(destination) != dependencies(source): + raise ValueError(f'{source}: stripping changed direct dependencies') + return destination.read_bytes() + + +def verify_storage(binary, mode): + with tempfile.TemporaryDirectory(prefix='pure-package-store-probe-') as directory: + root = Path(directory) + payload = root / 'insert' + payload.write_bytes(b'X') + result = subprocess.run([str(binary), '--program', str(HERE / 'examples/editor.pure'), + '--store', str(root / 'store'), 'edit', 'probe', '0', '0', '0', + str(payload)], capture_output=True, timeout=15) + expected = b'PUREB001' if mode == 'hybrid' else b'PUREP001' + if result.returncode or (root / 'store/obj-probe').read_bytes()[:8] != expected: + raise ValueError(f'{binary} is not a working {mode} CLI: {result.stderr[-300:]}') + if mode == 'hybrid': + payload.write_bytes(b'X' * (16 * 1024 + 1)) + result = subprocess.run([str(binary), '--program', str(HERE / 'examples/editor.pure'), + '--store', str(root / 'store'), 'edit', 'large', '0', '0', '0', + str(payload)], capture_output=True, timeout=15) + if result.returncode or (root / 'store/obj-large').read_bytes()[:8] != b'PUREP001': + raise ValueError(f'{binary} does not select PUREP001 for large objects') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--build', type=Path, default=HERE / 'build') + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--with-http', action='store_true') + parser.add_argument('--with-tui', action='store_true') + peer = parser.add_mutually_exclusive_group() + peer.add_argument('--with-peer', action='store_true', + help='include the combined native TLS client/server') + peer.add_argument('--with-peer-server', action='store_true', + help='include only the authenticated TLS server') + parser.add_argument('--with-window', action='store_true') + parser.add_argument('--fixed', action='store_true', + help='package only the parser-free build-time prepared editor') + storage = parser.add_mutually_exclusive_group() + storage.add_argument('--paged-storage', action='store_true', + help='package the historical PUREP001-only storage backend') + storage.add_argument('--hybrid-storage', action='store_true', + help='package the selected PUREB001/PUREP001 storage experiment') + args = parser.parse_args() + if platform.system() != 'Linux' or platform.machine() != 'aarch64': + parser.error('this reviewed package profile is Linux AArch64 only') + if args.output.exists(): + parser.error('output already exists') + if args.fixed and (args.with_peer or args.with_peer_server or args.with_window or + args.paged_storage or args.hybrid_storage): + parser.error('--fixed selects only the flat-store CLI and optional HTTP/TUI hosts') + if args.with_tui and args.paged_storage: + parser.error('--with-tui supports the default or selected hybrid store') + if (args.with_peer or args.with_peer_server) and (args.paged_storage or args.hybrid_storage): + parser.error('the document TLS peer currently uses the default flat store') + if args.fixed: + built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}', + 'FIXED_PROGRAM=examples/editor.pure', 'fixed-editor', + *(['fixed-server'] if args.with_http else []), + *(['fixed-tui'] if args.with_tui else [])], + capture_output=True, text=True) + if built.returncode: + parser.error(f'could not prepare the fixed editor: {built.stderr[-500:]}') + elif args.with_tui: + built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}', + *(['STORE=store-paged.c'] if args.hybrid_storage else []), + 'editor', 'source-tui'], capture_output=True, text=True) + if built.returncode: + parser.error(f'could not prepare the source editor: {built.stderr[-500:]}') + if args.with_peer_server: + built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}', + 'editor', 'document-server'], capture_output=True, text=True) + if built.returncode: + parser.error(f'could not prepare the server-only peer: {built.stderr[-500:]}') + if args.paged_storage or args.hybrid_storage: + verify_storage(args.build / 'pure-edit', 'hybrid' if args.hybrid_storage else 'paged') + helper = args.build.resolve() / 'measure-peak-rss' + built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}', + str(helper)], capture_output=True, text=True) + if built.returncode: + parser.error(f'could not prepare the RSS helper: {built.stderr[-500:]}') + binaries = ['pure-edit-fixed' if args.fixed else 'pure-edit'] + if args.with_http: + binaries.append('pure-connections-fixed' if args.fixed else 'pure-connections') + if args.with_tui: + binaries.append('pure-edit-tui-fixed' if args.fixed else 'pure-edit-tui') + if args.with_peer: + binaries.append('pure-document-peer') + if args.with_peer_server: + binaries.append('pure-document-server') + if args.with_window: + binaries.append('pure-edit-window') + entries = {} + manifest_files = {} + for binary in binaries: + source = args.build / binary + content = without_debug(source, args.fixed) + if content[:4] != b'\x7fELF': + parser.error(f'{source} is not an ELF binary') + name = 'bin/' + binary + entries[name] = (content, 0o755) + manifest_files[name] = {'sha256': hashlib.sha256(content).hexdigest(), + 'bytes': len(content), 'needed': dependencies(source)} + program = (HERE / 'examples/editor.pure').read_bytes() + if not args.fixed: + entries['share/editor.pure'] = (program, 0o644) + manifest_files['share/editor.pure'] = {'sha256': hashlib.sha256(program).hexdigest(), + 'bytes': len(program)} + entries['RUN.md'] = ((RUN_FIXED if args.fixed else RUN).encode(), 0o644) + manifest_files['RUN.md'] = {'sha256': hashlib.sha256(entries['RUN.md'][0]).hexdigest(), + 'bytes': len(entries['RUN.md'][0])} + if args.paged_storage or args.hybrid_storage: + entries['STORAGE.md'] = ((HYBRID_STORAGE if args.hybrid_storage else PAGED_STORAGE).encode(), 0o644) + manifest_files['STORAGE.md'] = {'sha256': hashlib.sha256(entries['STORAGE.md'][0]).hexdigest(), + 'bytes': len(entries['STORAGE.md'][0])} + budgets = [reviewed_budget(args.build, + 'fixed-budget.json' if args.fixed else + 'hybrid-budget.json' if args.hybrid_storage else + 'paged-budget.json' if args.paged_storage else 'profile-budget.json', + ['pure-edit-fixed'] + + (['pure-connections-fixed'] if args.with_http else []) if args.fixed else + ['pure-edit'] + (['pure-connections'] if args.with_http else []))] + if args.with_peer: + budgets.append(reviewed_budget(args.build, 'peer-budget.json')) + if args.with_peer_server: + budgets.append(reviewed_budget(args.build, 'peer-server-budget.json')) + if args.with_tui: + budgets.append(reviewed_budget(args.build, + 'tui-budget.json' if args.fixed else + 'hybrid-tui-budget.json' if args.hybrid_storage else + 'source-tui-budget.json')) + if args.with_window: + budgets.append(reviewed_budget(args.build, 'window-budget.json')) + manifest = {'format': 'pure-document-preview-1', 'platform': 'linux-aarch64', + 'selected_hosts': binaries, 'shared_libraries_bundled': False, + 'reviewed_budgets': budgets, 'reviewed_memory': reviewed_memory(args.build, args.fixed), + 'files': manifest_files} + if args.fixed: + manifest['program_mode'] = 'build-time-prepared' + manifest['source_sha256'] = hashlib.sha256(program).hexdigest() + if args.with_tui: + manifest['reviewed_tui_memory'] = reviewed_tui_memory(args.build, not args.fixed) + if args.with_window: + manifest['features'] = ['native-document-key-switch'] + if args.paged_storage: + manifest['storage_profile'] = 'PUREP001-16k-page-research' + if args.hybrid_storage: + manifest['storage_profile'] = 'PUREB001+PUREP001-hybrid-16k-research' + maximum = None + package_profile = None + if args.fixed: + budget_path = HERE / 'fixed-package-budget.json' + package_budget = json.loads(budget_path.read_text()) + package_profile = 'cli' + ('_http' if args.with_http else '') + ('_tui' if args.with_tui else '') + maximum = package_budget['max_archive_bytes'][package_profile] + manifest['reviewed_package_budget'] = { + 'profile': package_budget['profile'], 'selection': package_profile, + 'max_archive_bytes': maximum, + 'budget_sha256': hashlib.sha256(budget_path.read_bytes()).hexdigest()} + elif (args.with_peer or args.with_peer_server) and not ( + args.with_http or args.with_tui or args.with_window or + args.paged_storage or args.hybrid_storage): + budget_path = HERE / 'peer-package-budget.json' + package_budget = json.loads(budget_path.read_text()) + package_profile = 'server_only' if args.with_peer_server else 'combined' + maximum = package_budget['max_archive_bytes'][package_profile] + manifest['reviewed_package_budget'] = { + 'profile': package_budget['profile'], 'selection': package_profile, + 'max_archive_bytes': maximum, + 'budget_sha256': hashlib.sha256(budget_path.read_bytes()).hexdigest()} + entries['manifest.json'] = ((json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode(), 0o644) + stream = io.BytesIO() + with gzip.GzipFile(filename='', mode='wb', fileobj=stream, mtime=0) as compressed: + with tarfile.open(fileobj=compressed, mode='w') as archive: + for name in sorted(entries): + content, mode = entries[name] + add_bytes(archive, name, content, mode) + packed = stream.getvalue() + if maximum is not None and len(packed) > maximum: + parser.error(f'{package_profile} archive grew to {len(packed)} bytes ' + f'(reviewed maximum {maximum}); no package was published') + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open('xb') as output: + output.write(packed) + print(json.dumps({'archive': str(args.output.resolve()), + 'sha256': hashlib.sha256(packed).hexdigest(), + 'bytes': len(packed), + 'selected_hosts': binaries}, sort_keys=True)) + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/package_source.py b/prototypes/connections/package_source.py new file mode 100644 index 0000000..4a2487c --- /dev/null +++ b/prototypes/connections/package_source.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Make a deterministic, buildable source snapshot without result/build trees.""" +import argparse +import gzip +import hashlib +import io +import json +from pathlib import Path +import tarfile + +ROOT = Path(__file__).resolve().parents[2] +PREFIX = 'pure-source-preview' +SOURCE_ROOTS = ('prototypes/connections', 'prototypes/workbench', 'prototypes/common') +SKIP_DIRS = {'build', 'results', '__pycache__', 'node_modules', '.git', '.playwright-cli'} +SUFFIXES = {'.c', '.h', '.py', '.mjs', '.js', '.zig', '.json', '.pure', + '.html', '.css', '.svg', '.wasm', '.md', '.txt'} + + +def files(): + selected = {} + for relative_root in SOURCE_ROOTS: + for path in (ROOT / relative_root).rglob('*'): + relative = path.relative_to(ROOT) + if any(part in SKIP_DIRS for part in relative.parts) or path.is_symlink() or not path.is_file(): + continue + if path.name != 'Makefile' and path.suffix not in SUFFIXES: + continue + selected[relative.as_posix()] = path.read_bytes() + return selected + + +def add(archive, name, content): + info = tarfile.TarInfo(PREFIX + '/' + name) + info.size = len(content) + info.mode = 0o644 + info.mtime = info.uid = info.gid = 0 + info.uname = info.gname = '' + archive.addfile(info, io.BytesIO(content)) + + +def make_archive(output): + selected = files() + selected['RUN.md'] = selected.pop('prototypes/connections/SOURCE_PREVIEW.md') + manifest = {'format': 'pure-source-preview-1', 'files': { + name: {'bytes': len(content), 'sha256': hashlib.sha256(content).hexdigest()} + for name, content in sorted(selected.items())}} + selected['manifest.json'] = (json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode() + output.parent.mkdir(parents=True, exist_ok=True) + with output.open('xb') as stream: + with gzip.GzipFile(fileobj=stream, mode='wb', filename='', mtime=0, compresslevel=9) as compressed: + with tarfile.open(fileobj=compressed, mode='w', format=tarfile.USTAR_FORMAT) as archive: + for name, content in sorted(selected.items()): + add(archive, name, content) + return {'archive': str(output.resolve()), 'bytes': output.stat().st_size, + 'sha256': hashlib.sha256(output.read_bytes()).hexdigest(), + 'source_files': len(manifest['files'])} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + print(json.dumps(make_archive(args.output), sort_keys=True)) + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/paged-budget.json b/prototypes/connections/paged-budget.json new file mode 100644 index 0000000..1d578cd --- /dev/null +++ b/prototypes/connections/paged-budget.json @@ -0,0 +1,18 @@ +{ + "profile": "linux-aarch64-gcc14-selected-paged-document-storage", + "binaries": { + "pure-edit": { + "max_text_bytes": 40960, + "max_data_bytes": 1200, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + }, + "pure-connections": { + "max_text_bytes": 49152, + "max_data_bytes": 1300, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Selected 16 KiB-page document storage backend on Linux AArch64/GCC 14. Same linked-code and direct-dependency ceilings as the flat backend; excludes libc, process RAM, filesystem cache and journal, and optional TLS/GTK. Any failure requires review, not automatic relaxation." +} diff --git a/prototypes/connections/peer-budget.json b/prototypes/connections/peer-budget.json new file mode 100644 index 0000000..b0f7739 --- /dev/null +++ b/prototypes/connections/peer-budget.json @@ -0,0 +1,12 @@ +{ + "profile": "linux-aarch64-gcc14-selected-document-peer", + "binaries": { + "pure-document-peer": { + "max_text_bytes": 53248, + "max_data_bytes": 1600, + "max_bss_bytes": 64, + "needed": ["libssl.so.3", "libcrypto.so.3", "libc.so.6"] + } + }, + "scope": "Review threshold for the explicitly selected authenticated document host on Linux AArch64/GCC 14. Includes linked application code but excludes the shared OpenSSL/libc implementations, process heap, stack, mappings and kernel buffers. Check measured whole-process RSS separately." +} diff --git a/prototypes/connections/peer-package-budget.json b/prototypes/connections/peer-package-budget.json new file mode 100644 index 0000000..3403302 --- /dev/null +++ b/prototypes/connections/peer-package-budget.json @@ -0,0 +1,8 @@ +{ + "profile": "linux-aarch64-gcc14-cli-with-selected-document-peer", + "max_archive_bytes": { + "server_only": 51200, + "combined": 53248 + }, + "scope": "Compressed CLI plus one TLS peer, source definition and manifest on Linux AArch64. These are reviewed archive ceilings, not process memory or bundled OpenSSL size. Additional HTTP, TUI, GUI or research storage selections have separate costs." +} diff --git a/prototypes/connections/peer-server-budget.json b/prototypes/connections/peer-server-budget.json new file mode 100644 index 0000000..cbb3cc9 --- /dev/null +++ b/prototypes/connections/peer-server-budget.json @@ -0,0 +1,12 @@ +{ + "profile": "linux-aarch64-gcc14-selected-document-server", + "binaries": { + "pure-document-server": { + "max_text_bytes": 45056, + "max_data_bytes": 1408, + "max_bss_bytes": 64, + "needed": ["libssl.so.3", "libcrypto.so.3", "libc.so.6"] + } + }, + "scope": "Reviewed server-only TLS peer ceiling on Linux AArch64/GCC 14. Excludes linked native client code and shared OpenSSL/libc implementations; process RSS is measured separately." +} diff --git a/prototypes/connections/profile-budget.json b/prototypes/connections/profile-budget.json new file mode 100644 index 0000000..602f837 --- /dev/null +++ b/prototypes/connections/profile-budget.json @@ -0,0 +1,18 @@ +{ + "profile": "linux-aarch64-gcc14-default-document-hosts", + "binaries": { + "pure-edit": { + "max_text_bytes": 40960, + "max_data_bytes": 1200, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + }, + "pure-connections": { + "max_text_bytes": 49152, + "max_data_bytes": 1300, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation." +} diff --git a/prototypes/connections/program.c b/prototypes/connections/program.c new file mode 100644 index 0000000..780d11a --- /dev/null +++ b/prototypes/connections/program.c @@ -0,0 +1,374 @@ +#include "program.h" +#include "store.h" +#include +#include +#include + +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 (splice && p->token == ',') { + if (!next(p)) return false; + if (p->token != NUMBER || p->number > PURE_OBJECT_LIMIT) + return reject(p, "splice limit requires a constant within the object bound"); + p->program.splice_limit = (uint32_t)p->number; + if (!next(p)) return false; + } + } + 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, + .program.splice_limit = PURE_OBJECT_LIMIT}; + 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/program.h b/prototypes/connections/program.h new file mode 100644 index 0000000..078a382 --- /dev/null +++ b/prototypes/connections/program.h @@ -0,0 +1,50 @@ +#ifndef PURE_PROGRAM_H +#define PURE_PROGRAM_H +#include "flow.h" +#include + +#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; + uint32_t splice_limit; /* Final stored bytes; checked under the writer lock. */ + PureIndex splice_offset, splice_remove; +} PureProgram; + +#if SIZE_MAX == UINT64_MAX +_Static_assert(sizeof(PureProgram) == 680, "prepared document definition grew"); +#endif + +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/result.h b/prototypes/connections/result.h new file mode 100644 index 0000000..8abb0f2 --- /dev/null +++ b/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-byte-storage/README.md b/prototypes/connections/results/2026-09-23-byte-storage/README.md new file mode 100644 index 0000000..f28229b --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/README.md @@ -0,0 +1,31 @@ +# 019 검증 자료 + +> 작성 기능 도입 후 검증은 [020](../2026-09-23-prepared-connections/README.md)에 있다. +> 이 디렉터리의 소스·결과는 도입 전 기록으로 보존한다. + +SQL 의존성을 제거한 C17/POSIX 저장 경로와 실제 루프백 TCP·기존 Workbench CLI의 +기능·수명 검사다. 시간·처리량·전체 RSS 비교는 수행하지 않았다. + +- [native.json](native.json): 일반 빌드의 16개 통합 시나리오, 종료 시 자원 수, 환경·소스·실행 파일 해시. +- [sanitized.json](sanitized.json): ASan·UBSan 빌드의 같은 시나리오. 누수 검사를 끄지 않았다. +- [verification.json](verification.json): 명령, 단위 검사 결과, 링크 의존성, 소스 사본·실행 파일 일치 확인. +- `sources/`: 결과에 기록된 소스·Wasm 사본. 이후 작업 트리가 바뀌면 이 파일들을 별도 작업 트리에 사용한다. + +단위 검사는 필요한 범위만 읽기, 갱신 중 이전 참조, 공개 전후 오류, 공개 전 자식 프로세스 +종료 등을 포함한다. 처음 검사한 범위 fixture는 4 KiB를 가정했으나 현재 환경은 16 KiB +페이지였다. 최종 검사는 실제 OS 페이지 크기를 사용해 페이지 경계를 넘고, 서로 다른 +페이지의 잘못된 참조도 구분할 수 있는 데이터로 수정했다. 초기 실행 결과는 `initial/`에 +보존했다. 그때와 최종 소스의 유일한 차이는 `initial/test-flow.c`이며, 기록의 해시와 맞는다. +성능 비교 자료로 사용하지 않는다. + +전원 장애나 실제 디스크 고장을 재현한 것은 아니다. fsync 전후의 실패 주입과 프로세스 +종료를 검사했다. 본문 예산은 논리적인 보유 바이트 수이며 매핑·페이지 캐시·디스크·소켓· +클라이언트 메모리를 합친 상한이 아니다. 전송 시점에 따라 수신 접두부 복사량과 쓰기 경쟁의 +승자가 바뀔 수 있으므로 빌드 간 진단 수치의 차이를 최적화 결과로 해석하지 않는다. + +재현은 [구현 README](../../README.md)의 `make check` 명령을 따른다. +`EVIDENCE=/새/경로.json`을 추가하면 새 파일로 기록하며 기존 자료는 덮어쓰지 않는다. +이 환경에서는 소켓·자식 프로세스·LeakSanitizer의 샌드박스 제한 때문에 승인된 환경에서 +실행했다. 외부 서비스는 사용하지 않았다. macOS 실행은 아직 확인하지 않았다. + +이전 SQLite 경로의 자료는 [018](../2026-09-23-native-connections/README.md)에 별도로 있다. diff --git a/prototypes/connections/results/2026-09-23-byte-storage/initial/native.json b/prototypes/connections/results/2026-09-23-byte-storage/initial/native.json new file mode 100644 index 0000000..450a8ac --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/initial/native.json @@ -0,0 +1,116 @@ +{ + "kind": "native-byte-path-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" + ], + "server_runs": [ + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 6, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 39176 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 41, + "commits": 11, + "gets": 14, + "conflicts": 5, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115417, + "server_structure_bytes": 39176 + }, + { + "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": 39176 + }, + { + "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": 39176 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "2f8c664c61f0ea0a26574530c165a7fdff1058902b9938e73d146927b1fd44eb", + "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": "9b88dfd664bf85e3fd1e971bf5d1b5bd408f6c2a069ffd02d41c05973d8cd2dd", + "prototypes/connections/store.h": "8675d39a02558b8f877477b3715e15816d12f4b4864eda19a63635941503c774", + "prototypes/connections/server.c": "e85b6d7f91e14eed6b22a4615654c33e9cc3163020ffa80fbd201f8f055eb3c2", + "prototypes/connections/check.py": "30363c718294540fbc653223fdf2a07dbcafbfa8bf1470aa5704a33ce0ea2b01", + "prototypes/connections/test-flow.c": "e209f1d76e076d5f1a680908bbfbfb0b07efc79ec99b308e544fe3902ef26d05", + "prototypes/connections/Makefile": "82995e6c3eda516c2fae02d680dff90371fb595edc2c5c9d4b3e5fd05137d36b", + "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-byte-storage/initial/sanitized.json b/prototypes/connections/results/2026-09-23-byte-storage/initial/sanitized.json new file mode 100644 index 0000000..b1d57b3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/initial/sanitized.json @@ -0,0 +1,116 @@ +{ + "kind": "native-byte-path-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" + ], + "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": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 39176 + }, + { + "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": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 39176 + }, + { + "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": 39176 + }, + { + "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": 39176 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "bd016cc93b1bd2a9c01d7f84bcc212e049b28e41528d52a90bdb70fb13100cb1", + "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": "9b88dfd664bf85e3fd1e971bf5d1b5bd408f6c2a069ffd02d41c05973d8cd2dd", + "prototypes/connections/store.h": "8675d39a02558b8f877477b3715e15816d12f4b4864eda19a63635941503c774", + "prototypes/connections/server.c": "e85b6d7f91e14eed6b22a4615654c33e9cc3163020ffa80fbd201f8f055eb3c2", + "prototypes/connections/check.py": "30363c718294540fbc653223fdf2a07dbcafbfa8bf1470aa5704a33ce0ea2b01", + "prototypes/connections/test-flow.c": "e209f1d76e076d5f1a680908bbfbfb0b07efc79ec99b308e544fe3902ef26d05", + "prototypes/connections/Makefile": "82995e6c3eda516c2fae02d680dff90371fb595edc2c5c9d4b3e5fd05137d36b", + "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-byte-storage/initial/test-flow.c b/prototypes/connections/results/2026-09-23-byte-storage/initial/test-flow.c new file mode 100644 index 0000000..23798b4 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/initial/test-flow.c @@ -0,0 +1,178 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#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. */ + unsigned char bytes[12288]; + for (size_t i = 0; i < sizeof(bytes); ++i) bytes[i] = (unsigned char)i; + CHECK(pure_store_put(path, "range", 0, (PureSpan){bytes, sizeof(bytes)}, &revision) == PURE_OK); + CHECK(pure_store_get_range(path, "range", 4079, 4201, 4201, &first) == PURE_OK); + CHECK(first.body.size == 4201 && !memcmp(first.body.data, bytes + 4079, 4201)); + pure_store_release(&first); + 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-byte-storage/native.json b/prototypes/connections/results/2026-09-23-byte-storage/native.json new file mode 100644 index 0000000..52bea4c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/native.json @@ -0,0 +1,116 @@ +{ + "kind": "native-byte-path-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" + ], + "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": 39176 + }, + { + "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": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 39176 + }, + { + "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": 39176 + }, + { + "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": 39176 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "2f8c664c61f0ea0a26574530c165a7fdff1058902b9938e73d146927b1fd44eb", + "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": "9b88dfd664bf85e3fd1e971bf5d1b5bd408f6c2a069ffd02d41c05973d8cd2dd", + "prototypes/connections/store.h": "8675d39a02558b8f877477b3715e15816d12f4b4864eda19a63635941503c774", + "prototypes/connections/server.c": "e85b6d7f91e14eed6b22a4615654c33e9cc3163020ffa80fbd201f8f055eb3c2", + "prototypes/connections/check.py": "30363c718294540fbc653223fdf2a07dbcafbfa8bf1470aa5704a33ce0ea2b01", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/Makefile": "82995e6c3eda516c2fae02d680dff90371fb595edc2c5c9d4b3e5fd05137d36b", + "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-byte-storage/sanitized.json b/prototypes/connections/results/2026-09-23-byte-storage/sanitized.json new file mode 100644 index 0000000..e2db165 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sanitized.json @@ -0,0 +1,116 @@ +{ + "kind": "native-byte-path-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" + ], + "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": 39176 + }, + { + "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": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 39176 + }, + { + "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": 39176 + }, + { + "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": 39176 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "bd016cc93b1bd2a9c01d7f84bcc212e049b28e41528d52a90bdb70fb13100cb1", + "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": "9b88dfd664bf85e3fd1e971bf5d1b5bd408f6c2a069ffd02d41c05973d8cd2dd", + "prototypes/connections/store.h": "8675d39a02558b8f877477b3715e15816d12f4b4864eda19a63635941503c774", + "prototypes/connections/server.c": "e85b6d7f91e14eed6b22a4615654c33e9cc3163020ffa80fbd201f8f055eb3c2", + "prototypes/connections/check.py": "30363c718294540fbc653223fdf2a07dbcafbfa8bf1470aa5704a33ce0ea2b01", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/Makefile": "82995e6c3eda516c2fae02d680dff90371fb595edc2c5c9d4b3e5fd05137d36b", + "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-byte-storage/sources/prototypes/connections/Makefile b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/Makefile new file mode 100644 index 0000000..5f5005b --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/Makefile @@ -0,0 +1,20 @@ +CC ?= cc +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +BUILD ?= build + +.PHONY: all check +all: $(BUILD)/pure-connections + +$(BUILD): + mkdir -p $@ + +$(BUILD)/pure-connections: server.c store.c flow.c store.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) server.c store.c flow.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-flow: test-flow.c store.c flow.c store.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_STORE_TEST test-flow.c store.c flow.c $(LDFLAGS) $(LDLIBS) -o $@ + +check: $(BUILD)/test-flow $(BUILD)/pure-connections + $(BUILD)/test-flow + python3 check.py --server $(BUILD)/pure-connections $(if $(EVIDENCE),--output "$(EVIDENCE)") diff --git a/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/check.py b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/check.py new file mode 100644 index 0000000..1d7b403 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/check.py @@ -0,0 +1,317 @@ +#!/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 tempfile +import time + +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): + self.process = subprocess.Popen([str(binary), '--store', str(storage), '--port', '0', + '--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): + 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('= 1 and limited.stats['expired'] >= 9 + records.append(limited.stats) + return {'kind': 'native-byte-path-correctness-not-performance', 'checks': checks, + 'server_runs': records, 'storage': server.ready['storage']} + + +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) + 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)) + 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', 'server.c', 'check.py', 'test-flow.c', 'Makefile')), + *(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-byte-storage/sources/prototypes/connections/flow.c b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/flow.c new file mode 100644 index 0000000..1070c70 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/flow.c @@ -0,0 +1,41 @@ +#include "flow.h" +#include + +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-byte-storage/sources/prototypes/connections/flow.h b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include +#include + +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-byte-storage/sources/prototypes/connections/server.c b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/server.c new file mode 100644 index 0000000..cea5af8 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/server.c @@ -0,0 +1,347 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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, charged; + unsigned char *body; + bool put; + int64_t expected; + PureStoredView stored; + PureTransfer transfer; + Server *server; +} Connection; +struct Server { + const char *path; + size_t budget, retained, peak_retained, active, peak_active; + uint64_t accepted, commits, gets, conflicts, budget_rejections, expired; + uint64_t releases, prefix_copy_bytes, borrowed_output_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; + bool owned = c->body || c->stored.mapped_size || c->charged; + pure_store_release(&c->stored); + free(c->body); + c->body = NULL; + assert(c->charged <= c->server->retained); + c->server->retained -= c->charged; + c->charged = 0; + if (owned) ++c->server->releases; +} + +static void close_connection(Connection *c) { + if (c->fd < 0) return; + if (c->transfer.active) pure_transfer_abort(&c->transfer); + else if (c->body || c->stored.mapped_size || c->charged) release_payload(c); + close(c->fd); + --c->server->active; + *c = (Connection){.fd = -1}; +} + +static bool charge(Connection *c, size_t size) { + Server *s = c->server; + if (size > s->budget - s->retained) { ++s->budget_rejections; return false; } + s->retained += size; + c->charged += size; + if (s->retained > s->peak_retained) s->peak_retained = s->retained; + return true; +} + +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. */ + if (c->body || c->stored.mapped_size || c->charged) release_payload(c); + respond(c, status, reason, error, 0, (PureSpan){0}); +} + +static void run_request(Connection *c) { + PureResult result; + int64_t revision = 0; + if (c->put) { + result = pure_store_put(c->server->path, c->key, c->expected, + (PureSpan){c->body, c->body_size}, &revision); + /* The synchronous write no longer accesses this input on any result. */ + release_payload(c); + if (result == PURE_OK) ++c->server->commits; + } else { + result = pure_store_get(c->server->path, c->key, + c->server->budget - c->server->retained, &c->stored); + if (result == PURE_OK) { + if (!charge(c, c->stored.body.size)) abort(); /* Same owner thread. */ + revision = c->stored.revision; + ++c->server->gets; + c->server->borrowed_output_bytes += c->stored.body.size; + } else if (result == PURE_BUDGET) ++c->server->budget_rejections; + } + switch (result) { + case PURE_OK: respond(c, 200, "OK", "none", revision, c->stored.body); break; + case PURE_NOT_FOUND: fail(c, 404, "Not Found", "not_found"); break; + case PURE_CONFLICT: + ++c->server->conflicts; + respond(c, 412, "Precondition Failed", "conflict", 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_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; + } +} + +/* 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; } + if (!c->put) { run_request(c); return; } + if (!charge(c, c->body_size)) { fail(c, 503, "Service Unavailable", "payload_budget"); return; } + c->body = malloc(c->body_size ? c->body_size : 1); + if (!c->body) { fail(c, 503, "Service Unavailable", "allocation"); return; } + 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; + for (int i = 1; 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], "--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 (!server.path) goto usage; + if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize byte store directory\n", stderr); 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); + 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.retained, server.peak_retained, server.budget, server.peak_active, + server.accepted, server.commits, server.gets, server.conflicts, server.budget_rejections, + server.expired, server.releases, server.prefix_copy_bytes, server.borrowed_output_bytes, sizeof(server)); + return 0; +usage: + fputs("pure-connections --store DIRECTORY [--port 8767] [--budget BYTES] [--timeout-ms 10000] [--send-buffer BYTES]\n", stderr); + return 2; +} diff --git a/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.c b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.c new file mode 100644 index 0000000..d0111d0 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.c @@ -0,0 +1,225 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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}; +} + +static PureResult view_range(const char *path, const char *key, size_t offset, + size_t length, bool whole, 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 (whole) { offset = 0; length = size; } + 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 view_range(path, key, 0, 0, true, 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 view_range(path, key, offset, length, false, available, out); +} + +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, 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; + 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; + } + fd = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + size_t size; + if (!read_header(fd, revision, &size)) goto finish; + close(fd); + fd = -1; + } else if (errno != ENOENT) goto finish; + if (*revision != expected) { result = PURE_CONFLICT; goto finish; } + if (*revision == INT64_MAX) { result = PURE_LIMIT; goto finish; } + 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, body.size); + if (!write_bytes(fd, header, sizeof(header)) || fault(PURE_AFTER_HEADER) || + !write_bytes(fd, body.data, body.size) || 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 (*temporary) unlinkat(directory, temporary, 0); + if (lock >= 0) close(lock); + close(directory); + return result; +} diff --git a/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.h b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.h new file mode 100644 index 0000000..d9bcc1c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/store.h @@ -0,0 +1,34 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include + +#define PURE_OBJECT_LIMIT ((2u * 6u + 32u) * 32768u + 8192u) +typedef enum { + PURE_OK, PURE_NOT_FOUND, PURE_CONFLICT, PURE_BUSY, PURE_BUDGET, + PURE_LIMIT, PURE_STORE_ERROR, PURE_UNCERTAIN +} PureResult; +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); +void pure_store_release(PureStoredView *); +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, 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-byte-storage/sources/prototypes/connections/test-flow.c b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/test-flow.c new file mode 100644 index 0000000..5ff028d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/connections/test-flow.c @@ -0,0 +1,183 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#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-byte-storage/sources/prototypes/workbench/cli.mjs b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/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-byte-storage/sources/prototypes/workbench/examples/direct.pure b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/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-byte-storage/sources/prototypes/workbench/remote.mjs b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/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-byte-storage/sources/prototypes/workbench/web/compiler.mjs b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/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-byte-storage/sources/prototypes/workbench/web/core.wasm b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/core.wasm new file mode 100644 index 0000000..79c50a8 Binary files /dev/null and b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/core.wasm differ diff --git a/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/project.mjs b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/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-byte-storage/sources/prototypes/workbench/web/runtime.mjs b/prototypes/connections/results/2026-09-23-byte-storage/sources/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/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-byte-storage/verification.json b/prototypes/connections/results/2026-09-23-byte-storage/verification.json new file mode 100644 index 0000000..418c75a --- /dev/null +++ b/prototypes/connections/results/2026-09-23-byte-storage/verification.json @@ -0,0 +1,52 @@ +{ + "date": "2026-09-23", + "scope": "SQL-free storage correctness, lifetimes and admission bounds; not a performance comparison", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "os_page_size_bytes": 16384, + "cwd": "prototypes/connections", + "native_command": "make check EVIDENCE=results/2026-09-23-byte-storage/native.json", + "sanitized_command": "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-byte-storage/sanitized.json", + "core_checks": { + "native": "passed", + "asan_ubsan": "passed", + "leak_detection_disabled": false, + "coverage": [ + "partial transfer and single release", + "range access including actual page boundaries and nonzero mapping offsets", + "immutable read across replacement", + "budget, conflicts, empty data, revision exhaustion", + "invalid file rejection", + "failure before publication preserves prior version", + "failure after publication reports uncertain durability", + "child exit before publication preserves prior version and releases writer lock" + ], + "native_binary_sha256": "a3189c0db63960bdf7b464d3813403065d7222366c00895e8eaad66450fd2f27", + "sanitized_binary_sha256": "acf03ba3881be9d534fc2419abed964f27d4dbd23173a34ab3efdeb831b7547f" + }, + "integration_scenarios_each": 16, + "source_hashes_equal": true, + "source_snapshots": "sources/", + "source_snapshots_match_records": true, + "final_binaries_match_records": true, + "native_dynamic_dependencies_ldd": [ + "\tlinux-vdso.so.1 (0x00007ffed5098000)", + "\tlibc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x00007ffed4e50000)", + "\t/lib/ld-linux-aarch64.so.1 (0x00007ffed5060000)" + ], + "initial_run": { + "directory": "initial/", + "difference": "Only test-flow.c changed: page-boundary fixture now uses actual OS page size and distinct per-page contents", + "original_test_sha256_verified": true + }, + "limitations": [ + "Linux AArch64 only; macOS not executed", + "No actual power-loss or disk-fault testing", + "Logical payload budget excludes page rounding, page cache, disk retention, OS sockets and client allocations", + "Unpublished files left by crashes are ignored but not automatically collected", + "Synchronous filesystem operations can delay other connections and timeout enforcement", + "Single-threaded control API with one cooperating process writer lock per directory", + "Whole-object replacement, no multi-object transaction or payload checksum", + "Stored files must not be mutated in place by external writers", + "Pure-authored external effects and automatic path optimization remain unimplemented" + ] +} 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('= 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('= 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(' + +/* 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 +#include +#include + +/* 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 + +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(' 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 + +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 +#include + +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 +#include +#include + +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 + +#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 + +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 +#include + +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 +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 + +#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 +#include +#include +#include +#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 +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#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 +#include +#include +#include +#include + +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 +#include +#include +#include +#include +#include + +#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 new file mode 100644 index 0000000..79c50a8 Binary files /dev/null and b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/core.wasm differ 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" + ] +} diff --git a/prototypes/connections/results/2026-09-23-execution-core/README.md b/prototypes/connections/results/2026-09-23-execution-core/README.md new file mode 100644 index 0000000..fd92adb --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/README.md @@ -0,0 +1,28 @@ +# 실행 코어 검증 기록 + +2026-09-23, Linux AArch64, GCC 14.2.0. 첫 공개 체크포인트 진행 중의 증거다. + +- `native.json`: 일반 빌드의 실제 TCP 28개 시나리오, 실행 코어의 수명·예산·세대·내부 전달·실제 저장 검사. +- `sanitized.json`: 같은 경로를 ASan·UBSan 및 기본 누수 검사로 통과한 결과. +- `verification.json`: 두 결과의 소스 일치, 바이너리 해시, 종료 시 자원 회수, 코어 외부 심볼과 크기 기록. +- `sources/`: 결과에 기록된 30개 소스·클라이언트 파일의 해시 검증 후 복사본. + +당시 실행 명령은 `prototypes/connections`에서 다음과 같다. + +```sh +make all +make check EVIDENCE=results/2026-09-23-execution-core/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-execution-core/sanitized.json +size build/runtime.o build/evaluate.o build/pure-connections +nm -u build/libpure-execution.a +``` + +기존 증거 파일은 덮어쓰지 않는다. 재현할 때는 `EVIDENCE`를 생략하거나 새 경로를 사용한다. +동결된 소스는 `sources/prototypes/connections`에서 `make all`과 `make check`로 빌드·검사할 수 있다. +검사에는 C17 컴파일러·make·Python·Node와 로컬 소켓/자식 프로세스 실행이 필요하다. + +영역 192 B, 작업 슬롯 224 B, 티켓 24 B는 구조체 크기다. 코어 오브젝트의 `size text` 합 +5,672 B는 전체 RSS가 아니다. 준비 프로그램·작업 슬롯·실제 본문·호스트 자원 등은 별도이며, +서버 구조체는 기능 추가로 이전보다 1,664 B 증가했다. 처리량·지연의 우위를 측정한 자료가 아니다. +동기 효과·단일 소유 스레드·get/put 작성 프로필이라는 현재 한계는 +[022 보고서](../../../../docs/research/022-execution-core.md)에 기록했다. diff --git a/prototypes/connections/results/2026-09-23-execution-core/native.json b/prototypes/connections/results/2026-09-23-execution-core/native.json new file mode 100644 index 0000000..173e833 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/native.json @@ -0,0 +1,505 @@ +{ + "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" + ], + "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": 648, + "steps": [] + }, + "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": 648, + "steps": [] + } + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 8, + "length": 0, + "to_end": true + }, + "offset": { + "value": 0, + "from_u32le": true + }, + "length": { + "value": 4, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 11, + "length": 0, + "to_end": true + }, + "offset": { + "value": 7, + "from_u32le": true + }, + "length": { + "value": 3, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [] + } + }, + "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": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41488 + }, + { + "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": 11679, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 8, + "borrowed_output_bytes": 57, + "server_structure_bytes": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + } + ], + "storage": "immutable-byte-extents-v1", + "execution_core": { + "runtime_tests": "passed", + "domain_bytes": 192, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 648, + "forwarding_chain": 1024, + "native_storage_pipeline": true + }, + "execution_core_test_sha256": "f0700e597f634c2304ccb4cbc375d8b491964ed8be6e2aa573a58df9ee00c5fe", + "binary_sha256": "09ecc629b9bbddf09f5b6090c9b67eec7d2e70c6065624c60b902b5b3f8ae802", + "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": "b7e7d6806cf9785df1d01f2418d1e493eaf2d4b9cf43abc90082c60bd61dcd0b", + "prototypes/connections/store.h": "d2d7f8bd9845ce13095520f4401b9c48b1b4e8b6da10f8c4c63ab495c2950ef5", + "prototypes/connections/program.c": "b72b0aef7fc04ef044e3c0d177222aea688765526316fe2d0de988573ce8003b", + "prototypes/connections/program.h": "2c3c955967883a9a8ea4566a006eccd44f3a173ac0682c2f272d09700f209639", + "prototypes/connections/runtime.c": "d1163fdc6e5a1948ec0109fc1d04312a0b71edebf8f3762f38ac674ccd686af3", + "prototypes/connections/runtime.h": "03a69db466a217fff195183af174acba1949772c16523aab6f425e84600798e5", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/evaluate.c": "d3fbbd4ddba2b7fe29fc11ba46d3fc471945f724afff52bb5d7699f79cb80b5d", + "prototypes/connections/store-port.c": "5c7c3c1dcd2ae90a39ecc93b5bccf7f54fcafe1d0e10b2ad7b1eb0c89c1382bf", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6", + "prototypes/connections/test-runtime.c": "d1e2dc14185a8b4301cc6bfa001ee2cdfa674acad505b92c51f06311abf36be9", + "prototypes/connections/server.c": "21dc7edf1acb58f347586a92bab02f2d7edf6a7369c9b8212acab8b87f58aa74", + "prototypes/connections/check.py": "6bf6ccb8acd14a01f635733d3a9a42439a6f9dc37a9d43b5e3681301a9ab2235", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "1c4083e549bf2bb83e0127c910607e60cefd6f3af9e1fae355caadb2506ded09", + "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-execution-core/sanitized.json b/prototypes/connections/results/2026-09-23-execution-core/sanitized.json new file mode 100644 index 0000000..67c195e --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sanitized.json @@ -0,0 +1,505 @@ +{ + "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" + ], + "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": 648, + "steps": [] + }, + "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": 648, + "steps": [] + } + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 8, + "length": 0, + "to_end": true + }, + "offset": { + "value": 0, + "from_u32le": true + }, + "length": { + "value": 4, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 11, + "length": 0, + "to_end": true + }, + "offset": { + "value": 7, + "from_u32le": true + }, + "length": { + "value": 3, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [] + } + }, + "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": 41488 + }, + { + "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": 14232, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + }, + { + "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": 41488 + } + ], + "storage": "immutable-byte-extents-v1", + "execution_core": { + "runtime_tests": "passed", + "domain_bytes": 192, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 648, + "forwarding_chain": 1024, + "native_storage_pipeline": true + }, + "execution_core_test_sha256": "eb526e3dff8a87f1f53446efa25026a9720058bf414be08ce8cc57a9787c6bb8", + "binary_sha256": "49fb5aec3b1ec71cea66ab708e507583cc2403203845e8737a8c0fb2a050a899", + "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": "b7e7d6806cf9785df1d01f2418d1e493eaf2d4b9cf43abc90082c60bd61dcd0b", + "prototypes/connections/store.h": "d2d7f8bd9845ce13095520f4401b9c48b1b4e8b6da10f8c4c63ab495c2950ef5", + "prototypes/connections/program.c": "b72b0aef7fc04ef044e3c0d177222aea688765526316fe2d0de988573ce8003b", + "prototypes/connections/program.h": "2c3c955967883a9a8ea4566a006eccd44f3a173ac0682c2f272d09700f209639", + "prototypes/connections/runtime.c": "d1163fdc6e5a1948ec0109fc1d04312a0b71edebf8f3762f38ac674ccd686af3", + "prototypes/connections/runtime.h": "03a69db466a217fff195183af174acba1949772c16523aab6f425e84600798e5", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/evaluate.c": "d3fbbd4ddba2b7fe29fc11ba46d3fc471945f724afff52bb5d7699f79cb80b5d", + "prototypes/connections/store-port.c": "5c7c3c1dcd2ae90a39ecc93b5bccf7f54fcafe1d0e10b2ad7b1eb0c89c1382bf", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6", + "prototypes/connections/test-runtime.c": "d1e2dc14185a8b4301cc6bfa001ee2cdfa674acad505b92c51f06311abf36be9", + "prototypes/connections/server.c": "21dc7edf1acb58f347586a92bab02f2d7edf6a7369c9b8212acab8b87f58aa74", + "prototypes/connections/check.py": "6bf6ccb8acd14a01f635733d3a9a42439a6f9dc37a9d43b5e3681301a9ab2235", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "1c4083e549bf2bb83e0127c910607e60cefd6f3af9e1fae355caadb2506ded09", + "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-execution-core/sources/prototypes/connections/Makefile b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/Makefile new file mode 100644 index 0000000..ea15285 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/Makefile @@ -0,0 +1,43 @@ +CC ?= cc +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +BUILD ?= build + +.PHONY: all check +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: $(BUILD)/pure-connections $(BUILD)/libpure-execution.a + +$(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)/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 $@ + +check: $(BUILD)/test-flow $(BUILD)/test-program $(BUILD)/test-fields $(BUILD)/test-runtime $(BUILD)/pure-connections + $(BUILD)/test-flow + $(BUILD)/test-program + $(BUILD)/test-fields + python3 check.py --server $(BUILD)/pure-connections --runtime-test $(BUILD)/test-runtime $(if $(EVIDENCE),--output "$(EVIDENCE)") diff --git a/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/check.py b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/check.py new file mode 100644 index 0000000..a512748 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/check.py @@ -0,0 +1,536 @@ +#!/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 + +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): + 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('= 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) + return {'kind': 'prepared-connection-correctness-not-performance', 'checks': checks, + 'prepared_connections': prepared, + 'field_connections': fields, + '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(' + +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; +} + +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-execution-core/sources/prototypes/connections/examples/excerpt.pure b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/excerpt.pure new file mode 100644 index 0000000..622f9bb --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/connections/examples/select-text.py b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/select-text.py new file mode 100644 index 0000000..ffaf80b --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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(' 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-execution-core/sources/prototypes/connections/examples/selection.pure b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/selection.pure new file mode 100644 index 0000000..7d9d4ec --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/connections/examples/storage.pure b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/examples/storage.pure new file mode 100644 index 0000000..ff54ea3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/connections/flow.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/flow.c new file mode 100644 index 0000000..1070c70 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/flow.c @@ -0,0 +1,41 @@ +#include "flow.h" +#include + +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-execution-core/sources/prototypes/connections/flow.h b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include +#include + +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-execution-core/sources/prototypes/connections/program.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/program.c new file mode 100644 index 0000000..e394d11 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/program.c @@ -0,0 +1,357 @@ +#include "program.h" +#include "store.h" +#include +#include +#include + +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, '.') || !word(p, p->put ? "write" : "read") || + !token(p, '(') || !field(p, "request", "key")) return false; + if (p->put) { + Symbol value; + if (!token(p, ',') || !field(p, "request", "revision") || !token(p, ',') || + !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 store.write; 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-execution-core/sources/prototypes/connections/program.h b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/program.h new file mode 100644 index 0000000..f2b5b2f --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/program.h @@ -0,0 +1,39 @@ +#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; +} 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); +#endif diff --git a/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/result.h b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/result.h new file mode 100644 index 0000000..8abb0f2 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/connections/runtime.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/runtime.c new file mode 100644 index 0000000..3b7e930 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/runtime.c @@ -0,0 +1,265 @@ +#include "runtime.h" +#include + +#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->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; + if (!pure_program_input(r->program, w->resource.body, &value, &outcome.body)) + outcome.status = PURE_LIMIT; + 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-execution-core/sources/prototypes/connections/runtime.h b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/runtime.h new file mode 100644 index 0000000..4462e96 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/runtime.h @@ -0,0 +1,86 @@ +#ifndef PURE_RUNTIME_H +#define PURE_RUNTIME_H +#include "program.h" +#include "result.h" +#include + +/* 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); +} 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-execution-core/sources/prototypes/connections/server.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/server.c new file mode 100644 index 0000000..a65b634 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/server.c @@ -0,0 +1,368 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include "program.h" +#include "store-port.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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"); + } + puts("]}"); + 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-execution-core/sources/prototypes/connections/store-port.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store-port.c new file mode 100644 index 0000000..06ff114 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store-port.c @@ -0,0 +1,39 @@ +#include "store-port.h" +#include "store.h" +#include +#include + +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); +} + +PureHost pure_store_host(const char *path) { + return (PureHost){.context = (void *)path, .allocate = allocate, + .read = read_value, .write = write_value}; +} diff --git a/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store-port.h b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store-port.h new file mode 100644 index 0000000..4c0d49d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/connections/store.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store.c new file mode 100644 index 0000000..1c13a80 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store.c @@ -0,0 +1,228 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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); +} + +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, 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; + 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; + } + fd = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + size_t size; + if (!read_header(fd, revision, &size)) goto finish; + close(fd); + fd = -1; + } else if (errno != ENOENT) goto finish; + if (*revision != expected) { result = PURE_CONFLICT; goto finish; } + if (*revision == INT64_MAX) { result = PURE_LIMIT; goto finish; } + 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, body.size); + if (!write_bytes(fd, header, sizeof(header)) || fault(PURE_AFTER_HEADER) || + !write_bytes(fd, body.data, body.size) || 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 (*temporary) unlinkat(directory, temporary, 0); + if (lock >= 0) close(lock); + close(directory); + return result; +} diff --git a/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store.h b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store.h new file mode 100644 index 0000000..1c0e054 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/store.h @@ -0,0 +1,37 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include "result.h" +#include + +#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); + +/* 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-execution-core/sources/prototypes/connections/test-fields.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-fields.c new file mode 100644 index 0000000..63dfc3e --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-fields.c @@ -0,0 +1,153 @@ +#include "program.h" +#include +#include +#include +#include +#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-execution-core/sources/prototypes/connections/test-flow.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-flow.c new file mode 100644 index 0000000..5ff028d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-flow.c @@ -0,0 +1,183 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#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-execution-core/sources/prototypes/connections/test-program.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-program.c new file mode 100644 index 0000000..6a4a19d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-program.c @@ -0,0 +1,111 @@ +#include "program.h" +#include +#include +#include +#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-execution-core/sources/prototypes/connections/test-runtime.c b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/connections/test-runtime.c new file mode 100644 index 0000000..7ce2290 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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 +#include +#include +#include +#include + +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){host, allocate, read_value, 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-execution-core/sources/prototypes/workbench/cli.mjs b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/workbench/examples/direct.pure b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/workbench/remote.mjs b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/workbench/web/compiler.mjs b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/workbench/web/core.wasm b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/core.wasm new file mode 100755 index 0000000..79c50a8 Binary files /dev/null and b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/core.wasm differ diff --git a/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/project.mjs b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/sources/prototypes/workbench/web/runtime.mjs b/prototypes/connections/results/2026-09-23-execution-core/sources/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/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-execution-core/verification.json b/prototypes/connections/results/2026-09-23-execution-core/verification.json new file mode 100644 index 0000000..ad430f8 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-execution-core/verification.json @@ -0,0 +1,30 @@ +{ + "sources_verified_and_frozen": 30, + "native_and_sanitized_binaries_match": true, + "tcp_scenarios_per_build": 28, + "all_http_runs_drained": true, + "core_archive_external_symbols": [ + "memcpy", + "strlen" + ], + "core_archive_sha256": "59eccbea380378a6650c59a99eacd92132184e5b9aa64feada7a176cf03ccfcc", + "size_output": " text\t data\t bss\t dec\t hex\tfilename\n 4892\t 0\t 0\t 4892\t 131c\t/home/eunhhu/work/pure/prototypes/connections/build/runtime.o\n 780\t 0\t 0\t 780\t 30c\t/home/eunhhu/work/pure/prototypes/connections/build/evaluate.o\n 41144\t 1048\t 16\t 42208\t a4e0\t/home/eunhhu/work/pure/prototypes/connections/build/pure-connections\n", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "structure_sizes": { + "runtime_tests": "passed", + "domain_bytes": 192, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 648, + "forwarding_chain": 1024, + "native_storage_pipeline": true + }, + "limits": [ + "synchronous storage effects", + "one owner thread", + "storage authoring profile only", + "no TLS or native UI yet", + "sizes are not process RSS or performance comparisons" + ] +} diff --git a/prototypes/connections/results/2026-09-23-field-connections/README.md b/prototypes/connections/results/2026-09-23-field-connections/README.md new file mode 100644 index 0000000..d516556 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/README.md @@ -0,0 +1,31 @@ +# 021 검증 자료 + +이름 기반 필드와 실제 입력 값으로 계산하는 구간을 네이티브 저장·응답에 연결한 기록이다. +시간·처리량·전체 RSS 비교는 하지 않았다. + +- [native.json](native.json): 일반 빌드의 28개 통합 시나리오, 준비 결과, 자원 회수, 소스·실행 파일 해시. +- [sanitized.json](sanitized.json): ASan·UBSan 빌드의 같은 시나리오. 누수 검사를 끄지 않았다. +- [verification.json](verification.json): 명령, 세 단위 검사 결과, 소스 사본·실행 파일 일치 확인. +- `sources/`: 기록된 소스·예제·클라이언트·Wasm 사본. 이후 작업 트리가 바뀌면 별도 트리에 배치해 재현한다. + +기존 22개 검사에 6개 필드 시나리오를 추가했다. 실제 예제 클라이언트가 한글·이모지 문서의 +문자 위치를 바이트 위치로 바꿔 전송하고, 네이티브 서버가 필드 값에 따라 선택·저장·응답한다. +잘린 헤더·잘못된 범위·정렬되지 않은 숫자·바뀐 필드 순서·빈 선택·사용하지 않는 계산의 오류를 +확인한다. 별칭은 추가 동적 단계를 만들지 않으며, 느린 응답 동안 원본 수신 할당을 유지·계상한다. + +`field_connections`에는 선언에서 준비한 실제 접근 위치·데이터 의존 단계가 있다. `steps`의 +source_step 0은 입력의 정적 구간이고 양수는 앞 단계의 결과다. offset/length의 from_u32le가 +참이면 value는 입력에서 숫자를 읽을 위치이며, 거짓이면 상수다. write_step/reply_step이 양수인 +결과는 정적 offset/length 대신 해당 단계에서 정한다. 진단의 이전 whole 표기는 to_end로 바뀌었다. + +`runtime_view_steps`는 준비된 단계 수로 실행 시간 계측값이 아니다. `prepared_program_bytes` +648 B는 고정 구조체 하나의 크기이며 사용하지 않는 동적 단계 자리도 포함한다. 기존 112 B보다 +536 B 늘어난 기능 비용이다. 논리적 본문 예산에는 OS·페이지·클라이언트 메모리가 포함되지 않는다. + +`test-flow`는 기존 저장·전송 의미를, `test-program`은 상수 구간 준비 의미를, +`test-fields`는 필드 타입·배치·숫자·동적 범위·한도를 검사한다. 일반/검사 빌드 모두 통과했다. + +[구현 README](../../README.md)의 `make check`로 재현한다. `EVIDENCE=/새/경로.json`으로 +새 원자료를 남기며 기존 파일을 덮어쓰지 않는다. 이 환경에서는 소켓·자식 프로세스· +LeakSanitizer의 샌드박스 제한 때문에 승인된 환경에서 로컬 검사를 실행했다. +외부 서비스·macOS·실제 전원 장애·대규모 서비스는 검증하지 않았다. diff --git a/prototypes/connections/results/2026-09-23-field-connections/native.json b/prototypes/connections/results/2026-09-23-field-connections/native.json new file mode 100644 index 0000000..2d8ff24 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/native.json @@ -0,0 +1,488 @@ +{ + "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" + ], + "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": 648, + "steps": [] + }, + "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": 648, + "steps": [] + } + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 8, + "length": 0, + "to_end": true + }, + "offset": { + "value": 0, + "from_u32le": true + }, + "length": { + "value": 4, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 11, + "length": 0, + "to_end": true + }, + "offset": { + "value": 7, + "from_u32le": true + }, + "length": { + "value": 3, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [] + } + }, + "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": 39824 + }, + { + "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": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 18, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 57, + "server_structure_bytes": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "d478daf835c30152d05aeba6d2c07dd598166129f18cc5f7cb3b893966b60a18", + "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": "b7e7d6806cf9785df1d01f2418d1e493eaf2d4b9cf43abc90082c60bd61dcd0b", + "prototypes/connections/store.h": "03e23e349a19ffc3491d1bb9e4200b834cedf84a3f026613470a37a727ded11e", + "prototypes/connections/program.c": "54df0c5cd4579f64b2f32b965c520e2a4f36794cd3dece944d062454dc01df63", + "prototypes/connections/program.h": "2c3c955967883a9a8ea4566a006eccd44f3a173ac0682c2f272d09700f209639", + "prototypes/connections/server.c": "b905fa656fa626111ba765d6379aa03bd32fee97d19d48461cd752e75918ea31", + "prototypes/connections/check.py": "bf890ee92009fd57afcb6708ad325af360bf43a7c667c79c5dc90631889f5a3f", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "8c0353ae6643351ba69505de4092aa40e34e5318f27d670de944cd875c4d51cc", + "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-field-connections/sanitized.json b/prototypes/connections/results/2026-09-23-field-connections/sanitized.json new file mode 100644 index 0000000..1fbb30f --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sanitized.json @@ -0,0 +1,488 @@ +{ + "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" + ], + "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": 648, + "steps": [] + }, + "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": 648, + "steps": [] + } + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 8, + "length": 0, + "to_end": true + }, + "offset": { + "value": 0, + "from_u32le": true + }, + "length": { + "value": 4, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 11, + "length": 0, + "to_end": true + }, + "offset": { + "value": 7, + "from_u32le": true + }, + "length": { + "value": 3, + "from_u32le": true + } + } + ] + }, + "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": 648, + "steps": [] + } + }, + "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": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 39824 + }, + { + "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": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 18, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 57, + "server_structure_bytes": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + }, + { + "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": 39824 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "be8a599f33d340e04f9269de9cc9b66d817aea567ca9a26a6a26258c2011ed91", + "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": "b7e7d6806cf9785df1d01f2418d1e493eaf2d4b9cf43abc90082c60bd61dcd0b", + "prototypes/connections/store.h": "03e23e349a19ffc3491d1bb9e4200b834cedf84a3f026613470a37a727ded11e", + "prototypes/connections/program.c": "54df0c5cd4579f64b2f32b965c520e2a4f36794cd3dece944d062454dc01df63", + "prototypes/connections/program.h": "2c3c955967883a9a8ea4566a006eccd44f3a173ac0682c2f272d09700f209639", + "prototypes/connections/server.c": "b905fa656fa626111ba765d6379aa03bd32fee97d19d48461cd752e75918ea31", + "prototypes/connections/check.py": "bf890ee92009fd57afcb6708ad325af360bf43a7c667c79c5dc90631889f5a3f", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "8c0353ae6643351ba69505de4092aa40e34e5318f27d670de944cd875c4d51cc", + "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-field-connections/sources/prototypes/connections/Makefile b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/Makefile new file mode 100644 index 0000000..110acab --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/Makefile @@ -0,0 +1,28 @@ +CC ?= cc +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +BUILD ?= build + +.PHONY: all check +all: $(BUILD)/pure-connections + +$(BUILD): + mkdir -p $@ + +$(BUILD)/pure-connections: server.c store.c flow.c program.c store.h flow.h program.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) server.c store.c flow.c program.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-flow: test-flow.c store.c flow.c store.h flow.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 program.h store.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) test-program.c program.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-fields: test-fields.c program.c program.h store.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) test-fields.c program.c $(LDFLAGS) $(LDLIBS) -o $@ + +check: $(BUILD)/test-flow $(BUILD)/test-program $(BUILD)/test-fields $(BUILD)/pure-connections + $(BUILD)/test-flow + $(BUILD)/test-program + $(BUILD)/test-fields + python3 check.py --server $(BUILD)/pure-connections $(if $(EVIDENCE),--output "$(EVIDENCE)") diff --git a/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/check.py b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/check.py new file mode 100644 index 0000000..5c6a463 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/check.py @@ -0,0 +1,528 @@ +#!/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 + +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): + 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('= 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) + return {'kind': 'prepared-connection-correctness-not-performance', 'checks': checks, + 'prepared_connections': prepared, + 'field_connections': fields, + '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(' 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(' 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-field-connections/sources/prototypes/connections/examples/selection.pure b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/examples/selection.pure new file mode 100644 index 0000000..7d9d4ec --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/sources/prototypes/connections/examples/storage.pure b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/examples/storage.pure new file mode 100644 index 0000000..ff54ea3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/sources/prototypes/connections/flow.c b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/flow.c new file mode 100644 index 0000000..1070c70 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/flow.c @@ -0,0 +1,41 @@ +#include "flow.h" +#include + +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-field-connections/sources/prototypes/connections/flow.h b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include +#include + +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-field-connections/sources/prototypes/connections/program.c b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/program.c new file mode 100644 index 0000000..a01bb7c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/program.c @@ -0,0 +1,402 @@ +#include "program.h" +#include "store.h" +#include +#include +#include + +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, '.') || !word(p, p->put ? "write" : "read") || + !token(p, '(') || !field(p, "request", "key")) return false; + if (p->put) { + Symbol value; + if (!token(p, ',') || !field(p, "request", "revision") || !token(p, ',') || + !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 store.write; 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); +} + +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; +} + +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-field-connections/sources/prototypes/connections/program.h b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/program.h new file mode 100644 index 0000000..f2b5b2f --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/program.h @@ -0,0 +1,39 @@ +#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; +} 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); +#endif diff --git a/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/server.c b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/server.c new file mode 100644 index 0000000..e4e5b88 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/server.c @@ -0,0 +1,393 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include "program.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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, charged; + unsigned char *body; + bool put; + int64_t expected; + PureStoredView stored; + PureTransfer transfer; + Server *server; +} Connection; +struct Server { + const char *path; + PureProgram program; + size_t budget, retained, peak_retained, active, peak_active; + uint64_t accepted, commits, gets, conflicts, budget_rejections, expired; + uint64_t releases, prefix_copy_bytes, borrowed_output_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; + bool owned = c->body || c->stored.mapped_size || c->charged; + pure_store_release(&c->stored); + free(c->body); + c->body = NULL; + assert(c->charged <= c->server->retained); + c->server->retained -= c->charged; + c->charged = 0; + if (owned) ++c->server->releases; +} + +static void close_connection(Connection *c) { + if (c->fd < 0) return; + if (c->transfer.active) pure_transfer_abort(&c->transfer); + else if (c->body || c->stored.mapped_size || c->charged) release_payload(c); + close(c->fd); + --c->server->active; + *c = (Connection){.fd = -1}; +} + +static bool charge(Connection *c, size_t size) { + Server *s = c->server; + if (size > s->budget - s->retained) { ++s->budget_rejections; return false; } + s->retained += size; + c->charged += size; + if (s->retained > s->peak_retained) s->peak_retained = s->retained; + return true; +} + +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. */ + if (c->body || c->stored.mapped_size || c->charged) release_payload(c); + respond(c, status, reason, error, 0, (PureSpan){0}); +} + +static void run_request(Connection *c) { + PureResult result; + int64_t revision = 0; + PureProgram *program = &c->server->program; + PureSpan output = {0}; + if (c->put) { + PureSpan source = {c->body, c->body_size}, value; + /* Validate every authored view before performing an external effect, + * including unused views. Static preparation preserves these guards. */ + if (!pure_program_input(program, source, &value, &output)) { + fail(c, 413, "Content Too Large", "limit"); + return; + } + result = pure_store_put(c->server->path, c->key, c->expected, value, &revision); + /* A reply can consume the same input. Its owner lives until transfer + * completion; otherwise storage completion ends the input lifetime. */ + if (result != PURE_OK || !output.size) { release_payload(c); output = (PureSpan){0}; } + if (result == PURE_OK) { + ++c->server->commits; + c->server->borrowed_output_bytes += output.size; + } + } else { + result = pure_store_select(c->server->path, c->key, program->read.offset, + program->read.length, program->read.to_end, program->read_required, + c->server->budget - c->server->retained, &c->stored); + if (result == PURE_OK) { + if (!charge(c, c->stored.body.size)) abort(); /* Same owner thread. */ + revision = c->stored.revision; + output = c->stored.body; + ++c->server->gets; + c->server->borrowed_output_bytes += c->stored.body.size; + } else if (result == PURE_BUDGET) ++c->server->budget_rejections; + } + switch (result) { + case PURE_OK: respond(c, 200, "OK", "none", revision, output); break; + case PURE_NOT_FOUND: fail(c, 404, "Not Found", "not_found"); break; + case PURE_CONFLICT: + ++c->server->conflicts; + respond(c, 412, "Precondition Failed", "conflict", 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_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; + } +} + +/* 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; } + if (!c->put) { run_request(c); return; } + if (!charge(c, c->body_size)) { fail(c, 503, "Service Unavailable", "payload_budget"); return; } + c->body = malloc(c->body_size ? c->body_size : 1); + if (!c->body) { fail(c, 503, "Service Unavailable", "allocation"); return; } + 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"); + } + puts("]}"); + return 0; + } + if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize byte store directory\n", stderr); 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); + 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.retained, server.peak_retained, server.budget, server.peak_active, + server.accepted, server.commits, server.gets, server.conflicts, server.budget_rejections, + server.expired, server.releases, server.prefix_copy_bytes, server.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-field-connections/sources/prototypes/connections/store.c b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/store.c new file mode 100644 index 0000000..1c13a80 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/store.c @@ -0,0 +1,228 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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); +} + +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, 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; + 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; + } + fd = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + size_t size; + if (!read_header(fd, revision, &size)) goto finish; + close(fd); + fd = -1; + } else if (errno != ENOENT) goto finish; + if (*revision != expected) { result = PURE_CONFLICT; goto finish; } + if (*revision == INT64_MAX) { result = PURE_LIMIT; goto finish; } + 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, body.size); + if (!write_bytes(fd, header, sizeof(header)) || fault(PURE_AFTER_HEADER) || + !write_bytes(fd, body.data, body.size) || 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 (*temporary) unlinkat(directory, temporary, 0); + if (lock >= 0) close(lock); + close(directory); + return result; +} diff --git a/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/store.h b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/store.h new file mode 100644 index 0000000..c0b2564 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/store.h @@ -0,0 +1,40 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include + +#define PURE_OBJECT_LIMIT ((2u * 6u + 32u) * 32768u + 8192u) +typedef enum { + PURE_OK, PURE_NOT_FOUND, PURE_CONFLICT, PURE_BUSY, PURE_BUDGET, + PURE_LIMIT, PURE_STORE_ERROR, PURE_UNCERTAIN +} PureResult; +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); + +/* 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-field-connections/sources/prototypes/connections/test-fields.c b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-fields.c new file mode 100644 index 0000000..63dfc3e --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-fields.c @@ -0,0 +1,153 @@ +#include "program.h" +#include +#include +#include +#include +#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-field-connections/sources/prototypes/connections/test-flow.c b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-flow.c new file mode 100644 index 0000000..5ff028d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-flow.c @@ -0,0 +1,183 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#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-field-connections/sources/prototypes/connections/test-program.c b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-program.c new file mode 100644 index 0000000..6a4a19d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/connections/test-program.c @@ -0,0 +1,111 @@ +#include "program.h" +#include +#include +#include +#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-field-connections/sources/prototypes/workbench/cli.mjs b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/sources/prototypes/workbench/examples/direct.pure b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/sources/prototypes/workbench/remote.mjs b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/sources/prototypes/workbench/web/compiler.mjs b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/sources/prototypes/workbench/web/core.wasm b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/core.wasm new file mode 100644 index 0000000..79c50a8 Binary files /dev/null and b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/core.wasm differ diff --git a/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/project.mjs b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/sources/prototypes/workbench/web/runtime.mjs b/prototypes/connections/results/2026-09-23-field-connections/sources/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/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-field-connections/verification.json b/prototypes/connections/results/2026-09-23-field-connections/verification.json new file mode 100644 index 0000000..4befb2d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-field-connections/verification.json @@ -0,0 +1,62 @@ +{ + "date": "2026-09-23", + "scope": "Named boundary fields and data-dependent byte selection; correctness and source lifetimes, not a performance comparison", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "os_page_size_bytes": 16384, + "cwd": "prototypes/connections", + "native_command": "make check EVIDENCE=results/2026-09-23-field-connections/native.json", + "sanitized_command": "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-field-connections/sanitized.json", + "core_checks": { + "test-flow": { + "native": "passed", + "asan_ubsan": "passed" + }, + "test-program": { + "native": "passed", + "asan_ubsan": "passed", + "boundary_semantic_comparisons": 3584 + }, + "test-fields": { + "native": "passed", + "asan_ubsan": "passed", + "data_boundary_cases": 48 + }, + "leak_detection_disabled": false + }, + "unit_binary_sha256": { + "build/test-flow": "81bb00a859ffc6dad647159514e80d97ff2897687f6e904610fdd3b70ea30878", + "build/test-program": "c15f0f40c50a10b57d456e064d5b64d5a4cbef29bda55495fda0b4648105a866", + "build/test-fields": "8b274b9fe8b8c45198f8d536186aeefe0aecd8564f56d903cc4a634624889e00", + "build/sanitized/test-flow": "bff35676c65fcb9cd5cf3ee2daa1da33067c6648e436ad62d72ada4609542f93", + "build/sanitized/test-program": "e6487158d71ef075771965b4d80c7c116c689174a7cd2dee4f9e5a465def4325", + "build/sanitized/test-fields": "30852cde86252c52ff244f544b847ef05683ab0b33ed4dd4a134fe5fdd49a940" + }, + "integration_scenarios_each": 28, + "previous_scenarios": 22, + "field_scenarios": 6, + "source_snapshots": "sources/", + "source_hashes_equal": true, + "source_snapshots_match_records": true, + "final_binaries_match_records": true, + "prepared_connections_equal": true, + "field_connections_equal": true, + "prepared_program_bytes": 648, + "previous_prepared_program_bytes": 112, + "native_dynamic_dependencies_ldd": [ + "\tlinux-vdso.so.1 (0x00007fff973d8000)", + "\tlibc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x00007fff97190000)", + "\t/lib/ld-linux-aarch64.so.1 (0x00007fff973a0000)" + ], + "limitations": [ + "Declared boundary layouts are not a universal storage schema or application object model", + "Only u32le, fixed bytes, and a final variable byte field; no general arithmetic or schema version negotiation", + "Dynamic ranges operate on PUT input; GET numeric-dependent ranges are rejected", + "One storage effect per handler, no read-modify-write, branches or feedback loops", + "Program always reserves room for eight dynamic range steps, including programs that use none", + "Source receive allocation remains fully retained/charged even when a small selection is returned", + "Server byte fields do not validate UTF-8; the example Python client preserves code-point boundaries", + "Logical payload bounds exclude mapping pages, page cache, disk, OS sockets and client allocations", + "Synchronous storage can delay other connections and timeout enforcement", + "Linux AArch64 only; macOS, real power loss, and large-service performance not tested" + ] +} diff --git a/prototypes/connections/results/2026-09-23-native-connections/README.md b/prototypes/connections/results/2026-09-23-native-connections/README.md new file mode 100644 index 0000000..0e4f385 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/README.md @@ -0,0 +1,26 @@ +# 018 검증 자료 + +> 이 SQLite 구현은 [019](../2026-09-23-byte-storage/README.md)의 SQL 없는 경로로 교체했다. +> 이 디렉터리의 원자료와 소스 사본은 당시 결과를 보존한다. 현재 작업 트리에서 실행하면 +> 새 구현을 검사하므로, 당시 구현의 재현에는 `sources/`의 파일들을 별도 작업 트리에 사용한다. + +이 디렉터리는 실제 루프백 TCP·SQLite·기존 Workbench CLI를 연결한 기능·수명 검사 자료다. +속도·처리량·전체 RSS 비교는 수행하지 않았다. + +- [native.json](native.json): 일반 빌드의 15개 통합 시나리오, 종료 시 자원 수, 환경과 소스·실행 파일 해시. +- [sanitized.json](sanitized.json): ASan·UBSan 빌드에서 같은 시나리오. 누수 검사를 비활성화하지 않았다. +- [verification.json](verification.json): 빌드·실행 명령, 코어·CLI 결과와 파일 일치 확인. + +서버가 만든 임시 DB에 실제 작성물 배치를 저장하고 새 CLI에서 복원했다. 객체 버전의 +충돌과 64비트 정밀도, 저장 오류, DB 잠금, 읽는 중 다른 커밋, 부분 입력·출력, 연결 종료, +한도와 기한을 검사한다. 큰 본문은 부분 전송·한도 거절을 발생시키기 위한 정확성 fixture다. +작업량을 크게 만들어 성능을 주장하는 벤치마크가 아니다. + +`retained_payload_bytes`는 수신 할당과 빌린 DB 결과의 길이 합이다. 실제 SQLite·OS·전체 +프로세스 메모리는 포함하지 않는다. `ingress_prefix_copy_bytes`는 서버가 직접 복사한 +수신 접두부만 센다. 전송 배치에 따라 값이 바뀔 수 있어 두 빌드의 시간·복사 효율 비교에 쓰지 않는다. + +재현은 [구현 README](../../README.md)의 `make check` 명령을 따른다. `EVIDENCE=/새/경로.json`을 +추가하면 새 증거 파일을 저장한다. 이미 있는 파일을 덮어쓰지 않는다. 실제 소켓·자식 프로세스· +LeakSanitizer가 제한된 환경에서는 권한이 필요하다. 이 실행에서는 샌드박스가 해당 기능을 +차단해 승인된 샌드박스 밖의 로컬 검사로 확인했다. 외부 네트워크 서비스는 사용하지 않았다. diff --git a/prototypes/connections/results/2026-09-23-native-connections/native.json b/prototypes/connections/results/2026-09-23-native-connections/native.json new file mode 100644 index 0000000..5f7e53f --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/native.json @@ -0,0 +1,98 @@ +{ + "kind": "native-path-correctness-not-performance", + "checks": [ + "fragmented request and binary bytes round trip", + "conditional create and stale revision preserve committed bytes", + "busy and injected DB failure do not acknowledge or change data", + "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" + ], + "server_runs": [ + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 38, + "commits": 11, + "gets": 13, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 31, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115411, + "server_structure_bytes": 39112 + }, + { + "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": 39112 + }, + { + "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": 39112 + } + ], + "sqlite_runtime": "3.46.1", + "binary_sha256": "ff1c2d09d940d9f9a9170e6fea894aee9926e98f04567d389a0c00d5ddf4c7f7", + "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": "b0b50b0d85900b26257aac68177951e8319fd5482a1fcff4c91722f7ba30f74c", + "prototypes/connections/store.h": "07d347ae872a3d618dda1697b01bb32abee44b3451472a1b2225963085c11cf2", + "prototypes/connections/server.c": "3ff9d9497ad2a8ece657b50af9f8e6e564a974e6bbe3542f0ebe32279883d219", + "prototypes/connections/check.py": "be75db13ab2f11d3dd0e064aaa271bf28145aef11423e8ed536c755dcf96eaac", + "prototypes/connections/test-flow.c": "17472b90ebd3067b43a0cd06fb571111206b2025a2bd552b1602c3189f17ce0b", + "prototypes/connections/Makefile": "6c05917782adf733d31d9bfd42ee8f41b05fe94d965c362577189d6849b61e3e", + "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-native-connections/sanitized.json b/prototypes/connections/results/2026-09-23-native-connections/sanitized.json new file mode 100644 index 0000000..c391070 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sanitized.json @@ -0,0 +1,98 @@ +{ + "kind": "native-path-correctness-not-performance", + "checks": [ + "fragmented request and binary bytes round trip", + "conditional create and stale revision preserve committed bytes", + "busy and injected DB failure do not acknowledge or change data", + "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" + ], + "server_runs": [ + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 38, + "commits": 11, + "gets": 13, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 31, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115411, + "server_structure_bytes": 39112 + }, + { + "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": 39112 + }, + { + "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": 39112 + } + ], + "sqlite_runtime": "3.46.1", + "binary_sha256": "2e3bb0802082637989041063c2bc34ad6c9a072abc257d7adefdc65cba315298", + "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": "b0b50b0d85900b26257aac68177951e8319fd5482a1fcff4c91722f7ba30f74c", + "prototypes/connections/store.h": "07d347ae872a3d618dda1697b01bb32abee44b3451472a1b2225963085c11cf2", + "prototypes/connections/server.c": "3ff9d9497ad2a8ece657b50af9f8e6e564a974e6bbe3542f0ebe32279883d219", + "prototypes/connections/check.py": "be75db13ab2f11d3dd0e064aaa271bf28145aef11423e8ed536c755dcf96eaac", + "prototypes/connections/test-flow.c": "17472b90ebd3067b43a0cd06fb571111206b2025a2bd552b1602c3189f17ce0b", + "prototypes/connections/Makefile": "6c05917782adf733d31d9bfd42ee8f41b05fe94d965c362577189d6849b61e3e", + "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-native-connections/sources/prototypes/connections/Makefile b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/Makefile new file mode 100644 index 0000000..c520aa5 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/Makefile @@ -0,0 +1,22 @@ +CC ?= cc +CPPFLAGS += $(shell pkg-config --cflags sqlite3) +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +LDLIBS += $(shell pkg-config --libs sqlite3) +BUILD ?= build + +.PHONY: all check +all: $(BUILD)/pure-connections + +$(BUILD): + mkdir -p $@ + +$(BUILD)/pure-connections: server.c store.c flow.c store.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) server.c store.c flow.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-flow: test-flow.c store.c flow.c store.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) test-flow.c store.c flow.c $(LDFLAGS) $(LDLIBS) -o $@ + +check: $(BUILD)/test-flow $(BUILD)/pure-connections + $(BUILD)/test-flow + python3 check.py --server $(BUILD)/pure-connections $(if $(EVIDENCE),--output "$(EVIDENCE)") diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py new file mode 100644 index 0000000..c19fcaa --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Exercise the real local TCP/SQLite path and the existing workbench CLI.""" +import argparse +from contextlib import contextmanager +import hashlib +import http.client +import json +import platform +from pathlib import Path +import select +import socket +import sqlite3 +import struct +import subprocess +import tempfile +import time + +ROOT = Path(__file__).resolve().parent +WORKBENCH = ROOT.parent / 'workbench' +OBJECT_LIMIT = (2 * 6 + 32) * 32768 + 8192 + + +class Server: + def __init__(self, binary, database, budget=4 * 1024 * 1024, timeout=5000): + self.process = subprocess.Popen([str(binary), '--db', str(database), '--port', '0', + '--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): + database = directory / 'objects.db' + checks, records = [], [] + with serving(binary, database) 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') + + lock = sqlite3.connect(database, timeout=0) + lock.execute('BEGIN IMMEDIATE') + assert rpc(server, 'PUT', 'binary', b'blocked', 2)[0] == 503 + lock.rollback() + lock.execute("CREATE TRIGGER reject_write BEFORE UPDATE ON pure_objects BEGIN SELECT RAISE(ABORT,'test failure'); END") + lock.commit() + assert rpc(server, 'PUT', 'binary', b'failure', 2)[0] == 500 + assert rpc(server, 'GET', 'binary')[2] == b'new' + lock.execute('DROP TRIGGER reject_write') + lock.commit() + lock.close() + checks.append('busy and injected DB failure do not acknowledge or change data') + + 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 SQLite row 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 database. + 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 + with sqlite3.connect(database) as database_connection: + database_connection.execute('UPDATE pure_objects SET revision=? WHERE key=?', (9007199254740993, 'wide_revision')) + 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: + with sqlite3.connect(database) as database_connection: + committed = database_connection.execute('SELECT body FROM pure_objects WHERE key=?', ('unread_ack',)).fetchone() + if committed: + assert committed[0] == 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, database) 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, database, 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) + return {'kind': 'native-path-correctness-not-performance', 'checks': checks, + 'server_runs': records, 'sqlite_runtime': server.ready['sqlite']} + + +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) + 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)) + 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', 'server.c', 'check.py', 'test-flow.c', 'Makefile')), + *(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-native-connections/sources/prototypes/connections/flow.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.c new file mode 100644 index 0000000..1070c70 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.c @@ -0,0 +1,41 @@ +#include "flow.h" +#include + +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-native-connections/sources/prototypes/connections/flow.h b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include +#include + +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-native-connections/sources/prototypes/connections/server.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/server.c new file mode 100644 index 0000000..8f7b1ed --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/server.c @@ -0,0 +1,346 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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, charged; + unsigned char *body; + bool put; + int64_t expected; + PureStoredView stored; + PureTransfer transfer; + Server *server; +} Connection; +struct Server { + const char *path; + size_t budget, retained, peak_retained, active, peak_active; + uint64_t accepted, commits, gets, conflicts, budget_rejections, expired; + uint64_t releases, prefix_copy_bytes, borrowed_output_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; + bool owned = c->body || c->stored.db || c->charged; + pure_store_release(&c->stored); + free(c->body); + c->body = NULL; + assert(c->charged <= c->server->retained); + c->server->retained -= c->charged; + c->charged = 0; + if (owned) ++c->server->releases; +} + +static void close_connection(Connection *c) { + if (c->fd < 0) return; + if (c->transfer.active) pure_transfer_abort(&c->transfer); + else if (c->body || c->stored.db || c->charged) release_payload(c); + close(c->fd); + --c->server->active; + *c = (Connection){.fd = -1}; +} + +static bool charge(Connection *c, size_t size) { + Server *s = c->server; + if (size > s->budget - s->retained) { ++s->budget_rejections; return false; } + s->retained += size; + c->charged += size; + if (s->retained > s->peak_retained) s->peak_retained = s->retained; + return true; +} + +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. */ + if (c->body || c->stored.db || c->charged) release_payload(c); + respond(c, status, reason, error, 0, (PureSpan){0}); +} + +static void run_request(Connection *c) { + PureResult result; + int64_t revision = 0; + if (c->put) { + result = pure_store_put(c->server->path, c->key, c->expected, + (PureSpan){c->body, c->body_size}, &revision); + /* Binding has been finalized, including the failure path. */ + release_payload(c); + if (result == PURE_OK) ++c->server->commits; + } else { + result = pure_store_get(c->server->path, c->key, + c->server->budget - c->server->retained, &c->stored); + if (result == PURE_OK) { + if (!charge(c, c->stored.body.size)) abort(); /* Same owner thread. */ + revision = c->stored.revision; + ++c->server->gets; + c->server->borrowed_output_bytes += c->stored.body.size; + } else if (result == PURE_BUDGET) ++c->server->budget_rejections; + } + switch (result) { + case PURE_OK: respond(c, 200, "OK", "none", revision, c->stored.body); break; + case PURE_NOT_FOUND: fail(c, 404, "Not Found", "not_found"); break; + case PURE_CONFLICT: + ++c->server->conflicts; + respond(c, 412, "Precondition Failed", "conflict", revision, (PureSpan){0}); break; + case PURE_BUSY: fail(c, 503, "Service Unavailable", "db_busy"); break; + case PURE_BUDGET: fail(c, 503, "Service Unavailable", "payload_budget"); break; + case PURE_LIMIT: fail(c, 413, "Content Too Large", "limit"); break; + default: fail(c, 500, "Internal Server Error", "storage"); break; + } +} + +/* 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; } + if (!c->put) { run_request(c); return; } + if (!charge(c, c->body_size)) { fail(c, 503, "Service Unavailable", "payload_budget"); return; } + c->body = malloc(c->body_size ? c->body_size : 1); + if (!c->body) { fail(c, 503, "Service Unavailable", "allocation"); return; } + 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; + for (int i = 1; i < argc; i += 2) { + if (i + 1 == argc) goto usage; + uint64_t value; + if (!strcmp(argv[i], "--db")) server.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 (!server.path) goto usage; + if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize SQLite store\n", stderr); 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,\"sqlite\":\"%s\"}\n", ntohs(address.sin_port), sqlite3_libversion()); + 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); + 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.retained, server.peak_retained, server.budget, server.peak_active, + server.accepted, server.commits, server.gets, server.conflicts, server.budget_rejections, + server.expired, server.releases, server.prefix_copy_bytes, server.borrowed_output_bytes, sizeof(server)); + return 0; +usage: + fputs("pure-connections --db PATH [--port 8767] [--budget BYTES] [--timeout-ms 10000] [--send-buffer BYTES]\n", stderr); + return 2; +} diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c new file mode 100644 index 0000000..a805f72 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c @@ -0,0 +1,132 @@ +#include "store.h" +#include +#include + +static PureResult error_result(int rc) { + int code = rc & 255; + return code == SQLITE_BUSY || code == SQLITE_LOCKED ? PURE_BUSY : PURE_STORE_ERROR; +} + +static int execute(sqlite3 *db, const char *sql) { + return sqlite3_exec(db, sql, NULL, NULL, NULL); +} + +static int open_db(const char *path, sqlite3 **db, int flags) { + int rc = sqlite3_open_v2(path, db, flags | SQLITE_OPEN_NOMUTEX, NULL); + if (rc != SQLITE_OK) return rc; + sqlite3_busy_timeout(*db, 0); /* No hidden wait/retry on this owner thread. */ + sqlite3_limit(*db, SQLITE_LIMIT_LENGTH, (int)PURE_OBJECT_LIMIT + 4096); + return execute(*db, "PRAGMA cache_size=-64; PRAGMA synchronous=FULL;"); +} + +PureResult pure_store_init(const char *path) { + sqlite3 *db = NULL; + int rc = open_db(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + sqlite3_stmt *mode = NULL; + if (rc == SQLITE_OK) rc = sqlite3_prepare_v2(db, "PRAGMA journal_mode=WAL", -1, &mode, NULL); + if (rc == SQLITE_OK) { + rc = sqlite3_step(mode); + if (rc == SQLITE_ROW) { + const unsigned char *name = sqlite3_column_text(mode, 0); + rc = name && strcmp((const char *)name, "wal") == 0 ? SQLITE_OK : SQLITE_ERROR; + } + } + sqlite3_finalize(mode); + if (rc == SQLITE_OK) rc = execute(db, + "CREATE TABLE IF NOT EXISTS pure_objects(" + "key TEXT PRIMARY KEY, revision INTEGER NOT NULL CHECK(revision>0)," + "body BLOB NOT NULL CHECK(typeof(body)='blob'));"); + if (db) sqlite3_close(db); + return rc == SQLITE_OK ? PURE_OK : error_result(rc); +} + +void pure_store_release(PureStoredView *view) { + sqlite3_finalize(view->statement); + if (view->db) { + execute(view->db, "ROLLBACK"); + sqlite3_close(view->db); + } + *view = (PureStoredView){0}; +} + +PureResult pure_store_get(const char *path, const char *key, size_t available, + PureStoredView *out) { + *out = (PureStoredView){0}; + int rc = open_db(path, &out->db, SQLITE_OPEN_READONLY); + if (rc == SQLITE_OK) rc = execute(out->db, "BEGIN"); + if (rc == SQLITE_OK) rc = sqlite3_prepare_v2(out->db, + "SELECT revision,length(body) FROM pure_objects WHERE key=?", -1, &out->statement, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(out->statement, 1, key, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(out->statement); + PureResult result = PURE_STORE_ERROR; + if (rc == SQLITE_DONE) { result = PURE_NOT_FOUND; goto fail; } + if (rc != SQLITE_ROW) { result = error_result(rc); goto fail; } + out->revision = sqlite3_column_int64(out->statement, 0); + sqlite3_int64 size = sqlite3_column_int64(out->statement, 1); + if (out->revision <= 0 || size < 0) goto fail; + if (size > PURE_OBJECT_LIMIT) { result = PURE_LIMIT; goto fail; } + if ((uint64_t)size > available) { result = PURE_BUDGET; goto fail; } + sqlite3_finalize(out->statement); + out->statement = NULL; + /* Keep the read transaction so admission and body observe the same version. + * The result remains borrowed until socket completion/abort finalizes it. */ + rc = sqlite3_prepare_v2(out->db, "SELECT body FROM pure_objects WHERE key=?", -1, &out->statement, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(out->statement, 1, key, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(out->statement); + if (rc != SQLITE_ROW) { result = error_result(rc); goto fail; } + if (sqlite3_column_type(out->statement, 0) != SQLITE_BLOB) goto fail; + const unsigned char *data = sqlite3_column_blob(out->statement, 0); + int bytes = sqlite3_column_bytes(out->statement, 0); + if (bytes != size || (bytes && !data)) goto fail; + out->body = (PureSpan){data, (size_t)bytes}; + return PURE_OK; +fail: + pure_store_release(out); + return result; +} + +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, int64_t *revision) { + *revision = 0; + if (body.size > PURE_OBJECT_LIMIT || expected < 0) return PURE_LIMIT; + sqlite3 *db = NULL; + sqlite3_stmt *statement = NULL; + PureResult result = PURE_STORE_ERROR; + int rc = open_db(path, &db, SQLITE_OPEN_READWRITE); + if (rc == SQLITE_OK) rc = execute(db, "BEGIN IMMEDIATE"); + if (rc != SQLITE_OK) { result = error_result(rc); goto finish; } + rc = sqlite3_prepare_v2(db, "SELECT revision FROM pure_objects WHERE key=?", -1, &statement, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(statement, 1, key, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(statement); + if (rc == SQLITE_ROW) *revision = sqlite3_column_int64(statement, 0); + else if (rc != SQLITE_DONE) { result = error_result(rc); goto finish; } + sqlite3_finalize(statement); + statement = NULL; + if (*revision != expected) { result = PURE_CONFLICT; goto finish; } + if (*revision == INT64_MAX) { result = PURE_LIMIT; goto finish; } + const char *sql = *revision ? + "UPDATE pure_objects SET body=?1,revision=?2 WHERE key=?3" : + "INSERT INTO pure_objects(body,revision,key) VALUES(?1,?2,?3)"; + rc = sqlite3_prepare_v2(db, sql, -1, &statement, NULL); + static const unsigned char empty = 0; + /* Do not copy at the binding boundary. Finalize before returning the input. */ + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(statement, 1, body.size ? body.data : &empty, + (int)body.size, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(statement, 2, *revision + 1); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(statement, 3, key, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(statement); + if (rc != SQLITE_DONE) { result = error_result(rc); goto finish; } + rc = sqlite3_finalize(statement); + statement = NULL; + if (rc == SQLITE_OK) rc = execute(db, "COMMIT"); + if (rc != SQLITE_OK) { result = error_result(rc); goto finish; } + ++*revision; + result = PURE_OK; /* Acknowledgment is constructed only after COMMIT. */ +finish: + sqlite3_finalize(statement); + if (db) { + if (!sqlite3_get_autocommit(db)) execute(db, "ROLLBACK"); + sqlite3_close(db); + } + return result; +} diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h new file mode 100644 index 0000000..c0e9f96 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h @@ -0,0 +1,25 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include +#include + +#define PURE_OBJECT_LIMIT ((2u * 6u + 32u) * 32768u + 8192u) +typedef enum { + PURE_OK, PURE_NOT_FOUND, PURE_CONFLICT, PURE_BUSY, PURE_BUDGET, + PURE_LIMIT, PURE_STORE_ERROR +} PureResult; +typedef struct { + sqlite3 *db; + sqlite3_stmt *statement; + 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); +void pure_store_release(PureStoredView *); +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, int64_t *revision); +#endif diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c new file mode 100644 index 0000000..997e7df --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c @@ -0,0 +1,110 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include + +#define CHECK(expr) do { if (!(expr)) { fprintf(stderr, "line %d: %s\n", __LINE__, #expr); exit(1); } } while (0) +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.db && !first.statement); + 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.db && !first.statement); + 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); + + sqlite3 *other = NULL; + CHECK(sqlite3_open(path, &other) == SQLITE_OK); + CHECK(sqlite3_exec(other, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_OK); + CHECK(pure_store_put(path, "work", 2, (PureSpan){original, sizeof(original)}, &revision) == PURE_BUSY); + CHECK(sqlite3_exec(other, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); + CHECK(sqlite3_exec(other, "CREATE TRIGGER deny BEFORE UPDATE ON pure_objects " + "BEGIN SELECT RAISE(ABORT,'injected failure'); END", NULL, NULL, NULL) == SQLITE_OK); + 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); + CHECK(sqlite3_exec(other, "DROP TRIGGER deny; UPDATE pure_objects SET revision=9223372036854775807", NULL, NULL, NULL) == SQLITE_OK); + CHECK(pure_store_put(path, "work", INT64_MAX, (PureSpan){original, sizeof(original)}, &revision) == PURE_LIMIT); + CHECK(sqlite3_close(other) == SQLITE_OK); + 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); +} + +int main(void) { + transfers(); + char directory[] = "/tmp/pure-connection-unit-XXXXXX"; + CHECK(mkdtemp(directory)); + char path[256], sidecar[280]; + snprintf(path, sizeof(path), "%s/store.db", directory); + storage(path); + CHECK(unlink(path) == 0); + snprintf(sidecar, sizeof(sidecar), "%s-wal", path); unlink(sidecar); + snprintf(sidecar, sizeof(sidecar), "%s-shm", path); unlink(sidecar); + CHECK(rmdir(directory) == 0); + puts("{\"transfer\":\"partial-consumption-and-single-release\"," + "\"storage\":\"snapshot-budget-conflict-busy-rollback-overflow-empty\"}"); + return 0; +} diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/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-native-connections/sources/prototypes/workbench/cli.test.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.test.mjs new file mode 100644 index 0000000..f355d2d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.test.mjs @@ -0,0 +1,121 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => { + const file = join(root, 'examples/direct.pure'); + const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]]) + runtime.point('drag', phase, x, y, 'terminal'); + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert.equal(result.snapshots.at(-1).states.x, 350); + assert.equal(result.snapshots.at(-1).states.y, 210); + assert.deepEqual(result.snapshots.at(-1).held, []); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); + +test('terminal saves and restores placement independently of reset and preserves a browser draft', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-layout-cli-')); + t.after(() => rm(directory, {recursive:true,force:true})); + const saved=join(directory,'layout.json'), copied=join(directory,'copy.json'); + const first=run(`point drag down 90 170\npoint drag move 360 220\npoint drag up 90 170\nsave-project ${saved}\nreset\nrestore ${saved}\nshow\nquit\n`,join(root,'examples/direct.pure')); + assert.equal(first.status,0,first.stderr);assert.equal(first.snapshots.at(-1).states.x,350); + const document=JSON.parse(await readFile(saved,'utf8'));document.draft='state incomplete ='; + await writeFile(saved,JSON.stringify(document)); + const second=run(`save-project ${copied}\nreset\nshow\nquit\n`,saved); + assert.equal(second.status,0,second.stderr);assert.equal(second.snapshots.at(-1).states.x,80); + const copy=JSON.parse(await readFile(copied,'utf8'));assert.equal(copy.draft,document.draft);assert.deepEqual(copy.states,document.states); + const rejected=run(`save-project ${saved}\nquit\n`,saved); + assert.equal(rejected.status,1);assert.match(rejected.stderr,/EEXIST/); +}); + +test('invalid restoration leaves terminal code and state unchanged', async t => { + const directory=await mkdtemp(join(tmpdir(),'pure-invalid-layout-')); + t.after(()=>rm(directory,{recursive:true,force:true})); + const file=join(directory,'bad.json'); + await writeFile(file,JSON.stringify({format:'pure-workbench',version:2,source:'state x=1',states:[]})); + const result=run(`show\nrestore ${file}\nshow\nquit\n`,join(root,'examples/counter.pure')); + assert.equal(result.status,1);assert.deepEqual(result.snapshots[0],result.snapshots[1]); +}); diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/examples/direct.pure b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/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-native-connections/sources/prototypes/workbench/remote.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/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-native-connections/sources/prototypes/workbench/web/compiler.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/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-native-connections/sources/prototypes/workbench/web/core.wasm b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/core.wasm new file mode 100644 index 0000000..79c50a8 Binary files /dev/null and b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/core.wasm differ diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/project.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/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-native-connections/sources/prototypes/workbench/web/runtime.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/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-native-connections/verification.json b/prototypes/connections/results/2026-09-23-native-connections/verification.json new file mode 100644 index 0000000..3a8b2cc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-native-connections/verification.json @@ -0,0 +1,29 @@ +{ + "scope": "Correctness, lifetimes, admission bounds; no throughput, latency, or RSS comparison", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "native_command": "make check EVIDENCE=results/2026-09-23-native-connections/native.json", + "sanitized_command": "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-native-connections/sanitized.json", + "core_checks": { + "native": "passed", + "asan_ubsan": "passed", + "leak_detection_disabled": false + }, + "integration_scenarios_each": 15, + "cli_regression": { + "command": "node --test cli.test.mjs", + "cwd": "prototypes/workbench", + "passed": 8, + "failed": 0, + "test_sha256": "7e4688824e26dc7e56396f9d4d62726de0ccb1490e1a388bb64690ae2aae52f4" + }, + "source_hashes_equal": true, + "source_snapshots": "sources/", + "final_binaries_match_records": true, + "limitations": [ + "SQLite shared library was not rebuilt with sanitizers", + "Linux AArch64 only; no macOS execution", + "Bounded payload retention excludes SQLite internals, OS socket buffers and client allocations", + "SQLite operations remain synchronous; absolute deadlines are enforced between those operations", + "Native connection path is fixed in C; Pure-authored external effects are not implemented" + ] +} diff --git a/prototypes/connections/results/2026-09-23-prepared-connections/README.md b/prototypes/connections/results/2026-09-23-prepared-connections/README.md new file mode 100644 index 0000000..c79e954 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/README.md @@ -0,0 +1,31 @@ +# 020 검증 자료 + +> 필드와 데이터 의존 범위 도입 후 기록은 [021](../2026-09-23-field-connections/README.md)에 있다. +> 이 디렉터리의 원자료·소스 사본은 상수 구간 프로파일 당시의 결과로 보존한다. + +작성한 구간·저장·응답 관계를 준비하고 실제 네이티브 TCP 경로에서 실행한 기록이다. +속도·처리량·RSS 비교는 하지 않았다. + +- [native.json](native.json): 일반 빌드의 22개 통합 시나리오, 준비 결과, 종료 자원, 실행 파일·소스 해시. +- [sanitized.json](sanitized.json): ASan·UBSan 빌드의 같은 검사. 누수 검사를 끄지 않았다. +- [verification.json](verification.json): 명령, 저장/전송 및 준비 단위 검사 결과, 소스·바이너리 일치 확인. +- `sources/`: 기록된 파일의 사본. 현재 작업 트리가 바뀐 뒤에는 별도 작업 트리에 배치해 재현한다. + +`prepared_connections`는 검사 시 작성한 정의에서 나온 구간·길이 조건이다. +`runtime_view_steps`는 요청별 뷰 노드 실행기가 없다는 구조 설명값이고 시간이나 CPU 명령 +계측값은 아니다. `prepared_program_bytes`는 준비 구조체 하나의 `sizeof`다. 전체 메모리와 +혼동하지 않는다. 부모 구간이나 쓰지 않는 구간도 유효해야 하므로 최소 원본 길이가 최종 +응답 범위보다 클 수 있다. + +논리적 본문 예산 16 B로 1 MiB 파일에서 작성한 16 B를 응답한 검사는 요구 범위 전달을 +확인한다. mmap의 페이지·정렬 비용, 파일 보유 공간, OS 버퍼, 전체 RSS는 이 예산 밖이다. +입력의 일부를 응답하는 경우에는 원본 수신 할당 전체를 유지·계상한다. + +기존 저장 단위 검사 외에 `test-program`은 준비 결과를 순차 범위 검사와 대조하며, +잘못된 이름·타입·효과 위치·상수 범위·소스 한도 등을 거절하는지 확인한다. 기본 작성 파일로 +기존 Workbench 조작·저장·새 실행기 복원이 유지되는 것도 통합 검사에 포함했다. + +재현은 [구현 README](../../README.md)의 `make check`를 따른다. +`EVIDENCE=/새/경로.json`으로 원자료를 기록하며 기존 파일을 덮어쓰지 않는다. 이 환경은 +소켓·자식 프로세스·LeakSanitizer 제한 때문에 승인된 환경에서 로컬 검사를 실행했다. +외부 서비스는 사용하지 않았으며 macOS와 실제 전원 장애는 확인하지 않았다. diff --git a/prototypes/connections/results/2026-09-23-prepared-connections/native.json b/prototypes/connections/results/2026-09-23-prepared-connections/native.json new file mode 100644 index 0000000..4e04783 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/native.json @@ -0,0 +1,269 @@ +{ + "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" + ], + "prepared_connections": { + "excerpt": { + "read": { + "whole": false, + "offset": 32, + "length": 16, + "minimum_source_bytes": 80 + }, + "write": { + "whole": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "reply_input": false, + "view_definitions": 2, + "range_definitions": 2, + "runtime_view_steps": 0, + "prepared_program_bytes": 112 + }, + "write": { + "read": { + "whole": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "whole": false, + "offset": 5, + "length": 4, + "minimum_source_bytes": 12 + }, + "reply_input": true, + "view_definitions": 3, + "range_definitions": 2, + "runtime_view_steps": 0, + "prepared_program_bytes": 112 + } + }, + "server_runs": [ + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 6, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 39288 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 41, + "commits": 11, + "gets": 14, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115417, + "server_structure_bytes": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "ce76b1ff164623c8575c8c313a97ec4083c8a500eef88ab49072bc2e04d7520d", + "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": "7a506fb79a074f0d90eaa6f047a364e4fee28eea9ebea1f869dbd3770edc6a2e", + "prototypes/connections/store.h": "05c5a4e35e27a4d309c3209c84112fd38d65093ece79ddc4eb1b68adb8e5741f", + "prototypes/connections/program.c": "b6b848b261aa9da455126ec25fff0494b4eefb649ce0fd036f075c1efb41f9c8", + "prototypes/connections/program.h": "700880cba88b18b08c1b87aa4da46fedc81f3d665d74e40fc659095fe4c3f014", + "prototypes/connections/server.c": "d2cb3cc8d47cb5e6bf258cf62c70c039b5c5e945fa5c4d5edb7917d50395fe73", + "prototypes/connections/check.py": "7321ba3a48a10a7d1344bbf61df3fb693e9ac95e00d26d3d247bd6a39997505f", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "d58248c65d59fa31f37ba50aa9ad761d3e9698d1e17d61307b7e88089c70d385", + "prototypes/connections/Makefile": "85bc73e364fb5a273efd7bfbd2e8118091f634d0e263d0729bea445252dcd7f0", + "prototypes/connections/examples/storage.pure": "6f2fd3733f46b762836405f40a9720de0b865f92a8bcbf9388dda0532c903c74", + "prototypes/connections/examples/excerpt.pure": "7ca1b9a4b6a06c32b95e37bcdeb2202ef912ffbf16163f7eea2b00574c703c16", + "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-prepared-connections/sanitized.json b/prototypes/connections/results/2026-09-23-prepared-connections/sanitized.json new file mode 100644 index 0000000..d5cc604 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sanitized.json @@ -0,0 +1,269 @@ +{ + "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" + ], + "prepared_connections": { + "excerpt": { + "read": { + "whole": false, + "offset": 32, + "length": 16, + "minimum_source_bytes": 80 + }, + "write": { + "whole": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "reply_input": false, + "view_definitions": 2, + "range_definitions": 2, + "runtime_view_steps": 0, + "prepared_program_bytes": 112 + }, + "write": { + "read": { + "whole": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "whole": false, + "offset": 5, + "length": 4, + "minimum_source_bytes": 12 + }, + "reply_input": true, + "view_definitions": 3, + "range_definitions": 2, + "runtime_view_steps": 0, + "prepared_program_bytes": 112 + } + }, + "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": 39288 + }, + { + "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": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + }, + { + "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": 39288 + } + ], + "storage": "immutable-byte-extents-v1", + "binary_sha256": "3cb873b98d64992cc89f11186de1e97baf19fd7aa3fcb5784b061bea7828b206", + "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": "7a506fb79a074f0d90eaa6f047a364e4fee28eea9ebea1f869dbd3770edc6a2e", + "prototypes/connections/store.h": "05c5a4e35e27a4d309c3209c84112fd38d65093ece79ddc4eb1b68adb8e5741f", + "prototypes/connections/program.c": "b6b848b261aa9da455126ec25fff0494b4eefb649ce0fd036f075c1efb41f9c8", + "prototypes/connections/program.h": "700880cba88b18b08c1b87aa4da46fedc81f3d665d74e40fc659095fe4c3f014", + "prototypes/connections/server.c": "d2cb3cc8d47cb5e6bf258cf62c70c039b5c5e945fa5c4d5edb7917d50395fe73", + "prototypes/connections/check.py": "7321ba3a48a10a7d1344bbf61df3fb693e9ac95e00d26d3d247bd6a39997505f", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "d58248c65d59fa31f37ba50aa9ad761d3e9698d1e17d61307b7e88089c70d385", + "prototypes/connections/Makefile": "85bc73e364fb5a273efd7bfbd2e8118091f634d0e263d0729bea445252dcd7f0", + "prototypes/connections/examples/storage.pure": "6f2fd3733f46b762836405f40a9720de0b865f92a8bcbf9388dda0532c903c74", + "prototypes/connections/examples/excerpt.pure": "7ca1b9a4b6a06c32b95e37bcdeb2202ef912ffbf16163f7eea2b00574c703c16", + "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-prepared-connections/sources/prototypes/connections/Makefile b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/Makefile new file mode 100644 index 0000000..50ad1cb --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/Makefile @@ -0,0 +1,24 @@ +CC ?= cc +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +BUILD ?= build + +.PHONY: all check +all: $(BUILD)/pure-connections + +$(BUILD): + mkdir -p $@ + +$(BUILD)/pure-connections: server.c store.c flow.c program.c store.h flow.h program.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) server.c store.c flow.c program.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-flow: test-flow.c store.c flow.c store.h flow.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 program.h store.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) test-program.c program.c $(LDFLAGS) $(LDLIBS) -o $@ + +check: $(BUILD)/test-flow $(BUILD)/test-program $(BUILD)/pure-connections + $(BUILD)/test-flow + $(BUILD)/test-program + python3 check.py --server $(BUILD)/pure-connections $(if $(EVIDENCE),--output "$(EVIDENCE)") diff --git a/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/check.py b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/check.py new file mode 100644 index 0000000..c547ebf --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/check.py @@ -0,0 +1,419 @@ +#!/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 tempfile +import time + +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): + 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('= 1 and limited.stats['expired'] >= 9 + records.append(limited.stats) + prepared = authored_connections(binary, directory, storage, records, checks) + return {'kind': 'prepared-connection-correctness-not-performance', 'checks': checks, + 'prepared_connections': prepared, + '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'] == { + 'whole': 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 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) + 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)) + 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', + 'server.c', 'check.py', 'test-flow.c', 'test-program.c', 'Makefile', 'examples/storage.pure', 'examples/excerpt.pure')), + *(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-prepared-connections/sources/prototypes/connections/examples/excerpt.pure b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/examples/excerpt.pure new file mode 100644 index 0000000..622f9bb --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/sources/prototypes/connections/examples/storage.pure b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/examples/storage.pure new file mode 100644 index 0000000..ff54ea3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/sources/prototypes/connections/flow.c b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/flow.c new file mode 100644 index 0000000..1070c70 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/flow.c @@ -0,0 +1,41 @@ +#include "flow.h" +#include + +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-prepared-connections/sources/prototypes/connections/flow.h b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include +#include + +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-prepared-connections/sources/prototypes/connections/program.c b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/program.c new file mode 100644 index 0000000..79101db --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/program.c @@ -0,0 +1,233 @@ +#include "program.h" +#include "store.h" +#include +#include +#include + +enum { NAME = 256, NUMBER, SYMBOLS = 32 }; +typedef struct { + char name[32]; + bool result; + PureRegion region; +} Symbol; +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; + 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 reference(Parser *p, PureRegion *out) { + if (is_word(p, "request")) { + if (!p->put) return reject(p, "request.body is only available in on put"); + *out = (PureRegion){.whole = true}; + return field(p, "request", "body"); + } + Symbol *s = p->token == NAME ? find(p, p->name) : NULL; + if (!s) return reject(p, "unknown byte source"); + bool result = s->result; + *out = s->region; + if (!next(p)) return false; + if (!result) return true; + if (p->put) return reject(p, "write result has no body; use the input value"); + *out = (PureRegion){.whole = true}; + return token(p, '.') && word(p, "body"); +} + +static bool view(Parser *p, PureRegion *out) { + if (!is_word(p, "range")) return reference(p, out); + PureRegion parent; + size_t offset, length; + if (!next(p) || !token(p, '(') || !reference(p, &parent) || !token(p, ',')) return false; + if (p->token != NUMBER) return reject(p, "range offset must be a constant integer"); + offset = p->number; + if (!next(p) || !token(p, ',')) return false; + if (p->token != NUMBER) return reject(p, "range length must be a constant integer"); + length = p->number; + if (!next(p) || !token(p, ')')) return false; + size_t bound = parent.whole ? PURE_OBJECT_LIMIT : parent.length; + if (offset > bound || length > bound - offset) return reject(p, "range exceeds its source bound"); + *out = (PureRegion){.offset = parent.offset + offset, .length = length}; + size_t required = out->offset + out->length; + size_t *minimum = p->put ? &p->program.input_required : &p->program.read_required; + if (required > *minimum) *minimum = required; + ++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, '.') || !word(p, p->put ? "write" : "read") || + !token(p, '(') || !field(p, "request", "key")) return false; + if (p->put) { + if (!token(p, ',') || !field(p, "request", "revision") || !token(p, ',') || + !reference(p, &p->program.write)) return false; + } + if (!token(p, ')')) return false; + out->result = true; + 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 store.write; 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")) + return reject(p, "expected a non-reserved definition name"); + if (find(p, p->name)) return reject(p, "duplicate definition"); + Symbol symbol = {0}; + strcpy(symbol.name, p->name); + if (!next(p) || !token(p, '=')) return false; + if (is_word(p, "store")) { + if (!effect(p, &symbol)) return false; + } else { + if (!view(p, &symbol.region)) return false; + ++p->program.view_definitions; + } + if (!token(p, ';')) return false; + 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->result) return reject(p, "reply requires this handler's storage result"); + if (!next(p)) return false; + PureRegion body = {0}; + bool has_body = p->token == ','; + if (has_body && (!next(p) || !reference(p, &body))) return false; + if (!token(p, ')') || !token(p, ';') || !token(p, '}')) return false; + if (p->put) { + p->program.reply = body; + p->program.reply_input = has_body; + } else p->program.read = body; + 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; + 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); +} + +bool pure_region_span(PureRegion region, PureSpan source, PureSpan *out) { + *out = (PureSpan){0}; + if (region.whole) { *out = source; return true; } + if (region.offset > source.size || region.length > source.size - region.offset) return false; + *out = (PureSpan){region.length ? source.data + region.offset : NULL, region.length}; + return true; +} diff --git a/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/program.h b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/program.h new file mode 100644 index 0000000..29764db --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/program.h @@ -0,0 +1,26 @@ +#ifndef PURE_PROGRAM_H +#define PURE_PROGRAM_H +#include "flow.h" + +#define PURE_PROGRAM_LIMIT 16384u +typedef struct { + size_t offset, length; + bool whole; +} PureRegion; + +/* 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; + bool reply_input; +} 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); +#endif diff --git a/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/server.c b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/server.c new file mode 100644 index 0000000..3822c66 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/server.c @@ -0,0 +1,385 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include "program.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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, charged; + unsigned char *body; + bool put; + int64_t expected; + PureStoredView stored; + PureTransfer transfer; + Server *server; +} Connection; +struct Server { + const char *path; + PureProgram program; + size_t budget, retained, peak_retained, active, peak_active; + uint64_t accepted, commits, gets, conflicts, budget_rejections, expired; + uint64_t releases, prefix_copy_bytes, borrowed_output_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; + bool owned = c->body || c->stored.mapped_size || c->charged; + pure_store_release(&c->stored); + free(c->body); + c->body = NULL; + assert(c->charged <= c->server->retained); + c->server->retained -= c->charged; + c->charged = 0; + if (owned) ++c->server->releases; +} + +static void close_connection(Connection *c) { + if (c->fd < 0) return; + if (c->transfer.active) pure_transfer_abort(&c->transfer); + else if (c->body || c->stored.mapped_size || c->charged) release_payload(c); + close(c->fd); + --c->server->active; + *c = (Connection){.fd = -1}; +} + +static bool charge(Connection *c, size_t size) { + Server *s = c->server; + if (size > s->budget - s->retained) { ++s->budget_rejections; return false; } + s->retained += size; + c->charged += size; + if (s->retained > s->peak_retained) s->peak_retained = s->retained; + return true; +} + +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. */ + if (c->body || c->stored.mapped_size || c->charged) release_payload(c); + respond(c, status, reason, error, 0, (PureSpan){0}); +} + +static void run_request(Connection *c) { + PureResult result; + int64_t revision = 0; + PureProgram *program = &c->server->program; + PureSpan output = {0}; + if (c->put) { + PureSpan source = {c->body, c->body_size}, value; + /* Validate every authored view before performing an external effect, + * including unused views. Static preparation preserves these guards. */ + if (source.size < program->input_required || + !pure_region_span(program->write, source, &value) || + (program->reply_input && !pure_region_span(program->reply, source, &output))) { + fail(c, 413, "Content Too Large", "limit"); + return; + } + result = pure_store_put(c->server->path, c->key, c->expected, value, &revision); + /* A reply can consume the same input. Its owner lives until transfer + * completion; otherwise storage completion ends the input lifetime. */ + if (result != PURE_OK || !output.size) { release_payload(c); output = (PureSpan){0}; } + if (result == PURE_OK) { + ++c->server->commits; + c->server->borrowed_output_bytes += output.size; + } + } else { + result = pure_store_select(c->server->path, c->key, program->read.offset, + program->read.length, program->read.whole, program->read_required, + c->server->budget - c->server->retained, &c->stored); + if (result == PURE_OK) { + if (!charge(c, c->stored.body.size)) abort(); /* Same owner thread. */ + revision = c->stored.revision; + output = c->stored.body; + ++c->server->gets; + c->server->borrowed_output_bytes += c->stored.body.size; + } else if (result == PURE_BUDGET) ++c->server->budget_rejections; + } + switch (result) { + case PURE_OK: respond(c, 200, "OK", "none", revision, output); break; + case PURE_NOT_FOUND: fail(c, 404, "Not Found", "not_found"); break; + case PURE_CONFLICT: + ++c->server->conflicts; + respond(c, 412, "Precondition Failed", "conflict", 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_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; + } +} + +/* 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; } + if (!c->put) { run_request(c); return; } + if (!charge(c, c->body_size)) { fail(c, 503, "Service Unavailable", "payload_budget"); return; } + c->body = malloc(c->body_size ? c->body_size : 1); + if (!c->body) { fail(c, 503, "Service Unavailable", "allocation"); return; } + 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\":{\"whole\":%s,\"offset\":%zu,\"length\":%zu,\"minimum_source_bytes\":%zu}," + "\"write\":{\"whole\":%s,\"offset\":%zu,\"length\":%zu,\"minimum_source_bytes\":%zu}," + "\"reply_input\":%s,\"view_definitions\":%zu,\"range_definitions\":%zu," + "\"runtime_view_steps\":0,\"prepared_program_bytes\":%zu}\n", + p->read.whole ? "true" : "false", p->read.offset, p->read.length, p->read_required, + p->write.whole ? "true" : "false", p->write.offset, p->write.length, p->input_required, + p->reply_input ? "true" : "false", p->view_definitions, p->range_definitions, sizeof(*p)); + return 0; + } + if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize byte store directory\n", stderr); 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); + 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.retained, server.peak_retained, server.budget, server.peak_active, + server.accepted, server.commits, server.gets, server.conflicts, server.budget_rejections, + server.expired, server.releases, server.prefix_copy_bytes, server.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-prepared-connections/sources/prototypes/connections/store.c b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/store.c new file mode 100644 index 0000000..759f210 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/store.c @@ -0,0 +1,227 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 (whole) { offset = 0; length = size; } + 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); +} + +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, 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; + 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; + } + fd = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + size_t size; + if (!read_header(fd, revision, &size)) goto finish; + close(fd); + fd = -1; + } else if (errno != ENOENT) goto finish; + if (*revision != expected) { result = PURE_CONFLICT; goto finish; } + if (*revision == INT64_MAX) { result = PURE_LIMIT; goto finish; } + 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, body.size); + if (!write_bytes(fd, header, sizeof(header)) || fault(PURE_AFTER_HEADER) || + !write_bytes(fd, body.data, body.size) || 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 (*temporary) unlinkat(directory, temporary, 0); + if (lock >= 0) close(lock); + close(directory); + return result; +} diff --git a/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/store.h b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/store.h new file mode 100644 index 0000000..b1fbea7 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/store.h @@ -0,0 +1,39 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include + +#define PURE_OBJECT_LIMIT ((2u * 6u + 32u) * 32768u + 8192u) +typedef enum { + PURE_OK, PURE_NOT_FOUND, PURE_CONFLICT, PURE_BUSY, PURE_BUDGET, + PURE_LIMIT, PURE_STORE_ERROR, PURE_UNCERTAIN +} PureResult; +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. */ +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); + +/* 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-prepared-connections/sources/prototypes/connections/test-flow.c b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/test-flow.c new file mode 100644 index 0000000..5ff028d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/test-flow.c @@ -0,0 +1,183 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#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-prepared-connections/sources/prototypes/connections/test-program.c b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/test-program.c new file mode 100644 index 0000000..b8deeba --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/connections/test-program.c @@ -0,0 +1,111 @@ +#include "program.h" +#include +#include +#include +#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.whole && plan.write.whole && !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.whole); + 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.whole && !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-prepared-connections/sources/prototypes/workbench/cli.mjs b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/sources/prototypes/workbench/examples/direct.pure b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/sources/prototypes/workbench/remote.mjs b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/sources/prototypes/workbench/web/compiler.mjs b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/sources/prototypes/workbench/web/core.wasm b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/core.wasm new file mode 100644 index 0000000..79c50a8 Binary files /dev/null and b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/core.wasm differ diff --git a/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/project.mjs b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/sources/prototypes/workbench/web/runtime.mjs b/prototypes/connections/results/2026-09-23-prepared-connections/sources/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/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-prepared-connections/verification.json b/prototypes/connections/results/2026-09-23-prepared-connections/verification.json new file mode 100644 index 0000000..2c4036d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-prepared-connections/verification.json @@ -0,0 +1,51 @@ +{ + "date": "2026-09-23", + "scope": "Authored storage/response connections, prepared range semantics, lifetime correctness; no performance comparison", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "os_page_size_bytes": 16384, + "cwd": "prototypes/connections", + "native_command": "make check EVIDENCE=results/2026-09-23-prepared-connections/native.json", + "sanitized_command": "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-prepared-connections/sanitized.json", + "core_checks": { + "test-flow": { + "native": "passed", + "asan_ubsan": "passed" + }, + "test-program": { + "native": "passed", + "asan_ubsan": "passed", + "boundary_semantic_comparisons": 3584 + }, + "leak_detection_disabled": false + }, + "unit_binary_sha256": { + "build/test-flow": "63830f166dc326d95e91a30b24bba1f750de05c50b60cf454616b35eef1634dc", + "build/test-program": "cb8e9de15301090fda719144bd0fbbbeb9e6e91a14b2bae8e434df2ac510b527", + "build/sanitized/test-flow": "1b608d5ad31b21180e8a4bce0c0ceee79db281c93ef2e7adf0261ca0b5f96df2", + "build/sanitized/test-program": "4396abe70699580d8c27d74aeb50713a9452e6f4060289b97e2d48d5c478666d" + }, + "integration_scenarios_each": 22, + "original_storage_scenarios": 16, + "authored_connection_scenarios": 6, + "source_snapshots": "sources/", + "source_hashes_equal": true, + "source_snapshots_match_records": true, + "final_binaries_match_records": true, + "prepared_connections_equal": true, + "native_dynamic_dependencies_ldd": [ + "\tlinux-vdso.so.1 (0x00007ffee8528000)", + "\tlibc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x00007ffee82e0000)", + "\t/lib/ld-linux-aarch64.so.1 (0x00007ffee84f0000)" + ], + "limitations": [ + "Fixed byte-range authoring profile, not the general application language or graph runtime", + "One registered storage effect per handler; constant ranges, no dynamic branches or feedback loops", + "The native connection authoring parser is separate from the existing UI/scalar parser", + "runtime_view_steps is a structural description, not a measured instruction count", + "Logical payload bounds exclude mmap page rounding, page cache, disk retention, sockets and client allocations", + "Selecting input bytes for a reply retains and charges the complete original receive allocation", + "Synchronous storage calls delay other connections and timeout enforcement", + "Linux AArch64 only; macOS and real power loss have not been tested", + "Unpublished files left by crashes have no automatic recovery/collection policy" + ] +} diff --git a/prototypes/connections/results/2026-09-24-async-effects/async-core.json b/prototypes/connections/results/2026-09-24-async-effects/async-core.json new file mode 100644 index 0000000..d0932f0 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-effects/async-core.json @@ -0,0 +1,55 @@ +{ + "passed": true, + "failures": [], + "budget": { + "profile": "selected-owner-thread-async-effects", + "max_text_bytes": 8192, + "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, + "effect_bytes": 128 + }, + "archive_members": [ + "runtime-async.o", + "evaluate.o" + ], + "external_symbols_allowed": [ + "memcpy", + "memmove", + "memset", + "strlen" + ], + "scope": "Selected owner-thread completion extension. Host I/O and worker stacks are excluded. The default execution core and fixed document packages do not link this profile." + }, + "layout": { + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8, + "effect_bytes": 128 + }, + "sections": { + "text": 7312, + "data": 0, + "bss": 0 + }, + "members": [ + "runtime-async.o", + "evaluate.o" + ], + "external_symbols": [ + "memcpy", + "strlen" + ], + "size_output": " text\t data\t bss\t dec\t hex\tfilename\n 6392\t 0\t 0\t 6392\t 18f8\truntime-async.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution-async.a)\n 920\t 0\t 0\t 920\t 398\tevaluate.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution-async.a)\n", + "archive_sha256": "afa5efe941031832232aec8a64e0727b93927995edd1d395f8feeb7fa4f9611d", + "layout_binary_sha256": "71866a57a3a2af37a8e3d4f2f97669c482b11b3a408a1d918e5de068dab9e34a", + "budget_sha256": "496e34596e24418efb46e460126b7d67492137207be988525abda1397d70c539", + "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-24-async-effects/default-core.json b/prototypes/connections/results/2026-09-24-async-effects/default-core.json new file mode 100644 index 0000000..b92c015 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-effects/default-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": "2de4e84fe2c104368339145ffa7ade5c31a9fd0e2a4ff00ff5edd469759b3c79", + "layout_binary_sha256": "db112a9cfd8deed80b9bfbff7628958fdb290cadab1efc7223d8d15ef4c4c389", + "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-24-async-effects/flow.json b/prototypes/connections/results/2026-09-24-async-effects/flow.json new file mode 100644 index 0000000..75447c2 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-effects/flow.json @@ -0,0 +1 @@ +{"async_effects":"authored-document-worker-overlap-release-forward-budget-recovery"} diff --git a/prototypes/connections/results/2026-09-24-async-effects/sanitized.json b/prototypes/connections/results/2026-09-24-async-effects/sanitized.json new file mode 100644 index 0000000..75447c2 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-effects/sanitized.json @@ -0,0 +1 @@ +{"async_effects":"authored-document-worker-overlap-release-forward-budget-recovery"} diff --git a/prototypes/connections/results/2026-09-24-async-http/full-check.json b/prototypes/connections/results/2026-09-24-async-http/full-check.json new file mode 100644 index 0000000..55e7539 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-http/full-check.json @@ -0,0 +1,614 @@ +{ + "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": 43224 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115420, + "server_structure_bytes": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 9, + "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": 43224 + }, + { + "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": 43224 + }, + { + "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": 43224 + } + ], + "storage": "immutable-byte-extents-v1", + "editor_sha256": "4b9f3e1882d010784d04673a231fdf3ec47b8823527e74f049d5ee3401e6fc60", + "binary_sha256": "08a8283c573fd6512b13242ec8a97286be913db6843ccf971a0fe01dd93eb989", + "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/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "e8c84d688ba078e909c57d56c882c963ad5431afe10051bdec84486d3e155b54", + "prototypes/connections/runtime.h": "2a9d00a7f6f916041380cef3e3dc30602364b05288f62607b62b3de2be2595d1", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "acab2509cbde4431a2e0d9e18ad0e917f8f827b7eab46acb3d81afef442b3655", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "f39a7c411afa80812aec6e7a7250cdc833e97cffef8d0cce25d960fe435784f6", + "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": "31933e9d35521d2e9ae0efb1ff21fb4c9a3ab0e3e02444556ebb6db183155b0d", + "prototypes/connections/core-layout.c": "e3123bdf74edb99556b05d7cd3a36b6119ec30829541b0ddba2e23e5900b089d", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-async-http/gated.json b/prototypes/connections/results/2026-09-24-async-http/gated.json new file mode 100644 index 0000000..a4dd464 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-http/gated.json @@ -0,0 +1,38 @@ +{ + "async_http": "reactor-progress-deadline-recovery-draining-shutdown", + "deadline_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 13, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 3, + "commits": 1, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 1, + "releases": 2, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 10, + "server_structure_bytes": 43224 + }, + "shutdown_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 13, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 5, + "server_structure_bytes": 43224 + }, + "gated_test_binary_sha256": "aaa9c62fbc1533444467d61ad9a5fe3443b4e37939a3a6aa23301f5e19f5f7b1" +} diff --git a/prototypes/connections/results/2026-09-24-async-http/profile.json b/prototypes/connections/results/2026-09-24-async-http/profile.json new file mode 100644 index 0000000..6599363 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-http/profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-one-worker-document-http", + "scope": "Optional one-worker HTTP host on the recorded Linux AArch64/GCC 14 toolchain. Section limits exclude the 128 KiB worker stack, libc, heap, mapped files and kernel buffers. Default CLI and HTTP do not link this option. Inspect process RSS and VM separately.", + "budget_sha256": "4c851b44ad47629459b78de3bdc8ba984f45cc799c7d4933d3edbcbaa12f21e8", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-connections-async": { + "sections": { + "text": 47897, + "data": 1180, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "08a8283c573fd6512b13242ec8a97286be913db6843ccf971a0fe01dd93eb989" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-async-http/sanitized.json b/prototypes/connections/results/2026-09-24-async-http/sanitized.json new file mode 100644 index 0000000..5dc5c69 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-http/sanitized.json @@ -0,0 +1,38 @@ +{ + "async_http": "reactor-progress-deadline-recovery-draining-shutdown", + "deadline_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 13, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 3, + "commits": 1, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 1, + "releases": 2, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 10, + "server_structure_bytes": 43224 + }, + "shutdown_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 13, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 5, + "server_structure_bytes": 43224 + }, + "gated_test_binary_sha256": "f2d3e9abaaa2a389baff6c5bcfdb760ede69ff7e05aea479192b4f0c94ab05cf" +} diff --git a/prototypes/connections/results/2026-09-24-async-http/service-scale-memory.json b/prototypes/connections/results/2026-09-24-async-http/service-scale-memory.json new file mode 100644 index 0000000..5507b88 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-http/service-scale-memory.json @@ -0,0 +1,100 @@ +{ + "profile": "document-app-many-objects-concurrent-clients", + "documents_per_variant": 1024, + "clients": 8, + "body_bytes": 8192, + "operations_per_variant": 4096, + "server_budget_bytes": 1048576, + "peak_rss_limit_kib": 4096, + "same_final_documents": "pass", + "fixed": { + "binary_sha256": "584b85fce5833d6a1eb5919a6f79c0648200e4ab3f65f2e90d008ea88db3be51", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1504, + "peak_rss_kib": 1696, + "idle_vm_kib": 2448, + "peak_vm_kib": 2512, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49184, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 449903, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 40848 + } + }, + "source": { + "binary_sha256": "f49ad1538f8ebeecc65511db052a7610c966d33992c5eb076bd36124680aebd1", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1536, + "peak_rss_kib": 1712, + "idle_vm_kib": 2464, + "peak_vm_kib": 2528, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 41001, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 481696, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 41528 + } + }, + "source_async": { + "binary_sha256": "08a8283c573fd6512b13242ec8a97286be913db6843ccf971a0fe01dd93eb989", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1536, + "peak_rss_kib": 1712, + "idle_vm_kib": 2656, + "peak_vm_kib": 2736, + "idle_threads": 2, + "peak_threads": 2, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 57368, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 226974, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 43224 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-async-read-write/full-check.json b/prototypes/connections/results/2026-09-24-async-read-write/full-check.json new file mode 100644 index 0000000..914e9cd --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-read-write/full-check.json @@ -0,0 +1,614 @@ +{ + "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": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 43320 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 42, + "commits": 11, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115421, + "server_structure_bytes": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 54, + "borrowed_output_bytes": 57, + "server_structure_bytes": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "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": 43320 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 9, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 9, + "borrowed_output_bytes": 1, + "server_structure_bytes": 43320 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 1, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 16, + "borrowed_output_bytes": 113, + "server_structure_bytes": 43320 + }, + { + "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": 43320 + } + ], + "storage": "immutable-byte-extents-v1", + "editor_sha256": "4b9f3e1882d010784d04673a231fdf3ec47b8823527e74f049d5ee3401e6fc60", + "binary_sha256": "63211eebd1952dbc9f31bc2eee76c1c661df9bce14e3932b4e15e52fdae5d763", + "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/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "e8c84d688ba078e909c57d56c882c963ad5431afe10051bdec84486d3e155b54", + "prototypes/connections/runtime.h": "2a9d00a7f6f916041380cef3e3dc30602364b05288f62607b62b3de2be2595d1", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "e8632667de6feaf4d495f87abb159775650e3e4eaf6713b9dfdc5f82807c2bf9", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "f39a7c411afa80812aec6e7a7250cdc833e97cffef8d0cce25d960fe435784f6", + "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": "f9a0c9fa7d0f40d6d72bc72a429dd36e136e27207aa6b0c01df9505d82189b6d", + "prototypes/connections/core-layout.c": "e3123bdf74edb99556b05d7cd3a36b6119ec30829541b0ddba2e23e5900b089d", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-async-read-write/gated.json b/prototypes/connections/results/2026-09-24-async-read-write/gated.json new file mode 100644 index 0000000..04d5e53 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-read-write/gated.json @@ -0,0 +1,53 @@ +{ + "async_http": "snapshot-read-write-overlap-deadline-recovery-draining-shutdown", + "before_publication": { + "status": 200, + "etag": "\"1\"", + "body": "old" + }, + "after_publication": { + "status": 200, + "etag": "\"2\"", + "body": "oldafter" + }, + "shutdown_restore": { + "revision": "2", + "body": "oldafter" + }, + "deadline_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 16, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 4, + "commits": 1, + "gets": 2, + "conflicts": 0, + "budget_rejections": 0, + "expired": 1, + "releases": 3, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 16, + "server_structure_bytes": 43320 + }, + "shutdown_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 13, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 5, + "server_structure_bytes": 43320 + }, + "test_source_sha256": "f38c9d3d784676ce815c276fbffc10e00f767aaa803793d8db311c19ef3b9085", + "gated_test_binary_sha256": "e1764fd031a24851d3fab665df1538fa947e2bf653accaa528bb60cd6daf7105" +} diff --git a/prototypes/connections/results/2026-09-24-async-read-write/profile.json b/prototypes/connections/results/2026-09-24-async-read-write/profile.json new file mode 100644 index 0000000..dee3dc7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-read-write/profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-read-write-document-http", + "scope": "Optional one-read/one-write-worker HTTP host on the recorded Linux AArch64/GCC 14 toolchain. Section limits exclude the two 128 KiB worker stacks, libc, heap, mapped files and kernel buffers. Default CLI and HTTP do not link this option. Inspect process RSS and VM separately.", + "budget_sha256": "acd82386572fc2f874ca2fee5f4eed68ee145cf990d7315a9f1aff06daf51995", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-connections-async": { + "sections": { + "text": 48401, + "data": 1180, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "63211eebd1952dbc9f31bc2eee76c1c661df9bce14e3932b4e15e52fdae5d763" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-async-read-write/sanitized.json b/prototypes/connections/results/2026-09-24-async-read-write/sanitized.json new file mode 100644 index 0000000..f867695 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-read-write/sanitized.json @@ -0,0 +1,53 @@ +{ + "async_http": "snapshot-read-write-overlap-deadline-recovery-draining-shutdown", + "before_publication": { + "status": 200, + "etag": "\"1\"", + "body": "old" + }, + "after_publication": { + "status": 200, + "etag": "\"2\"", + "body": "oldafter" + }, + "shutdown_restore": { + "revision": "2", + "body": "oldafter" + }, + "deadline_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 16, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 4, + "commits": 1, + "gets": 2, + "conflicts": 0, + "budget_rejections": 0, + "expired": 1, + "releases": 3, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 16, + "server_structure_bytes": 43320 + }, + "shutdown_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 13, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 5, + "server_structure_bytes": 43320 + }, + "test_source_sha256": "f38c9d3d784676ce815c276fbffc10e00f767aaa803793d8db311c19ef3b9085", + "gated_test_binary_sha256": "b3b89925aabe1f6edd3a782d99c5adb1dff68bf727066fb00372a16d00d92e53" +} diff --git a/prototypes/connections/results/2026-09-24-async-read-write/service-scale.json b/prototypes/connections/results/2026-09-24-async-read-write/service-scale.json new file mode 100644 index 0000000..eb73971 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-async-read-write/service-scale.json @@ -0,0 +1,100 @@ +{ + "profile": "document-app-many-objects-concurrent-clients", + "documents_per_variant": 1024, + "clients": 8, + "body_bytes": 8192, + "operations_per_variant": 4096, + "server_budget_bytes": 1048576, + "peak_rss_limit_kib": 4096, + "same_final_documents": "pass", + "fixed": { + "binary_sha256": "a16cc0c8790cfc6745c6d78f09f527dcd5d9e749669fe4ad9b0d817f83f6cfee", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1504, + "peak_rss_kib": 1680, + "idle_vm_kib": 2448, + "peak_vm_kib": 2528, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49201, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 899599, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 40848 + } + }, + "source": { + "binary_sha256": "64959669514855565554abb4622dc2bee34a0792485f19bd6466eb63a68e9688", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1520, + "peak_rss_kib": 1696, + "idle_vm_kib": 2464, + "peak_vm_kib": 2528, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49201, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 465705, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 41528 + } + }, + "source_async": { + "binary_sha256": "63211eebd1952dbc9f31bc2eee76c1c661df9bce14e3932b4e15e52fdae5d763", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1552, + "peak_rss_kib": 1728, + "idle_vm_kib": 2848, + "peak_vm_kib": 2928, + "idle_threads": 3, + "peak_threads": 3, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49192, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 238875, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 43320 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/policy-peer.json b/prototypes/connections/results/2026-09-24-atomic-policy/policy-peer.json new file mode 100644 index 0000000..e93d7b0 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-atomic-policy/policy-peer.json @@ -0,0 +1,12 @@ +{ + "checks": [ + "authored final size is checked against the locked document version", + "a rejected growth leaves revision and bytes unchanged", + "stale revision reports conflict before the policy decision", + "source CLI and HTTP share a policy and committed document", + "source policy changes behavior after the HTTP host restarts", + "fixed CLI and HTTP retain their compiled policy after source edits", + "source and fixed terminal editors preserve unsaved drafts on policy denial", + "authenticated peer shares the policy and current stored version" + ] +} diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/policy.json b/prototypes/connections/results/2026-09-24-atomic-policy/policy.json new file mode 100644 index 0000000..4e85685 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-atomic-policy/policy.json @@ -0,0 +1,11 @@ +{ + "checks": [ + "authored final size is checked against the locked document version", + "a rejected growth leaves revision and bytes unchanged", + "stale revision reports conflict before the policy decision", + "source CLI and HTTP share a policy and committed document", + "source policy changes behavior after the HTTP host restarts", + "fixed CLI and HTTP retain their compiled policy after source edits", + "source and fixed terminal editors preserve unsaved drafts on policy denial" + ] +} diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz new file mode 100644 index 0000000..6383ee8 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..a9977dd --- /dev/null +++ b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "c82d4815c55e44d1293204d3151aea16fd1c2a439e342337a7a04ef191787bb6", + "archive_bytes": 12966, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..013ccec Binary files /dev/null and b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..98c8fec --- /dev/null +++ b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "9dee9779e621d46c6099367cdca8dc65942cf7eb32480a06b79e577be8f7e1a9", + "archive_bytes": 32144, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz new file mode 100644 index 0000000..b82c68c Binary files /dev/null and b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..bb760ea --- /dev/null +++ b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-linux-aarch64.tar.gz.check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "3056f56b80c89e9b92755b36f4ea22a579c336ab48a32a22b9fa619eef810dd0", + "archive_bytes": 22862, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..73fdfe3 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..6b54198 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-atomic-policy/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "308766d5f31ac289ce65f0af849add3564d90a3c42148b46bc6a1db02e64a564", + "archive_bytes": 51398, + "selected_hosts": [ + "pure-edit", + "pure-edit-tui" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "relocated source-loaded TUI and CLI share UTF-8, large and many-document paths" + ] +} diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-atomic-policy/pure-source-preview.tar.gz new file mode 100644 index 0000000..0da15d4 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-atomic-policy/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-atomic-policy/source-package-check.json b/prototypes/connections/results/2026-09-24-atomic-policy/source-package-check.json new file mode 100644 index 0000000..71cd254 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-atomic-policy/source-package-check.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "e68c5336dd0d4c20b5f6eed2d3fc132cec64419498fc8028b065774770f8f47c", + "archive_bytes": 337937, + "source_files": 173, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean native TLS client and both server profiles pass authenticated document work", + "clean selected page store exports mixed revisions to the default flat store", + "clean one-MiB document fans out to two same-process Pure domains with independent conflict and release", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/cli-check.json b/prototypes/connections/results/2026-09-24-compact-runtime/cli-check.json new file mode 100644 index 0000000..3e93243 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/cli-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "c69d3934b3567338c5a9c607b2a7f5df19963f3b45d6172d5538e8444125437b", + "archive_bytes": 12885, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/cli-tui-check.json b/prototypes/connections/results/2026-09-24-compact-runtime/cli-tui-check.json new file mode 100644 index 0000000..5d70484 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/cli-tui-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "5d554fc24ce273306efe77be9e137424cac08f16c067f4691206a33030af844b", + "archive_bytes": 31971, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/cli-tui.tar.gz b/prototypes/connections/results/2026-09-24-compact-runtime/cli-tui.tar.gz new file mode 100644 index 0000000..1846344 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-compact-runtime/cli-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/cli.tar.gz b/prototypes/connections/results/2026-09-24-compact-runtime/cli.tar.gz new file mode 100644 index 0000000..b216469 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-compact-runtime/cli.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/core.json b/prototypes/connections/results/2026-09-24-compact-runtime/core.json new file mode 100644 index 0000000..63eb227 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/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": 128, + "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": "Compact release object sections and C bookkeeping, without opt-in PURE_RUNTIME_METRICS, 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": 128, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8 + }, + "sections": { + "text": 5908, + "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 4988\t 0\t 0\t 4988\t 137c\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": "bb5750a47f6f8ac4307dd633d836460d6045347880a6c53df9669e675e49929e", + "layout_binary_sha256": "04c008fb7e0b1ecc8435bce6fbc4478de6180a54da6538d8742acb9b51992fe3", + "budget_sha256": "e418419db6231305342ea8780149948e0e41e28b3afb937268e80b4695d3d5cd", + "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-24-compact-runtime/default-profile.json b/prototypes/connections/results/2026-09-24-compact-runtime/default-profile.json new file mode 100644 index 0000000..b12bfac --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/default-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35783, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "e1d49cb048b3e9c25183ab0a4cf30c55486f696c13d20a85d69e0ddc04e040c7" + }, + "pure-connections": { + "sections": { + "text": 43938, + "data": 1068, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "662e2e9dfa6b77d06781d36912b9c2d79427a95e9ec14bc174454fb3f54deecb" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/fixed-memory.json b/prototypes/connections/results/2026-09-24-compact-runtime/fixed-memory.json new file mode 100644 index 0000000..cddbe7b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/fixed-memory.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "fixed-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, the embedded prepared program, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1104, + "one_mib_edit_rss_kib": 2112, + "one_mib_show_rss_kib": 1072 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "df77304f4ab30eaee3ef41f8ac1f353d8faebcba163cfd76f0b18973e6dc16f5", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/fixed-profile.json b/prototypes/connections/results/2026-09-24-compact-runtime/fixed-profile.json new file mode 100644 index 0000000..ff98463 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/fixed-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "df77304f4ab30eaee3ef41f8ac1f353d8faebcba163cfd76f0b18973e6dc16f5" + }, + "pure-connections-fixed": { + "sections": { + "text": 26549, + "data": 1028, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "9cc73e8f8da266083e8da0628bb30441f6fa9f73317d75e65df8b5231007c746" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/source-cli-check.json b/prototypes/connections/results/2026-09-24-compact-runtime/source-cli-check.json new file mode 100644 index 0000000..78c01d5 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/source-cli-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "0d2785a8893f7cb4b9947c64eb278587f96720926163575c3ec51e106fafedc7", + "archive_bytes": 22454, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/source-cli.tar.gz b/prototypes/connections/results/2026-09-24-compact-runtime/source-cli.tar.gz new file mode 100644 index 0000000..cb6492f Binary files /dev/null and b/prototypes/connections/results/2026-09-24-compact-runtime/source-cli.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/tui-profile.json b/prototypes/connections/results/2026-09-24-compact-runtime/tui-profile.json new file mode 100644 index 0000000..986fd17 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/tui-profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app, including the bounded document browser. The raised text ceiling reviews its measured 2,541-byte addition; default CLI and core ceilings are unchanged. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS, including 1,024-key browsing, is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "4d3097c9ec3c0139cbdd82161a74d610c15d31c6ee5625101410627664f925e7", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 32015, + "data": 1036, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "3118fa11fe6ee6587bb122ae30a0d3c5cf1a8cec3131bb65b11246480c9d3b9e" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-compact-runtime/tui.json b/prototypes/connections/results/2026-09-24-compact-runtime/tui.json new file mode 100644 index 0000000..639ae64 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-compact-runtime/tui.json @@ -0,0 +1,22 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1808, + "one_mib_peak_rss_kib": 3168, + "one_thousand_key_list_peak_rss_kib": 1888, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "3118fa11fe6ee6587bb122ae30a0d3c5cf1a8cec3131bb65b11246480c9d3b9e" +} diff --git a/prototypes/connections/results/2026-09-24-copy-range-source/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-copy-range-source/pure-source-preview.tar.gz new file mode 100644 index 0000000..b0a8e7a Binary files /dev/null and b/prototypes/connections/results/2026-09-24-copy-range-source/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-copy-range-source/source-check-linux-aarch64.json b/prototypes/connections/results/2026-09-24-copy-range-source/source-check-linux-aarch64.json new file mode 100644 index 0000000..aad35c2 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range-source/source-check-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "ce48ef8e330cff470b78aec9c9e6c6d8877e4c8c371512f190b91b6036ddc8ba", + "archive_bytes": 324261, + "source_files": 166, + "checks": [ + "every extracted source file matches the manifest", + "clean C17 build and host-neutral behavior tests pass", + "clean fixed and source-loaded native terminal PTY scenarios pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean native TLS client and both server profiles pass authenticated document work", + "clean selected Linux copy path passes app, scale, size, RSS and traced one-MiB splice", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/copy-path.json b/prototypes/connections/results/2026-09-24-copy-range/copy-path.json new file mode 100644 index 0000000..5c714ed --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/copy-path.json @@ -0,0 +1,27 @@ +{ + "profile": "linux-immutable-one-mib-last-byte-splice", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "baseline": { + "binary_sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded", + "result_sha256": "06cb5e32505f66804f6e8c79e3230105ca4769444e1389ee36f6f11fd61f2d3a", + "object_pread_calls": 257, + "copy_file_range_calls": 0, + "copy_file_range_bytes": 0, + "pending_write_calls": 258, + "pending_write_bytes": 1048600, + "one_mib_splice_peak_rss_kib": 1056 + }, + "candidate": { + "binary_sha256": "52c2b828b0cba3564a37e3874f60bec793d1dd7031aff77ba850cee7683977f9", + "result_sha256": "06cb5e32505f66804f6e8c79e3230105ca4769444e1389ee36f6f11fd61f2d3a", + "object_pread_calls": 1, + "copy_file_range_calls": 1, + "copy_file_range_bytes": 1048575, + "pending_write_calls": 2, + "pending_write_bytes": 25, + "one_mib_splice_peak_rss_kib": 1056 + }, + "filesystem": "ext4", + "scratch_parent": "/home/eunhhu/work/pure/prototypes/connections/build", + "scope": "Successful object-file syscalls and child peak RSS; excludes physical device writes, page cache and proof of lower latency." +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/default-core.json b/prototypes/connections/results/2026-09-24-copy-range/default-core.json new file mode 100644 index 0000000..63eb227 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/default-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": 128, + "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": "Compact release object sections and C bookkeeping, without opt-in PURE_RUNTIME_METRICS, 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": 128, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8 + }, + "sections": { + "text": 5908, + "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 4988\t 0\t 0\t 4988\t 137c\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": "bb5750a47f6f8ac4307dd633d836460d6045347880a6c53df9669e675e49929e", + "layout_binary_sha256": "04c008fb7e0b1ecc8435bce6fbc4478de6180a54da6538d8742acb9b51992fe3", + "budget_sha256": "e418419db6231305342ea8780149948e0e41e28b3afb937268e80b4695d3d5cd", + "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-24-copy-range/default-fixed-memory.json b/prototypes/connections/results/2026-09-24-copy-range/default-fixed-memory.json new file mode 100644 index 0000000..6c6d1cd --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/default-fixed-memory.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "fixed-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, the embedded prepared program, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1104, + "one_mib_edit_rss_kib": 2128, + "one_mib_show_rss_kib": 1072 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "04974e2d3c4d7f5fb22aff67e7399aa2ebcbd85eb24da0c0e41f3ef047d5ddec", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/default-fixed-profile.json b/prototypes/connections/results/2026-09-24-copy-range/default-fixed-profile.json new file mode 100644 index 0000000..297b9b1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/default-fixed-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "04974e2d3c4d7f5fb22aff67e7399aa2ebcbd85eb24da0c0e41f3ef047d5ddec" + }, + "pure-connections-fixed": { + "sections": { + "text": 26549, + "data": 1028, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "0f1c24533177357c49270c01070a7376345763393b3914276ca028e52e8589b9" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/default-functional.json b/prototypes/connections/results/2026-09-24-copy-range/default-functional.json new file mode 100644 index 0000000..7e54edf --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/default-functional.json @@ -0,0 +1,636 @@ +{ + "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": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115420, + "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": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 1, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 113, + "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, + "idle_slot_binding": true + }, + "execution_core_test_sha256": "1e483f9c0525862041e843bba153106d7819052a787b683440d0babee80f8975", + "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": "35554177ec2109f6603d88dd4b4bdccc33497e9f229e486666f224db8e24de9b", + "editor_sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded", + "binary_sha256": "6cce6c7c18d8a24059455a2f6d4aa19c98e503a5b2e7462e2c8d3940f23bd827", + "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/store.c": "0d3df2df40fa9035cd98a1052d4c9fdad298475e9df5d49cfa7d475188fda072", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.h": "7389c1343d6253344c57d4f12bd9c8c472ed5c11d386839a1a2e3ee19f5f66ca", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/runtime.h": "e217b7acd94f3572d2780be1d6b13c5d5556227bab93b4f773e7db8b0eee6856", + "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": "8c275bf564a674f0f8232026d0d7612df48f10054af75a43af5ed0bce7653e24", + "prototypes/connections/server.c": "e8632667de6feaf4d495f87abb159775650e3e4eaf6713b9dfdc5f82807c2bf9", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "f39a7c411afa80812aec6e7a7250cdc833e97cffef8d0cce25d960fe435784f6", + "prototypes/connections/test-splice.c": "80a942fe7cc05533df346fe656c0a1f9bbbbb803d72415aa56110f089de18322", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "72d30528cb59a3a8ca8cf8cc414873fe0b26d6093cd8f06bfe9585235ba5befb", + "prototypes/connections/core-layout.c": "e3123bdf74edb99556b05d7cd3a36b6119ec30829541b0ddba2e23e5900b089d", + "prototypes/connections/check_core.py": "f80ead11764e870f0bde434c93233d518e5874fdb32f77444e7bd8bbe7398a10", + "prototypes/connections/core-budget.json": "e418419db6231305342ea8780149948e0e41e28b3afb937268e80b4695d3d5cd", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-copy-range/default-memory.json b/prototypes/connections/results/2026-09-24-copy-range/default-memory.json new file mode 100644 index 0000000..0ed783a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/default-memory.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "source-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/default-profile.json b/prototypes/connections/results/2026-09-24-copy-range/default-profile.json new file mode 100644 index 0000000..fa1c423 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/default-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35783, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded" + }, + "pure-connections": { + "sections": { + "text": 43938, + "data": 1068, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "6cce6c7c18d8a24059455a2f6d4aa19c98e503a5b2e7462e2c8d3940f23bd827" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/functional.json b/prototypes/connections/results/2026-09-24-copy-range/functional.json new file mode 100644 index 0000000..e645c99 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/functional.json @@ -0,0 +1,636 @@ +{ + "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": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115420, + "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": 12, + "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": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 1, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 113, + "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, + "idle_slot_binding": true + }, + "execution_core_test_sha256": "74e4a101d4c44c9aa6b544d6370cee570870386fb15c7444f6f077b2d975af8c", + "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": "893349eba46c9e555ce8452296d4f776020d14310fcd7a55a6e78e1ba4b592fe", + "editor_sha256": "52c2b828b0cba3564a37e3874f60bec793d1dd7031aff77ba850cee7683977f9", + "binary_sha256": "34b501dec69c00aec2c5a4aaad2f6aca57de1b782f96d894a00536a440633c38", + "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/store.c": "0d3df2df40fa9035cd98a1052d4c9fdad298475e9df5d49cfa7d475188fda072", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.h": "7389c1343d6253344c57d4f12bd9c8c472ed5c11d386839a1a2e3ee19f5f66ca", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/runtime.h": "e217b7acd94f3572d2780be1d6b13c5d5556227bab93b4f773e7db8b0eee6856", + "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": "8c275bf564a674f0f8232026d0d7612df48f10054af75a43af5ed0bce7653e24", + "prototypes/connections/server.c": "e8632667de6feaf4d495f87abb159775650e3e4eaf6713b9dfdc5f82807c2bf9", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "f39a7c411afa80812aec6e7a7250cdc833e97cffef8d0cce25d960fe435784f6", + "prototypes/connections/test-splice.c": "80a942fe7cc05533df346fe656c0a1f9bbbbb803d72415aa56110f089de18322", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "d4f130cc782730bc712268bbf4f0426bdf138ca03696fa7a32d781efded4f5cb", + "prototypes/connections/core-layout.c": "e3123bdf74edb99556b05d7cd3a36b6119ec30829541b0ddba2e23e5900b089d", + "prototypes/connections/check_core.py": "f80ead11764e870f0bde434c93233d518e5874fdb32f77444e7bd8bbe7398a10", + "prototypes/connections/core-budget.json": "e418419db6231305342ea8780149948e0e41e28b3afb937268e80b4695d3d5cd", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-copy-range/memory.json b/prototypes/connections/results/2026-09-24-copy-range/memory.json new file mode 100644 index 0000000..a1bb66d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/memory.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "source-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "52c2b828b0cba3564a37e3874f60bec793d1dd7031aff77ba850cee7683977f9", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/profile.json b/prototypes/connections/results/2026-09-24-copy-range/profile.json new file mode 100644 index 0000000..9975cfa --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 36052, + "data": 952, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "52c2b828b0cba3564a37e3874f60bec793d1dd7031aff77ba850cee7683977f9" + }, + "pure-connections": { + "sections": { + "text": 44207, + "data": 1076, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "34b501dec69c00aec2c5a4aaad2f6aca57de1b782f96d894a00536a440633c38" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-copy-range/service-scale.json b/prototypes/connections/results/2026-09-24-copy-range/service-scale.json new file mode 100644 index 0000000..6ff4bbb --- /dev/null +++ b/prototypes/connections/results/2026-09-24-copy-range/service-scale.json @@ -0,0 +1,70 @@ +{ + "profile": "document-app-many-objects-concurrent-clients", + "documents_per_variant": 1024, + "clients": 8, + "body_bytes": 8192, + "operations_per_variant": 4096, + "server_budget_bytes": 1048576, + "peak_rss_limit_kib": 4096, + "same_final_documents": "pass", + "fixed": { + "binary_sha256": "2ca0c718fb8b38bd2bb6be2a1296c5346a427ca2fa8cf12607d81f81217f085b", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1504, + "peak_rss_kib": 1664, + "idle_vm_kib": 2448, + "peak_vm_kib": 2512, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49185, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 545480, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 40848 + } + }, + "source": { + "binary_sha256": "34b501dec69c00aec2c5a4aaad2f6aca57de1b782f96d894a00536a440633c38", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1552, + "peak_rss_kib": 1712, + "idle_vm_kib": 2464, + "peak_vm_kib": 2544, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 40992, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 326641, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 41528 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-document-peer/clean-peer.log b/prototypes/connections/results/2026-09-24-document-peer/clean-peer.log new file mode 100644 index 0000000..ff472a7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/clean-peer.log @@ -0,0 +1,43 @@ +make: Entering directory '/home/eunhhu/work/pure/prototypes/connections' +python3 check_profile.py --build build/checkpoint-clean --budget peer-budget.json +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-document-peer", + "scope": "Review threshold for the explicitly selected authenticated document host on Linux AArch64/GCC 14. Includes linked application code but excludes the shared OpenSSL/libc implementations, process heap, stack, mappings and kernel buffers. Check measured whole-process RSS separately.", + "budget_sha256": "defe8efb4d544cdbf615de4d43d4ceba78fc7f1d958f245731202625052eb2ff", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-document-peer": { + "sections": { + "text": 46878, + "data": 1384, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libcrypto.so.3", + "libssl.so.3" + ], + "sha256": "e0bec4f698ce6b561a6caa8464636317409011e3101c48649cf5585c33ec9d31" + } + } +} +python3 check_document_peer.py --peer build/checkpoint-clean/pure-document-peer --editor build/checkpoint-clean/pure-edit +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7840, + "server_after_large_work_rss_kib": 9488 +} +make: Leaving directory '/home/eunhhu/work/pure/prototypes/connections' diff --git a/prototypes/connections/results/2026-09-24-document-peer/clean-release.log b/prototypes/connections/results/2026-09-24-document-peer/clean-release.log new file mode 100644 index 0000000..db3d23d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/clean-release.log @@ -0,0 +1,737 @@ +make: Entering directory '/home/eunhhu/work/pure/prototypes/connections' +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror core-layout.c -o build/checkpoint-clean/core-layout +python3 check_core.py --archive build/checkpoint-clean/libpure-execution.a --layout build/checkpoint-clean/core-layout +{ + "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/checkpoint-clean/libpure-execution.a)\n 920\t 0\t 0\t 920\t 398\tevaluate.o (ex /home/eunhhu/work/pure/prototypes/connections/build/checkpoint-clean/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" +} +python3 check_profile.py --build build/checkpoint-clean +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35807, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "pure-connections": { + "sections": { + "text": 42761, + "data": 1040, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "e76d4c0a721c9ceb7142db0edbaa1f5c44bd78a2c55029658109c83e8076875f" + } + } +} +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror -DPURE_STORE_TEST test-flow.c store.c flow.c -o build/checkpoint-clean/test-flow +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror test-program.c program.c evaluate.c -o build/checkpoint-clean/test-program +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror test-fields.c program.c evaluate.c -o build/checkpoint-clean/test-fields +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror test-runtime.c program.c runtime.c evaluate.c store.c store-port.c -o build/checkpoint-clean/test-runtime +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror -DPURE_STORE_TEST test-splice.c program.c runtime.c evaluate.c store.c store-port.c -o build/checkpoint-clean/test-splice +build/checkpoint-clean/test-flow +{"transfer":"partial-consumption-and-single-release","storage":"mapped-ranges-snapshot-budget-conflict-failure-uncertain-crash-overflow-empty"} +build/checkpoint-clean/test-program +{"program":"prepare-views-preserve-guards-types-effects-and-atomic-install","boundary_semantic_comparisons":3584} +build/checkpoint-clean/test-fields +{"fields":"named-layouts-unaligned-u32le-tail-dynamic-ranges-aliases-failures-limits","data_boundary_cases":48} +python3 check.py --server build/checkpoint-clean/pure-connections --runtime-test build/checkpoint-clean/test-runtime --splice-test build/checkpoint-clean/test-splice --editor build/checkpoint-clean/pure-edit +{ + "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": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 1, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 113, + "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": "c0767afe55bdb13e1308414116047ad76d5b105c7b4162d7220c78c1d1a4076e", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a" + } +} +make: Leaving directory '/home/eunhhu/work/pure/prototypes/connections' diff --git a/prototypes/connections/results/2026-09-24-document-peer/dynamic.txt b/prototypes/connections/results/2026-09-24-document-peer/dynamic.txt new file mode 100644 index 0000000..ff4f898 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/dynamic.txt @@ -0,0 +1,31 @@ + +Dynamic section at offset 0xfda8 contains 28 entries: + Tag Type Name/Value + 0x0000000000000001 (NEEDED) Shared library: [libssl.so.3] + 0x0000000000000001 (NEEDED) Shared library: [libcrypto.so.3] + 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] + 0x000000000000000c (INIT) 0x1c18 + 0x000000000000000d (FINI) 0x92f8 + 0x0000000000000019 (INIT_ARRAY) 0x1fd98 + 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) + 0x000000000000001a (FINI_ARRAY) 0x1fda0 + 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) + 0x000000006ffffef5 (GNU_HASH) 0x2b0 + 0x0000000000000005 (STRTAB) 0xc48 + 0x0000000000000006 (SYMTAB) 0x2d0 + 0x000000000000000a (STRSZ) 1217 (bytes) + 0x000000000000000b (SYMENT) 24 (bytes) + 0x0000000000000015 (DEBUG) 0x0 + 0x0000000000000003 (PLTGOT) 0x1ffe8 + 0x0000000000000002 (PLTRELSZ) 2256 (bytes) + 0x0000000000000014 (PLTREL) RELA + 0x0000000000000017 (JMPREL) 0x1348 + 0x0000000000000007 (RELA) 0x1258 + 0x0000000000000008 (RELASZ) 240 (bytes) + 0x0000000000000009 (RELAENT) 24 (bytes) + 0x000000006ffffffb (FLAGS_1) Flags: PIE + 0x000000006ffffffe (VERNEED) 0x11d8 + 0x000000006fffffff (VERNEEDNUM) 3 + 0x000000006ffffff0 (VERSYM) 0x110a + 0x000000006ffffff9 (RELACOUNT) 4 + 0x0000000000000000 (NULL) 0x0 diff --git a/prototypes/connections/results/2026-09-24-document-peer/file-sizes.txt b/prototypes/connections/results/2026-09-24-document-peer/file-sizes.txt new file mode 100644 index 0000000..c625025 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/file-sizes.txt @@ -0,0 +1,2 @@ +68312 /tmp/pure-document-peer.stripped +213080 prototypes/connections/build/pure-document-peer diff --git a/prototypes/connections/results/2026-09-24-document-peer/hashes.txt b/prototypes/connections/results/2026-09-24-document-peer/hashes.txt new file mode 100644 index 0000000..0546b43 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/hashes.txt @@ -0,0 +1,4 @@ +7d6f7cac8422a800fa4407e76c29ce21aa0931020ee2f4aa7b0ae646b88aa034 prototypes/connections/document-peer.c +df259073c5e1cd911495b48d4ccd43c8346d955a19a8773964abe58089fbebf9 prototypes/common/tls-link.c +297a5d41b129ad134f5a398069565a1de0793024e6aa483def1dc1e09ebd1443 prototypes/common/tls-link.h +e0bec4f698ce6b561a6caa8464636317409011e3101c48649cf5585c33ec9d31 prototypes/connections/build/pure-document-peer diff --git a/prototypes/connections/results/2026-09-24-document-peer/package-check.json b/prototypes/connections/results/2026-09-24-document-peer/package-check.json new file mode 100644 index 0000000..a6031fc --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/package-check.json @@ -0,0 +1,19 @@ +{ + "archive_sha256": "dc3f7339bf5aa22b11eb5ac4c8b4a547168e0e06e65c8003a545f70a31500081", + "archive_bytes": 316927, + "selected_hosts": [ + "pure-edit", + "pure-connections", + "pure-document-peer", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted C/Python TLS peers share a 1 MiB document and recover", + "extracted GTK window, CLI and HTTP adapter edit the same document" + ], + "peer_idle_rss_kib": 7840, + "peer_after_large_work_rss_kib": 9488 +} diff --git a/prototypes/connections/results/2026-09-24-document-peer/package-hashes.txt b/prototypes/connections/results/2026-09-24-document-peer/package-hashes.txt new file mode 100644 index 0000000..825f3e9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/package-hashes.txt @@ -0,0 +1,3 @@ +dc3f7339bf5aa22b11eb5ac4c8b4a547168e0e06e65c8003a545f70a31500081 dist/pure-document-linux-aarch64.tar.gz +dc3f7339bf5aa22b11eb5ac4c8b4a547168e0e06e65c8003a545f70a31500081 /tmp/pure-document-repro.tar.gz +dc3f7339bf5aa22b11eb5ac4c8b4a547168e0e06e65c8003a545f70a31500081 prototypes/connections/results/2026-09-24-document-peer/pure-document-linux-aarch64.tar.gz diff --git a/prototypes/connections/results/2026-09-24-document-peer/peer-budget.json b/prototypes/connections/results/2026-09-24-document-peer/peer-budget.json new file mode 100644 index 0000000..369a71c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/peer-budget.json @@ -0,0 +1,23 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-document-peer", + "scope": "Review threshold for the explicitly selected authenticated document host on Linux AArch64/GCC 14. Includes linked application code but excludes the shared OpenSSL/libc implementations, process heap, stack, mappings and kernel buffers. Check measured whole-process RSS separately.", + "budget_sha256": "defe8efb4d544cdbf615de4d43d4ceba78fc7f1d958f245731202625052eb2ff", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-document-peer": { + "sections": { + "text": 46878, + "data": 1384, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libcrypto.so.3", + "libssl.so.3" + ], + "sha256": "e0bec4f698ce6b561a6caa8464636317409011e3101c48649cf5585c33ec9d31" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-document-peer/peer-flow.json b/prototypes/connections/results/2026-09-24-document-peer/peer-flow.json new file mode 100644 index 0000000..32f3991 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/peer-flow.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7840, + "server_after_large_work_rss_kib": 9488 +} diff --git a/prototypes/connections/results/2026-09-24-document-peer/peer-sanitized.json b/prototypes/connections/results/2026-09-24-document-peer/peer-sanitized.json new file mode 100644 index 0000000..c1e698c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/peer-sanitized.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 22592, + "server_after_large_work_rss_kib": 31632 +} diff --git a/prototypes/connections/results/2026-09-24-document-peer/pure-document-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-document-peer/pure-document-linux-aarch64.tar.gz new file mode 100644 index 0000000..006e71d Binary files /dev/null and b/prototypes/connections/results/2026-09-24-document-peer/pure-document-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-document-peer/scalar-refactor.json b/prototypes/connections/results/2026-09-24-document-peer/scalar-refactor.json new file mode 100644 index 0000000..a1cf089 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/scalar-refactor.json @@ -0,0 +1 @@ +{"alpn": "pass", "disconnect_release": "pass", "idle_deadline": "pass", "malformed_frames": "pass", "mutual_authentication": "pass", "persist_restart": "pass", "python_client": "pass", "revision_conflict": "pass", "server_connected_rss_kib": 8400, "server_hostname": "pass", "server_idle_rss_kib": 7824} diff --git a/prototypes/connections/results/2026-09-24-document-peer/size.txt b/prototypes/connections/results/2026-09-24-document-peer/size.txt new file mode 100644 index 0000000..a37699e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-document-peer/size.txt @@ -0,0 +1,2 @@ + text data bss dec hex filename + 46878 1384 16 48278 bc96 prototypes/connections/build/pure-document-peer diff --git a/prototypes/connections/results/2026-09-24-fixed-http/backpressure.json b/prototypes/connections/results/2026-09-24-fixed-http/backpressure.json new file mode 100644 index 0000000..5723e36 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/backpressure.json @@ -0,0 +1,61 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "slow_consumer_budget_and_release": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "limited_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048586, + "server_structure_bytes": 40848 + }, + "fixed_sha256": "02acf468f18610332d3fe4b9c093b2bbff08496000dbae2b61151d7f3a7a46fa", + "source_sha256": "e76d4c0a721c9ceb7142db0edbaa1f5c44bd78a2c55029658109c83e8076875f" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-http/final.json b/prototypes/connections/results/2026-09-24-fixed-http/final.json new file mode 100644 index 0000000..1594bfa --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/final.json @@ -0,0 +1,65 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "slow_consumer_budget_and_release": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 3983, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "limited_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048586, + "server_structure_bytes": 40848 + }, + "fixed_idle_rss_kib": 1520, + "source_idle_rss_kib": 1568, + "fixed_after_large_rss_kib": 1648, + "source_after_large_rss_kib": 1696, + "fixed_sha256": "02acf468f18610332d3fe4b9c093b2bbff08496000dbae2b61151d7f3a7a46fa", + "source_sha256": "e76d4c0a721c9ceb7142db0edbaa1f5c44bd78a2c55029658109c83e8076875f" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-http/fixed-profile.json b/prototypes/connections/results/2026-09-24-fixed-http/fixed-profile.json new file mode 100644 index 0000000..3b3afbf --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/fixed-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8" + }, + "pure-connections-fixed": { + "sections": { + "text": 25228, + "data": 1000, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "02acf468f18610332d3fe4b9c093b2bbff08496000dbae2b61151d7f3a7a46fa" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-fixed-http/package.json b/prototypes/connections/results/2026-09-24-fixed-http/package.json new file mode 100644 index 0000000..7bea960 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "dd1c29ca367271c5be88275695886e3d2b038b20565c016dc63bd761ed62c76e", + "archive_bytes": 31733, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-fixed-http/parity.json b/prototypes/connections/results/2026-09-24-fixed-http/parity.json new file mode 100644 index 0000000..7566d43 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/parity.json @@ -0,0 +1,43 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "fixed_sha256": "02acf468f18610332d3fe4b9c093b2bbff08496000dbae2b61151d7f3a7a46fa", + "source_sha256": "e76d4c0a721c9ceb7142db0edbaa1f5c44bd78a2c55029658109c83e8076875f" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-http/peak-memory.json b/prototypes/connections/results/2026-09-24-fixed-http/peak-memory.json new file mode 100644 index 0000000..11902b5 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/peak-memory.json @@ -0,0 +1,67 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "slow_consumer_budget_and_release": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "limited_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048586, + "server_structure_bytes": 40848 + }, + "fixed_idle_rss_kib": 1520, + "source_idle_rss_kib": 1552, + "fixed_after_large_rss_kib": 1648, + "source_after_large_rss_kib": 1680, + "fixed_peak_rss_kib": 2176, + "source_peak_rss_kib": 2096, + "fixed_sha256": "02acf468f18610332d3fe4b9c093b2bbff08496000dbae2b61151d7f3a7a46fa", + "source_sha256": "f39eaa24cad84a3ba34a5dd4b1c5bd6c23cdf18862ee3bb9bd6ba27cdba3f895" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-http/pure-document-fixed-http-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-fixed-http/pure-document-fixed-http-linux-aarch64.tar.gz new file mode 100644 index 0000000..c53ece6 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-fixed-http/pure-document-fixed-http-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-fixed-http/source-profile.json b/prototypes/connections/results/2026-09-24-fixed-http/source-profile.json new file mode 100644 index 0000000..50b7913 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/source-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35807, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "bce466328ac730ccb7cd22c7a0c4e2960bc94a0f20ac9f036e232bd499521ff7" + }, + "pure-connections": { + "sections": { + "text": 43009, + "data": 1040, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "f39eaa24cad84a3ba34a5dd4b1c5bd6c23cdf18862ee3bb9bd6ba27cdba3f895" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-fixed-http/verified.json b/prototypes/connections/results/2026-09-24-fixed-http/verified.json new file mode 100644 index 0000000..93a7ff3 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-http/verified.json @@ -0,0 +1,68 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "slow_consumer_budget_and_release": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "limited_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048586, + "server_structure_bytes": 40848 + }, + "fixed_idle_rss_kib": 1520, + "source_idle_rss_kib": 1552, + "fixed_after_large_rss_kib": 1648, + "source_after_large_rss_kib": 1680, + "fixed_peak_rss_kib": 2128, + "source_peak_rss_kib": 1680, + "fixed_peak_rss_limit_kib": 4096, + "fixed_sha256": "02acf468f18610332d3fe4b9c093b2bbff08496000dbae2b61151d7f3a7a46fa", + "source_sha256": "f39eaa24cad84a3ba34a5dd4b1c5bd6c23cdf18862ee3bb9bd6ba27cdba3f895" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/memory-final.json b/prototypes/connections/results/2026-09-24-fixed-program/memory-final.json new file mode 100644 index 0000000..a85054d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/memory-final.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "fixed-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, the embedded prepared program, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1104, + "one_mib_edit_rss_kib": 2128, + "one_mib_show_rss_kib": 1088 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "00f0316cb318cf168d4437080642fe1a6db013e7d4ad82be80e57ad9e12a0a3b", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/memory-verified.json b/prototypes/connections/results/2026-09-24-fixed-program/memory-verified.json new file mode 100644 index 0000000..7ccf6ea --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/memory-verified.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "fixed-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, the embedded prepared program, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1104, + "one_mib_edit_rss_kib": 2112, + "one_mib_show_rss_kib": 1088 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/memory.json b/prototypes/connections/results/2026-09-24-fixed-program/memory.json new file mode 100644 index 0000000..a4474ff --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/memory.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "fixed-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1088, + "one_mib_edit_rss_kib": 2128, + "one_mib_show_rss_kib": 1104 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "00f0316cb318cf168d4437080642fe1a6db013e7d4ad82be80e57ad9e12a0a3b", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/package-final.json b/prototypes/connections/results/2026-09-24-fixed-program/package-final.json new file mode 100644 index 0000000..385f64a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/package-final.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "84aaf54f6471ddf5cb6fe735c2c3621baf567e186e7252f87e2930024d89ec58", + "archive_bytes": 14385, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/package.json b/prototypes/connections/results/2026-09-24-fixed-program/package.json new file mode 100644 index 0000000..6671dd5 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/package.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "5514e75fa29a9e2375e6ab5069a4b22f275d447725e63cb8b5c7245a9c966d7c", + "archive_bytes": 14386, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/parity-final-v2.json b/prototypes/connections/results/2026-09-24-fixed-program/parity-final-v2.json new file mode 100644 index 0000000..73c6a66 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/parity-final-v2.json @@ -0,0 +1,12 @@ +{ + "profile": "build-time-prepared-native-document-runner", + "editor_create_edit_conflict_large_restore": "pass", + "dynamic_selection_and_invalid_range": "pass", + "nested_read_and_required_extent": "pass", + "invalid_compilation_preserves_previous_plan": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8", + "source_sha256": "bce466328ac730ccb7cd22c7a0c4e2960bc94a0f20ac9f036e232bd499521ff7", + "selection_sha256": "7d3394586fc9837a11a01bdae3aeeba2356b3abab8518e84545ede2ec1a6ab1c", + "excerpt_sha256": "b83a1fe42d45574ddf5941fdac2b759754d44da5705e6f33e5a45dabf7fd7205" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/parity-final.json b/prototypes/connections/results/2026-09-24-fixed-program/parity-final.json new file mode 100644 index 0000000..6aa3d12 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/parity-final.json @@ -0,0 +1,11 @@ +{ + "profile": "build-time-prepared-native-document-runner", + "editor_create_edit_conflict_large_restore": "pass", + "dynamic_selection_and_invalid_range": "pass", + "nested_read_and_required_extent": "pass", + "invalid_compilation_preserves_previous_plan": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_sha256": "00f0316cb318cf168d4437080642fe1a6db013e7d4ad82be80e57ad9e12a0a3b", + "source_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03", + "selection_sha256": "836f883efb9dc86218c9606292b6c37241cc98277ce3ece03597795e076427a2" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/parity-reconfigured.json b/prototypes/connections/results/2026-09-24-fixed-program/parity-reconfigured.json new file mode 100644 index 0000000..561a923 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/parity-reconfigured.json @@ -0,0 +1,13 @@ +{ + "profile": "build-time-prepared-native-document-runner", + "editor_create_edit_conflict_large_restore": "pass", + "dynamic_selection_and_invalid_range": "pass", + "nested_read_and_required_extent": "pass", + "invalid_compilation_preserves_previous_plan": "pass", + "same_directory_program_switch_rebuilds": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8", + "source_sha256": "bce466328ac730ccb7cd22c7a0c4e2960bc94a0f20ac9f036e232bd499521ff7", + "selection_sha256": "7d3394586fc9837a11a01bdae3aeeba2356b3abab8518e84545ede2ec1a6ab1c", + "excerpt_sha256": "b83a1fe42d45574ddf5941fdac2b759754d44da5705e6f33e5a45dabf7fd7205" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/parity-release.json b/prototypes/connections/results/2026-09-24-fixed-program/parity-release.json new file mode 100644 index 0000000..994b69d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/parity-release.json @@ -0,0 +1,14 @@ +{ + "profile": "build-time-prepared-native-document-runner", + "editor_create_edit_conflict_large_restore": "pass", + "dynamic_selection_and_invalid_range": "pass", + "nested_read_and_required_extent": "pass", + "invalid_compilation_preserves_previous_plan": "pass", + "compiler_rejects_source_as_output": "pass", + "same_directory_program_switch_rebuilds": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8", + "source_sha256": "bce466328ac730ccb7cd22c7a0c4e2960bc94a0f20ac9f036e232bd499521ff7", + "selection_sha256": "7d3394586fc9837a11a01bdae3aeeba2356b3abab8518e84545ede2ec1a6ab1c", + "excerpt_sha256": "b83a1fe42d45574ddf5941fdac2b759754d44da5705e6f33e5a45dabf7fd7205" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/parity-verified.json b/prototypes/connections/results/2026-09-24-fixed-program/parity-verified.json new file mode 100644 index 0000000..1350cfe --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/parity-verified.json @@ -0,0 +1,12 @@ +{ + "profile": "build-time-prepared-native-document-runner", + "editor_create_edit_conflict_large_restore": "pass", + "dynamic_selection_and_invalid_range": "pass", + "nested_read_and_required_extent": "pass", + "invalid_compilation_preserves_previous_plan": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_sha256": "00f0316cb318cf168d4437080642fe1a6db013e7d4ad82be80e57ad9e12a0a3b", + "source_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03", + "selection_sha256": "836f883efb9dc86218c9606292b6c37241cc98277ce3ece03597795e076427a2", + "excerpt_sha256": "221543dd8cee08b19c862d11d7a7f9c9c55d974964fb8fc88bf63792d96e14b9" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/parity.json b/prototypes/connections/results/2026-09-24-fixed-program/parity.json new file mode 100644 index 0000000..76e1469 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/parity.json @@ -0,0 +1,10 @@ +{ + "profile": "build-time-prepared-native-document-runner", + "editor_create_edit_conflict_large_restore": "pass", + "dynamic_selection_and_invalid_range": "pass", + "invalid_compilation_preserves_previous_plan": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_sha256": "00f0316cb318cf168d4437080642fe1a6db013e7d4ad82be80e57ad9e12a0a3b", + "source_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03", + "selection_sha256": "836f883efb9dc86218c9606292b6c37241cc98277ce3ece03597795e076427a2" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/profile-final.json b/prototypes/connections/results/2026-09-24-fixed-program/profile-final.json new file mode 100644 index 0000000..e6e530d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/profile-final.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked document runner with one build-time prepared app. The source parser and compiler are separate build tools. This 20 KiB text ceiling applies only to the recorded Linux AArch64/GCC 14 toolchain and excludes libc, heap, stack, and storage files.", + "budget_sha256": "e6f4e727172d1d75bf109f426e169c98fa635c9cc4757ffd3871035593aa00d8", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/profile.json b/prototypes/connections/results/2026-09-24-fixed-program/profile.json new file mode 100644 index 0000000..771271e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked document runner with one build-time prepared app. The source parser and compiler are separate build tools. This 20 KiB text ceiling applies only to the recorded Linux AArch64/GCC 14 toolchain and excludes libc, heap, stack, and storage files.", + "budget_sha256": "e6f4e727172d1d75bf109f426e169c98fa635c9cc4757ffd3871035593aa00d8", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "00f0316cb318cf168d4437080642fe1a6db013e7d4ad82be80e57ad9e12a0a3b" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-final-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-final-linux-aarch64.tar.gz new file mode 100644 index 0000000..511d53f Binary files /dev/null and b/prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-final-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-linux-aarch64.tar.gz new file mode 100644 index 0000000..ea09bcb Binary files /dev/null and b/prototypes/connections/results/2026-09-24-fixed-program/pure-document-fixed-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-fixed-program/window-regression.json b/prototypes/connections/results/2026-09-24-fixed-program/window-regression.json new file mode 100644 index 0000000..44483d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-program/window-regression.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_line_width": 80, "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [47328, 48048], "large_window_rss_kib": 44112, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui-final/package.json b/prototypes/connections/results/2026-09-24-fixed-tui-final/package.json new file mode 100644 index 0000000..86651ee --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui-final/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "3c5aa21f36a410bcef5b42c4928b2b24e35a55f7b6d5e62d322b749b48512db6", + "archive_bytes": 33654, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui-final/profile.json b/prototypes/connections/results/2026-09-24-fixed-tui-final/profile.json new file mode 100644 index 0000000..52ebb29 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui-final/profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "e378cb302b2a4bcd2ee674af048116f139b214b59f24244d6c825bc71a8e8a31", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 28226, + "data": 988, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "dd6fb1f48a0212b59c4b997bd72ce099ad53f0b2d0d63cdebbc5264fc9bf23f5" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui-final/pty-flow.json b/prototypes/connections/results/2026-09-24-fixed-tui-final/pty-flow.json new file mode 100644 index 0000000..8e134d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui-final/pty-flow.json @@ -0,0 +1,16 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "UTF-8 codepoint movement and edit", + "resize redraw", + "external revision conflict preserves draft", + "one MiB document edit and fresh CLI restore", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1744, + "one_mib_peak_rss_kib": 3168, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "dd6fb1f48a0212b59c4b997bd72ce099ad53f0b2d0d63cdebbc5264fc9bf23f5" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui-final/pty-sanitized.json b/prototypes/connections/results/2026-09-24-fixed-tui-final/pty-sanitized.json new file mode 100644 index 0000000..a5ca7da --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui-final/pty-sanitized.json @@ -0,0 +1,16 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "UTF-8 codepoint movement and edit", + "resize redraw", + "external revision conflict preserves draft", + "one MiB document edit and fresh CLI restore", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 9840, + "one_mib_peak_rss_kib": 11712, + "one_mib_peak_rss_limit_kib": 0, + "binary_sha256": "746ebdfa531accd578ed04132a6d4155a0c8cf44842f30c46cc49ef804f19615" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui-final/pure-document-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-fixed-tui-final/pure-document-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..596d28b Binary files /dev/null and b/prototypes/connections/results/2026-09-24-fixed-tui-final/pure-document-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/package-cursor.json b/prototypes/connections/results/2026-09-24-fixed-tui/package-cursor.json new file mode 100644 index 0000000..ae2716d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui/package-cursor.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "0e7d03741e17cc2f39db95aa28e52b8fdd02a54b68c8e978df06407cc241c2bb", + "archive_bytes": 33625, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/package.json b/prototypes/connections/results/2026-09-24-fixed-tui/package.json new file mode 100644 index 0000000..ae2716d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "0e7d03741e17cc2f39db95aa28e52b8fdd02a54b68c8e978df06407cc241c2bb", + "archive_bytes": 33625, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/profile.json b/prototypes/connections/results/2026-09-24-fixed-tui/profile.json new file mode 100644 index 0000000..acc451b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui/profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "e378cb302b2a4bcd2ee674af048116f139b214b59f24244d6c825bc71a8e8a31", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 28098, + "data": 988, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "ae3f8ff479899929cb108dd3435750aa24b8f793744a9e7794315974182ad1f7" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-flow.json b/prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-flow.json new file mode 100644 index 0000000..c85112d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-flow.json @@ -0,0 +1,16 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "UTF-8 codepoint movement and edit", + "resize redraw", + "external revision conflict preserves draft", + "one MiB document edit and fresh CLI restore", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1744, + "one_mib_peak_rss_kib": 3168, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "ae3f8ff479899929cb108dd3435750aa24b8f793744a9e7794315974182ad1f7" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-sanitized.json b/prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-sanitized.json new file mode 100644 index 0000000..7852c5d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui/pty-cursor-sanitized.json @@ -0,0 +1,16 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "UTF-8 codepoint movement and edit", + "resize redraw", + "external revision conflict preserves draft", + "one MiB document edit and fresh CLI restore", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 9808, + "one_mib_peak_rss_kib": 11760, + "one_mib_peak_rss_limit_kib": 0, + "binary_sha256": "71eeef473231fe7b693022fad39fe3b9b90e89913bbc252a2b64e6d087b1ebf6" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/pty-flow.json b/prototypes/connections/results/2026-09-24-fixed-tui/pty-flow.json new file mode 100644 index 0000000..50f5cfa --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui/pty-flow.json @@ -0,0 +1,16 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "create and save through terminal", + "open another key", + "UTF-8 edit and save", + "resize redraw", + "external revision conflict preserves draft", + "one MiB document edit and fresh CLI restore", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1744, + "one_mib_peak_rss_kib": 2784, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "ae3f8ff479899929cb108dd3435750aa24b8f793744a9e7794315974182ad1f7" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/pty-sanitized.json b/prototypes/connections/results/2026-09-24-fixed-tui/pty-sanitized.json new file mode 100644 index 0000000..443e9cd --- /dev/null +++ b/prototypes/connections/results/2026-09-24-fixed-tui/pty-sanitized.json @@ -0,0 +1,16 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "create and save through terminal", + "open another key", + "UTF-8 edit and save", + "resize redraw", + "external revision conflict preserves draft", + "one MiB document edit and fresh CLI restore", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 9776, + "one_mib_peak_rss_kib": 11712, + "one_mib_peak_rss_limit_kib": 0, + "binary_sha256": "71eeef473231fe7b693022fad39fe3b9b90e89913bbc252a2b64e6d087b1ebf6" +} diff --git a/prototypes/connections/results/2026-09-24-fixed-tui/pure-document-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-fixed-tui/pure-document-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..05a8713 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-fixed-tui/pure-document-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-flat-export-source/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-flat-export-source/pure-source-preview.tar.gz new file mode 100644 index 0000000..096ffe4 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-flat-export-source/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-flat-export-source/source-check-linux-aarch64.json b/prototypes/connections/results/2026-09-24-flat-export-source/source-check-linux-aarch64.json new file mode 100644 index 0000000..854f2d0 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-flat-export-source/source-check-linux-aarch64.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "16126434bdbef5b75210639cf67f5e27711aa7c540ba1ca6d3949dd45009df9d", + "archive_bytes": 329118, + "source_files": 168, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean selected page store exports mixed revisions to the default flat store", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-flat-export/export-check.json b/prototypes/connections/results/2026-09-24-flat-export/export-check.json new file mode 100644 index 0000000..c64ad9e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-flat-export/export-check.json @@ -0,0 +1,27 @@ +{ + "objects": 5, + "logical_bytes": 1081346, + "document_digest": "efaa71ebcdaf493723b82bc98532aba3fd5beace476f8b9f61f93706bddf850b", + "paged_reader_sha256": "f29440fcc167d1202b226f0faee450cb299989c8977277e062a330587cec5e39", + "flat_reader_sha256": "04974e2d3c4d7f5fb22aff67e7399aa2ebcbd85eb24da0c0e41f3ef047d5ddec", + "passed": true, + "extra_documents": 0, + "source_revisions": { + "empty": 1, + "small": 1, + "boundary": 1, + "large": 1, + "megabyte": 66 + }, + "readback_sha256": { + "empty": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "small": "4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015", + "boundary": "1bd4db450abc8914c2fac721cace2704ff4c16028e6d07293154dad289835694", + "large": "b10b6985790c800b653eeb8e979f9c1551293ee9c293cbca1c4741df09cc8284", + "megabyte": "3cacf45be36c19ea18974a474bd2c668f24622b9132ab99294b3778ba0124094" + }, + "flat_edit_after_export": "revision 67", + "writer_busy_during_export_and_resumed_after": true, + "partial_export_not_published": true, + "existing_target_and_symlink_rejected": true +} diff --git a/prototypes/connections/results/2026-09-24-flat-export/export-ext4.json b/prototypes/connections/results/2026-09-24-flat-export/export-ext4.json new file mode 100644 index 0000000..3fc4829 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-flat-export/export-ext4.json @@ -0,0 +1,28 @@ +{ + "objects": 5, + "logical_bytes": 1081346, + "document_digest": "efaa71ebcdaf493723b82bc98532aba3fd5beace476f8b9f61f93706bddf850b", + "paged_reader_sha256": "f29440fcc167d1202b226f0faee450cb299989c8977277e062a330587cec5e39", + "flat_reader_sha256": "04974e2d3c4d7f5fb22aff67e7399aa2ebcbd85eb24da0c0e41f3ef047d5ddec", + "passed": true, + "extra_documents": 0, + "scratch_parent": "/home/eunhhu/work/pure/prototypes/connections/build", + "source_revisions": { + "empty": 1, + "small": 1, + "boundary": 1, + "large": 1, + "megabyte": 66 + }, + "readback_sha256": { + "empty": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "small": "4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015", + "boundary": "1bd4db450abc8914c2fac721cace2704ff4c16028e6d07293154dad289835694", + "large": "b10b6985790c800b653eeb8e979f9c1551293ee9c293cbca1c4741df09cc8284", + "megabyte": "3cacf45be36c19ea18974a474bd2c668f24622b9132ab99294b3778ba0124094" + }, + "flat_edit_after_export": "revision 67", + "writer_busy_during_export_and_resumed_after": true, + "partial_export_not_published": true, + "existing_target_and_symlink_rejected": true +} diff --git a/prototypes/connections/results/2026-09-24-flat-export/export-scale-1024.json b/prototypes/connections/results/2026-09-24-flat-export/export-scale-1024.json new file mode 100644 index 0000000..20a2d33 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-flat-export/export-scale-1024.json @@ -0,0 +1,27 @@ +{ + "objects": 1029, + "logical_bytes": 1212418, + "document_digest": "59dabf364a2b8d06e74c275d3679a15b71095544981e90ccbb0932b096c380d1", + "paged_reader_sha256": "f29440fcc167d1202b226f0faee450cb299989c8977277e062a330587cec5e39", + "flat_reader_sha256": "04974e2d3c4d7f5fb22aff67e7399aa2ebcbd85eb24da0c0e41f3ef047d5ddec", + "passed": true, + "extra_documents": 1024, + "source_revisions": { + "empty": 1, + "small": 1, + "boundary": 1, + "large": 1, + "megabyte": 66 + }, + "readback_sha256": { + "empty": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "small": "4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015", + "boundary": "1bd4db450abc8914c2fac721cace2704ff4c16028e6d07293154dad289835694", + "large": "b10b6985790c800b653eeb8e979f9c1551293ee9c293cbca1c4741df09cc8284", + "megabyte": "3cacf45be36c19ea18974a474bd2c668f24622b9132ab99294b3778ba0124094" + }, + "flat_edit_after_export": "revision 67", + "writer_busy_during_export_and_resumed_after": true, + "partial_export_not_published": true, + "existing_target_and_symlink_rejected": true +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-asan.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-asan.json new file mode 100644 index 0000000..9bd2b7c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-asan.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": 1, + "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": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 42, + "commits": 11, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115421, + "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": 60, + "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": 14, + "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": 45, + "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": 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": 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": 15, + "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": "bf418bdfd09f865d327c42bd598c3f251138f19e7e6ae0031c4b497affc55002", + "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": "4ae184e15f9f4120333882bd90ea15668bbdaf1cf4fa39f3eb76ba311d129061", + "editor_sha256": "4f2875d8cc8ccd170d4d959f2491c124b1ac11b1debb38ba7f318be1982c8fb6", + "binary_sha256": "ae727ef73c5118acb3a96337ffee7f990f061c597aa22d3d818b1865da06ca6c", + "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/store-paged.c": "da01d9e0e4607ac78d9b057431e1ae6d9bd9aeb9036178b388a1bf1fbe2e7b17", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "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": "584f6671c2daf40ee3cc4233760b0004d5035bd0e0cd10aa0fe6a0b814f66937", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-hybrid-storage-v2/app-release.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-release.json new file mode 100644 index 0000000..ca41eb7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/app-release.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": 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": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11753, + "borrowed_output_bytes": 2115420, + "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": 34, + "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": 15, + "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": "b9642410766ffc6dad9ed3ac38ce9ef550e3a861deb21a4e53dea219687894b6", + "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": "a7e9ec3814b9adf81254bb781a98ac5c2896f69f665e80307f5ec1d824d1089f", + "editor_sha256": "e024d1829ea1a2211c58c33b8ae457794cbda0dbf927cdcb1126cd4de13a16e9", + "binary_sha256": "09b8fb1695ea4ff3a326e75660e77a4e5532f908de7e6fb01a9dec6c840a0d53", + "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/store-paged.c": "da01d9e0e4607ac78d9b057431e1ae6d9bd9aeb9036178b388a1bf1fbe2e7b17", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "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": "584f6671c2daf40ee3cc4233760b0004d5035bd0e0cd10aa0fe6a0b814f66937", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-hybrid-storage-v2/gtk-flow.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/gtk-flow.json new file mode 100644 index 0000000..233cfc4 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/gtk-flow.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 43632, "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/memory.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/memory.json new file mode 100644 index 0000000..e2068b8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/memory.json @@ -0,0 +1,14 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "e024d1829ea1a2211c58c33b8ae457794cbda0dbf927cdcb1126cd4de13a16e9", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-full.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-full.json new file mode 100644 index 0000000..a753696 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-full.json @@ -0,0 +1,21 @@ +{ + "archive_sha256": "40e635bdd6746e13c1073a1d809d8753ddfab94cf973be2771ea9f8ffff0fb3c", + "archive_bytes": 114025, + "selected_hosts": [ + "pure-edit", + "pure-connections", + "pure-document-peer", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "selected storage keeps small values inline and maps large values", + "editing the packaged .pure source changes the executed reply", + "extracted HTTP host updates the same document as the native CLI", + "extracted C/Python TLS peers share a 1 MiB document and recover", + "extracted GTK window and selected external host edit the same document" + ], + "peer_idle_rss_kib": 7840, + "peer_after_large_work_rss_kib": 9504 +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-micro.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-micro.json new file mode 100644 index 0000000..dccd086 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/package-micro.json @@ -0,0 +1,13 @@ +{ + "archive_sha256": "0d3a5da21ebd94cd235259ccfee9173c44afee57e57514e05acc1a43beea8a36", + "archive_bytes": 26045, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "selected storage keeps small values inline and maps large values", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/peer.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/peer.json new file mode 100644 index 0000000..99de24a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/peer.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7856, + "server_after_large_work_rss_kib": 9520 +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/profile.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/profile.json new file mode 100644 index 0000000..c0ae382 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-hybrid-document-storage", + "scope": "Opt-in inline/PUREP001 document store on Linux AArch64/GCC 14. The CLI text ceiling is 42 KiB: 2 KiB above the original selected-page gate because the measured 41064-byte dual-format build fails 40 KiB. This is an explicit research budget, not the default micro-runtime budget. Direct dependencies only; excludes libc, process RAM, filesystem cache and journal, optional TLS/GTK.", + "budget_sha256": "038a323d8492731611dfc56e799e7da751946fe0ea5dc428550ff02ffd781b29", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 41064, + "data": 1000, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "e024d1829ea1a2211c58c33b8ae457794cbda0dbf927cdcb1126cd4de13a16e9" + }, + "pure-connections": { + "sections": { + "text": 47928, + "data": 1088, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "09b8fb1695ea4ff3a326e75660e77a4e5532f908de7e6fb01a9dec6c840a0d53" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-authoring-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-authoring-linux-aarch64.tar.gz new file mode 100644 index 0000000..20dec18 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-authoring-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-micro-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-micro-linux-aarch64.tar.gz new file mode 100644 index 0000000..0234d2b Binary files /dev/null and b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/pure-document-hybrid-micro-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/small-scan.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/small-scan.json new file mode 100644 index 0000000..4dda494 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/small-scan.json @@ -0,0 +1,5 @@ +{ + "small_edit_store_directory_scans": 0, + "unrelated_directory_entries": 256, + "binary_sha256": "e024d1829ea1a2211c58c33b8ae457794cbda0dbf927cdcb1126cd4de13a16e9" +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit-asan.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit-asan.json new file mode 100644 index 0000000..8da4fd1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit-asan.json @@ -0,0 +1 @@ +{"hybrid_storage":"snapshot-patch-range-compaction-crash-conflict-uncertain-inline-transitions","mixed_edits":128} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit.json new file mode 100644 index 0000000..8da4fd1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/unit.json @@ -0,0 +1 @@ +{"hybrid_storage":"snapshot-patch-range-compaction-crash-conflict-uncertain-inline-transitions","mixed_edits":128} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage-v2/write-cost.json b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/write-cost.json new file mode 100644 index 0000000..266c4c8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage-v2/write-cost.json @@ -0,0 +1,49 @@ +{ + "profile": "linux-aarch64-16k-page-document-replacement", + "candidate_mode": "hybrid", + "workloads": [ + "replace last byte of 1 MiB document", + "replace one-byte document" + ], + "unit": "successful user-space write syscall bytes targeting object files", + "excludes": "physical device writes, filesystem journal, metadata and directory sync cost", + "one_mib": { + "flat": { + "object_write_syscall_bytes": 1048600, + "object_write_calls": 258, + "object_files": { + "obj-large": 1048600 + }, + "binary_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "hybrid": { + "object_write_syscall_bytes": 16944, + "object_write_calls": 3, + "object_files": { + "obj-large": 560, + "dat-large-0000000000000001": 1064960 + }, + "binary_sha256": "e024d1829ea1a2211c58c33b8ae457794cbda0dbf927cdcb1126cd4de13a16e9" + }, + "flat_to_hybrid_ratio": 61.8862134088763 + }, + "one_byte": { + "flat": { + "object_write_syscall_bytes": 25, + "object_write_calls": 2, + "object_files": { + "obj-tiny": 25 + }, + "binary_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "hybrid": { + "object_write_syscall_bytes": 25, + "object_write_calls": 2, + "object_files": { + "obj-tiny": 25 + }, + "binary_sha256": "e024d1829ea1a2211c58c33b8ae457794cbda0dbf927cdcb1126cd4de13a16e9" + }, + "hybrid_to_flat_ratio": 1.0 + } +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/app-asan.json b/prototypes/connections/results/2026-09-24-hybrid-storage/app-asan.json new file mode 100644 index 0000000..77ff376 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/app-asan.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": 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": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115420, + "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": "03ae899315468bf32215398b094b120c528c36e6337e6da5ebb3435204c2ab82", + "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": "f4761e52b1b65af4b7fd89f3eaa456ca69aa5c0e61e3ade24b0b04c9377af974", + "editor_sha256": "d94ddba81b79796aab53b4131d05332e02e654c35007f48d9073ce410c5ecee0", + "binary_sha256": "db7fd98350bf3a33435921035871f33e18dad0a3b28360671544a3d34a6a93ce", + "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/store-paged.c": "02c5992465d117e576199c7348daa2b628e6c6efd41d906faa3b50713ece9f53", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "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": "584f6671c2daf40ee3cc4233760b0004d5035bd0e0cd10aa0fe6a0b814f66937", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-hybrid-storage/app-release.json b/prototypes/connections/results/2026-09-24-hybrid-storage/app-release.json new file mode 100644 index 0000000..452cadb --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/app-release.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": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 14227, + "borrowed_output_bytes": 2115420, + "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": 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": 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": 16, + "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": "dd505a9c4ecf73205f33edb491ed990234cd940c6164cd9fe56c1606dc4a280f", + "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": "9b7366c1d005a21c393425e28c18bd161fcbb86af3961180780d67f5d62ba3d8", + "editor_sha256": "5ea320034f2c36f378bed30bb12705f35723dd9f7f334c976aec7d6f5927ac57", + "binary_sha256": "8bc87de02efca32d1281976a8e8bea2e5a7242eb5ec07fc204b7762c2081021c", + "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/store-paged.c": "02c5992465d117e576199c7348daa2b628e6c6efd41d906faa3b50713ece9f53", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "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": "584f6671c2daf40ee3cc4233760b0004d5035bd0e0cd10aa0fe6a0b814f66937", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-hybrid-storage/gtk-flow.json b/prototypes/connections/results/2026-09-24-hybrid-storage/gtk-flow.json new file mode 100644 index 0000000..4bff12d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/gtk-flow.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 43760, "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/memory.json b/prototypes/connections/results/2026-09-24-hybrid-storage/memory.json new file mode 100644 index 0000000..64304e0 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/memory.json @@ -0,0 +1,14 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "5ea320034f2c36f378bed30bb12705f35723dd9f7f334c976aec7d6f5927ac57", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/package-full.json b/prototypes/connections/results/2026-09-24-hybrid-storage/package-full.json new file mode 100644 index 0000000..0675a20 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/package-full.json @@ -0,0 +1,21 @@ +{ + "archive_sha256": "0aa6141240a44039ec5c3e542c9c2857678620c77c81203dd984b4121b18adce", + "archive_bytes": 114009, + "selected_hosts": [ + "pure-edit", + "pure-connections", + "pure-document-peer", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "selected storage keeps small values inline and maps large values", + "editing the packaged .pure source changes the executed reply", + "extracted HTTP host updates the same document as the native CLI", + "extracted C/Python TLS peers share a 1 MiB document and recover", + "extracted GTK window and selected external host edit the same document" + ], + "peer_idle_rss_kib": 7840, + "peer_after_large_work_rss_kib": 9536 +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/package-micro.json b/prototypes/connections/results/2026-09-24-hybrid-storage/package-micro.json new file mode 100644 index 0000000..62cfd78 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/package-micro.json @@ -0,0 +1,13 @@ +{ + "archive_sha256": "c67a92e534a43aed24254995078bb72992015766da9c3e57ec7c993fa50f3654", + "archive_bytes": 26040, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "selected storage keeps small values inline and maps large values", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/peer.json b/prototypes/connections/results/2026-09-24-hybrid-storage/peer.json new file mode 100644 index 0000000..fa238e3 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/peer.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7840, + "server_after_large_work_rss_kib": 9536 +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/profile.json b/prototypes/connections/results/2026-09-24-hybrid-storage/profile.json new file mode 100644 index 0000000..96ca222 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-hybrid-document-storage", + "scope": "Opt-in inline/PUREP001 document store on Linux AArch64/GCC 14. The CLI text ceiling is 42 KiB: 2 KiB above the original selected-page gate because the measured 41064-byte dual-format build fails 40 KiB. This is an explicit research budget, not the default micro-runtime budget. Direct dependencies only; excludes libc, process RAM, filesystem cache and journal, optional TLS/GTK.", + "budget_sha256": "038a323d8492731611dfc56e799e7da751946fe0ea5dc428550ff02ffd781b29", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 41064, + "data": 1000, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "5ea320034f2c36f378bed30bb12705f35723dd9f7f334c976aec7d6f5927ac57" + }, + "pure-connections": { + "sections": { + "text": 47928, + "data": 1088, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "8bc87de02efca32d1281976a8e8bea2e5a7242eb5ec07fc204b7762c2081021c" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-authoring-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-authoring-linux-aarch64.tar.gz new file mode 100644 index 0000000..5280fd4 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-authoring-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-micro-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-micro-linux-aarch64.tar.gz new file mode 100644 index 0000000..c2fb13f Binary files /dev/null and b/prototypes/connections/results/2026-09-24-hybrid-storage/pure-document-hybrid-micro-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/unit-asan.json b/prototypes/connections/results/2026-09-24-hybrid-storage/unit-asan.json new file mode 100644 index 0000000..8da4fd1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/unit-asan.json @@ -0,0 +1 @@ +{"hybrid_storage":"snapshot-patch-range-compaction-crash-conflict-uncertain-inline-transitions","mixed_edits":128} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/unit.json b/prototypes/connections/results/2026-09-24-hybrid-storage/unit.json new file mode 100644 index 0000000..8da4fd1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/unit.json @@ -0,0 +1 @@ +{"hybrid_storage":"snapshot-patch-range-compaction-crash-conflict-uncertain-inline-transitions","mixed_edits":128} diff --git a/prototypes/connections/results/2026-09-24-hybrid-storage/write-cost.json b/prototypes/connections/results/2026-09-24-hybrid-storage/write-cost.json new file mode 100644 index 0000000..a3dfa76 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-hybrid-storage/write-cost.json @@ -0,0 +1,49 @@ +{ + "profile": "linux-aarch64-16k-page-document-replacement", + "candidate_mode": "hybrid", + "workloads": [ + "replace last byte of 1 MiB document", + "replace one-byte document" + ], + "unit": "successful user-space write syscall bytes targeting object files", + "excludes": "physical device writes, filesystem journal, metadata and directory sync cost", + "one_mib": { + "flat": { + "object_write_syscall_bytes": 1048600, + "object_write_calls": 258, + "object_files": { + "obj-large": 1048600 + }, + "binary_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "hybrid": { + "object_write_syscall_bytes": 16944, + "object_write_calls": 3, + "object_files": { + "obj-large": 560, + "dat-large-0000000000000001": 1064960 + }, + "binary_sha256": "5ea320034f2c36f378bed30bb12705f35723dd9f7f334c976aec7d6f5927ac57" + }, + "flat_to_hybrid_ratio": 61.8862134088763 + }, + "one_byte": { + "flat": { + "object_write_syscall_bytes": 25, + "object_write_calls": 2, + "object_files": { + "obj-tiny": 25 + }, + "binary_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "hybrid": { + "object_write_syscall_bytes": 25, + "object_write_calls": 2, + "object_files": { + "obj-tiny": 25 + }, + "binary_sha256": "5ea320034f2c36f378bed30bb12705f35723dd9f7f334c976aec7d6f5927ac57" + }, + "hybrid_to_flat_ratio": 1.0 + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-wake/archived-comparison.json b/prototypes/connections/results/2026-09-24-idle-wake/archived-comparison.json new file mode 100644 index 0000000..f90f1a9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-wake/archived-comparison.json @@ -0,0 +1,21 @@ +{ + "profile": "idle-http-host-poll-syscalls", + "observation_seconds": 1.25, + "before": { + "poll_calls": 25, + "timeout_values_ms": [ + 50 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=PO", + "binary_sha256": "7dd0010fa6d6024afa2fe35d57a4c73f33ac632e732616ac8f8853d7b22d79b3" + }, + "after": { + "poll_calls": 1, + "timeout_values_ms": [ + -1 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, NULL, NULL, 0) = ? ERESTARTNOHAND (To be restarted if no handler)\n--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=927636, si_uid=1000} ---\n", + "binary_sha256": "3e8162f6851ba407061d1109a87b9b2b17c87a5c5eadbc75ea7cc0fb27a84501" + }, + "deadline_driven_idle_wait": "pass" +} diff --git a/prototypes/connections/results/2026-09-24-idle-wake/fixed-profile.json b/prototypes/connections/results/2026-09-24-idle-wake/fixed-profile.json new file mode 100644 index 0000000..9a57d8c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-wake/fixed-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8" + }, + "pure-connections-fixed": { + "sections": { + "text": 25292, + "data": 1000, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "3e8162f6851ba407061d1109a87b9b2b17c87a5c5eadbc75ea7cc0fb27a84501" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-wake/fixed-server-regression.json b/prototypes/connections/results/2026-09-24-idle-wake/fixed-server-regression.json new file mode 100644 index 0000000..5400394 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-wake/fixed-server-regression.json @@ -0,0 +1,68 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "slow_consumer_budget_and_release": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "limited_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048586, + "server_structure_bytes": 40848 + }, + "fixed_idle_rss_kib": 1504, + "source_idle_rss_kib": 1536, + "fixed_after_large_rss_kib": 1632, + "source_after_large_rss_kib": 1664, + "fixed_peak_rss_kib": 2128, + "source_peak_rss_kib": 1664, + "fixed_peak_rss_limit_kib": 4096, + "fixed_sha256": "3e8162f6851ba407061d1109a87b9b2b17c87a5c5eadbc75ea7cc0fb27a84501", + "source_sha256": "186fd0ab0f7cd32cc46a341cc4e6ec784126193810473c6ac28a8c95a0d44bc3" +} diff --git a/prototypes/connections/results/2026-09-24-idle-wake/idle.json b/prototypes/connections/results/2026-09-24-idle-wake/idle.json new file mode 100644 index 0000000..dee7a74 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-wake/idle.json @@ -0,0 +1,21 @@ +{ + "profile": "idle-http-host-poll-syscalls", + "observation_seconds": 1.25, + "before": { + "poll_calls": 25, + "timeout_values_ms": [ + 50 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=PO", + "binary_sha256": "02acf468f18610332d3fe4b9c093b2bbff08496000dbae2b61151d7f3a7a46fa" + }, + "after": { + "poll_calls": 1, + "timeout_values_ms": [ + -1 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, NULL, NULL, 0) = ? ERESTARTNOHAND (To be restarted if no handler)\n--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=927237, si_uid=1000} ---\n", + "binary_sha256": "3e8162f6851ba407061d1109a87b9b2b17c87a5c5eadbc75ea7cc0fb27a84501" + }, + "deadline_driven_idle_wait": "pass" +} diff --git a/prototypes/connections/results/2026-09-24-idle-wake/package.json b/prototypes/connections/results/2026-09-24-idle-wake/package.json new file mode 100644 index 0000000..61d484b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-wake/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "669d28751faa4da34dd63cb52e83fa2cfb11146a9844b0f6378f9f63500a0b71", + "archive_bytes": 31773, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-idle-wake/pure-document-fixed-http-idle-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-idle-wake/pure-document-fixed-http-idle-linux-aarch64.tar.gz new file mode 100644 index 0000000..24d9b3d Binary files /dev/null and b/prototypes/connections/results/2026-09-24-idle-wake/pure-document-fixed-http-idle-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-idle-wake/reproducible-comparison.json b/prototypes/connections/results/2026-09-24-idle-wake/reproducible-comparison.json new file mode 100644 index 0000000..4a17350 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-wake/reproducible-comparison.json @@ -0,0 +1,21 @@ +{ + "profile": "idle-http-host-poll-syscalls", + "observation_seconds": 1.25, + "before": { + "poll_calls": 25, + "timeout_values_ms": [ + 50 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=PO", + "binary_sha256": "7dd0010fa6d6024afa2fe35d57a4c73f33ac632e732616ac8f8853d7b22d79b3" + }, + "after": { + "poll_calls": 1, + "timeout_values_ms": [ + -1 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, NULL, NULL, 0) = ? ERESTARTNOHAND (To be restarted if no handler)\n--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=928742, si_uid=1000} ---\n", + "binary_sha256": "3e8162f6851ba407061d1109a87b9b2b17c87a5c5eadbc75ea7cc0fb27a84501" + }, + "deadline_driven_idle_wait": "pass" +} diff --git a/prototypes/connections/results/2026-09-24-idle-wake/source-profile.json b/prototypes/connections/results/2026-09-24-idle-wake/source-profile.json new file mode 100644 index 0000000..05a247c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-wake/source-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35807, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "bce466328ac730ccb7cd22c7a0c4e2960bc94a0f20ac9f036e232bd499521ff7" + }, + "pure-connections": { + "sections": { + "text": 43073, + "data": 1040, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "186fd0ab0f7cd32cc46a341cc4e6ec784126193810473c6ac28a8c95a0d44bc3" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/async-core.json b/prototypes/connections/results/2026-09-24-idle-work-binding/async-core.json new file mode 100644 index 0000000..4843a89 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/async-core.json @@ -0,0 +1,55 @@ +{ + "passed": true, + "failures": [], + "budget": { + "profile": "selected-owner-thread-async-effects", + "max_text_bytes": 8192, + "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, + "effect_bytes": 128 + }, + "archive_members": [ + "runtime-async.o", + "evaluate.o" + ], + "external_symbols_allowed": [ + "memcpy", + "memmove", + "memset", + "strlen" + ], + "scope": "Selected owner-thread completion extension. Host I/O and worker stacks are excluded. The default execution core and fixed document packages do not link this profile." + }, + "layout": { + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8, + "effect_bytes": 128 + }, + "sections": { + "text": 7456, + "data": 0, + "bss": 0 + }, + "members": [ + "runtime-async.o", + "evaluate.o" + ], + "external_symbols": [ + "memcpy", + "strlen" + ], + "size_output": " text\t data\t bss\t dec\t hex\tfilename\n 6536\t 0\t 0\t 6536\t 1988\truntime-async.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution-async.a)\n 920\t 0\t 0\t 920\t 398\tevaluate.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution-async.a)\n", + "archive_sha256": "897fb2f6eff5dd07efcf0789107bfcf87423fc90aae084df169601451f23d07f", + "layout_binary_sha256": "71866a57a3a2af37a8e3d4f2f97669c482b11b3a408a1d918e5de068dab9e34a", + "budget_sha256": "496e34596e24418efb46e460126b7d67492137207be988525abda1397d70c539", + "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-24-idle-work-binding/async-gated.json b/prototypes/connections/results/2026-09-24-idle-work-binding/async-gated.json new file mode 100644 index 0000000..92f9838 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/async-gated.json @@ -0,0 +1,53 @@ +{ + "async_http": "snapshot-read-write-overlap-deadline-recovery-draining-shutdown", + "before_publication": { + "status": 200, + "etag": "\"1\"", + "body": "old" + }, + "after_publication": { + "status": 200, + "etag": "\"2\"", + "body": "oldafter" + }, + "shutdown_restore": { + "revision": "2", + "body": "oldafter" + }, + "deadline_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 16, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 4, + "commits": 1, + "gets": 2, + "conflicts": 0, + "budget_rejections": 0, + "expired": 1, + "releases": 3, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 16, + "server_structure_bytes": 43320 + }, + "shutdown_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 13, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 13, + "borrowed_output_bytes": 5, + "server_structure_bytes": 43320 + }, + "test_source_sha256": "f38c9d3d784676ce815c276fbffc10e00f767aaa803793d8db311c19ef3b9085", + "gated_test_binary_sha256": "cb4e7209c53261b63647823aaa8ee408a860d1ccde6c85bc404d77a336128eb2" +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/async-server-profile.json b/prototypes/connections/results/2026-09-24-idle-work-binding/async-server-profile.json new file mode 100644 index 0000000..a3a89b8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/async-server-profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-read-write-document-http", + "scope": "Optional one-read/one-write-worker HTTP host on the recorded Linux AArch64/GCC 14 toolchain. Section limits exclude the two 128 KiB worker stacks, libc, heap, mapped files and kernel buffers. Default CLI and HTTP do not link this option. Inspect process RSS and VM separately.", + "budget_sha256": "acd82386572fc2f874ca2fee5f4eed68ee145cf990d7315a9f1aff06daf51995", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-connections-async": { + "sections": { + "text": 48561, + "data": 1180, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "9485151b8902d66052bb689e821b502810b66561e0a251d3ce091c4919b7c67e" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli-check.json b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-check.json new file mode 100644 index 0000000..1f60a56 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "927e9b0f7bc672e661da0d82abe60aaf3743f6753fbb00e6b39035a3239bae22", + "archive_bytes": 12987, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-check.json b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-check.json new file mode 100644 index 0000000..0f372bf --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "177bb19c9790712ccb8f75fd65eb59b02c68b526b5155d3b1e83a795c845f031", + "archive_bytes": 28719, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui-check.json b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui-check.json new file mode 100644 index 0000000..5a6506c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui-check.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "081e5fe6205760e343ca237b74ff969a10d169a5f51b3bcd07a50ac8c5e0ba23", + "archive_bytes": 47419, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui.tar.gz b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui.tar.gz new file mode 100644 index 0000000..de378e4 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http.tar.gz b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http.tar.gz new file mode 100644 index 0000000..904f570 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-http.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui-check.json b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui-check.json new file mode 100644 index 0000000..e1b39e7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "565d32fcccab9dfa10c8af7e21514fb1f4187b0bebac52053db02a6b75bf741f", + "archive_bytes": 31726, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui.tar.gz b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui.tar.gz new file mode 100644 index 0000000..33a17ee Binary files /dev/null and b/prototypes/connections/results/2026-09-24-idle-work-binding/cli-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/cli.tar.gz b/prototypes/connections/results/2026-09-24-idle-work-binding/cli.tar.gz new file mode 100644 index 0000000..739dda5 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-idle-work-binding/cli.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/core.json b/prototypes/connections/results/2026-09-24-idle-work-binding/core.json new file mode 100644 index 0000000..735a4df --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/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": 6076, + "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 5156\t 0\t 0\t 5156\t 1424\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": "a02135602c3b18129cc9b80cb808994426d0c7869754eac1370c304294ad0be4", + "layout_binary_sha256": "db112a9cfd8deed80b9bfbff7628958fdb290cadab1efc7223d8d15ef4c4c389", + "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-24-idle-work-binding/default-profile.json b/prototypes/connections/results/2026-09-24-idle-work-binding/default-profile.json new file mode 100644 index 0000000..a997add --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/default-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35967, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "dfe2b9d03c87eed8cde0d598b182dfc8aa4bba85eca1b64849c77e9c4ef302d3" + }, + "pure-connections": { + "sections": { + "text": 43938, + "data": 1068, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "1b7555764d059bca2be05e406d42305dfc68e3fd87813bcfcfaa390945467a8d" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/document-peer-profile.json b/prototypes/connections/results/2026-09-24-idle-work-binding/document-peer-profile.json new file mode 100644 index 0000000..18dbba5 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/document-peer-profile.json @@ -0,0 +1,23 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-document-peer", + "scope": "Review threshold for the explicitly selected authenticated document host on Linux AArch64/GCC 14. Includes linked application code but excludes the shared OpenSSL/libc implementations, process heap, stack, mappings and kernel buffers. Check measured whole-process RSS separately.", + "budget_sha256": "defe8efb4d544cdbf615de4d43d4ceba78fc7f1d958f245731202625052eb2ff", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-document-peer": { + "sections": { + "text": 47038, + "data": 1384, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libcrypto.so.3", + "libssl.so.3" + ], + "sha256": "3ec358021988217c212f5527f8947c301f39d6c2022f82c51deff515ce16acbf" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/document-peer.json b/prototypes/connections/results/2026-09-24-idle-work-binding/document-peer.json new file mode 100644 index 0000000..05c62cc --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/document-peer.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7968, + "server_after_large_work_rss_kib": 9712 +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/fixed-memory.json b/prototypes/connections/results/2026-09-24-idle-work-binding/fixed-memory.json new file mode 100644 index 0000000..641bc64 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/fixed-memory.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "fixed-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, the embedded prepared program, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1088, + "one_mib_edit_rss_kib": 2128, + "one_mib_show_rss_kib": 1088 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "3d2bf30330330ca377d28db0a7d8f4598365c3cfba9c2d43dc5843246fff19b5", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/fixed-profile.json b/prototypes/connections/results/2026-09-24-idle-work-binding/fixed-profile.json new file mode 100644 index 0000000..6097cdf --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/fixed-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19404, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "3d2bf30330330ca377d28db0a7d8f4598365c3cfba9c2d43dc5843246fff19b5" + }, + "pure-connections-fixed": { + "sections": { + "text": 26549, + "data": 1028, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "2869469f4c40674c6904729141005c7705bbc5130019d1ce091ffb135bffe60a" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/full-check.json b/prototypes/connections/results/2026-09-24-idle-work-binding/full-check.json new file mode 100644 index 0000000..0e4d482 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/full-check.json @@ -0,0 +1,636 @@ +{ + "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": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 14227, + "borrowed_output_bytes": 2115420, + "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": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 1, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 113, + "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, + "idle_slot_binding": true + }, + "execution_core_test_sha256": "80aaa8831c44101fc03364d393dd99ac1f3ca3fa861e2561447adad6d965ac0a", + "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": "43bff520b326cfe3f17fa0e0b48efc27e393f6cb7bfff3051fe0115b23e4e5fb", + "editor_sha256": "dfe2b9d03c87eed8cde0d598b182dfc8aa4bba85eca1b64849c77e9c4ef302d3", + "binary_sha256": "1b7555764d059bca2be05e406d42305dfc68e3fd87813bcfcfaa390945467a8d", + "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/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "ead1331794f8e4fb851e21eb7f7d3e7c408316d7481c51996e6ee62a81665a0c", + "prototypes/connections/runtime.h": "beff8a8a10f21c017987d69682bb279701d5627227aca60029a711b9e64cca85", + "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": "8c275bf564a674f0f8232026d0d7612df48f10054af75a43af5ed0bce7653e24", + "prototypes/connections/server.c": "e8632667de6feaf4d495f87abb159775650e3e4eaf6713b9dfdc5f82807c2bf9", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "f39a7c411afa80812aec6e7a7250cdc833e97cffef8d0cce25d960fe435784f6", + "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": "f9a0c9fa7d0f40d6d72bc72a429dd36e136e27207aa6b0c01df9505d82189b6d", + "prototypes/connections/core-layout.c": "e3123bdf74edb99556b05d7cd3a36b6119ec30829541b0ddba2e23e5900b089d", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-idle-work-binding/package-reproducibility.json b/prototypes/connections/results/2026-09-24-idle-work-binding/package-reproducibility.json new file mode 100644 index 0000000..d9bb9fe --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/package-reproducibility.json @@ -0,0 +1,31 @@ +{ + "profile": "native-package-repeat-build", + "same_archive_bytes": "pass", + "variants": { + "source-cli": { + "bytes": 22595, + "sha256": "e770e93d15524070b254a46796667e6b0077bc0fa95fbf6ecca051742ec754ca", + "runs": 2 + }, + "cli": { + "bytes": 12987, + "sha256": "927e9b0f7bc672e661da0d82abe60aaf3743f6753fbb00e6b39035a3239bae22", + "runs": 2 + }, + "cli-http": { + "bytes": 28719, + "sha256": "177bb19c9790712ccb8f75fd65eb59b02c68b526b5155d3b1e83a795c845f031", + "runs": 2 + }, + "cli-tui": { + "bytes": 31726, + "sha256": "565d32fcccab9dfa10c8af7e21514fb1f4187b0bebac52053db02a6b75bf741f", + "runs": 2 + }, + "cli-http-tui": { + "bytes": 47419, + "sha256": "081e5fe6205760e343ca237b74ff969a10d169a5f51b3bcd07a50ac8c5e0ba23", + "runs": 2 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/precompact-budget-failure.txt b/prototypes/connections/results/2026-09-24-idle-work-binding/precompact-budget-failure.txt new file mode 100644 index 0000000..28b393a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/precompact-budget-failure.txt @@ -0,0 +1,9 @@ +Before fixed packages switched from strip --strip-debug to strip --strip-unneeded: + +python3 package_native.py --fixed --with-tui --output results/2026-09-24-idle-work-binding/cli-tui.tar.gz + +package_native.py: error: cli_tui archive grew to 35847 bytes (reviewed maximum 35840); no package was published + +This is the captured result on Linux AArch64 with the updated runtime and the +earlier packaging mode. The final package archive and relocation check are +cli-tui.tar.gz and cli-tui-check.json in this directory. diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/release.log b/prototypes/connections/results/2026-09-24-idle-work-binding/release.log new file mode 100644 index 0000000..5c3a893 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/release.log @@ -0,0 +1,800 @@ +python3 check_core.py --archive build/libpure-execution.a --layout build/core-layout --output "results/2026-09-24-idle-work-binding/core.json" +{ + "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": 6076, + "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 5156\t 0\t 0\t 5156\t 1424\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": "a02135602c3b18129cc9b80cb808994426d0c7869754eac1370c304294ad0be4", + "layout_binary_sha256": "db112a9cfd8deed80b9bfbff7628958fdb290cadab1efc7223d8d15ef4c4c389", + "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" +} +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror edit.c store.c store-port.c program.c runtime.c evaluate.c -o build/pure-edit +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror server.c store.c store-port.c flow.c program.c runtime.c evaluate.c -o build/pure-connections +python3 check_profile.py --build build --output "results/2026-09-24-idle-work-binding/default-profile.json" +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35967, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "dfe2b9d03c87eed8cde0d598b182dfc8aa4bba85eca1b64849c77e9c4ef302d3" + }, + "pure-connections": { + "sections": { + "text": 43938, + "data": 1068, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "1b7555764d059bca2be05e406d42305dfc68e3fd87813bcfcfaa390945467a8d" + } + } +} +build/pure-compile examples/editor.pure build/embedded-plan.h +python3 check_profile.py --build build --budget fixed-budget.json --output "results/2026-09-24-idle-work-binding/fixed-profile.json" +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19404, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "3d2bf30330330ca377d28db0a7d8f4598365c3cfba9c2d43dc5843246fff19b5" + }, + "pure-connections-fixed": { + "sections": { + "text": 26549, + "data": 1028, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "2869469f4c40674c6904729141005c7705bbc5130019d1ce091ffb135bffe60a" + } + } +} +python3 check_micro_memory.py --build build --fixed --output "results/2026-09-24-idle-work-binding/fixed-memory.json" +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "fixed-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, the embedded prepared program, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1088, + "one_mib_edit_rss_kib": 2128, + "one_mib_show_rss_kib": 1088 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "3d2bf30330330ca377d28db0a7d8f4598365c3cfba9c2d43dc5843246fff19b5", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror test-runtime.c program.c runtime.c evaluate.c store.c store-port.c -o build/test-runtime +cc -O2 -g -std=c17 -Wall -Wextra -Wpedantic -Werror -DPURE_STORE_TEST test-splice.c program.c runtime.c evaluate.c store.c store-port.c -o build/test-splice +build/test-flow +{"transfer":"partial-consumption-and-single-release","storage":"mapped-ranges-snapshot-budget-conflict-failure-uncertain-crash-overflow-empty"} +build/test-program +{"program":"prepare-views-preserve-guards-types-effects-and-atomic-install","boundary_semantic_comparisons":3584} +build/test-fields +{"fields":"named-layouts-unaligned-u32le-tail-dynamic-ranges-aliases-failures-limits","data_boundary_cases":48} +python3 check.py --server build/pure-connections --runtime-test build/test-runtime --splice-test build/test-splice --editor build/pure-edit --store-source store.c --output "results/2026-09-24-idle-work-binding/full-check.json" +{ + "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": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 14227, + "borrowed_output_bytes": 2115420, + "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": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 1, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 113, + "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, + "idle_slot_binding": true + }, + "execution_core_test_sha256": "80aaa8831c44101fc03364d393dd99ac1f3ca3fa861e2561447adad6d965ac0a", + "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": "43bff520b326cfe3f17fa0e0b48efc27e393f6cb7bfff3051fe0115b23e4e5fb", + "editor_sha256": "dfe2b9d03c87eed8cde0d598b182dfc8aa4bba85eca1b64849c77e9c4ef302d3", + "binary_sha256": "1b7555764d059bca2be05e406d42305dfc68e3fd87813bcfcfaa390945467a8d", + "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/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "ead1331794f8e4fb851e21eb7f7d3e7c408316d7481c51996e6ee62a81665a0c", + "prototypes/connections/runtime.h": "beff8a8a10f21c017987d69682bb279701d5627227aca60029a711b9e64cca85", + "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": "8c275bf564a674f0f8232026d0d7612df48f10054af75a43af5ed0bce7653e24", + "prototypes/connections/server.c": "e8632667de6feaf4d495f87abb159775650e3e4eaf6713b9dfdc5f82807c2bf9", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "f39a7c411afa80812aec6e7a7250cdc833e97cffef8d0cce25d960fe435784f6", + "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": "f9a0c9fa7d0f40d6d72bc72a429dd36e136e27207aa6b0c01df9505d82189b6d", + "prototypes/connections/core-layout.c": "e3123bdf74edb99556b05d7cd3a36b6119ec30829541b0ddba2e23e5900b089d", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a" + } +} +python3 check_micro_memory.py --build build --output "results/2026-09-24-idle-work-binding/source-memory.json" +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "source-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "dfe2b9d03c87eed8cde0d598b182dfc8aa4bba85eca1b64849c77e9c4ef302d3", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/runtime-sanitized.json b/prototypes/connections/results/2026-09-24-idle-work-binding/runtime-sanitized.json new file mode 100644 index 0000000..0c1b5b0 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/runtime-sanitized.json @@ -0,0 +1 @@ +{"runtime_tests":"passed","domain_bytes":200,"work_slot_bytes":224,"ticket_bytes":24,"prepared_program_bytes":680,"forwarding_chain":1024,"native_storage_pipeline":true,"idle_slot_binding":true} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/service-scale.json b/prototypes/connections/results/2026-09-24-idle-work-binding/service-scale.json new file mode 100644 index 0000000..b0fe27b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/service-scale.json @@ -0,0 +1,100 @@ +{ + "profile": "document-app-many-objects-concurrent-clients", + "documents_per_variant": 1024, + "clients": 8, + "body_bytes": 8192, + "operations_per_variant": 4096, + "server_budget_bytes": 1048576, + "peak_rss_limit_kib": 4096, + "same_final_documents": "pass", + "fixed": { + "binary_sha256": "2869469f4c40674c6904729141005c7705bbc5130019d1ce091ffb135bffe60a", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1504, + "peak_rss_kib": 1680, + "idle_vm_kib": 2448, + "peak_vm_kib": 2528, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49184, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 390209, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 40848 + } + }, + "source": { + "binary_sha256": "1b7555764d059bca2be05e406d42305dfc68e3fd87813bcfcfaa390945467a8d", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1552, + "peak_rss_kib": 1728, + "idle_vm_kib": 2464, + "peak_vm_kib": 2528, + "idle_threads": 1, + "peak_threads": 1, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49184, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 442020, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 41528 + } + }, + "source_async": { + "binary_sha256": "9485151b8902d66052bb689e821b502810b66561e0a251d3ce091c4919b7c67e", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1600, + "peak_rss_kib": 1776, + "idle_vm_kib": 2864, + "peak_vm_kib": 2944, + "idle_threads": 3, + "peak_threads": 3, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 40993, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 270839, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 43320 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/source-cli-check.json b/prototypes/connections/results/2026-09-24-idle-work-binding/source-cli-check.json new file mode 100644 index 0000000..729710b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/source-cli-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "e770e93d15524070b254a46796667e6b0077bc0fa95fbf6ecca051742ec754ca", + "archive_bytes": 22595, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/source-cli.tar.gz b/prototypes/connections/results/2026-09-24-idle-work-binding/source-cli.tar.gz new file mode 100644 index 0000000..4981e60 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-idle-work-binding/source-cli.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/source-memory.json b/prototypes/connections/results/2026-09-24-idle-work-binding/source-memory.json new file mode 100644 index 0000000..aa663a6 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/source-memory.json @@ -0,0 +1,15 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "variant": "source-program", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "dfe2b9d03c87eed8cde0d598b182dfc8aa4bba85eca1b64849c77e9c4ef302d3", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/tui-profile.json b/prototypes/connections/results/2026-09-24-idle-work-binding/tui-profile.json new file mode 100644 index 0000000..ead0b8f --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/tui-profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app, including the bounded document browser. The raised text ceiling reviews its measured 2,541-byte addition; default CLI and core ceilings are unchanged. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS, including 1,024-key browsing, is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "4d3097c9ec3c0139cbdd82161a74d610c15d31c6ee5625101410627664f925e7", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 31463, + "data": 1036, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "297acfbeee36de36f4651e3ae5438c8a91604d97be9bc753fe2af380887b8693" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-idle-work-binding/tui.json b/prototypes/connections/results/2026-09-24-idle-work-binding/tui.json new file mode 100644 index 0000000..d482848 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-idle-work-binding/tui.json @@ -0,0 +1,22 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, resized and last key opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1808, + "one_mib_peak_rss_kib": 2832, + "one_thousand_key_list_peak_rss_kib": 1888, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "297acfbeee36de36f4651e3ae5438c8a91604d97be9bc753fe2af380887b8693" +} diff --git a/prototypes/connections/results/2026-09-24-in-process-routing/mirror-ext4.json b/prototypes/connections/results/2026-09-24-in-process-routing/mirror-ext4.json new file mode 100644 index 0000000..0b1454e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-in-process-routing/mirror-ext4.json @@ -0,0 +1,56 @@ +{ + "passed": true, + "source_body_sha256": "06cb5e32505f66804f6e8c79e3230105ca4769444e1389ee36f6f11fd61f2d3a", + "mirror_sha256": "3c22ef868198f805f4cd1fe7c9c55b69f32c599f5076444f9af4f1297cb874e2", + "editor_sha256": "04974e2d3c4d7f5fb22aff67e7399aa2ebcbd85eb24da0c0e41f3ef047d5ddec", + "source_one_mib_to_two_domains": { + "source_revision": "1", + "bytes": 1048576, + "targets": [ + { + "index": 0, + "status": "ok", + "revision": "1" + }, + { + "index": 1, + "status": "ok", + "revision": "1" + } + ] + }, + "independent_partial_conflict": { + "source_revision": "2", + "bytes": 1048576, + "targets": [ + { + "index": 0, + "status": "ok", + "revision": "2" + }, + { + "index": 1, + "status": "conflict", + "revision": "1" + } + ] + }, + "conflict_recovered_revision": "2", + "empty_body_forwarded": true, + "target_budget_checked_before_document_write": true, + "all_admissions_before_document_writes": true, + "source_failure_before_target_creation": true, + "one_mib_two_target_peak_rss": { + "exit_code": 0, + "max_rss_kib": 2144, + "raw_max_rss": 2144, + "raw_unit": "KiB" + }, + "one_mib_eight_targets_verified": true, + "one_mib_eight_target_peak_rss": { + "exit_code": 0, + "max_rss_kib": 2144, + "raw_max_rss": 2144, + "raw_unit": "KiB" + } +} diff --git a/prototypes/connections/results/2026-09-24-in-process-routing/mirror-sanitized.json b/prototypes/connections/results/2026-09-24-in-process-routing/mirror-sanitized.json new file mode 100644 index 0000000..12f3550 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-in-process-routing/mirror-sanitized.json @@ -0,0 +1,46 @@ +{ + "passed": true, + "source_body_sha256": "06cb5e32505f66804f6e8c79e3230105ca4769444e1389ee36f6f11fd61f2d3a", + "mirror_sha256": "0c25b281c3b746f74a451bb3eefb2345c98f3ba01a08d5751347246f5bc48ee1", + "editor_sha256": "6d0b6b2b06ed25e0fc0db229091d19fe56de01bae5f5a25c72ee228589412541", + "source_one_mib_to_two_domains": { + "source_revision": "1", + "bytes": 1048576, + "targets": [ + { + "index": 0, + "status": "ok", + "revision": "1" + }, + { + "index": 1, + "status": "ok", + "revision": "1" + } + ] + }, + "independent_partial_conflict": { + "source_revision": "2", + "bytes": 1048576, + "targets": [ + { + "index": 0, + "status": "ok", + "revision": "2" + }, + { + "index": 1, + "status": "conflict", + "revision": "1" + } + ] + }, + "conflict_recovered_revision": "2", + "empty_body_forwarded": true, + "target_budget_checked_before_document_write": true, + "all_admissions_before_document_writes": true, + "source_failure_before_target_creation": true, + "one_mib_two_target_peak_rss": null, + "one_mib_eight_targets_verified": true, + "one_mib_eight_target_peak_rss": null +} diff --git a/prototypes/connections/results/2026-09-24-in-process-routing/mirror-tmp.json b/prototypes/connections/results/2026-09-24-in-process-routing/mirror-tmp.json new file mode 100644 index 0000000..01f80dd --- /dev/null +++ b/prototypes/connections/results/2026-09-24-in-process-routing/mirror-tmp.json @@ -0,0 +1,56 @@ +{ + "passed": true, + "source_body_sha256": "06cb5e32505f66804f6e8c79e3230105ca4769444e1389ee36f6f11fd61f2d3a", + "mirror_sha256": "3c22ef868198f805f4cd1fe7c9c55b69f32c599f5076444f9af4f1297cb874e2", + "editor_sha256": "04974e2d3c4d7f5fb22aff67e7399aa2ebcbd85eb24da0c0e41f3ef047d5ddec", + "source_one_mib_to_two_domains": { + "source_revision": "1", + "bytes": 1048576, + "targets": [ + { + "index": 0, + "status": "ok", + "revision": "1" + }, + { + "index": 1, + "status": "ok", + "revision": "1" + } + ] + }, + "independent_partial_conflict": { + "source_revision": "2", + "bytes": 1048576, + "targets": [ + { + "index": 0, + "status": "ok", + "revision": "2" + }, + { + "index": 1, + "status": "conflict", + "revision": "1" + } + ] + }, + "conflict_recovered_revision": "2", + "empty_body_forwarded": true, + "target_budget_checked_before_document_write": true, + "all_admissions_before_document_writes": true, + "source_failure_before_target_creation": true, + "one_mib_two_target_peak_rss": { + "exit_code": 0, + "max_rss_kib": 2128, + "raw_max_rss": 2128, + "raw_unit": "KiB" + }, + "one_mib_eight_targets_verified": true, + "one_mib_eight_target_peak_rss": { + "exit_code": 0, + "max_rss_kib": 2128, + "raw_max_rss": 2128, + "raw_unit": "KiB" + } +} diff --git a/prototypes/connections/results/2026-09-24-micro-default/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-micro-default/pure-source-preview.tar.gz new file mode 100644 index 0000000..20674be Binary files /dev/null and b/prototypes/connections/results/2026-09-24-micro-default/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-micro-default/source-package-check.json b/prototypes/connections/results/2026-09-24-micro-default/source-package-check.json new file mode 100644 index 0000000..bfd1940 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-micro-default/source-package-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "26bdd3f8768bc5c66c282f989026ac4d9de64398ed3c31e6dc8abbc3d84491d0", + "archive_bytes": 338097, + "source_files": 173, + "checks": [ + "every extracted source file matches the manifest", + "clean default build contains only the core, parser-free CLI and its build-time compiler", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli-check.json b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli-check.json new file mode 100644 index 0000000..da74f01 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "3e5b064bbf81b13be24494a5d5a9bd55fa5ea057d2d5614965f3e5556babf9ab", + "archive_bytes": 14692, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli.tar.gz b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli.tar.gz new file mode 100644 index 0000000..7467329 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http-check.json b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http-check.json new file mode 100644 index 0000000..1d8281a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "d7f35c8add62551bfaf9819e4edd64b8b3f25dc084b2c519d172d4344f66b160", + "archive_bytes": 32548, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http.tar.gz b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http.tar.gz new file mode 100644 index 0000000..4ce4a1a Binary files /dev/null and b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui-check.json b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui-check.json new file mode 100644 index 0000000..e247b61 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui-check.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "6dc051ebc74eaf460c97e7259d0d408b2424ae4cfd19078a4227e12a1382faef", + "archive_bytes": 51424, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui.tar.gz b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui.tar.gz new file mode 100644 index 0000000..529f2b6 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_http_tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui-check.json b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui-check.json new file mode 100644 index 0000000..769e922 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "daf25aaf480559f4b9cfffa13ce91dcf1b84f008f0323ea59d00566dcfdc5d89", + "archive_bytes": 33761, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui.tar.gz b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui.tar.gz new file mode 100644 index 0000000..905c339 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-micro-package-boundary/cli_tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-micro-profile/default-host-budget.json b/prototypes/connections/results/2026-09-24-micro-profile/default-host-budget.json new file mode 100644 index 0000000..9ea33b6 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-micro-profile/default-host-budget.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35807, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "pure-connections": { + "sections": { + "text": 42761, + "data": 1040, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "e76d4c0a721c9ceb7142db0edbaa1f5c44bd78a2c55029658109c83e8076875f" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-mirror-source/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-mirror-source/pure-source-preview.tar.gz new file mode 100644 index 0000000..7f3ea7a Binary files /dev/null and b/prototypes/connections/results/2026-09-24-mirror-source/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-mirror-source/source-check-linux-aarch64.json b/prototypes/connections/results/2026-09-24-mirror-source/source-check-linux-aarch64.json new file mode 100644 index 0000000..8fdefbb --- /dev/null +++ b/prototypes/connections/results/2026-09-24-mirror-source/source-check-linux-aarch64.json @@ -0,0 +1,15 @@ +{ + "archive_sha256": "7da3049ad80d1f54770524ba04583b5e2aef2e59e6d252d347947d32e7327f0b", + "archive_bytes": 332792, + "source_files": 170, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean selected page store exports mixed revisions to the default flat store", + "clean one-MiB document fans out to two same-process Pure domains with independent conflict and release", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-final.json b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-final.json new file mode 100644 index 0000000..253130f --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-final.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 135936, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-repeated.json b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-repeated.json new file mode 100644 index 0000000..541ec3e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan-repeated.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_rss_samples_kib": [362224, 385120], "large_window_rss_kib": 118016, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan.json b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan.json new file mode 100644 index 0000000..605d189 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-asan.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 142944, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-final.json b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-final.json new file mode 100644 index 0000000..4365549 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-final.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 43936, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-repeated.json b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-repeated.json new file mode 100644 index 0000000..7d36eca --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow-repeated.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_rss_samples_kib": [46672, 48960], "large_window_rss_kib": 43952, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow.json b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow.json new file mode 100644 index 0000000..4f6d78b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/gtk-flow.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 43952, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/hybrid-gtk-flow.json b/prototypes/connections/results/2026-09-24-multi-document-window/hybrid-gtk-flow.json new file mode 100644 index 0000000..8d132e5 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/hybrid-gtk-flow.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 44032, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-recheck.json b/prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-recheck.json new file mode 100644 index 0000000..dcae905 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-recheck.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "b72bc0b5321fa6efcc0ca8fbc3e328bc220333b0af134b1e75b7c5b517999a72", + "archive_bytes": 48934, + "selected_hosts": [ + "pure-edit", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted archived GTK window edits the same document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-repeated.json b/prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-repeated.json new file mode 100644 index 0000000..dcae905 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/legacy-package-repeated.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "b72bc0b5321fa6efcc0ca8fbc3e328bc220333b0af134b1e75b7c5b517999a72", + "archive_bytes": 48934, + "selected_hosts": [ + "pure-edit", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted archived GTK window edits the same document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/package-final.json b/prototypes/connections/results/2026-09-24-multi-document-window/package-final.json new file mode 100644 index 0000000..fd4343a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/package-final.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "59a5110ef5dd186c7ba9e5c3fbe74217c10c98014c2823aabf141d3f354f8dd6", + "archive_bytes": 49746, + "selected_hosts": [ + "pure-edit", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted GTK window switches documents without losing drafts" + ] +} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/package-repeated.json b/prototypes/connections/results/2026-09-24-multi-document-window/package-repeated.json new file mode 100644 index 0000000..fd4343a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/package-repeated.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "59a5110ef5dd186c7ba9e5c3fbe74217c10c98014c2823aabf141d3f354f8dd6", + "archive_bytes": 49746, + "selected_hosts": [ + "pure-edit", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted GTK window switches documents without losing drafts" + ] +} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/package.json b/prototypes/connections/results/2026-09-24-multi-document-window/package.json new file mode 100644 index 0000000..9616e80 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "40b7fef5e3202840794c91c5eb9c7e5592244dab443ce3597cc6fb9745b97746", + "archive_bytes": 49722, + "selected_hosts": [ + "pure-edit", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted GTK window and selected external host edit the same document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-final-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-final-linux-aarch64.tar.gz new file mode 100644 index 0000000..ded86dd Binary files /dev/null and b/prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-final-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-linux-aarch64.tar.gz new file mode 100644 index 0000000..1633c3d Binary files /dev/null and b/prototypes/connections/results/2026-09-24-multi-document-window/pure-document-window-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-fresh-buffer.json b/prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-fresh-buffer.json new file mode 100644 index 0000000..0e0b7be --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-fresh-buffer.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_rss_samples_kib": [48416, 49472, 51216, 52208, 54176, 55504, 57728, 59568], "large_window_rss_kib": 43968, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-initial.json b/prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-initial.json new file mode 100644 index 0000000..71658ed --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/switch-memory-initial.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_rss_samples_kib": [47328, 48048, 49888, 51744, 53840, 55824, 56864, 58752], "large_window_rss_kib": 43920, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-multi-document-window/window-profile.json b/prototypes/connections/results/2026-09-24-multi-document-window/window-profile.json new file mode 100644 index 0000000..d47571f --- /dev/null +++ b/prototypes/connections/results/2026-09-24-multi-document-window/window-profile.json @@ -0,0 +1,25 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-native-document-authoring", + "scope": "Review ceiling for the selected GTK document-and-definition host on Linux AArch64/GCC 14. Linked code sections and direct dependencies only; the measured 44 MiB whole-process RSS and shared GTK stack remain separate costs.", + "budget_sha256": "27ee292bf3fe9a15d27942f64cff01dfccde0ef5054acbb53faf76208d89d110", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-window": { + "sections": { + "text": 49882, + "data": 1504, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libgio-2.0.so.0", + "libglib-2.0.so.0", + "libgobject-2.0.so.0", + "libgtk-4.so.1" + ], + "sha256": "4d9806ce6f82413d17bc93452d9fd3bcc2b18272d15a2edaf81f8888e73b4e02" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-native-checkpoint/fixed-memory.json b/prototypes/connections/results/2026-09-24-native-checkpoint/fixed-memory.json new file mode 100644 index 0000000..84c7eb4 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-checkpoint/fixed-memory.json @@ -0,0 +1,43 @@ +{ + "passed": true, + "failures": [], + "profile": "portable-measurement-no-budget", + "variant": "fixed-program", + "scope": "Child-process peak RSS for native CLI document creation, read and one-byte revision edit; includes libc and the selected program form, excludes filesystem cache, kernel buffers, TLS, GUI and other apps. No cross-platform memory ceiling is asserted.", + "samples": { + "small_edit_rss_kib": 1104, + "one_mib_edit_rss_kib": 2128, + "one_mib_show_rss_kib": 1072, + "one_mib_splice_rss_kib": 1104 + }, + "raw_samples": { + "small_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1104, + "raw_max_rss": 1104, + "raw_unit": "KiB" + }, + "one_mib_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 2128, + "raw_max_rss": 2128, + "raw_unit": "KiB" + }, + "one_mib_show_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1072, + "raw_max_rss": 1072, + "raw_unit": "KiB" + }, + "one_mib_splice_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1104, + "raw_max_rss": 1104, + "raw_unit": "KiB" + } + }, + "budget_sha256": null, + "binary_sha256": "435e087adf62253078678b42c169426adcaddd514c78717e3be570f88dbcc248", + "binary_bytes": 135344, + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-native-checkpoint/fixed-tui.json b/prototypes/connections/results/2026-09-24-native-checkpoint/fixed-tui.json new file mode 100644 index 0000000..05a5d34 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-checkpoint/fixed-tui.json @@ -0,0 +1,24 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": null, + "one_mib_peak_rss_kib": null, + "one_thousand_key_list_peak_rss_kib": null, + "one_mib_peak_rss_limit_kib": 0, + "portable": true, + "terminal_locale": "C.UTF-8", + "binary_sha256": "d49431543f233d7979a3cdc07ec2c9e938acf014df6257f0165f76f30a0e248c" +} diff --git a/prototypes/connections/results/2026-09-24-native-checkpoint/source-memory.json b/prototypes/connections/results/2026-09-24-native-checkpoint/source-memory.json new file mode 100644 index 0000000..3f45eb1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-checkpoint/source-memory.json @@ -0,0 +1,43 @@ +{ + "passed": true, + "failures": [], + "profile": "portable-measurement-no-budget", + "variant": "source-program", + "scope": "Child-process peak RSS for native CLI document creation, read and one-byte revision edit; includes libc and the selected program form, excludes filesystem cache, kernel buffers, TLS, GUI and other apps. No cross-platform memory ceiling is asserted.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056, + "one_mib_splice_rss_kib": 1104 + }, + "raw_samples": { + "small_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1056, + "raw_max_rss": 1056, + "raw_unit": "KiB" + }, + "one_mib_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 2080, + "raw_max_rss": 2080, + "raw_unit": "KiB" + }, + "one_mib_show_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1056, + "raw_max_rss": 1056, + "raw_unit": "KiB" + }, + "one_mib_splice_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1104, + "raw_max_rss": 1104, + "raw_unit": "KiB" + } + }, + "budget_sha256": null, + "binary_sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded", + "binary_bytes": 178336, + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-native-checkpoint/source-tui.json b/prototypes/connections/results/2026-09-24-native-checkpoint/source-tui.json new file mode 100644 index 0000000..0e2c5ad --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-checkpoint/source-tui.json @@ -0,0 +1,27 @@ +{ + "profile": "native-source-document-terminal-editor", + "checks": [ + "source parse and incompatible definitions fail before store creation", + "source CLI and TUI read the same document", + "changed source field layout drives TUI input encoding", + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": null, + "one_mib_peak_rss_kib": null, + "one_thousand_key_list_peak_rss_kib": null, + "one_mib_peak_rss_limit_kib": 0, + "portable": true, + "terminal_locale": "C.UTF-8", + "binary_sha256": "8e5ac649f2ab9a4dec02d32153f70aa13d97266cca1c9a942ba8fba27d04c8cf" +} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/full-package-check.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/full-package-check.json new file mode 100644 index 0000000..05264e9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/full-package-check.json @@ -0,0 +1,20 @@ +{ + "archive_sha256": "831b049f8f4c38bc219bab7cd50138da862593e1e1c768b16705816438cac95f", + "archive_bytes": 100900, + "selected_hosts": [ + "pure-edit", + "pure-connections", + "pure-document-peer", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted HTTP host updates the same document as the native CLI", + "extracted C/Python TLS peers share a 1 MiB document and recover", + "extracted GTK window and selected external host edit the same document" + ], + "peer_idle_rss_kib": 7840, + "peer_after_large_work_rss_kib": 9520 +} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow-final.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow-final.json new file mode 100644 index 0000000..4bac323 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow-final.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 43792, "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow.json new file mode 100644 index 0000000..3372af3 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-flow.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 44032, "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized-final.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized-final.json new file mode 100644 index 0000000..b65d2a2 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized-final.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 147696, "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized.json new file mode 100644 index 0000000..fe540f2 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/gtk-sanitized.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 147952, "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/http-only-package-check.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/http-only-package-check.json new file mode 100644 index 0000000..06ca49e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/http-only-package-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "a6c81aaf7ffef0cf1604e727f318d40bd51989ccb4b350793723ad3434d49148", + "archive_bytes": 47527, + "selected_hosts": [ + "pure-edit", + "pure-connections" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted HTTP host updates the same document as the native CLI" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/micro-memory.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/micro-memory.json new file mode 100644 index 0000000..2bf6a8d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/micro-memory.json @@ -0,0 +1,14 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1104 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/micro-package-check.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/micro-package-check.json new file mode 100644 index 0000000..12dc47f --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/micro-package-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "b010450dfe11be3196e6b57071f9ac6b79e1fd79d5d5b2a314ea8780096ad7c6", + "archive_bytes": 22482, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/peer-only-package-check.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/peer-only-package-check.json new file mode 100644 index 0000000..acf8029 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/peer-only-package-check.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "18a171c2f1a7b75f4fe030d6a087d6c2f3471fca731e2c9d7f103940315852e5", + "archive_bytes": 49186, + "selected_hosts": [ + "pure-edit", + "pure-document-peer" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted C/Python TLS peers share a 1 MiB document and recover" + ], + "peer_idle_rss_kib": 7840, + "peer_after_large_work_rss_kib": 9520 +} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-authoring-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-authoring-linux-aarch64.tar.gz new file mode 100644 index 0000000..0e64e74 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-authoring-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-http-only-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-http-only-linux-aarch64.tar.gz new file mode 100644 index 0000000..3c32eb4 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-http-only-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-micro-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-micro-linux-aarch64.tar.gz new file mode 100644 index 0000000..9b3fddc Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-micro-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-peer-only-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-peer-only-linux-aarch64.tar.gz new file mode 100644 index 0000000..d2b11d3 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-peer-only-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-window-only-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-window-only-linux-aarch64.tar.gz new file mode 100644 index 0000000..911348b Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-definition-authoring/pure-document-window-only-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/window-authoring.png b/prototypes/connections/results/2026-09-24-native-definition-authoring/window-authoring.png new file mode 100644 index 0000000..9a7945c Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-definition-authoring/window-authoring.png differ diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/window-budget.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/window-budget.json new file mode 100644 index 0000000..547b408 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/window-budget.json @@ -0,0 +1,25 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-native-document-authoring", + "scope": "Review ceiling for the selected GTK document-and-definition host on Linux AArch64/GCC 14. Linked code sections and direct dependencies only; the measured 44 MiB whole-process RSS and shared GTK stack remain separate costs.", + "budget_sha256": "27ee292bf3fe9a15d27942f64cff01dfccde0ef5054acbb53faf76208d89d110", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-window": { + "sections": { + "text": 48247, + "data": 1456, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libgio-2.0.so.0", + "libglib-2.0.so.0", + "libgobject-2.0.so.0", + "libgtk-4.so.1" + ], + "sha256": "b16b34dfb1b05bf6437b022992fa185787631734f76043149c410b0caf165895" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-native-definition-authoring/window-only-package-check.json b/prototypes/connections/results/2026-09-24-native-definition-authoring/window-only-package-check.json new file mode 100644 index 0000000..f0fa500 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-definition-authoring/window-only-package-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "b72bc0b5321fa6efcc0ca8fbc3e328bc220333b0af134b1e75b7c5b517999a72", + "archive_bytes": 48934, + "selected_hosts": [ + "pure-edit", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted GTK window and selected external host edit the same document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-document-window/build.json b/prototypes/connections/results/2026-09-24-native-document-window/build.json new file mode 100644 index 0000000..52bcb73 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/build.json @@ -0,0 +1,16 @@ +{ + "host": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "gtk": "4.18.6", + "binary_sha256": { + "build/pure-edit": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03", + "build/pure-connections": "e76d4c0a721c9ceb7142db0edbaa1f5c44bd78a2c55029658109c83e8076875f", + "build/pure-edit-window": "97b12dbfb3af430bb13547d4d0f7791ab35a265fe9e22ca6265c1b4ec5d3165f" + }, + "sources": { + "edit-window.c": "b96687857e2fd66fc25d5ba8e3d91f06a3eae060a4161c30259174c15ee13603", + "check_window.py": "208cc9bb56123c7db0596dd1c97c6da0026f3bc9f0bb9f545f2cbb1acf30b712", + "Makefile": "d7fb2dce9f4b6854207eeb8b6b6e8b2dc294c6c1faf18bf3f486344834fae6d2", + "examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + } +} diff --git a/prototypes/connections/results/2026-09-24-native-document-window/connection-check.json b/prototypes/connections/results/2026-09-24-native-document-window/connection-check.json new file mode 100644 index 0000000..221db03 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/connection-check.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": "0cb23dd0cff6bd0c9dc154f6f9ec253af72a23d93bf1a3797cd45ddd6091c9d0", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-native-document-window/core-budget.json b/prototypes/connections/results/2026-09-24-native-document-window/core-budget.json new file mode 100644 index 0000000..8c0caf7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/core-budget.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-24-native-document-window/dependencies.txt b/prototypes/connections/results/2026-09-24-native-document-window/dependencies.txt new file mode 100644 index 0000000..e61b79d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/dependencies.txt @@ -0,0 +1,10 @@ +pure-edit +0x0000000000000001 (NEEDED) Shared library: [libc.so.6] +pure-connections +0x0000000000000001 (NEEDED) Shared library: [libc.so.6] +pure-edit-window +0x0000000000000001 (NEEDED) Shared library: [libgtk-4.so.1] +0x0000000000000001 (NEEDED) Shared library: [libgio-2.0.so.0] +0x0000000000000001 (NEEDED) Shared library: [libgobject-2.0.so.0] +0x0000000000000001 (NEEDED) Shared library: [libglib-2.0.so.0] +0x0000000000000001 (NEEDED) Shared library: [libc.so.6] diff --git a/prototypes/connections/results/2026-09-24-native-document-window/gtk-flow.json b/prototypes/connections/results/2026-09-24-native-document-window/gtk-flow.json new file mode 100644 index 0000000..1e4aa66 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/gtk-flow.json @@ -0,0 +1 @@ +{"empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "large_window_rss_kib": 44208, "one_mib_document_one_byte_edit": "pass", "profile": "optional-native-document-window", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-native-document-window/gtk-sanitized.json b/prototypes/connections/results/2026-09-24-native-document-window/gtk-sanitized.json new file mode 100644 index 0000000..babeafb --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/gtk-sanitized.json @@ -0,0 +1 @@ +{"empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "large_window_rss_kib": 143168, "one_mib_document_one_byte_edit": "pass", "profile": "optional-native-document-window", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-native-document-window/size.txt b/prototypes/connections/results/2026-09-24-native-document-window/size.txt new file mode 100644 index 0000000..dc609de --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/size.txt @@ -0,0 +1,6 @@ + text data bss dec hex filename + 5012 0 0 5012 1394 runtime.o (ex prototypes/connections/build/libpure-execution.a) + 920 0 0 920 398 evaluate.o (ex prototypes/connections/build/libpure-execution.a) + 35807 944 16 36767 8f9f prototypes/connections/build/pure-edit + 42761 1040 16 43817 ab29 prototypes/connections/build/pure-connections + 43308 1376 16 44700 ae9c prototypes/connections/build/pure-edit-window diff --git a/prototypes/connections/results/2026-09-24-native-document-window/stripped.txt b/prototypes/connections/results/2026-09-24-native-document-window/stripped.txt new file mode 100644 index 0000000..6eb0197 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-document-window/stripped.txt @@ -0,0 +1,3 @@ +pure-edit elf_bytes=177384 stripped_bytes=67904 +pure-connections elf_bytes=199080 stripped_bytes=68000 +pure-edit-window elf_bytes=199184 stripped_bytes=68280 diff --git a/prototypes/connections/results/2026-09-24-native-document-window/window.png b/prototypes/connections/results/2026-09-24-native-document-window/window.png new file mode 100644 index 0000000..809f57a Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-document-window/window.png differ diff --git a/prototypes/connections/results/2026-09-24-native-save/fixed-cli-package-check.json b/prototypes/connections/results/2026-09-24-native-save/fixed-cli-package-check.json new file mode 100644 index 0000000..8e62057 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/fixed-cli-package-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "2f491df77c2d5c39c838f7235bcfcaadbd89b981e5b40624e624553a74604421", + "archive_bytes": 12966, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-save/fixed-package-check.json b/prototypes/connections/results/2026-09-24-native-save/fixed-package-check.json new file mode 100644 index 0000000..97dafe5 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/fixed-package-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "629dca525ec849fe9ab822ca5ff931477438534235ab00f7d34f873ee18b41e7", + "archive_bytes": 32072, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-save/flat-fixed-tui.json b/prototypes/connections/results/2026-09-24-native-save/flat-fixed-tui.json new file mode 100644 index 0000000..97fb4d7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/flat-fixed-tui.json @@ -0,0 +1,24 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1808, + "one_mib_peak_rss_kib": 2832, + "one_thousand_key_list_peak_rss_kib": 1856, + "one_mib_peak_rss_limit_kib": 4096, + "portable": false, + "terminal_locale": "C.UTF-8", + "binary_sha256": "e8b224e3ce5f5d83d597418d4027ec36dd44bfe84d17f6e9224135dc457316c1" +} diff --git a/prototypes/connections/results/2026-09-24-native-save/flat-package-check.json b/prototypes/connections/results/2026-09-24-native-save/flat-package-check.json new file mode 100644 index 0000000..f3db134 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/flat-package-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "0f8d1620cf36c968174687e7ca350d97d8f3101ce38bb70239b673ab104fe6f6", + "archive_bytes": 51117, + "selected_hosts": [ + "pure-edit", + "pure-edit-tui" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "relocated source-loaded TUI and CLI share UTF-8, large and many-document paths" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-save/flat-source-tui.json b/prototypes/connections/results/2026-09-24-native-save/flat-source-tui.json new file mode 100644 index 0000000..aa7d0b9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/flat-source-tui.json @@ -0,0 +1,27 @@ +{ + "profile": "native-source-document-terminal-editor", + "checks": [ + "source parse and incompatible definitions fail before store creation", + "source CLI and TUI read the same document", + "changed source field layout drives TUI input encoding", + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1824, + "one_mib_peak_rss_kib": 3216, + "one_thousand_key_list_peak_rss_kib": 1936, + "one_mib_peak_rss_limit_kib": 4096, + "portable": false, + "terminal_locale": "C.UTF-8", + "binary_sha256": "46bdfde2b8997213bf69c612bd63913330859fd46d77ecb9b7fca99e552ee143" +} diff --git a/prototypes/connections/results/2026-09-24-native-save/hybrid-fixed-tui.json b/prototypes/connections/results/2026-09-24-native-save/hybrid-fixed-tui.json new file mode 100644 index 0000000..5b5e97c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/hybrid-fixed-tui.json @@ -0,0 +1,24 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1840, + "one_mib_peak_rss_kib": 3184, + "one_thousand_key_list_peak_rss_kib": 1904, + "one_mib_peak_rss_limit_kib": 4096, + "portable": false, + "terminal_locale": "C.UTF-8", + "binary_sha256": "09303ec33d07c12b07834b07fb5eeb836005c23fcb3a6b6cc2683631a9d4b13c" +} diff --git a/prototypes/connections/results/2026-09-24-native-save/hybrid-source-tui.json b/prototypes/connections/results/2026-09-24-native-save/hybrid-source-tui.json new file mode 100644 index 0000000..2da02ec --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/hybrid-source-tui.json @@ -0,0 +1,27 @@ +{ + "profile": "native-source-document-terminal-editor", + "checks": [ + "source parse and incompatible definitions fail before store creation", + "source CLI and TUI read the same document", + "changed source field layout drives TUI input encoding", + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1824, + "one_mib_peak_rss_kib": 3200, + "one_thousand_key_list_peak_rss_kib": 1936, + "one_mib_peak_rss_limit_kib": 4096, + "portable": false, + "terminal_locale": "C.UTF-8", + "binary_sha256": "a3812efdf7d22cc98369eb878b91a1e5341d5bed18fd99b9fe4264177fd41973" +} diff --git a/prototypes/connections/results/2026-09-24-native-save/package-check.json b/prototypes/connections/results/2026-09-24-native-save/package-check.json new file mode 100644 index 0000000..e57b8ca --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/package-check.json @@ -0,0 +1,15 @@ +{ + "archive_sha256": "b3c156eb7880ee4cb91b3fe52854cb5069ef035d082b7d539fa2a79592247ec5", + "archive_bytes": 57788, + "selected_hosts": [ + "pure-edit", + "pure-edit-tui" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "selected storage keeps small values inline and maps large values", + "editing the packaged .pure source changes the executed reply", + "relocated source-loaded TUI and CLI share UTF-8, large and many-document paths" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-linux-aarch64.tar.gz new file mode 100644 index 0000000..9f81ca0 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..5114f58 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-save/pure-document-fixed-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-save/pure-document-flat-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-save/pure-document-flat-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..59abe7f Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-save/pure-document-flat-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-save/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-native-save/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..313c49d Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-save/pure-document-hybrid-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-save/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-native-save/pure-source-preview.tar.gz new file mode 100644 index 0000000..9f29727 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-save/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-save/source-package-check.json b/prototypes/connections/results/2026-09-24-native-save/source-package-check.json new file mode 100644 index 0000000..4c93ea8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-save/source-package-check.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "b258788cc9f90d8e6eccc964c8c9f4e2c5460df3dfbb9da07268da07799691e4", + "archive_bytes": 333925, + "source_files": 171, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean selected page store exports mixed revisions to the default flat store", + "clean one-MiB document fans out to two same-process Pure domains with independent conflict and release", + "clean selected page store runs the source-loaded TUI and relocatable CLI+TUI package", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-native-source/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-native-source/pure-source-preview.tar.gz new file mode 100644 index 0000000..40d2e5e Binary files /dev/null and b/prototypes/connections/results/2026-09-24-native-source/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-native-source/source-check-linux-aarch64.json b/prototypes/connections/results/2026-09-24-native-source/source-check-linux-aarch64.json new file mode 100644 index 0000000..8fc4225 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-native-source/source-check-linux-aarch64.json @@ -0,0 +1,13 @@ +{ + "archive_sha256": "39a450a8a2e2cbfad39b95b9e91ede9899f2530cf9574ebff5d7b3748b9b0c3a", + "archive_bytes": 325436, + "source_files": 166, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz new file mode 100644 index 0000000..26c92da Binary files /dev/null and b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..b20331e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-linux-aarch64.tar.gz.check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "8a6dc8691eca1eb0a2b6daaccfd9e58c64777e5b42a4bb73c4b21b72375b880c", + "archive_bytes": 12886, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..f208ccf Binary files /dev/null and b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..2ca530a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-fixed-cli-tui-linux-aarch64.tar.gz.check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "5cb94eed37f7c7acbaad85d20715f2cc42d84d5d0de3af4788db284c9ede3e9e", + "archive_bytes": 31965, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz new file mode 100644 index 0000000..b026d9a Binary files /dev/null and b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..fcc2a79 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-linux-aarch64.tar.gz.check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "d83048e6804252857ae80b6ee843fe8c9346802b9f1f9df30188845338e227af", + "archive_bytes": 22655, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..101a1fa Binary files /dev/null and b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json new file mode 100644 index 0000000..1f72434 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-opaque-core/pure-document-source-cli-tui-linux-aarch64.tar.gz.check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "6f7a534dca12bb670cb4a4f6d28e3d9e569f0667015415c3c0473b5a0f909361", + "archive_bytes": 51005, + "selected_hosts": [ + "pure-edit", + "pure-edit-tui" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "relocated source-loaded TUI and CLI share UTF-8, large and many-document paths" + ] +} diff --git a/prototypes/connections/results/2026-09-24-opaque-core/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-opaque-core/pure-source-preview.tar.gz new file mode 100644 index 0000000..ce3e1b1 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-opaque-core/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-opaque-core/source-package-check.json b/prototypes/connections/results/2026-09-24-opaque-core/source-package-check.json new file mode 100644 index 0000000..898639f --- /dev/null +++ b/prototypes/connections/results/2026-09-24-opaque-core/source-package-check.json @@ -0,0 +1,15 @@ +{ + "archive_sha256": "c0c3983f25088eb75ccf621cee264c210ea99d888f675e3bbb2ca2c59ea3246e", + "archive_bytes": 334682, + "source_files": 171, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean selected page store exports mixed revisions to the default flat store", + "clean one-MiB document fans out to two same-process Pure domains with independent conflict and release", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/app-asan.json b/prototypes/connections/results/2026-09-24-paged-storage/app-asan.json new file mode 100644 index 0000000..121a548 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/app-asan.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": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11679, + "borrowed_output_bytes": 2115420, + "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": "52e81f7277f11cddc8178a6be1f0693a39210445b7d338e3907c50c2f696f6b6", + "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": "4e4bf4bd1f107510521703d49b207cad40b4d05e4d391fb997a34c1128cf2739", + "editor_sha256": "d2e3720a54597506cbe1262fdd6ef380a7bf3a7e7ef9012f4ae7dabf07fc781d", + "binary_sha256": "14e8472baf7c536c40dfbd1197b4e8be03099d1829e361456370ff678bb31cb6", + "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/store-paged.c": "98ac90368b5be8fba5f9cd998145739d13c0003b822f048e4acbc517228e147e", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "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": "26c44763b13cbe80800ece1c6f1ab6c330d1d201edb9ea421e67cf24e8546fd2", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-paged-storage/app-release.json b/prototypes/connections/results/2026-09-24-paged-storage/app-release.json new file mode 100644 index 0000000..bdaf2f7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/app-release.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": 6, + "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": 42, + "commits": 12, + "gets": 15, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 32, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115420, + "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": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 1, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 113, + "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": "ecc89dcd0868ae701fa3d3ffbebae1f90447899d7802441a0062adc45e4f171f", + "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": "2d230f27ab5e3e6cd4d65d52015e456995aabc55b50ef665880755b9d01cca5d", + "editor_sha256": "497577bf9811b3ce787e27c6170a322ea553cdab2371b4f71eb51115a9045a16", + "binary_sha256": "b3df35ce2e25dc00578c79e47f6c5a21334da31df38cd482d9e71b835597f9cf", + "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/store-paged.c": "98ac90368b5be8fba5f9cd998145739d13c0003b822f048e4acbc517228e147e", + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "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": "58526592ee906833718101f1d33878d66c5e357097d5347d02755d922c36dda9", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "546ab85ba3495daa09a6299b23e05308718287b77bde326849ce9c26c8068eb2", + "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": "26c44763b13cbe80800ece1c6f1ab6c330d1d201edb9ea421e67cf24e8546fd2", + "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": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "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-24-paged-storage/gtk-flow.json b/prototypes/connections/results/2026-09-24-paged-storage/gtk-flow.json new file mode 100644 index 0000000..4a58c4b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/gtk-flow.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_window_rss_kib": 44656, "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/memory.json b/prototypes/connections/results/2026-09-24-paged-storage/memory.json new file mode 100644 index 0000000..512da26 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/memory.json @@ -0,0 +1,14 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-native-document-cli-memory", + "scope": "Peak process RSS via a small fork/wait4 helper on Linux AArch64. Covers this CLI, libc, program parsing, store, and one 1 MiB document workload; excludes filesystem cache, kernel buffers, other processes, selected TLS/GTK hosts, and other applications. These are regression ceilings for this toolchain, not a universal memory guarantee.", + "samples": { + "small_edit_rss_kib": 1104, + "one_mib_edit_rss_kib": 2128, + "one_mib_show_rss_kib": 1056 + }, + "budget_sha256": "53144cabe646e0478f25f4e770ce01bad06f4984ef47f5160d212c02c0b7569f", + "binary_sha256": "497577bf9811b3ce787e27c6170a322ea553cdab2371b4f71eb51115a9045a16", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/package-full.json b/prototypes/connections/results/2026-09-24-paged-storage/package-full.json new file mode 100644 index 0000000..88f0795 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/package-full.json @@ -0,0 +1,20 @@ +{ + "archive_sha256": "08565ad139a5f9ad8ab52f96adde37dc092524e9bf699d9441787c489d8d11f1", + "archive_bytes": 111765, + "selected_hosts": [ + "pure-edit", + "pure-connections", + "pure-document-peer", + "pure-edit-window" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted HTTP host updates the same document as the native CLI", + "extracted C/Python TLS peers share a 1 MiB document and recover", + "extracted GTK window and selected external host edit the same document" + ], + "peer_idle_rss_kib": 7856, + "peer_after_large_work_rss_kib": 9552 +} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/package-micro.json b/prototypes/connections/results/2026-09-24-paged-storage/package-micro.json new file mode 100644 index 0000000..552cea9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/package-micro.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "b247ba1a453c2500ec41e9161e130ec9a3b6c9316910fe655920d6fcfb62ec45", + "archive_bytes": 25456, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/paged-profile.json b/prototypes/connections/results/2026-09-24-paged-storage/paged-profile.json new file mode 100644 index 0000000..d898c44 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/paged-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-paged-document-storage", + "scope": "Selected 16 KiB-page document storage backend on Linux AArch64/GCC 14. Same linked-code and direct-dependency ceilings as the flat backend; excludes libc, process RAM, filesystem cache and journal, and optional TLS/GTK. Any failure requires review, not automatic relaxation.", + "budget_sha256": "728cbe5488a86c7cb588a1b9e53d5ddc8aafe617c229624d7abe3e3a68f50699", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 39960, + "data": 1000, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "497577bf9811b3ce787e27c6170a322ea553cdab2371b4f71eb51115a9045a16" + }, + "pure-connections": { + "sections": { + "text": 46824, + "data": 1088, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "b3df35ce2e25dc00578c79e47f6c5a21334da31df38cd482d9e71b835597f9cf" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/peer.json b/prototypes/connections/results/2026-09-24-paged-storage/peer.json new file mode 100644 index 0000000..fa238e3 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/peer.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7840, + "server_after_large_work_rss_kib": 9536 +} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-authoring-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-authoring-linux-aarch64.tar.gz new file mode 100644 index 0000000..259dac1 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-authoring-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-micro-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-micro-linux-aarch64.tar.gz new file mode 100644 index 0000000..ed06d92 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-paged-storage/pure-document-paged-micro-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-paged-storage/unit-asan.json b/prototypes/connections/results/2026-09-24-paged-storage/unit-asan.json new file mode 100644 index 0000000..697b69c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/unit-asan.json @@ -0,0 +1 @@ +{"paged_storage":"snapshot-patch-range-compaction-crash-conflict-uncertain","mixed_edits":128} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/unit.json b/prototypes/connections/results/2026-09-24-paged-storage/unit.json new file mode 100644 index 0000000..697b69c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/unit.json @@ -0,0 +1 @@ +{"paged_storage":"snapshot-patch-range-compaction-crash-conflict-uncertain","mixed_edits":128} diff --git a/prototypes/connections/results/2026-09-24-paged-storage/write-cost.json b/prototypes/connections/results/2026-09-24-paged-storage/write-cost.json new file mode 100644 index 0000000..f3197a8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-paged-storage/write-cost.json @@ -0,0 +1,49 @@ +{ + "profile": "linux-aarch64-16k-page-document-replacement", + "workloads": [ + "replace last byte of 1 MiB document", + "replace one-byte document" + ], + "unit": "successful user-space write syscall bytes targeting object files", + "excludes": "physical device writes, filesystem journal, metadata and directory sync cost", + "one_mib": { + "flat": { + "object_write_syscall_bytes": 1048600, + "object_write_calls": 258, + "object_files": { + "obj-large": 1048600 + }, + "binary_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "paged": { + "object_write_syscall_bytes": 16944, + "object_write_calls": 3, + "object_files": { + "obj-large": 560, + "dat-large-0000000000000001": 1064960 + }, + "binary_sha256": "497577bf9811b3ce787e27c6170a322ea553cdab2371b4f71eb51115a9045a16" + }, + "flat_to_paged_ratio": 61.8862134088763 + }, + "one_byte": { + "flat": { + "object_write_syscall_bytes": 25, + "object_write_calls": 2, + "object_files": { + "obj-tiny": 25 + }, + "binary_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03" + }, + "paged": { + "object_write_syscall_bytes": 16440, + "object_write_calls": 3, + "object_files": { + "obj-tiny": 56, + "dat-tiny-0000000000000001": 32768 + }, + "binary_sha256": "497577bf9811b3ce787e27c6170a322ea553cdab2371b4f71eb51115a9045a16" + }, + "paged_to_flat_ratio": 657.6 + } +} diff --git a/prototypes/connections/results/2026-09-24-portable-peer-source/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-portable-peer-source/pure-source-preview.tar.gz new file mode 100644 index 0000000..ac8ed09 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-portable-peer-source/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-portable-peer-source/source-check-linux-aarch64.json b/prototypes/connections/results/2026-09-24-portable-peer-source/source-check-linux-aarch64.json new file mode 100644 index 0000000..0b54aff --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-peer-source/source-check-linux-aarch64.json @@ -0,0 +1,15 @@ +{ + "archive_sha256": "bf51f79c400f820b48dbf78522449cda6625563ce860809e86d76a8023377dbb", + "archive_bytes": 321958, + "source_files": 165, + "checks": [ + "every extracted source file matches the manifest", + "clean C17 build and host-neutral behavior tests pass", + "clean fixed and source-loaded native terminal PTY scenarios pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean native TLS client and both server profiles pass authenticated document work", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-aarch64.json b/prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-aarch64.json new file mode 100644 index 0000000..b967ae6 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-aarch64.json @@ -0,0 +1,17 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": null, + "server_after_large_work_rss_kib": null, + "portable": true +} diff --git a/prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-rss-regression.json b/prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-rss-regression.json new file mode 100644 index 0000000..635361a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-peer/client-server-linux-rss-regression.json @@ -0,0 +1,17 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7968, + "server_after_large_work_rss_kib": 9712, + "portable": false +} diff --git a/prototypes/connections/results/2026-09-24-portable-peer/client-server-profile.json b/prototypes/connections/results/2026-09-24-portable-peer/client-server-profile.json new file mode 100644 index 0000000..f8e8511 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-peer/client-server-profile.json @@ -0,0 +1,23 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-document-peer", + "scope": "Review threshold for the explicitly selected authenticated document host on Linux AArch64/GCC 14. Includes linked application code but excludes the shared OpenSSL/libc implementations, process heap, stack, mappings and kernel buffers. Check measured whole-process RSS separately.", + "budget_sha256": "defe8efb4d544cdbf615de4d43d4ceba78fc7f1d958f245731202625052eb2ff", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-document-peer": { + "sections": { + "text": 46934, + "data": 1384, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libcrypto.so.3", + "libssl.so.3" + ], + "sha256": "06fc08105cb9833599b9c778797cf2be4ba8a6c1db514c836bb76667a8c7265e" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-aarch64.json b/prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-aarch64.json new file mode 100644 index 0000000..b967ae6 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-aarch64.json @@ -0,0 +1,17 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": null, + "server_after_large_work_rss_kib": null, + "portable": true +} diff --git a/prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-rss-regression.json b/prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-rss-regression.json new file mode 100644 index 0000000..635361a --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-peer/server-only-linux-rss-regression.json @@ -0,0 +1,17 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7968, + "server_after_large_work_rss_kib": 9712, + "portable": false +} diff --git a/prototypes/connections/results/2026-09-24-portable-peer/server-only-profile.json b/prototypes/connections/results/2026-09-24-portable-peer/server-only-profile.json new file mode 100644 index 0000000..0f6a3ce --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-peer/server-only-profile.json @@ -0,0 +1,23 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-document-server", + "scope": "Reviewed server-only TLS peer ceiling on Linux AArch64/GCC 14. Excludes linked native client code and shared OpenSSL/libc implementations; process RSS is measured separately.", + "budget_sha256": "deae8827323061543193779ada90d1bca4db3a6dda80293e4ad28a9526be9819", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-document-server": { + "sections": { + "text": 43462, + "data": 1352, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libcrypto.so.3", + "libssl.so.3" + ], + "sha256": "728025cfbdbcdb6d212a2ec8297a0740967653c77ae8908d923c59ddd4ea0315" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-portable-source/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-portable-source/pure-source-preview.tar.gz new file mode 100644 index 0000000..8ef47db Binary files /dev/null and b/prototypes/connections/results/2026-09-24-portable-source/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-portable-source/source-check-linux-aarch64.json b/prototypes/connections/results/2026-09-24-portable-source/source-check-linux-aarch64.json new file mode 100644 index 0000000..57a1027 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-source/source-check-linux-aarch64.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "5f1766fefe2c319d86c3ff57c881638a9d1ee24374dea33c614e25577d087814", + "archive_bytes": 321399, + "source_files": 165, + "checks": [ + "every extracted source file matches the manifest", + "clean C17 build and host-neutral behavior tests pass", + "clean fixed and source-loaded native terminal PTY scenarios pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-aarch64.json b/prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-aarch64.json new file mode 100644 index 0000000..3e0fe7d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-aarch64.json @@ -0,0 +1,24 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": null, + "one_mib_peak_rss_kib": null, + "one_thousand_key_list_peak_rss_kib": null, + "one_mib_peak_rss_limit_kib": 0, + "portable": true, + "terminal_locale": "C.UTF-8", + "binary_sha256": "45491c3e28193024d6eb77cbd6569ee8b7470d11ec4126ff4515dba116790308" +} diff --git a/prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-rss-regression.json b/prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-rss-regression.json new file mode 100644 index 0000000..19090b0 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-tui/fixed-linux-rss-regression.json @@ -0,0 +1,24 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1776, + "one_mib_peak_rss_kib": 3168, + "one_thousand_key_list_peak_rss_kib": 1888, + "one_mib_peak_rss_limit_kib": 4096, + "portable": false, + "terminal_locale": "C.UTF-8", + "binary_sha256": "45491c3e28193024d6eb77cbd6569ee8b7470d11ec4126ff4515dba116790308" +} diff --git a/prototypes/connections/results/2026-09-24-portable-tui/source-linux-aarch64.json b/prototypes/connections/results/2026-09-24-portable-tui/source-linux-aarch64.json new file mode 100644 index 0000000..53b3218 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-tui/source-linux-aarch64.json @@ -0,0 +1,27 @@ +{ + "profile": "native-source-document-terminal-editor", + "checks": [ + "source parse and incompatible definitions fail before store creation", + "source CLI and TUI read the same document", + "changed source field layout drives TUI input encoding", + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": null, + "one_mib_peak_rss_kib": null, + "one_thousand_key_list_peak_rss_kib": null, + "one_mib_peak_rss_limit_kib": 0, + "portable": true, + "terminal_locale": "C.UTF-8", + "binary_sha256": "e4fb890935f77a7b359163758ce4c44f3710d1a99130a7d883a4f7329a20a70d" +} diff --git a/prototypes/connections/results/2026-09-24-portable-tui/source-linux-rss-regression.json b/prototypes/connections/results/2026-09-24-portable-tui/source-linux-rss-regression.json new file mode 100644 index 0000000..b112863 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-portable-tui/source-linux-rss-regression.json @@ -0,0 +1,27 @@ +{ + "profile": "native-source-document-terminal-editor", + "checks": [ + "source parse and incompatible definitions fail before store creation", + "source CLI and TUI read the same document", + "changed source field layout drives TUI input encoding", + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1856, + "one_mib_peak_rss_kib": 3664, + "one_thousand_key_list_peak_rss_kib": 1936, + "one_mib_peak_rss_limit_kib": 4096, + "portable": false, + "terminal_locale": "C.UTF-8", + "binary_sha256": "e4fb890935f77a7b359163758ce4c44f3710d1a99130a7d883a4f7329a20a70d" +} diff --git a/prototypes/connections/results/2026-09-24-posix-idle/fixed-profile.json b/prototypes/connections/results/2026-09-24-posix-idle/fixed-profile.json new file mode 100644 index 0000000..b56deb9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-posix-idle/fixed-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8" + }, + "pure-connections-fixed": { + "sections": { + "text": 26261, + "data": 1028, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "1999ad534e9091fbaca096402ba2fbeaeb92c9fb4d4150cacaf309e9a9cd9555" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-posix-idle/fixed-server-regression.json b/prototypes/connections/results/2026-09-24-posix-idle/fixed-server-regression.json new file mode 100644 index 0000000..d96f70c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-posix-idle/fixed-server-regression.json @@ -0,0 +1,68 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "slow_consumer_budget_and_release": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 11, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "limited_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048586, + "server_structure_bytes": 40848 + }, + "fixed_idle_rss_kib": 1520, + "source_idle_rss_kib": 1552, + "fixed_after_large_rss_kib": 1648, + "source_after_large_rss_kib": 1680, + "fixed_peak_rss_kib": 2144, + "source_peak_rss_kib": 1680, + "fixed_peak_rss_limit_kib": 4096, + "fixed_sha256": "1999ad534e9091fbaca096402ba2fbeaeb92c9fb4d4150cacaf309e9a9cd9555", + "source_sha256": "1d4e858f18e7cea6df263e2d716898ba7a15b966ecda7e1145b21b80f8fe8f88" +} diff --git a/prototypes/connections/results/2026-09-24-posix-idle/package.json b/prototypes/connections/results/2026-09-24-posix-idle/package.json new file mode 100644 index 0000000..5085bdd --- /dev/null +++ b/prototypes/connections/results/2026-09-24-posix-idle/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "47f8a4943187edc3733f5900790d5c240d25e2042a0b6c3567eb5da2ece68dd1", + "archive_bytes": 32346, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-posix-idle/pure-document-fixed-http-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-posix-idle/pure-document-fixed-http-linux-aarch64.tar.gz new file mode 100644 index 0000000..8b50236 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-posix-idle/pure-document-fixed-http-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-posix-idle/reproducible-comparison.json b/prototypes/connections/results/2026-09-24-posix-idle/reproducible-comparison.json new file mode 100644 index 0000000..7548186 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-posix-idle/reproducible-comparison.json @@ -0,0 +1,21 @@ +{ + "profile": "idle-http-host-poll-syscalls", + "observation_seconds": 1.25, + "before": { + "poll_calls": 25, + "timeout_values_ms": [ + 50 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=PO", + "binary_sha256": "7dd0010fa6d6024afa2fe35d57a4c73f33ac632e732616ac8f8853d7b22d79b3" + }, + "after": { + "poll_calls": 1, + "timeout_values_ms": [ + -1 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=4, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 10, NULL, NULL, 0) = ? ERESTARTNOHAND (To be restarted if no handler)\n--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=941369, si_uid=1000} ---\n", + "binary_sha256": "1999ad534e9091fbaca096402ba2fbeaeb92c9fb4d4150cacaf309e9a9cd9555" + }, + "deadline_driven_idle_wait": "pass" +} diff --git a/prototypes/connections/results/2026-09-24-posix-idle/service-scale.json b/prototypes/connections/results/2026-09-24-posix-idle/service-scale.json new file mode 100644 index 0000000..c1396ab --- /dev/null +++ b/prototypes/connections/results/2026-09-24-posix-idle/service-scale.json @@ -0,0 +1,62 @@ +{ + "profile": "document-app-many-objects-concurrent-clients", + "documents_per_variant": 1024, + "clients": 8, + "body_bytes": 8192, + "operations_per_variant": 4096, + "server_budget_bytes": 1048576, + "peak_rss_limit_kib": 4096, + "same_final_documents": "pass", + "fixed": { + "binary_sha256": "1999ad534e9091fbaca096402ba2fbeaeb92c9fb4d4150cacaf309e9a9cd9555", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1504, + "peak_rss_kib": 1680, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49185, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 326623, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 40848 + } + }, + "source": { + "binary_sha256": "1d4e858f18e7cea6df263e2d716898ba7a15b966ecda7e1145b21b80f8fe8f88", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1552, + "peak_rss_kib": 1712, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49168, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 366236, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 41528 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-posix-idle/source-profile.json b/prototypes/connections/results/2026-09-24-posix-idle/source-profile.json new file mode 100644 index 0000000..d5795da --- /dev/null +++ b/prototypes/connections/results/2026-09-24-posix-idle/source-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35807, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "bce466328ac730ccb7cd22c7a0c4e2960bc94a0f20ac9f036e232bd499521ff7" + }, + "pure-connections": { + "sections": { + "text": 43778, + "data": 1068, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "1d4e858f18e7cea6df263e2d716898ba7a15b966ecda7e1145b21b80f8fe8f88" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-prefix-find/cli-tui.tar.gz b/prototypes/connections/results/2026-09-24-prefix-find/cli-tui.tar.gz new file mode 100644 index 0000000..dff004b Binary files /dev/null and b/prototypes/connections/results/2026-09-24-prefix-find/cli-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-prefix-find/package-check.json b/prototypes/connections/results/2026-09-24-prefix-find/package-check.json new file mode 100644 index 0000000..440bc20 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-prefix-find/package-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "54a579102d6f9563ee8d077e49eaf156ad3fc826e42532607fce45a75283b10d", + "archive_bytes": 32233, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-prefix-find/tui-profile.json b/prototypes/connections/results/2026-09-24-prefix-find/tui-profile.json new file mode 100644 index 0000000..fd11cb1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-prefix-find/tui-profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app, including the bounded document browser. The raised text ceiling reviews its measured 2,541-byte addition; default CLI and core ceilings are unchanged. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS, including 1,024-key browsing, is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "4d3097c9ec3c0139cbdd82161a74d610c15d31c6ee5625101410627664f925e7", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 32175, + "data": 1036, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "39b5bcf605739eabd731e471a3f54e03592e2bfc0a405db5faacdb4df84d0b22" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-prefix-find/tui.json b/prototypes/connections/results/2026-09-24-prefix-find/tui.json new file mode 100644 index 0000000..6588e17 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-prefix-find/tui.json @@ -0,0 +1,22 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1824, + "one_mib_peak_rss_kib": 3200, + "one_thousand_key_list_peak_rss_kib": 1904, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "39b5bcf605739eabd731e471a3f54e03592e2bfc0a405db5faacdb4df84d0b22" +} diff --git a/prototypes/connections/results/2026-09-24-release-callback/async-core.json b/prototypes/connections/results/2026-09-24-release-callback/async-core.json new file mode 100644 index 0000000..6e5371e --- /dev/null +++ b/prototypes/connections/results/2026-09-24-release-callback/async-core.json @@ -0,0 +1,55 @@ +{ + "passed": true, + "failures": [], + "budget": { + "profile": "selected-owner-thread-async-effects", + "max_text_bytes": 8192, + "max_data_bytes": 0, + "max_bss_bytes": 0, + "layout_max_bytes": { + "domain_bytes": 128, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "effect_bytes": 128 + }, + "archive_members": [ + "runtime-async.o", + "evaluate.o" + ], + "external_symbols_allowed": [ + "memcpy", + "memmove", + "memset", + "strlen" + ], + "scope": "Selected compact owner-thread completion extension without opt-in PURE_RUNTIME_METRICS. Host I/O and worker stacks are excluded. The default execution core and fixed document packages do not link this profile." + }, + "layout": { + "domain_bytes": 128, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8, + "effect_bytes": 128 + }, + "sections": { + "text": 7372, + "data": 0, + "bss": 0 + }, + "members": [ + "runtime-async.o", + "evaluate.o" + ], + "external_symbols": [ + "memcpy", + "strlen" + ], + "size_output": " text\t data\t bss\t dec\t hex\tfilename\n 6452\t 0\t 0\t 6452\t 1934\truntime-async.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution-async.a)\n 920\t 0\t 0\t 920\t 398\tevaluate.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution-async.a)\n", + "archive_sha256": "e6b90a42ca7dc894483c920d59c606895a83d16fe2e6b4f1d0a6d36a83d7efb5", + "layout_binary_sha256": "be9307acf8264bc1b333dc05fa0e4b162f912ce8e8b82e3d20eddf49e269d42c", + "budget_sha256": "33afabc20949bc322af9e86b946d39147cd714e32d4e095bc0cc404d11aa79ad", + "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-24-release-callback/core.json b/prototypes/connections/results/2026-09-24-release-callback/core.json new file mode 100644 index 0000000..6182c51 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-release-callback/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": 128, + "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": "Compact release object sections and C bookkeeping, without opt-in PURE_RUNTIME_METRICS, 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": 128, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8 + }, + "sections": { + "text": 5980, + "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 5060\t 0\t 0\t 5060\t 13c4\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": "c5f05b1fb9d51f244687425a1b1d2f8922f6f899f0b617b4de490c730d106e97", + "layout_binary_sha256": "04c008fb7e0b1ecc8435bce6fbc4478de6180a54da6538d8742acb9b51992fe3", + "budget_sha256": "e418419db6231305342ea8780149948e0e41e28b3afb937268e80b4695d3d5cd", + "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-24-release-callback/micro-package-check.json b/prototypes/connections/results/2026-09-24-release-callback/micro-package-check.json new file mode 100644 index 0000000..9db433b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-release-callback/micro-package-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "722d68e58d948810c4a181d0dc832ad97bc5c65b5f5629d1af5866aff920e0b5", + "archive_bytes": 22758, + "selected_hosts": [ + "pure-edit" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply" + ] +} diff --git a/prototypes/connections/results/2026-09-24-release-callback/pure-document-micro-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-release-callback/pure-document-micro-linux-aarch64.tar.gz new file mode 100644 index 0000000..44f9c7f Binary files /dev/null and b/prototypes/connections/results/2026-09-24-release-callback/pure-document-micro-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-release-callback/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-release-callback/pure-source-preview.tar.gz new file mode 100644 index 0000000..68e4f0a Binary files /dev/null and b/prototypes/connections/results/2026-09-24-release-callback/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-release-callback/runtime.json b/prototypes/connections/results/2026-09-24-release-callback/runtime.json new file mode 100644 index 0000000..6f6527d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-release-callback/runtime.json @@ -0,0 +1 @@ +{"runtime_tests":"passed","domain_bytes":200,"work_slot_bytes":224,"ticket_bytes":24,"prepared_program_bytes":680,"forwarding_chain":1024,"native_storage_pipeline":true,"idle_slot_binding":true,"release_reentrancy_guard":true} diff --git a/prototypes/connections/results/2026-09-24-release-callback/source-package-check.json b/prototypes/connections/results/2026-09-24-release-callback/source-package-check.json new file mode 100644 index 0000000..459429c --- /dev/null +++ b/prototypes/connections/results/2026-09-24-release-callback/source-package-check.json @@ -0,0 +1,15 @@ +{ + "archive_sha256": "cff657597c0cc33bd9185c8460e0d84f0843da20a786578324ac612fa255c2b4", + "archive_bytes": 333037, + "source_files": 170, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean selected page store exports mixed revisions to the default flat store", + "clean one-MiB document fans out to two same-process Pure domains with independent conflict and release", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-server-only-package/combined-peer-package-check.json b/prototypes/connections/results/2026-09-24-server-only-package/combined-peer-package-check.json new file mode 100644 index 0000000..d8ed889 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-server-only-package/combined-peer-package-check.json @@ -0,0 +1,27 @@ +{ + "archive_sha256": "d1592ec4e4085ebe328c01ccf49911c383fc87908344cfea5fb9bca56a0fef96", + "archive_bytes": 49382, + "selected_hosts": [ + "pure-edit", + "pure-document-peer" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted C/Python TLS peers share a 1 MiB document and recover" + ], + "peer_checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "peer_idle_rss_kib": 7968, + "peer_after_large_work_rss_kib": 9712 +} diff --git a/prototypes/connections/results/2026-09-24-server-only-package/pure-document-combined-peer-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-server-only-package/pure-document-combined-peer-linux-aarch64.tar.gz new file mode 100644 index 0000000..ebd24da Binary files /dev/null and b/prototypes/connections/results/2026-09-24-server-only-package/pure-document-combined-peer-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-server-only-package/pure-document-server-only-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-server-only-package/pure-document-server-only-linux-aarch64.tar.gz new file mode 100644 index 0000000..2a47d7e Binary files /dev/null and b/prototypes/connections/results/2026-09-24-server-only-package/pure-document-server-only-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-server-only-package/server-only-package-check.json b/prototypes/connections/results/2026-09-24-server-only-package/server-only-package-check.json new file mode 100644 index 0000000..997287d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-server-only-package/server-only-package-check.json @@ -0,0 +1,23 @@ +{ + "archive_sha256": "34fb507c160e4aa719cf44fb4f8bbcb422d2c0ee6dc3f206f6a5fe3bc4d089b6", + "archive_bytes": 47686, + "selected_hosts": [ + "pure-edit", + "pure-document-server" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "extracted server-only TLS peer and external client share a 1 MiB document and recover" + ], + "peer_checks": [ + "relocated server rejects invalid client identity, ALPN and hostname", + "external client and extracted CLI share a conditional document", + "fragmented one MiB ingress, selected reply and CLI-visible splice", + "truncated and invalid frames do not publish a change", + "relocated server restart restores the committed one MiB document" + ], + "peer_idle_rss_kib": 7968, + "peer_after_large_work_rss_kib": 8672 +} diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-profile.json b/prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-profile.json new file mode 100644 index 0000000..733fa60 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-runner", + "scope": "Release-linked CLI and HTTP document hosts with one build-time prepared app. The source parser and compiler are separate build tools. These text ceilings apply only to the recorded Linux AArch64/GCC 14 toolchain and exclude libc, heap, stack, and storage files.", + "budget_sha256": "2b7e550992abb1872e52846dec712fab59b78692b776fd7e648d7976299f4b2a", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-fixed": { + "sections": { + "text": 19244, + "data": 920, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8" + }, + "pure-connections-fixed": { + "sections": { + "text": 25790, + "data": 1024, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "fd474f7331ac47d22360f2bfe8ba491ff8ffac448107b41da74a00142ae97958" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-server-regression.json b/prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-server-regression.json new file mode 100644 index 0000000..7857bde --- /dev/null +++ b/prototypes/connections/results/2026-09-24-signal-safe-idle/fixed-server-regression.json @@ -0,0 +1,68 @@ +{ + "profile": "build-time-prepared-http-document-host", + "fixed_source_cli_same_document": "pass", + "conditional_conflict_and_invalid_splice": "pass", + "one_mib_roundtrip_and_native_edit": "pass", + "slow_consumer_budget_and_release": "pass", + "runtime_parser_symbols_absent": "pass", + "fixed_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 7, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097177, + "server_structure_bytes": 40848 + }, + "source_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 6, + "commits": 1, + "gets": 3, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 6, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2097162, + "server_structure_bytes": 41528 + }, + "limited_stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048586, + "server_structure_bytes": 40848 + }, + "fixed_idle_rss_kib": 1504, + "source_idle_rss_kib": 1552, + "fixed_after_large_rss_kib": 1632, + "source_after_large_rss_kib": 1680, + "fixed_peak_rss_kib": 2112, + "source_peak_rss_kib": 2096, + "fixed_peak_rss_limit_kib": 4096, + "fixed_sha256": "fd474f7331ac47d22360f2bfe8ba491ff8ffac448107b41da74a00142ae97958", + "source_sha256": "9818530bdadd3da2c3cd63feb24f0585f8527887bcc9c8528d67736ee3a97653" +} diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/package.json b/prototypes/connections/results/2026-09-24-signal-safe-idle/package.json new file mode 100644 index 0000000..8304c17 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-signal-safe-idle/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "413f88a31272c5a0851ab598673de5523fa12d913e0a077441aa342b506498e2", + "archive_bytes": 32023, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document" + ] +} diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/pure-document-fixed-http-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-signal-safe-idle/pure-document-fixed-http-linux-aarch64.tar.gz new file mode 100644 index 0000000..54c21a2 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-signal-safe-idle/pure-document-fixed-http-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/reproducible-comparison.json b/prototypes/connections/results/2026-09-24-signal-safe-idle/reproducible-comparison.json new file mode 100644 index 0000000..79f8c49 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-signal-safe-idle/reproducible-comparison.json @@ -0,0 +1,21 @@ +{ + "profile": "idle-http-host-poll-syscalls", + "observation_seconds": 1.25, + "before": { + "poll_calls": 25, + "timeout_values_ms": [ + 50 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, {tv_sec=0, tv_nsec=50000000}, NULL, 0) = 0 (Timeout)\nppoll([{fd=3, events=PO", + "binary_sha256": "7dd0010fa6d6024afa2fe35d57a4c73f33ac632e732616ac8f8853d7b22d79b3" + }, + "after": { + "poll_calls": 1, + "timeout_values_ms": [ + -1 + ], + "trace_excerpt": "ppoll([{fd=3, events=POLLIN}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}, {fd=-1}], 9, NULL, [], 8) = ? ERESTARTNOHAND (To be restarted if no handler)\n--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=932958, si_uid=1000} ---\n", + "binary_sha256": "fd474f7331ac47d22360f2bfe8ba491ff8ffac448107b41da74a00142ae97958" + }, + "deadline_driven_idle_wait": "pass" +} diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale-restored.json b/prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale-restored.json new file mode 100644 index 0000000..a4a9764 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale-restored.json @@ -0,0 +1,62 @@ +{ + "profile": "document-app-many-objects-concurrent-clients", + "documents_per_variant": 1024, + "clients": 8, + "body_bytes": 8192, + "operations_per_variant": 4096, + "server_budget_bytes": 1048576, + "peak_rss_limit_kib": 4096, + "same_final_documents": "pass", + "fixed": { + "binary_sha256": "fd474f7331ac47d22360f2bfe8ba491ff8ffac448107b41da74a00142ae97958", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1520, + "peak_rss_kib": 1696, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49200, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 696751, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 40848 + } + }, + "source": { + "binary_sha256": "9818530bdadd3da2c3cd63feb24f0585f8527887bcc9c8528d67736ee3a97653", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "fresh_cli_restore_samples": 4, + "idle_rss_kib": 1552, + "peak_rss_kib": 1712, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 40993, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 410074, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 41528 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale.json b/prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale.json new file mode 100644 index 0000000..4bedc63 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-signal-safe-idle/service-scale.json @@ -0,0 +1,60 @@ +{ + "profile": "document-app-many-objects-concurrent-clients", + "documents_per_variant": 1024, + "clients": 8, + "body_bytes": 8192, + "operations_per_variant": 4096, + "server_budget_bytes": 1048576, + "peak_rss_limit_kib": 4096, + "same_final_documents": "pass", + "fixed": { + "binary_sha256": "fd474f7331ac47d22360f2bfe8ba491ff8ffac448107b41da74a00142ae97958", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "idle_rss_kib": 1520, + "peak_rss_kib": 1712, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 49201, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 899986, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 40848 + } + }, + "source": { + "binary_sha256": "9818530bdadd3da2c3cd63feb24f0585f8527887bcc9c8528d67736ee3a97653", + "document_digest": "e54baed00186fb020edbbcd407d86471c136287de346e3386ce52d16883e488b", + "stored_objects": 1024, + "stored_file_bytes": 8413184, + "idle_rss_kib": 1552, + "peak_rss_kib": 1728, + "stats": { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 41010, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 4096, + "commits": 2048, + "gets": 2048, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 4096, + "ingress_prefix_copy_bytes": 939752, + "borrowed_output_bytes": 25166848, + "server_structure_bytes": 41528 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-signal-safe-idle/source-profile.json b/prototypes/connections/results/2026-09-24-signal-safe-idle/source-profile.json new file mode 100644 index 0000000..5bc9266 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-signal-safe-idle/source-profile.json @@ -0,0 +1,32 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-default-document-hosts", + "scope": "Review thresholds for release-built default CLI and HTTP adapter on the recorded Linux AArch64/GCC 14 toolchain. These section sizes exclude shared libc, heap, stack, mapped files, kernel buffers, and optional GTK/TLS hosts. Any failure requires a cost review, not automatic relaxation.", + "budget_sha256": "8b1eb0dd4844757705c24a0674142e76d9ee83fdce80a841e87a08e757f1183d", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit": { + "sections": { + "text": 35807, + "data": 944, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "bce466328ac730ccb7cd22c7a0c4e2960bc94a0f20ac9f036e232bd499521ff7" + }, + "pure-connections": { + "sections": { + "text": 43451, + "data": 1064, + "bss": 16 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "9818530bdadd3da2c3cd63feb24f0585f8527887bcc9c8528d67736ee3a97653" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-source-preview/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-source-preview/pure-source-preview.tar.gz new file mode 100644 index 0000000..45e83a0 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-source-preview/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-source-preview/source-check.json b/prototypes/connections/results/2026-09-24-source-preview/source-check.json new file mode 100644 index 0000000..a3b7681 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-preview/source-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "a11a058fa0ba71878a0fed0ee63100566f580721fa5cb602ad25cd46e6b94f6f", + "archive_bytes": 316748, + "source_files": 161, + "checks": [ + "every extracted source file matches the manifest", + "clean C17 build and host-neutral behavior tests pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean Linux release behavior, code, dependency and RSS gates pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-source-tui/core.json b/prototypes/connections/results/2026-09-24-source-tui/core.json new file mode 100644 index 0000000..63eb227 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-tui/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": 128, + "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": "Compact release object sections and C bookkeeping, without opt-in PURE_RUNTIME_METRICS, 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": 128, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8 + }, + "sections": { + "text": 5908, + "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 4988\t 0\t 0\t 4988\t 137c\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": "bb5750a47f6f8ac4307dd633d836460d6045347880a6c53df9669e675e49929e", + "layout_binary_sha256": "04c008fb7e0b1ecc8435bce6fbc4478de6180a54da6538d8742acb9b51992fe3", + "budget_sha256": "e418419db6231305342ea8780149948e0e41e28b3afb937268e80b4695d3d5cd", + "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-24-source-tui/fixed-cli-tui-check.json b/prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui-check.json new file mode 100644 index 0000000..49d2f70 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "c0d600fd9b80a5bdeacb4c015d0050ddf3fde6f8a3ff1dd86905e3fcb681fd30", + "archive_bytes": 31929, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui.tar.gz b/prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui.tar.gz new file mode 100644 index 0000000..e576af8 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-source-tui/fixed-cli-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-source-tui/fixed-tui-profile.json b/prototypes/connections/results/2026-09-24-source-tui/fixed-tui-profile.json new file mode 100644 index 0000000..1c5a451 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-tui/fixed-tui-profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app, including the bounded document browser. The raised text ceiling reviews its measured 2,541-byte addition; default CLI and core ceilings are unchanged. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS, including 1,024-key browsing, is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "4d3097c9ec3c0139cbdd82161a74d610c15d31c6ee5625101410627664f925e7", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 31999, + "data": 1036, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "45491c3e28193024d6eb77cbd6569ee8b7470d11ec4126ff4515dba116790308" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-source-tui/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-source-tui/pure-source-preview.tar.gz new file mode 100644 index 0000000..265a111 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-source-tui/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-source-tui/source-check.json b/prototypes/connections/results/2026-09-24-source-tui/source-check.json new file mode 100644 index 0000000..e7569bb --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-tui/source-check.json @@ -0,0 +1,13 @@ +{ + "archive_sha256": "6134ff966d09c76134b921008d16b828a349b7031bdf029379de6eea9e745eaa", + "archive_bytes": 318619, + "source_files": 162, + "checks": [ + "every extracted source file matches the manifest", + "clean C17 build and host-neutral behavior tests pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/results/2026-09-24-source-tui/source-cli-tui-check.json b/prototypes/connections/results/2026-09-24-source-tui/source-cli-tui-check.json new file mode 100644 index 0000000..1c3dd89 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-tui/source-cli-tui-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "6b2671f7e2faf91e96ec9c29d03f5087a1e66f26fbfcc93865700a22e90df9de", + "archive_bytes": 50942, + "selected_hosts": [ + "pure-edit", + "pure-edit-tui" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "extracted native CLI creates and restores the real document", + "editing the packaged .pure source changes the executed reply", + "relocated source-loaded TUI and CLI share UTF-8, large and many-document paths" + ] +} diff --git a/prototypes/connections/results/2026-09-24-source-tui/source-cli-tui.tar.gz b/prototypes/connections/results/2026-09-24-source-tui/source-cli-tui.tar.gz new file mode 100644 index 0000000..0e2e38f Binary files /dev/null and b/prototypes/connections/results/2026-09-24-source-tui/source-cli-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-source-tui/tui-profile.json b/prototypes/connections/results/2026-09-24-source-tui/tui-profile.json new file mode 100644 index 0000000..348807d --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-tui/tui-profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-source-document-terminal-editor", + "scope": "Optional source-loaded POSIX terminal editor for the same document app. Includes the runtime .pure parser and bounded document browser. Default core, fixed CLI and fixed TUI do not link the parser. These are Linux AArch64/GCC 14 section regression ceilings, not whole-process RAM; check_tui.py measures the selected whole process separately.", + "budget_sha256": "52df1c0a75119edbe0cfd75d6961c1713237146d44a26cd3b2ce096e4d80d624", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui": { + "sections": { + "text": 48604, + "data": 1084, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "e4fb890935f77a7b359163758ce4c44f3710d1a99130a7d883a4f7329a20a70d" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-source-tui/tui.json b/prototypes/connections/results/2026-09-24-source-tui/tui.json new file mode 100644 index 0000000..184936b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-source-tui/tui.json @@ -0,0 +1,25 @@ +{ + "profile": "native-source-document-terminal-editor", + "checks": [ + "source parse and incompatible definitions fail before store creation", + "source CLI and TUI read the same document", + "changed source field layout drives TUI input encoding", + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, searched, resized and opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1856, + "one_mib_peak_rss_kib": 3216, + "one_thousand_key_list_peak_rss_kib": 1952, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "e4fb890935f77a7b359163758ce4c44f3710d1a99130a7d883a4f7329a20a70d" +} diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/cli-check.json b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-check.json new file mode 100644 index 0000000..d0bb1fc --- /dev/null +++ b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-check.json @@ -0,0 +1,12 @@ +{ + "archive_sha256": "19573b079d8d28b08aebeb185acd1cdfc66c006f183e955fba156f8d94d3c4c8", + "archive_bytes": 14706, + "selected_hosts": [ + "pure-edit-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits" + ] +} diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui-check.json b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui-check.json new file mode 100644 index 0000000..47d26f1 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui-check.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "9948083ef0b3b4db1d4efbc4991da1887b2b21a33670c435cd364fcada953f56", + "archive_bytes": 53385, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui.tar.gz b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui.tar.gz new file mode 100644 index 0000000..5d5c967 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-http-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui-check.json b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui-check.json new file mode 100644 index 0000000..6dfc3e8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui-check.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "a497e58e0c8942386f31a3fdbed11f038aa796afb87aff26eeea81871f2465ed", + "archive_bytes": 35642, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui.tar.gz b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui.tar.gz new file mode 100644 index 0000000..ddcd49f Binary files /dev/null and b/prototypes/connections/results/2026-09-24-terminal-document-list/cli-tui.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/cli.tar.gz b/prototypes/connections/results/2026-09-24-terminal-document-list/cli.tar.gz new file mode 100644 index 0000000..7c121af Binary files /dev/null and b/prototypes/connections/results/2026-09-24-terminal-document-list/cli.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/profile.json b/prototypes/connections/results/2026-09-24-terminal-document-list/profile.json new file mode 100644 index 0000000..a46d089 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-terminal-document-list/profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app, including the bounded document browser. The raised text ceiling reviews its measured 2,541-byte addition; default CLI and core ceilings are unchanged. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS, including 1,024-key browsing, is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "4d3097c9ec3c0139cbdd82161a74d610c15d31c6ee5625101410627664f925e7", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 31303, + "data": 1036, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "8b831e7582a0872b90c8b8b3dc4d30088393c3a88713a4eae938c8816bdcda68" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/sanitized.json b/prototypes/connections/results/2026-09-24-terminal-document-list/sanitized.json new file mode 100644 index 0000000..d49a34b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-terminal-document-list/sanitized.json @@ -0,0 +1,22 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, resized and last key opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 9856, + "one_mib_peak_rss_kib": 11744, + "one_thousand_key_list_peak_rss_kib": 9904, + "one_mib_peak_rss_limit_kib": 0, + "binary_sha256": "4b4b553289e4cec10aa58e0ef5442905f84d2ddef72fa3d36f301825b2b9ac84" +} diff --git a/prototypes/connections/results/2026-09-24-terminal-document-list/tui.json b/prototypes/connections/results/2026-09-24-terminal-document-list/tui.json new file mode 100644 index 0000000..d8d45f8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-terminal-document-list/tui.json @@ -0,0 +1,22 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "bounded document browser selects keys and protects unsaved draft", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "1,024 real documents listed, resized and last key opened within RSS gate", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1824, + "one_mib_peak_rss_kib": 2832, + "one_thousand_key_list_peak_rss_kib": 1856, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "8b831e7582a0872b90c8b8b3dc4d30088393c3a88713a4eae938c8816bdcda68" +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/latency-baseline.json b/prototypes/connections/results/2026-09-24-tui-viewport/latency-baseline.json new file mode 100644 index 0000000..03c1ff9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-tui-viewport/latency-baseline.json @@ -0,0 +1,176 @@ +{ + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "terminal": "PTY 24x80, C.UTF-8", + "tui_sha256": "6a38883fef9fcfaf0b24516d0ca1b46edb595f988a9f756c030a3a84ef215107", + "cli_sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8", + "workloads": { + "256-byte-ascii": { + "document_bytes": 256, + "samples": [ + { + "nanoseconds": 42778, + "frame_bytes": 467, + "frame_sha256": "79ce175afbd5e1168173ac158c7cba370251a226142ec00780e735b7d71de36d" + }, + { + "nanoseconds": 32612, + "frame_bytes": 468, + "frame_sha256": "84aeb3b92beaccfaa8b4b537280122c8aece74222c9ece27a7c2e0a7a5575371" + }, + { + "nanoseconds": 28296, + "frame_bytes": 469, + "frame_sha256": "b04bcefe68401c190b7dab9c3c29bb09cec9f4db6140d285bd2fe09ff0fa5203" + }, + { + "nanoseconds": 28074, + "frame_bytes": 470, + "frame_sha256": "01c628338d94e9cc3371dcdb33d643c6d87d17808e04e388dc0305f2e2bf0a01" + }, + { + "nanoseconds": 27944, + "frame_bytes": 471, + "frame_sha256": "02d4c8ab2f98fe5e99cf17237d3daa75b484472928e4105d241c06c68b082ccc" + }, + { + "nanoseconds": 28612, + "frame_bytes": 472, + "frame_sha256": "825d74dda937507686530ce7acbff1aafcc3c236feb6befa218f2ccbe33d480b" + }, + { + "nanoseconds": 32815, + "frame_bytes": 473, + "frame_sha256": "9458d218e52f093480dccf23b816dac00c5cc5517b4b145820061f98b61d2b32" + }, + { + "nanoseconds": 28093, + "frame_bytes": 474, + "frame_sha256": "02d7602be4f267cdc2db7b853ecfb743dc594c941b9f6a4be9d3262b16d71fda" + }, + { + "nanoseconds": 27556, + "frame_bytes": 475, + "frame_sha256": "21426683adeee1036c8fe02233ab5a8dde1e6f5d2da8f97d67f9b41419c745ca" + }, + { + "nanoseconds": 28297, + "frame_bytes": 476, + "frame_sha256": "d5c581fddffb80146c1b617be580a65f5936c79dc04deeada0c5b90eaa10b193" + } + ], + "median_nanoseconds": 28296 + }, + "one-mib-ascii": { + "document_bytes": 1048576, + "samples": [ + { + "nanoseconds": 15694866, + "frame_bytes": 1950, + "frame_sha256": "1ee95680f91eda6c69e7b4009274591735b5b9d2825ed2076cf372acc70c613f" + }, + { + "nanoseconds": 15542421, + "frame_bytes": 1951, + "frame_sha256": "5121e7e36f445a218af46da6796e3124997a7e25b652225f25d93b60d5e09b40" + }, + { + "nanoseconds": 15586588, + "frame_bytes": 1952, + "frame_sha256": "c7b503e012baefa46d8663b98a59f1d1faf4d2b3831750f736984e6443245075" + }, + { + "nanoseconds": 15921866, + "frame_bytes": 1953, + "frame_sha256": "b7edf856b10b641faf4825a8444f386f3faf1ac1e1ccf9a187db6f1465b5264b" + }, + { + "nanoseconds": 15594088, + "frame_bytes": 1954, + "frame_sha256": "cbaaf4f2990d9c060f48cf4dd6e6fab3d872c2370302960a893bcd4c13fa4c47" + }, + { + "nanoseconds": 15704829, + "frame_bytes": 1955, + "frame_sha256": "95ce7a29f9dd111a0a319455231b79bcc41fdf897e8db3411a24821c6cc298c6" + }, + { + "nanoseconds": 15593014, + "frame_bytes": 1956, + "frame_sha256": "440d25b06f6d7dda71bd02225191f0f430a4f4278a10d3bb21b56788ccbd62c4" + }, + { + "nanoseconds": 15696681, + "frame_bytes": 1957, + "frame_sha256": "5a0f58e4f0e16194202e3f56608a6411488aa517378abedf8ff428644e63c9cb" + }, + { + "nanoseconds": 15719440, + "frame_bytes": 1958, + "frame_sha256": "4f3d92e9f165d680c4546040e0ac141db54176f6576893d1a24e89053b487b03" + }, + { + "nanoseconds": 15660421, + "frame_bytes": 1959, + "frame_sha256": "ee37bf811fd99378f57715a638fd589d5c518b34dfdc76d973c64203c2ef16f8" + } + ], + "median_nanoseconds": 15677643 + }, + "one-mib-utf8": { + "document_bytes": 1048572, + "samples": [ + { + "nanoseconds": 37168940, + "frame_bytes": 2745, + "frame_sha256": "e7275636645244ab12463d96a08bacd5c87aec3307bf86f91399ca654531140a" + }, + { + "nanoseconds": 37198756, + "frame_bytes": 2746, + "frame_sha256": "7d183f69c1dce6fee9b86e6b2c10e01f3779b13753fb1c11160de78505f9a8e0" + }, + { + "nanoseconds": 37112959, + "frame_bytes": 2747, + "frame_sha256": "7d0190b9b944eaa7fcd07af573882df6d436a7847e51dca759a65e93d9100c8e" + }, + { + "nanoseconds": 37318718, + "frame_bytes": 2748, + "frame_sha256": "858d20af32237847e72800093ec28e1998e71c22d0991d75011bf661682323e8" + }, + { + "nanoseconds": 37213200, + "frame_bytes": 2749, + "frame_sha256": "2e848f851e174be238ce270f71749536e473c97ad2f075a55aaf020fc5438c77" + }, + { + "nanoseconds": 37296497, + "frame_bytes": 2750, + "frame_sha256": "77d4bb9a8028ea5420435bdbe23b0c188d6f6065a8eda79c6357defb32605be8" + }, + { + "nanoseconds": 37084125, + "frame_bytes": 2751, + "frame_sha256": "18d8fcba8c9d11f0cf44e2171fb613d49e45f94408b4307526fc90bec5b61e5a" + }, + { + "nanoseconds": 37309700, + "frame_bytes": 2752, + "frame_sha256": "977adff244a908e5b59448e1b8075139fbcc7b35410d0d335ad67580d3d54c44" + }, + { + "nanoseconds": 37275311, + "frame_bytes": 2753, + "frame_sha256": "1e6c90831630b4573e9dec18872d26056e6e96d68e2cfcbe7da8102f0ef8b490" + }, + { + "nanoseconds": 37298940, + "frame_bytes": 2754, + "frame_sha256": "b3599665cc5fb2a72f9a582604c5caaeceff475fa2986f3242adfb4799089600" + } + ], + "median_nanoseconds": 37244255 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/latency-current.json b/prototypes/connections/results/2026-09-24-tui-viewport/latency-current.json new file mode 100644 index 0000000..92f6cd0 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-tui-viewport/latency-current.json @@ -0,0 +1,176 @@ +{ + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "terminal": "PTY 24x80, C.UTF-8", + "tui_sha256": "4373eab2cdb033ce54b812232c7787a3a572de26e303daa9dd5001489638c899", + "cli_sha256": "8b594a7b1a4ae5b33cde5c842ec1e11c79b6368babc08ac2ffea378b38ac24f8", + "workloads": { + "256-byte-ascii": { + "document_bytes": 256, + "samples": [ + { + "nanoseconds": 59019, + "frame_bytes": 467, + "frame_sha256": "79ce175afbd5e1168173ac158c7cba370251a226142ec00780e735b7d71de36d" + }, + { + "nanoseconds": 39463, + "frame_bytes": 468, + "frame_sha256": "84aeb3b92beaccfaa8b4b537280122c8aece74222c9ece27a7c2e0a7a5575371" + }, + { + "nanoseconds": 32555, + "frame_bytes": 469, + "frame_sha256": "b04bcefe68401c190b7dab9c3c29bb09cec9f4db6140d285bd2fe09ff0fa5203" + }, + { + "nanoseconds": 30074, + "frame_bytes": 470, + "frame_sha256": "01c628338d94e9cc3371dcdb33d643c6d87d17808e04e388dc0305f2e2bf0a01" + }, + { + "nanoseconds": 26759, + "frame_bytes": 471, + "frame_sha256": "02d4c8ab2f98fe5e99cf17237d3daa75b484472928e4105d241c06c68b082ccc" + }, + { + "nanoseconds": 27667, + "frame_bytes": 472, + "frame_sha256": "825d74dda937507686530ce7acbff1aafcc3c236feb6befa218f2ccbe33d480b" + }, + { + "nanoseconds": 27371, + "frame_bytes": 473, + "frame_sha256": "9458d218e52f093480dccf23b816dac00c5cc5517b4b145820061f98b61d2b32" + }, + { + "nanoseconds": 29278, + "frame_bytes": 474, + "frame_sha256": "02d7602be4f267cdc2db7b853ecfb743dc594c941b9f6a4be9d3262b16d71fda" + }, + { + "nanoseconds": 28167, + "frame_bytes": 475, + "frame_sha256": "21426683adeee1036c8fe02233ab5a8dde1e6f5d2da8f97d67f9b41419c745ca" + }, + { + "nanoseconds": 37815, + "frame_bytes": 476, + "frame_sha256": "d5c581fddffb80146c1b617be580a65f5936c79dc04deeada0c5b90eaa10b193" + } + ], + "median_nanoseconds": 29676 + }, + "one-mib-ascii": { + "document_bytes": 1048576, + "samples": [ + { + "nanoseconds": 111481, + "frame_bytes": 1950, + "frame_sha256": "1ee95680f91eda6c69e7b4009274591735b5b9d2825ed2076cf372acc70c613f" + }, + { + "nanoseconds": 73685, + "frame_bytes": 1951, + "frame_sha256": "5121e7e36f445a218af46da6796e3124997a7e25b652225f25d93b60d5e09b40" + }, + { + "nanoseconds": 71222, + "frame_bytes": 1952, + "frame_sha256": "c7b503e012baefa46d8663b98a59f1d1faf4d2b3831750f736984e6443245075" + }, + { + "nanoseconds": 69871, + "frame_bytes": 1953, + "frame_sha256": "b7edf856b10b641faf4825a8444f386f3faf1ac1e1ccf9a187db6f1465b5264b" + }, + { + "nanoseconds": 69945, + "frame_bytes": 1954, + "frame_sha256": "cbaaf4f2990d9c060f48cf4dd6e6fab3d872c2370302960a893bcd4c13fa4c47" + }, + { + "nanoseconds": 69833, + "frame_bytes": 1955, + "frame_sha256": "95ce7a29f9dd111a0a319455231b79bcc41fdf897e8db3411a24821c6cc298c6" + }, + { + "nanoseconds": 70259, + "frame_bytes": 1956, + "frame_sha256": "440d25b06f6d7dda71bd02225191f0f430a4f4278a10d3bb21b56788ccbd62c4" + }, + { + "nanoseconds": 69537, + "frame_bytes": 1957, + "frame_sha256": "5a0f58e4f0e16194202e3f56608a6411488aa517378abedf8ff428644e63c9cb" + }, + { + "nanoseconds": 69648, + "frame_bytes": 1958, + "frame_sha256": "4f3d92e9f165d680c4546040e0ac141db54176f6576893d1a24e89053b487b03" + }, + { + "nanoseconds": 70092, + "frame_bytes": 1959, + "frame_sha256": "ee37bf811fd99378f57715a638fd589d5c518b34dfdc76d973c64203c2ef16f8" + } + ], + "median_nanoseconds": 70018 + }, + "one-mib-utf8": { + "document_bytes": 1048572, + "samples": [ + { + "nanoseconds": 239129, + "frame_bytes": 2745, + "frame_sha256": "e7275636645244ab12463d96a08bacd5c87aec3307bf86f91399ca654531140a" + }, + { + "nanoseconds": 228593, + "frame_bytes": 2746, + "frame_sha256": "7d183f69c1dce6fee9b86e6b2c10e01f3779b13753fb1c11160de78505f9a8e0" + }, + { + "nanoseconds": 227148, + "frame_bytes": 2747, + "frame_sha256": "7d0190b9b944eaa7fcd07af573882df6d436a7847e51dca759a65e93d9100c8e" + }, + { + "nanoseconds": 227500, + "frame_bytes": 2748, + "frame_sha256": "858d20af32237847e72800093ec28e1998e71c22d0991d75011bf661682323e8" + }, + { + "nanoseconds": 237037, + "frame_bytes": 2749, + "frame_sha256": "2e848f851e174be238ce270f71749536e473c97ad2f075a55aaf020fc5438c77" + }, + { + "nanoseconds": 228370, + "frame_bytes": 2750, + "frame_sha256": "77d4bb9a8028ea5420435bdbe23b0c188d6f6065a8eda79c6357defb32605be8" + }, + { + "nanoseconds": 226463, + "frame_bytes": 2751, + "frame_sha256": "18d8fcba8c9d11f0cf44e2171fb613d49e45f94408b4307526fc90bec5b61e5a" + }, + { + "nanoseconds": 261797, + "frame_bytes": 2752, + "frame_sha256": "977adff244a908e5b59448e1b8075139fbcc7b35410d0d335ad67580d3d54c44" + }, + { + "nanoseconds": 272018, + "frame_bytes": 2753, + "frame_sha256": "1e6c90831630b4573e9dec18872d26056e6e96d68e2cfcbe7da8102f0ef8b490" + }, + { + "nanoseconds": 235852, + "frame_bytes": 2754, + "frame_sha256": "b3599665cc5fb2a72f9a582604c5caaeceff475fa2986f3242adfb4799089600" + } + ], + "median_nanoseconds": 232222 + } + } +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/package-http-tui.json b/prototypes/connections/results/2026-09-24-tui-viewport/package-http-tui.json new file mode 100644 index 0000000..2730260 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-tui-viewport/package-http-tui.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "270edeb0f897b3ea1eeacc4e89bcff2c45fefbbe77fd1546307298d801a2ed9b", + "archive_bytes": 51844, + "selected_hosts": [ + "pure-edit-fixed", + "pure-connections-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated parser-free HTTP host and CLI share the document", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/package.json b/prototypes/connections/results/2026-09-24-tui-viewport/package.json new file mode 100644 index 0000000..c16b305 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-tui-viewport/package.json @@ -0,0 +1,14 @@ +{ + "archive_sha256": "aa29b551905788ec436628fac1cca119fbf783d2c620ead866fd2e842429fb12", + "archive_bytes": 34216, + "selected_hosts": [ + "pure-edit-fixed", + "pure-edit-tui-fixed" + ], + "checks": [ + "relocated archive has only declared files and every file hash matches", + "relocated parser-free runner creates and restores a document", + "relocated parser-free runner performs revision-checked edits", + "relocated terminal editor saves UTF-8 and one MiB documents" + ] +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/profile.json b/prototypes/connections/results/2026-09-24-tui-viewport/profile.json new file mode 100644 index 0000000..84e6783 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-tui-viewport/profile.json @@ -0,0 +1,21 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS is checked by check_tui.py on the recorded Linux AArch64 host.", + "budget_sha256": "e378cb302b2a4bcd2ee674af048116f139b214b59f24244d6c825bc71a8e8a31", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-edit-tui-fixed": { + "sections": { + "text": 28762, + "data": 988, + "bss": 24 + }, + "needed": [ + "libc.so.6" + ], + "sha256": "4373eab2cdb033ce54b812232c7787a3a572de26e303daa9dd5001489638c899" + } + } +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/pty-flow.json b/prototypes/connections/results/2026-09-24-tui-viewport/pty-flow.json new file mode 100644 index 0000000..a8b63d4 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-tui-viewport/pty-flow.json @@ -0,0 +1,19 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 1792, + "one_mib_peak_rss_kib": 3168, + "one_mib_peak_rss_limit_kib": 4096, + "binary_sha256": "4373eab2cdb033ce54b812232c7787a3a572de26e303daa9dd5001489638c899" +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/pty-sanitized.json b/prototypes/connections/results/2026-09-24-tui-viewport/pty-sanitized.json new file mode 100644 index 0000000..218e7d8 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-tui-viewport/pty-sanitized.json @@ -0,0 +1,19 @@ +{ + "profile": "native-fixed-document-terminal-editor", + "checks": [ + "cursor insertion and save through terminal", + "open another key", + "UTF-8 codepoint movement, visible redraw and edit", + "resize redraw", + "external revision conflict preserves draft", + "stored multiline and tabbed document survives edit and reopen", + "cached viewport preserves vertical movement across many rows", + "one MiB document viewport, edit and fresh CLI restore", + "one MiB UTF-8 viewport and edit before cached anchor", + "terminal mode restored after key and signal exit" + ], + "small_peak_rss_kib": 9696, + "one_mib_peak_rss_kib": 11680, + "one_mib_peak_rss_limit_kib": 0, + "binary_sha256": "0f1e1a886d3c1617c8aa73e3348892681bae0c1c1428e4b89eac5985410cc988" +} diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-http-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-http-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..3cc8f4a Binary files /dev/null and b/prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-http-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..d8e21cf Binary files /dev/null and b/prototypes/connections/results/2026-09-24-tui-viewport/pure-document-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/final-native-window.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/final-native-window.json new file mode 100644 index 0000000..9b952aa --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/final-native-window.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 15600, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 16880, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 18720, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 20576, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 22672, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 24528, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 25568, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [47216, 48432, 49712, 51552, 53536, 55632, 57488, 58528], "large_window_rss_kib": 44000, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/line-width-4096.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/line-width-4096.json new file mode 100644 index 0000000..f595f21 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/line-width-4096.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_line_width": 4096, "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31344, "heap": 13088, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31344, "heap": 15984, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31344, "heap": 17936, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31344, "heap": 20544, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31472, "heap": 23440, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31472, "heap": 25392, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31472, "heap": 28000, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31472, "heap": 30880, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [46000, 48896, 50848, 53456, 56480, 58432, 61040, 63920], "large_window_rss_kib": 42736, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/mallinfo.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/mallinfo.json new file mode 100644 index 0000000..9c77082 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/mallinfo.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_malloc_samples_bytes": [{"arena": 5849088, "free": 1371840, "key": "large", "mapped": 1392640, "used": 4477248}, {"arena": 14450688, "free": 5148272, "key": "large_other", "mapped": 606208, "used": 9302416}, {"arena": 16990208, "free": 7444160, "key": "large", "mapped": 606208, "used": 9546048}, {"arena": 16990208, "free": 7217216, "key": "large_other", "mapped": 606208, "used": 9772992}, {"arena": 16990208, "free": 6992480, "key": "large", "mapped": 606208, "used": 9997728}, {"arena": 16990208, "free": 6767584, "key": "large_other", "mapped": 606208, "used": 10222624}, {"arena": 16990208, "free": 6543008, "key": "large", "mapped": 606208, "used": 10447200}, {"arena": 16990208, "free": 6317776, "key": "large_other", "mapped": 606208, "used": 10672432}, {"arena": 16990208, "free": 6092896, "key": "large", "mapped": 606208, "used": 10897312}, {"arena": 16990208, "free": 5867616, "key": "large_other", "mapped": 606208, "used": 11122592}, {"arena": 16990208, "free": 5643472, "key": "large", "mapped": 606208, "used": 11346736}, {"arena": 16990208, "free": 5417776, "key": "large_other", "mapped": 606208, "used": 11572432}, {"arena": 17711104, "free": 5915184, "key": "large", "mapped": 606208, "used": 11795920}, {"arena": 17711104, "free": 5689968, "key": "large_other", "mapped": 606208, "used": 12021136}, {"arena": 17711104, "free": 5465984, "key": "large", "mapped": 606208, "used": 12245120}, {"arena": 17711104, "free": 5236704, "key": "large_other", "mapped": 606208, "used": 12474400}, {"arena": 17711104, "free": 5013232, "key": "large", "mapped": 606208, "used": 12697872}, {"arena": 18759680, "free": 5836384, "key": "large_other", "mapped": 606208, "used": 12923296}, {"arena": 18759680, "free": 5613104, "key": "large", "mapped": 606208, "used": 13146576}, {"arena": 18759680, "free": 5387408, "key": "large_other", "mapped": 606208, "used": 13372272}, {"arena": 18759680, "free": 5163520, "key": "large", "mapped": 606208, "used": 13596160}, {"arena": 19611648, "free": 5790528, "key": "large_other", "mapped": 606208, "used": 13821120}, {"arena": 19611648, "free": 5567040, "key": "large", "mapped": 606208, "used": 14044608}, {"arena": 19611648, "free": 5341600, "key": "large_other", "mapped": 606208, "used": 14270048}, {"arena": 19611648, "free": 5118352, "key": "large", "mapped": 606208, "used": 14493296}, {"arena": 20660224, "free": 5941504, "key": "large_other", "mapped": 606208, "used": 14718720}, {"arena": 20660224, "free": 5717552, "key": "large", "mapped": 606208, "used": 14942672}, {"arena": 20660224, "free": 5492400, "key": "large_other", "mapped": 606208, "used": 15167824}, {"arena": 20660224, "free": 5268896, "key": "large", "mapped": 606208, "used": 15391328}, {"arena": 20660224, "free": 5043504, "key": "large_other", "mapped": 606208, "used": 15616720}, {"arena": 21708800, "free": 5868816, "key": "large", "mapped": 606208, "used": 15839984}, {"arena": 21708800, "free": 5643168, "key": "large_other", "mapped": 606208, "used": 16065632}, {"arena": 21708800, "free": 5419264, "key": "large", "mapped": 606208, "used": 16289536}, {"arena": 21708800, "free": 5194128, "key": "large_other", "mapped": 606208, "used": 16514672}, {"arena": 21708800, "free": 4969600, "key": "large", "mapped": 606208, "used": 16739200}, {"arena": 22806528, "free": 5842912, "key": "large_other", "mapped": 606208, "used": 16963616}, {"arena": 22806528, "free": 5619648, "key": "large", "mapped": 606208, "used": 17186880}, {"arena": 22806528, "free": 5441248, "key": "large_other", "mapped": 606208, "used": 17365280}, {"arena": 22806528, "free": 5217472, "key": "large", "mapped": 606208, "used": 17589056}, {"arena": 23658496, "free": 5843872, "key": "large_other", "mapped": 606208, "used": 17814624}, {"arena": 23658496, "free": 5620592, "key": "large", "mapped": 606208, "used": 18037904}, {"arena": 23658496, "free": 5395136, "key": "large_other", "mapped": 606208, "used": 18263360}, {"arena": 23658496, "free": 5171792, "key": "large", "mapped": 606208, "used": 18486704}, {"arena": 24707072, "free": 5994912, "key": "large_other", "mapped": 606208, "used": 18712160}, {"arena": 24707072, "free": 5770480, "key": "large", "mapped": 606208, "used": 18936592}, {"arena": 24707072, "free": 5545520, "key": "large_other", "mapped": 606208, "used": 19161552}, {"arena": 24707072, "free": 5321792, "key": "large", "mapped": 606208, "used": 19385280}, {"arena": 24707072, "free": 5096368, "key": "large_other", "mapped": 606208, "used": 19610704}, {"arena": 25755648, "free": 5921760, "key": "large", "mapped": 606208, "used": 19833888}, {"arena": 25755648, "free": 5696016, "key": "large_other", "mapped": 606208, "used": 20059632}, {"arena": 25755648, "free": 5471760, "key": "large", "mapped": 606208, "used": 20283888}, {"arena": 25755648, "free": 5247072, "key": "large_other", "mapped": 606208, "used": 20508576}, {"arena": 25755648, "free": 5022720, "key": "large", "mapped": 606208, "used": 20732928}, {"arena": 26853376, "free": 5895392, "key": "large_other", "mapped": 606208, "used": 20957984}, {"arena": 26853376, "free": 5671920, "key": "large", "mapped": 606208, "used": 21181456}, {"arena": 26853376, "free": 5446704, "key": "large_other", "mapped": 606208, "used": 21406672}, {"arena": 26853376, "free": 5222576, "key": "large", "mapped": 606208, "used": 21630800}, {"arena": 27705344, "free": 5849056, "key": "large_other", "mapped": 606208, "used": 21856288}, {"arena": 27705344, "free": 5625392, "key": "large", "mapped": 606208, "used": 22079952}, {"arena": 27705344, "free": 5400048, "key": "large_other", "mapped": 606208, "used": 22305296}, {"arena": 27705344, "free": 5176640, "key": "large", "mapped": 606208, "used": 22528704}, {"arena": 28753920, "free": 5999456, "key": "large_other", "mapped": 606208, "used": 22754464}, {"arena": 28753920, "free": 5775264, "key": "large", "mapped": 606208, "used": 22978656}, {"arena": 28753920, "free": 5550336, "key": "large_other", "mapped": 606208, "used": 23203584}, {"arena": 28753920, "free": 5326416, "key": "large", "mapped": 606208, "used": 23427504}], "large_switch_map_samples_kib": [{"anonymous": 2352, "file": 31280, "heap": 14464, "other": 16, "stack": 112}, {"anonymous": 2352, "file": 31280, "heap": 14928, "other": 16, "stack": 112}, {"anonymous": 2352, "file": 31280, "heap": 16784, "other": 16, "stack": 112}, {"anonymous": 2352, "file": 31280, "heap": 18832, "other": 16, "stack": 112}, {"anonymous": 2352, "file": 31408, "heap": 20736, "other": 16, "stack": 112}, {"anonymous": 2352, "file": 31408, "heap": 22016, "other": 16, "stack": 112}, {"anonymous": 2352, "file": 31408, "heap": 23856, "other": 16, "stack": 112}, {"anonymous": 2352, "file": 31408, "heap": 25712, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [48224, 48688, 50544, 52592, 54624, 55904, 57744, 59600], "large_window_rss_kib": 44800, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/no-document-undo.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/no-document-undo.json new file mode 100644 index 0000000..7b8cf24 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/no-document-undo.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31280, "heap": 14576, "other": 16, "stack": 128}, {"anonymous": 1440, "file": 31280, "heap": 15024, "other": 16, "stack": 128}, {"anonymous": 1440, "file": 31280, "heap": 17152, "other": 16, "stack": 128}, {"anonymous": 1440, "file": 31280, "heap": 19008, "other": 16, "stack": 128}, {"anonymous": 1440, "file": 31408, "heap": 20240, "other": 16, "stack": 128}, {"anonymous": 1440, "file": 31408, "heap": 22144, "other": 16, "stack": 128}, {"anonymous": 1440, "file": 31408, "heap": 23984, "other": 16, "stack": 128}, {"anonymous": 1440, "file": 31408, "heap": 26016, "other": 16, "stack": 128}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [47440, 47888, 50016, 51872, 53232, 55136, 56976, 59008], "large_window_rss_kib": 44048, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/no-key-undo.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/no-key-undo.json new file mode 100644 index 0000000..6009187 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/no-key-undo.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1456, "file": 31280, "heap": 15600, "other": 16, "stack": 112}, {"anonymous": 1456, "file": 31280, "heap": 16784, "other": 16, "stack": 112}, {"anonymous": 1456, "file": 31280, "heap": 18608, "other": 16, "stack": 112}, {"anonymous": 1456, "file": 31280, "heap": 20320, "other": 16, "stack": 112}, {"anonymous": 1456, "file": 31280, "heap": 21488, "other": 16, "stack": 112}, {"anonymous": 1456, "file": 31280, "heap": 23584, "other": 16, "stack": 112}, {"anonymous": 1456, "file": 31280, "heap": 26800, "other": 16, "stack": 112}, {"anonymous": 1456, "file": 31280, "heap": 27344, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [48464, 49648, 51472, 53184, 54352, 56448, 59664, 60208], "large_window_rss_kib": 44048, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/no-wrap.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/no-wrap.json new file mode 100644 index 0000000..b51a374 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/no-wrap.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31264, "heap": 13056, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 13904, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 15184, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 17024, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 19168, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 20448, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 22288, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31392, "heap": 24480, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [45888, 46736, 48016, 49856, 52128, 53408, 55248, 57440], "large_window_rss_kib": 44000, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/replace-view.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/replace-view.json new file mode 100644 index 0000000..409e1d9 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/replace-view.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_line_width": 80, "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31280, "heap": 14416, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 15632, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 17696, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 19280, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 21248, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 22608, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 24624, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 25968, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [47264, 48480, 50544, 52128, 54096, 55456, 57472, 58816], "large_window_rss_kib": 44032, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/same-body.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/same-body.json new file mode 100644 index 0000000..c8b8149 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/same-body.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31280, "heap": 14368, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 16416, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 18512, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 19696, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 21344, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 23104, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 25280, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 26832, "other": 16, "stack": 112}], "large_switch_mode": "same-body", "large_switch_rss_samples_kib": [47216, 49264, 51360, 52544, 54192, 55952, 58128, 59680], "large_window_rss_kib": 44016, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/same-key.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/same-key.json new file mode 100644 index 0000000..fac529b --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/same-key.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31264, "heap": 14224, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14224, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14224, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14224, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14224, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14240, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14240, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14240, "other": 16, "stack": 112}], "large_switch_mode": "same", "large_switch_rss_samples_kib": [47056, 47056, 47056, 47056, 47056, 47072, 47072, 47072], "large_window_rss_kib": 44016, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/settled-final.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/settled-final.json new file mode 100644 index 0000000..bdbdd78 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/settled-final.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31328, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31328, "heap": 16336, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31328, "heap": 17600, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31328, "heap": 19776, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31328, "heap": 21872, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31328, "heap": 23056, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31328, "heap": 25184, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31328, "heap": 26432, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [47280, 49232, 50496, 52672, 54768, 55952, 58080, 59328], "large_switch_settled_map_kib": {"anonymous": 1440, "file": 31456, "heap": 26432, "other": 16, "stack": 112}, "large_switch_settled_rss_kib": 59456, "large_window_rss_kib": 44064, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/skip-text.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/skip-text.json new file mode 100644 index 0000000..86c3fe7 --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/skip-text.json @@ -0,0 +1 @@ +{"cli_conflict_preserves_draft": "pass", "definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31264, "heap": 14384, "other": 16, "stack": 112}], "large_switch_mode": "alternate", "large_switch_rss_samples_kib": [47216, 47216, 47216, 47216, 47216, 47216, 47216, 47216], "large_window_rss_kib": 44000, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-window-memory-analysis/smaps.json b/prototypes/connections/results/2026-09-24-window-memory-analysis/smaps.json new file mode 100644 index 0000000..1b88acf --- /dev/null +++ b/prototypes/connections/results/2026-09-24-window-memory-analysis/smaps.json @@ -0,0 +1 @@ +{"definition_restores_in_new_window": "pass", "empty_document_restore": "pass", "gtk_cli_write": "pass", "http_conflict_preserves_draft": "pass", "invalid_and_external_definition_preserve_draft": "pass", "large_switch_map_samples_kib": [{"anonymous": 1440, "file": 31280, "heap": 14624, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 15328, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 17184, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31280, "heap": 19040, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31408, "heap": 21136, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31408, "heap": 22992, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31408, "heap": 24032, "other": 16, "stack": 112}, {"anonymous": 1440, "file": 31408, "heap": 25920, "other": 16, "stack": 112}], "large_switch_rss_samples_kib": [47472, 48176, 50032, 51888, 54112, 55968, 57008, 58896], "large_window_rss_kib": 44032, "multi_document_switch_preserves_draft": "pass", "native_definition_apply_and_running_output": "pass", "one_mib_document_one_byte_edit": "pass", "partial_read_definition_rejected": "pass", "profile": "optional-native-document-window", "reloaded_definition_installs_without_rewriting_source": "pass", "source_write_failure_preserves_active_definition": "pass", "unchanged_save_keeps_revision": "pass", "unsaved_close_guard": "pass", "utf8_boundary_and_restart": "pass"} diff --git a/prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-linux-aarch64.tar.gz new file mode 100644 index 0000000..b2f35c3 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..298e8c2 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-work-slot/pure-document-fixed-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-linux-aarch64.tar.gz new file mode 100644 index 0000000..57a22b7 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-tui-linux-aarch64.tar.gz b/prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-tui-linux-aarch64.tar.gz new file mode 100644 index 0000000..38ec704 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-work-slot/pure-document-source-cli-tui-linux-aarch64.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-work-slot/pure-source-preview.tar.gz b/prototypes/connections/results/2026-09-24-work-slot/pure-source-preview.tar.gz new file mode 100644 index 0000000..458b215 Binary files /dev/null and b/prototypes/connections/results/2026-09-24-work-slot/pure-source-preview.tar.gz differ diff --git a/prototypes/connections/results/2026-09-24-work-slot/source-package-check.json b/prototypes/connections/results/2026-09-24-work-slot/source-package-check.json new file mode 100644 index 0000000..708e0fa --- /dev/null +++ b/prototypes/connections/results/2026-09-24-work-slot/source-package-check.json @@ -0,0 +1,16 @@ +{ + "archive_sha256": "a1ba85942463f765c1f908f19419969423f3e474e608dc023e8a3d09b61de50f", + "archive_bytes": 334267, + "source_files": 171, + "checks": [ + "every extracted source file matches the manifest", + "clean C17, both real terminal PTY scenarios and fixed/source CLI document RSS checks pass", + "source-loaded CLI and parser-free CLI share the real stored document", + "editing .pure changes the native source-loaded reply", + "clean selected page store exports mixed revisions to the default flat store", + "clean one-MiB document fans out to two same-process Pure domains with independent conflict and release", + "clean selected page store runs the source-loaded TUI and relocatable CLI+TUI package", + "clean Linux release behavior, code, dependency and RSS gates pass", + "clean source-loaded terminal editor PTY scenarios pass" + ] +} diff --git a/prototypes/connections/runtime.c b/prototypes/connections/runtime.c new file mode 100644 index 0000000..0bfb7bf --- /dev/null +++ b/prototypes/connections/runtime.c @@ -0,0 +1,393 @@ +#include "runtime.h" +#include + +#define NONE SIZE_MAX +enum { FREE, RECEIVING, READY, RUNNING, DONE }; +#ifdef PURE_RUNTIME_METRICS +#define METRIC(...) do { __VA_ARGS__; } while (0) +#else +#define METRIC(...) ((void)0) +#endif + +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; +} + +/* WRITE results are loans from the admitted input, not a second allocation. + * Integer offsets avoid relational comparisons between unrelated C pointers. */ +static bool borrowed_reply(PureSpan input, PureSpan reply) { + if (!reply.size) return true; + if (!input.data || !reply.data) return false; + uintptr_t base = (uintptr_t)input.data, start = (uintptr_t)reply.data; + return start >= base && start - base <= input.size && + reply.size <= input.size - (start - base); +} + +static void dispose(PureRuntime *r, PureResource *resource) { + PureResource old = *resource; + *resource = (PureResource){0}; + if (old.release) { + bool running = r->running; + r->running = true; + old.release(old.allocation, old.allocation_size); + r->running = running; + } +} + +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; +#ifdef PURE_RUNTIME_METRICS + bool released_payload = w->charged || w->resource.release || parent.runtime; +#endif + dispose(r, &w->resource); + r->retained -= w->charged; + METRIC(if (released_payload) ++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; +#ifdef PURE_RUNTIME_METRICS + bool released_payload = w->charged || w->resource.release || parent.runtime; +#endif + dispose(r, &w->resource); + r->retained -= w->charged; + METRIC(if (released_payload) ++r->releases); + w->charged = 0; + w->parent = (PureTicket){0}; + 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 void *definition, PureHost host, size_t budget) { + if (!r || ((work == NULL) != (capacity == 0))) return false; + *r = (PureRuntime){.definition = definition, .host = host, .free_head = NONE, + .ready_head = NONE, .ready_tail = NONE, + .budget = budget, .accepting = true}; + return pure_runtime_bind(r, work, capacity); +} + +bool pure_runtime_bind(PureRuntime *r, PureWork *work, size_t capacity) { + if (!r || r->running || r->active || r->retained || + r->ready_head != NONE || r->ready_tail != NONE || + ((work == NULL) != (capacity == 0))) + return false; + r->work = work; + r->capacity = capacity; + r->free_head = capacity ? 0 : NONE; + 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 void *definition, PureHost host, size_t budget) { + if (!r || r->running || r->active) return false; + r->definition = definition; + 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; + bool missing_effect = input == PURE_INPUT_READ ? !r->host.read : !r->host.write; +#ifdef PURE_ASYNC_EFFECTS + if (missing_effect && !r->async_effects) return PURE_DENIED; +#else + if (missing_effect) return PURE_DENIED; +#endif + if (r->free_head == NONE) return PURE_BUSY; + if (r->serial == UINT64_MAX) return PURE_LIMIT; + if (bytes > r->budget - r->retained) { METRIC(++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; + METRIC(++r->accepted); + r->retained += bytes; + METRIC(if (r->active > r->peak_active) r->peak_active = r->active); + METRIC(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(r, &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; +#ifdef PURE_ASYNC_EFFECTS + PureWork *head = &r->work[r->ready_head]; + if (head->input == PURE_INPUT_READ ? !r->host.read : !r->host.write) return false; +#endif + 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 reply = {0}; + outcome.status = r->host.write(r->host.context, r->definition, w->key, + w->expected, w->resource.body, &reply, + &outcome.revision); + if (outcome.status == PURE_OK) { + if (borrowed_reply(w->resource.body, reply)) outcome.body = reply; + else outcome.status = PURE_STORE_ERROR; + } + METRIC(if (outcome.status == PURE_OK) ++r->commits; + else if (outcome.status == PURE_CONFLICT) ++r->conflicts); + } else { + outcome.status = r->host.read(r->host.context, r->definition, w->key, + 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; + METRIC(if (r->retained > r->peak_retained) r->peak_retained = r->retained); + outcome.body = w->resource.body; + METRIC(++r->gets); + } + } + METRIC(if (outcome.status == PURE_BUDGET) ++r->budget_rejections); + } + if (outcome.status != PURE_OK || !outcome.body.size) { + outcome.body = (PureSpan){0}; + payload_release(r, w); + } + METRIC(r->borrowed_output_bytes += outcome.body.size); + w->outcome = outcome; + w->state = DONE; + r->running = false; + return true; +} + +#ifdef PURE_ASYNC_EFFECTS +bool pure_runtime_enable_async(PureRuntime *r) { + if (!r || r->running || r->active) return false; + r->async_effects = true; + return true; +} + +PureTake pure_runtime_take(PureRuntime *r, PureEffect *out) { + if (!r || !out || !r->async_effects || r->running || r->ready_head == NONE) + return PURE_TAKE_EMPTY; + 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; + *out = (PureEffect){.ticket = {r, slot, w->generation}, .input = w->input, + .key = w->key, .expected = w->expected, + .value = w->resource.body, + .available = r->budget - r->retained}; + return PURE_TAKE_EFFECT; +} + +bool pure_runtime_complete(PureTicket ticket, PureResult status, + PureResource *read_resource, PureSpan write_reply, + int64_t revision) { + PureWork *w = lookup(ticket, false); + PureRuntime *r = ticket.runtime; + if (!w || r->running || w->state != RUNNING || + (w->input == PURE_INPUT_WRITE && read_resource) || + (w->input == PURE_INPUT_READ && write_reply.size) || + (w->input == PURE_INPUT_WRITE && status == PURE_OK && + !borrowed_reply(w->resource.body, write_reply)) || + (read_resource && !read_resource->release && + (read_resource->body.size || read_resource->allocation))) return false; + r->running = true; + PureOutcome outcome = {.status = status, .revision = revision}; + if (w->input == PURE_INPUT_READ) { + PureResource acquired = {0}; + if (read_resource) { + acquired = *read_resource; + *read_resource = (PureResource){0}; + } + if (outcome.status == PURE_OK) { + if (acquired.body.size > r->budget - r->retained) + outcome.status = PURE_BUDGET; + else if (acquired.body.size && (!acquired.body.data || !acquired.release)) + outcome.status = PURE_STORE_ERROR; + else { + w->resource = acquired; + acquired = (PureResource){0}; + w->charged = w->resource.body.size; + r->retained += w->charged; + METRIC(if (r->retained > r->peak_retained) r->peak_retained = r->retained); + outcome.body = w->resource.body; + METRIC(++r->gets); + } + } + dispose(r, &acquired); + METRIC(if (outcome.status == PURE_BUDGET) ++r->budget_rejections); + } else { + if (status == PURE_OK) outcome.body = write_reply; + METRIC(if (outcome.status == PURE_OK) ++r->commits; + else if (outcome.status == PURE_CONFLICT) ++r->conflicts); + } + if (outcome.status != PURE_OK || !outcome.body.size) { + outcome.body = (PureSpan){0}; + payload_release(r, w); + } + METRIC(r->borrowed_output_bytes += outcome.body.size); + w->outcome = outcome; + w->state = DONE; + r->running = false; + if (!w->held) retire(r, w); + return true; +} +#endif + +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; +#ifdef PURE_ASYNC_EFFECTS + if (w->state == RUNNING) return true; +#endif + retire(ticket.runtime, w); + return true; +} diff --git a/prototypes/connections/runtime.h b/prototypes/connections/runtime.h new file mode 100644 index 0000000..0add777 --- /dev/null +++ b/prototypes/connections/runtime.h @@ -0,0 +1,147 @@ +#ifndef PURE_RUNTIME_H +#define PURE_RUNTIME_H +#include "flow.h" +#include "result.h" +#include + +/* The optional metrics profile changes PureRuntime's layout. Give its init + * function a distinct link name so mixed-profile objects cannot link quietly. */ +#ifdef PURE_RUNTIME_METRICS +#define pure_runtime_init pure_runtime_init_metrics +#endif + +/* One owner thread controls admission, payload credits, completion and loans. + * An application definition is opaque to this core. Hosts interpret it and + * perform effects; no parser, storage, socket or OS type is linked here. */ +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. It runs + * while that domain is busy; calls back into the same domain are rejected. */ +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 void *definition, const char *, + size_t available, PureResource *, int64_t *revision); + /* A successful reply may borrow only from input. The runtime retains its + * backing until all reply consumers finish. Effect errors publish no reply. */ + PureResult (*write)(void *, const void *definition, const char *, + int64_t expected, PureSpan input, PureSpan *reply, + 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. */ +/* Internal storage order packs the fixed key around small flags. This is not + * a public ABI; the layout is checked as part of the core cost gate. */ +struct PureWork { + PureTicket parent; + unsigned state; + PureInput input; + bool held; + char key[65]; + int64_t expected; + PureOutcome outcome; + PureResource resource; + uint64_t generation; + size_t next, charged, pins; +}; +struct PureRuntime { + const void *definition; + PureHost host; + PureWork *work; + size_t capacity, free_head, ready_head, ready_tail; + size_t budget, retained; +#ifdef PURE_RUNTIME_METRICS + size_t peak_retained; +#endif + size_t active; +#ifdef PURE_RUNTIME_METRICS + size_t peak_active; +#endif + uint64_t serial; +#ifdef PURE_RUNTIME_METRICS + uint64_t accepted, commits, gets, conflicts, budget_rejections; + uint64_t releases, borrowed_output_bytes; +#endif + bool accepting, running, async_effects; +}; + +/* Enforce repeated storage cost on 64-bit C hosts, including macOS where the + * Linux object-section checker is unavailable. */ +#if SIZE_MAX == UINT64_MAX && UINTPTR_MAX == UINT64_MAX +_Static_assert(sizeof(PureWork) == 216, "work-slot layout exceeded its reviewed size"); +#ifdef PURE_RUNTIME_METRICS +_Static_assert(sizeof(PureRuntime) == 192, "metrics domain layout changed"); +#else +_Static_assert(sizeof(PureRuntime) == 120, "domain layout changed"); +#endif +#endif + +/* Initialize fresh storage once. A NULL work pointer with capacity zero starts + * an idle domain; bind work before admission. Use restart, not init, to reuse + * a domain so its monotonic ticket sequence survives restart. definition is + * host-owned and remains alive until all work has drained. */ +bool pure_runtime_init(PureRuntime *, PureWork *, size_t capacity, + const void *definition, PureHost, size_t budget); +bool pure_runtime_restart(PureRuntime *, const void *definition, PureHost, size_t budget); +void pure_runtime_stop(PureRuntime *); /* stop admission, allow queued work to drain */ +/* Replace caller-owned work storage only after all tickets and loans retire. + * A zero-capacity binding releases idle slot storage without resetting budget, + * ticket generation or optional metrics. The caller retains the bound array's life. */ +bool pure_runtime_bind(PureRuntime *, PureWork *, size_t capacity); + +/* 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 */ + +#ifdef PURE_ASYNC_EFFECTS +/* Optional owner-thread effect suspension. Enable before accepting work. The + * host may complete effects in any order, but must call complete on the owner + * thread after workers stop accessing its borrowed spans. An in-flight effect + * keeps its input alive even if the caller releases the ticket. Effects + * borrowed by take stay valid until completion. */ +typedef enum { PURE_TAKE_EMPTY, PURE_TAKE_EFFECT } PureTake; +typedef struct { + PureTicket ticket; + PureInput input; + const char *key; + int64_t expected; + PureSpan value; /* raw ingress for WRITE; empty for READ */ + size_t available; /* remaining payload credits for READ */ +} PureEffect; +bool pure_runtime_enable_async(PureRuntime *); +PureTake pure_runtime_take(PureRuntime *, PureEffect *); +/* A valid completion consumes read_resource (even on failure); a rejected call + * leaves it untouched. write_reply borrows from the retained ingress. */ +bool pure_runtime_complete(PureTicket, PureResult status, + PureResource *read_resource, PureSpan write_reply, + int64_t revision); +#endif +#endif diff --git a/prototypes/connections/server.c b/prototypes/connections/server.c new file mode 100644 index 0000000..505bb71 --- /dev/null +++ b/prototypes/connections/server.c @@ -0,0 +1,645 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include "program.h" +#include "store-port.h" +#ifdef PURE_EMBEDDED_PROGRAM +#include "embedded-plan.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#ifdef PURE_HTTP_ASYNC +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(PURE_HTTP_ASYNC) && !defined(PURE_ASYNC_EFFECTS) +#error PURE_HTTP_ASYNC requires PURE_ASYNC_EFFECTS +#endif + +enum { SLOTS = 8, HEADER_LIMIT = 4096, CHUNK = 65536 }; +typedef enum { READING_HEADER, READING_BODY, WRITING, WAITING_EFFECT } Phase; +typedef struct Server Server; +#ifdef PURE_HTTP_ASYNC +typedef enum { JOB_FREE, JOB_QUEUED, JOB_RUNNING, JOB_DONE } JobState; +typedef struct { + PureEffect effect; + PureResource resource; + PureSpan reply; + PureResult status; + int64_t revision; + JobState state; +} EffectJob; +typedef struct { Server *server; PureInput input; } EffectWorker; +#endif +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; +#ifndef PURE_EMBEDDED_PROGRAM + PureProgram program; +#endif + 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]; +#ifdef PURE_HTTP_ASYNC + PureHost effect_host; + pthread_t worker[2]; + EffectWorker worker_context[2]; + pthread_mutex_t job_mutex; + pthread_cond_t job_ready[2]; + EffectJob jobs[SLOTS]; + size_t next_job[2]; + int effect_wake; + bool worker_stop; +#endif +}; +static volatile sig_atomic_t stopping; +static volatile sig_atomic_t wake_write = -1; +static void stop_server(int sig) { + (void)sig; + int saved_errno = errno; + stopping = 1; + if (wake_write >= 0) { + const char byte = 1; + (void)write((int)wake_write, &byte, 1); + } + errno = saved_errno; +} + +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; + } +} + +#ifdef PURE_HTTP_ASYNC +/* Immutable reads may run beside one writer; only the write worker touches + * the store's temporary-name sequence and publication lock. Runtime and + * connections remain on poll's owner thread. Borrowed spans outlive work. */ +static void *effect_worker(void *context) { + EffectWorker *worker = context; + Server *server = worker->server; + PureInput input = worker->input; + for (;;) { + pthread_mutex_lock(&server->job_mutex); + size_t slot = SLOTS; + while (slot == SLOTS) { + for (size_t offset = 0; offset < SLOTS; ++offset) { + size_t candidate = (server->next_job[input] + offset) % SLOTS; + if (server->jobs[candidate].state == JOB_QUEUED && + server->jobs[candidate].effect.input == input) { + slot = candidate; + break; + } + } + if (slot < SLOTS || server->worker_stop) break; + pthread_cond_wait(&server->job_ready[input], &server->job_mutex); + } + if (slot == SLOTS) { pthread_mutex_unlock(&server->job_mutex); return NULL; } + EffectJob *job = &server->jobs[slot]; + server->next_job[input] = (slot + 1) % SLOTS; + job->state = JOB_RUNNING; + PureEffect effect = job->effect; + pthread_mutex_unlock(&server->job_mutex); + + PureResource resource = {0}; + PureSpan reply = {0}; + int64_t revision = 0; + PureHost host = server->effect_host; + PureResult status = effect.input == PURE_INPUT_READ ? + host.read(host.context, server->runtime.definition, effect.key, + effect.available, &resource, &revision) : + host.write(host.context, server->runtime.definition, effect.key, + effect.expected, effect.value, &reply, &revision); + + pthread_mutex_lock(&server->job_mutex); + job->resource = resource; + job->reply = reply; + job->revision = revision; + job->status = status; + job->state = JOB_DONE; + pthread_mutex_unlock(&server->job_mutex); + const char byte = 1; + (void)write(server->effect_wake, &byte, 1); + } +} + +static void finish_effects(Server *server) { + for (size_t i = 0; i < SLOTS; ++i) { + pthread_mutex_lock(&server->job_mutex); + EffectJob *job = &server->jobs[i]; + if (job->state != JOB_DONE) { pthread_mutex_unlock(&server->job_mutex); continue; } + PureTicket ticket = job->effect.ticket; + PureInput input = job->effect.input; + PureResource resource = job->resource; + PureSpan reply = job->reply; + PureResult status = job->status; + int64_t revision = job->revision; + *job = (EffectJob){0}; + pthread_mutex_unlock(&server->job_mutex); + if (!pure_runtime_complete(ticket, status, + input == PURE_INPUT_READ ? &resource : NULL, + reply, revision)) abort(); + for (size_t j = 0; j < SLOTS; ++j) { + Connection *c = &server->connections[j]; + if (c->fd < 0 || c->phase != WAITING_EFFECT || + c->ticket.slot != ticket.slot || c->ticket.generation != ticket.generation) + continue; + PureOutcome result; + if (!pure_runtime_result(ticket, &result)) abort(); + c->body = NULL; + answer(c, result); + break; + } + } +} +#endif + +static void run_request(Connection *c) { + if (c->put && !pure_runtime_ready(c->ticket)) abort(); +#ifdef PURE_HTTP_ASYNC + Server *server = c->server; + PureEffect effect; + PureTake taken = pure_runtime_take(&server->runtime, &effect); + if (taken == PURE_TAKE_EFFECT) { + pthread_mutex_lock(&server->job_mutex); + EffectJob *job = &server->jobs[effect.ticket.slot]; + if (job->state != JOB_FREE) abort(); + *job = (EffectJob){.effect = effect, .state = JOB_QUEUED}; + pthread_cond_signal(&server->job_ready[effect.input]); + pthread_mutex_unlock(&server->job_mutex); + c->phase = WAITING_EFFECT; + return; + } + abort(); +#else + PureOutcome result; + while (!pure_runtime_result(c->ticket, &result)) + if (!pure_runtime_step(&c->server->runtime)) abort(); +#endif + c->body = NULL; /* Core now owns input until all consumers release it. */ +#ifdef PURE_HTTP_ASYNC + PureOutcome result; + if (!pure_runtime_result(c->ticket, &result)) abort(); +#endif + 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; +} + +static bool close_on_exec(int fd) { + int flags = fcntl(fd, F_GETFD, 0); + return flags >= 0 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == 0; +} + +int main(int argc, char **argv) { + Server server = {.budget = 4u * 1024u * 1024u, .timeout_ms = 10000}; + unsigned port = 8767; +#ifndef PURE_EMBEDDED_PROGRAM + const char *program_path = NULL; + bool explain = argc == 3 && !strcmp(argv[1], "--check-program"); + char program_error[160]; + if (explain) program_path = argv[2]; +#else + bool explain = false; +#endif + 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]; +#ifndef PURE_EMBEDDED_PROGRAM + else if (!strcmp(argv[i], "--program")) program_path = argv[i + 1]; +#endif + 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 (!server.path && !explain) goto usage; +#ifndef PURE_EMBEDDED_PROGRAM + if (!program_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; + } +#endif + const PureProgram *program = +#ifdef PURE_EMBEDDED_PROGRAM + &pure_embedded_program; +#else + &server.program; +#endif + if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize byte store directory\n", stderr); return 1; } + PureHost host = pure_store_host(server.path); +#ifdef PURE_HTTP_ASYNC + server.effect_host = host; + host.read = NULL; + host.write = NULL; +#endif + if (!pure_runtime_init(&server.runtime, server.work, SLOTS, program, + host, server.budget)) return 1; +#ifdef PURE_HTTP_ASYNC + if (!pure_runtime_enable_async(&server.runtime)) abort(); +#endif + signal(SIGPIPE, SIG_IGN); + 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; } + int wake[2]; + if (pipe(wake)) { perror("pipe"); close(listener); return 1; } + if (!nonblocking(wake[1]) || !close_on_exec(wake[0]) || !close_on_exec(wake[1]) +#ifdef PURE_HTTP_ASYNC + || !nonblocking(wake[0]) +#endif + ) { + perror("wake pipe"); close(wake[0]); close(wake[1]); close(listener); return 1; + } + wake_write = wake[1]; + struct sigaction stop_action = {.sa_handler = stop_server}; + sigemptyset(&stop_action.sa_mask); + if (sigaction(SIGINT, &stop_action, NULL) || sigaction(SIGTERM, &stop_action, NULL)) { + perror("sigaction"); wake_write = -1; + close(wake[0]); close(wake[1]); close(listener); return 1; + } +#ifdef PURE_HTTP_ASYNC + server.effect_wake = wake[1]; + pthread_attr_t worker_attr; + if (pthread_mutex_init(&server.job_mutex, NULL) || + pthread_cond_init(&server.job_ready[PURE_INPUT_READ], NULL) || + pthread_cond_init(&server.job_ready[PURE_INPUT_WRITE], NULL) || + pthread_attr_init(&worker_attr) || + pthread_attr_setstacksize(&worker_attr, 128u * 1024u)) { + fputs("Cannot initialize bounded storage workers\n", stderr); + wake_write = -1; + close(wake[0]); close(wake[1]); close(listener); return 1; + } + size_t workers_started = 0; + for (size_t i = 0; i < 2; ++i) { + server.worker_context[i] = (EffectWorker){.server = &server, .input = (PureInput)i}; + if (pthread_create(&server.worker[i], &worker_attr, + effect_worker, &server.worker_context[i])) break; + ++workers_started; + } + pthread_attr_destroy(&worker_attr); + if (workers_started != 2) { + pthread_mutex_lock(&server.job_mutex); + server.worker_stop = true; + pthread_cond_signal(&server.job_ready[PURE_INPUT_READ]); + pthread_cond_signal(&server.job_ready[PURE_INPUT_WRITE]); + pthread_mutex_unlock(&server.job_mutex); + for (size_t i = 0; i < workers_started; ++i) pthread_join(server.worker[i], NULL); + pthread_cond_destroy(&server.job_ready[PURE_INPUT_READ]); + pthread_cond_destroy(&server.job_ready[PURE_INPUT_WRITE]); + pthread_mutex_destroy(&server.job_mutex); + fputs("Cannot start bounded storage workers\n", stderr); + wake_write = -1; + close(wake[0]); close(wake[1]); close(listener); return 1; + } +#endif + 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 + 2] = { + {.fd = server.active < SLOTS ? listener : -1, .events = POLLIN}, + {.fd = wake[0], .events = POLLIN} + }; + uint64_t nearest_deadline = UINT64_MAX; + for (size_t i = 0; i < SLOTS; ++i) { + Connection *c = &server.connections[i]; + polls[i + 2] = (struct pollfd){.fd = c->phase == WAITING_EFFECT ? -1 : c->fd, + .events = c->phase == WRITING ? POLLOUT : POLLIN}; + if (c->fd >= 0 && c->deadline < nearest_deadline) nearest_deadline = c->deadline; + } + int wait_ms = -1; + if (nearest_deadline != UINT64_MAX) { + uint64_t now = now_ms(); + uint64_t remaining = nearest_deadline > now ? nearest_deadline - now : 0; + wait_ms = remaining > INT_MAX ? INT_MAX : (int)remaining; + } + int ready = poll(polls, SLOTS + 2, wait_ms); + if (ready < 0 && errno != EINTR) { perror("poll"); stopping = 1; } +#ifdef PURE_HTTP_ASYNC + if (polls[1].revents & POLLIN) { + char bytes[64]; + while (read(wake[0], bytes, sizeof(bytes)) > 0) {} + } + finish_effects(&server); +#endif + 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 + 2].revents; + if (events & (POLLERR | POLLNVAL)) { close_connection(c); continue; } +#ifdef PURE_HTTP_ASYNC + if (c->phase == WAITING_EFFECT) continue; +#endif + 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); + wake_write = -1; +#ifndef PURE_HTTP_ASYNC + close(wake[0]); + close(wake[1]); +#endif + pure_runtime_stop(&server.runtime); + for (size_t i = 0; i < SLOTS; ++i) close_connection(&server.connections[i]); +#ifdef PURE_HTTP_ASYNC + pthread_mutex_lock(&server.job_mutex); + server.worker_stop = true; + pthread_cond_signal(&server.job_ready[PURE_INPUT_READ]); + pthread_cond_signal(&server.job_ready[PURE_INPUT_WRITE]); + pthread_mutex_unlock(&server.job_mutex); + for (size_t i = 0; i < 2; ++i) if (pthread_join(server.worker[i], NULL)) abort(); + finish_effects(&server); + close(wake[0]); + close(wake[1]); + pthread_cond_destroy(&server.job_ready[PURE_INPUT_READ]); + pthread_cond_destroy(&server.job_ready[PURE_INPUT_WRITE]); + pthread_mutex_destroy(&server.job_mutex); +#endif + 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: +#ifdef PURE_EMBEDDED_PROGRAM + fputs("pure-connections-fixed --store DIRECTORY [--port 8767] [--budget BYTES] [--timeout-ms 10000] [--send-buffer BYTES]\n", stderr); +#else + 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); +#endif + return 2; +} diff --git a/prototypes/connections/source-tui-budget.json b/prototypes/connections/source-tui-budget.json new file mode 100644 index 0000000..31716a4 --- /dev/null +++ b/prototypes/connections/source-tui-budget.json @@ -0,0 +1,12 @@ +{ + "profile": "linux-aarch64-gcc14-source-document-terminal-editor", + "binaries": { + "pure-edit-tui": { + "max_text_bytes": 49152, + "max_data_bytes": 1152, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Optional source-loaded POSIX terminal editor for the same document app. Includes the runtime .pure parser and bounded document browser. Default core, fixed CLI and fixed TUI do not link the parser. These are Linux AArch64/GCC 14 section regression ceilings, not whole-process RAM; check_tui.py measures the selected whole process separately." +} diff --git a/prototypes/connections/store-paged.c b/prototypes/connections/store-paged.c new file mode 100644 index 0000000..89c0148 --- /dev/null +++ b/prototypes/connections/store-paged.c @@ -0,0 +1,529 @@ +#define _DEFAULT_SOURCE +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Selected Linux/16 KiB-page storage experiment. Small values retain the + * existing PUREB001 inline file. Large roots name an append-only page + * generation. Both forms expose one immutable, contiguous mapped read. */ +enum { PAGE = 16384, INLINE_BYTES = 24, ROOT_BYTES = 48, + MAX_PAGES = (PURE_OBJECT_LIMIT + PAGE - 1) / PAGE }; +static const unsigned char magic[8] = {'P','U','R','E','P','0','0','1'}; +static const unsigned char inline_magic[8] = {'P','U','R','E','B','0','0','1'}; +static uint64_t temporary_sequence; + +typedef struct { + int64_t revision; + size_t size; + uint64_t generation; + uint64_t refs[MAX_PAGES]; + bool inline_data; +} Root; + +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 void segment_name(const char *key, uint64_t generation, char name[96]) { + snprintf(name, 96, "dat-%s-%016" PRIx64, key, generation); +} + +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 result; + do { result = fsync(fd); } while (result && errno == EINTR); + return result == 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_bytes(int fd, unsigned char *bytes, size_t length, off_t offset) { + while (length) { + ssize_t n = pread(fd, bytes, length, offset); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return false; + bytes += (size_t)n; + length -= (size_t)n; + offset += n; + } + return true; +} + +static size_t pages(size_t size) { return (size + PAGE - 1) / PAGE; } + +static size_t segment_limit(size_t size) { + size_t limit = 2 * pages(size) * PAGE; +#ifdef PURE_STORE_TEST + if (limit > 6 * PAGE) limit = 6 * PAGE; +#else + if (limit < 8 * PAGE) limit = 8 * PAGE; +#endif + return limit; +} + +static bool read_root(int fd, Root *root) { + struct stat info; + unsigned char header[ROOT_BYTES]; + if (fstat(fd, &info) || !S_ISREG(info.st_mode) || + !read_bytes(fd, header, INLINE_BYTES, 0)) return false; + uint64_t revision = decode64(header + 8), size = decode64(header + 16); + if (!revision || revision > INT64_MAX || size > PURE_OBJECT_LIMIT) return false; + root->revision = (int64_t)revision; + root->size = (size_t)size; + if (!memcmp(header, inline_magic, sizeof(inline_magic))) { + if (info.st_size != (off_t)(INLINE_BYTES + size)) return false; + root->inline_data = true; + root->generation = 0; + return true; + } + if (memcmp(header, magic, sizeof(magic)) || + !read_bytes(fd, header + INLINE_BYTES, ROOT_BYTES - INLINE_BYTES, INLINE_BYTES)) return false; + uint64_t generation = decode64(header + 24), page = decode64(header + 32); + uint64_t count = decode64(header + 40); + if (!generation || page != PAGE || count != pages((size_t)size) || + info.st_size != (off_t)(ROOT_BYTES + count * 8)) return false; + root->inline_data = false; + root->generation = generation; + unsigned char bytes[MAX_PAGES * 8]; + if (count && !read_bytes(fd, bytes, (size_t)count * 8, ROOT_BYTES)) return false; + for (size_t i = 0; i < (size_t)count; ++i) root->refs[i] = decode64(bytes + i * 8); + return true; +} + +static int open_segment(int directory, const char *key, const Root *root, int flags) { + char name[96]; + segment_name(key, root->generation, name); + int fd = openat(directory, name, flags | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); + if (fd < 0) return -1; + struct stat info; + if (fstat(fd, &info) || !S_ISREG(info.st_mode) || info.st_size < 0) goto bad; + for (size_t i = 0; i < pages(root->size); ++i) + if (root->refs[i] > (uint64_t)INT64_MAX / PAGE || + (uint64_t)info.st_size / PAGE <= root->refs[i]) goto bad; + return fd; +bad: + close(fd); + return -1; +} + +static PureResult open_locked(const char *path, bool writer, int *directory, int *lock) { + *directory = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (*directory < 0) return PURE_STORE_ERROR; + *lock = openat(*directory, ".writer.lock", + (writer ? O_RDWR : O_RDONLY) | O_NOFOLLOW | O_CLOEXEC); + struct stat info; + if (*lock < 0 || fstat(*lock, &info) || !S_ISREG(info.st_mode)) return PURE_STORE_ERROR; + struct flock hold = {.l_type = writer ? F_WRLCK : F_RDLCK, .l_whence = SEEK_SET, + .l_start = 0, .l_len = 0}; + int rc; + do { rc = fcntl(*lock, writer ? F_SETLK : F_SETLKW, &hold); } + while (rc && errno == EINTR && !writer); + if (rc) return writer && (errno == EACCES || errno == EAGAIN) ? PURE_BUSY : PURE_STORE_ERROR; + return PURE_OK; +} + +PureResult pure_store_init(const char *path) { + if (!path || !*path || sysconf(_SC_PAGESIZE) != PAGE) 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; + 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) && 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 = -1, lock = -1, head = -1, data = -1; + PureResult result = open_locked(path, false, &directory, &lock); + if (result != PURE_OK) goto finish; + head = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + if (head < 0) { result = errno == ENOENT ? PURE_NOT_FOUND : PURE_STORE_ERROR; goto finish; } + Root root; + if (!read_root(head, &root)) { result = PURE_STORE_ERROR; goto finish; } + if (root.size < minimum || offset > root.size) { result = PURE_LIMIT; goto finish; } + if (whole) length = root.size - offset; + if (length > root.size - offset) { result = PURE_LIMIT; goto finish; } + if (length > available) { result = PURE_BUDGET; goto finish; } + if (root.inline_data) { + if (length) { + size_t start = INLINE_BYTES + offset; + size_t aligned = start - start % PAGE; + size_t prefix = start - aligned; + size_t mapped_size = prefix + length; + void *mapped = mmap(NULL, mapped_size, PROT_READ, MAP_PRIVATE, head, (off_t)aligned); + if (mapped == MAP_FAILED) { result = PURE_STORE_ERROR; goto finish; } + out->mapping = mapped; + out->mapped_size = mapped_size; + out->body = (PureSpan){(const unsigned char *)mapped + prefix, length}; + } + } else { + data = open_segment(directory, key, &root, O_RDONLY); + if (data < 0) { result = PURE_STORE_ERROR; goto finish; } + if (length) { + size_t first = offset / PAGE, count = pages(offset % PAGE + length); + size_t mapped_size = count * PAGE; + void *base = mmap(NULL, mapped_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (base == MAP_FAILED) { result = PURE_STORE_ERROR; goto finish; } + bool valid = true; + for (size_t at = 0; at < count;) { + size_t run = 1; + while (at + run < count && root.refs[first + at + run] == + root.refs[first + at] + run) ++run; + void *address = (unsigned char *)base + at * PAGE; + void *mapped = mmap(address, run * PAGE, PROT_READ, MAP_PRIVATE | MAP_FIXED, + data, (off_t)(root.refs[first + at] * PAGE)); + if (mapped != address) { valid = false; break; } + at += run; + } + if (!valid) { munmap(base, mapped_size); result = PURE_STORE_ERROR; goto finish; } + out->mapping = base; + out->mapped_size = mapped_size; + out->body = (PureSpan){(const unsigned char *)base + offset % PAGE, length}; + } + } + out->revision = root.revision; + out->object_size = root.size; + result = PURE_OK; +finish: + if (data >= 0) close(data); + if (head >= 0) close(head); + if (lock >= 0) close(lock); + if (directory >= 0) close(directory); + 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 read_logical(int data, const Root *root, size_t offset, + unsigned char *destination, size_t length) { + if (root->inline_data) + return read_bytes(data, destination, length, (off_t)(INLINE_BYTES + offset)); + while (length) { + size_t page = offset / PAGE, inside = offset % PAGE; + size_t n = PAGE - inside < length ? PAGE - inside : length; + if (!read_bytes(data, destination, n, (off_t)(root->refs[page] * PAGE + inside))) return false; + destination += n; + offset += n; + length -= n; + } + return true; +} + +static bool copy_logical(int source, const Root *root, int destination, + size_t offset, size_t length) { + unsigned char scratch[4096]; + while (length) { + size_t n = length < sizeof(scratch) ? length : sizeof(scratch); + if (!read_logical(source, root, offset, scratch, n) || + !write_bytes(destination, scratch, n)) return false; + offset += n; + length -= n; + } + return true; +} + +static bool write_full_pages(int old_data, const Root *old, int destination, + bool splice, size_t offset, size_t remove, PureSpan body, + Root *next) { + size_t count = pages(next->size); + for (size_t page = 0; page < count; ++page) { + unsigned char bytes[PAGE] = {0}; + size_t start = page * PAGE, end = start + PAGE; + if (end > next->size) end = next->size; + size_t at = start; + while (at < end) { + size_t n; + if (splice && at < offset) { + n = offset - at < end - at ? offset - at : end - at; + if (!read_logical(old_data, old, at, bytes + at - start, n)) return false; + } else if (at < offset + body.size || !splice) { + size_t source = splice ? at - offset : at; + n = body.size - source < end - at ? body.size - source : end - at; + memcpy(bytes + at - start, body.data + source, n); + } else { + n = end - at; + if (!read_logical(old_data, old, at - body.size + remove, + bytes + at - start, n)) return false; + } + at += n; + } + if (!write_bytes(destination, bytes, PAGE)) return false; + next->refs[page] = page; + } + return true; +} + +static bool append_patch(int data, const Root *old, Root *next, + size_t offset, PureSpan insert) { + if (!insert.size) return true; + struct stat info; + if (fstat(data, &info) || info.st_size < 0 || + (uint64_t)info.st_size > INT64_MAX - PAGE) return false; + off_t start = (info.st_size + PAGE - 1) / PAGE * PAGE; + if (ftruncate(data, start) || lseek(data, start, SEEK_SET) != start) return false; + size_t first = offset / PAGE, end = pages(offset + insert.size); + for (size_t page = first; page < end; ++page) { + unsigned char bytes[PAGE]; + if (!read_bytes(data, bytes, PAGE, (off_t)(old->refs[page] * PAGE))) return false; + size_t begin = page * PAGE < offset ? offset : page * PAGE; + size_t limit = (page + 1) * PAGE > offset + insert.size ? + offset + insert.size : (page + 1) * PAGE; + memcpy(bytes + begin - page * PAGE, insert.data + begin - offset, limit - begin); + if (!write_bytes(data, bytes, PAGE)) return false; + next->refs[page] = (uint64_t)start / PAGE + page - first; + } + return sync_fd(data); +} + +static bool publish_root(int directory, const char *name, Root *next, int64_t *revision, + bool *published, int old_data, const Root *old, + bool splice, size_t offset, size_t remove, PureSpan body) { + char temporary[96] = ""; + int fd = -1; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + if (temporary_sequence == UINT64_MAX) break; + 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; + if (errno != EEXIST) break; + } + if (fd < 0) return false; + unsigned char bytes[ROOT_BYTES + MAX_PAGES * 8]; + memcpy(bytes, next->inline_data ? inline_magic : magic, 8); + encode64(bytes + 8, (uint64_t)next->revision); + encode64(bytes + 16, next->size); + bool ok; + if (next->inline_data) { + ok = write_bytes(fd, bytes, INLINE_BYTES) && !fault(PURE_AFTER_HEADER); + if (ok && splice) + ok = copy_logical(old_data, old, fd, 0, offset) && + write_bytes(fd, body.data, body.size) && + copy_logical(old_data, old, fd, offset + remove, + old->size - offset - remove); + else if (ok) ok = write_bytes(fd, body.data, body.size); + } else { + encode64(bytes + 24, next->generation); + encode64(bytes + 32, PAGE); + encode64(bytes + 40, pages(next->size)); + for (size_t i = 0; i < pages(next->size); ++i) + encode64(bytes + ROOT_BYTES + i * 8, next->refs[i]); + ok = write_bytes(fd, bytes, ROOT_BYTES) && !fault(PURE_AFTER_HEADER) && + write_bytes(fd, bytes + ROOT_BYTES, pages(next->size) * 8); + } + if (ok) ok = !fault(PURE_BEFORE_FILE_SYNC) && sync_fd(fd); + if (close(fd)) ok = false; + if (ok) ok = !fault(PURE_BEFORE_RENAME) && !renameat(directory, temporary, directory, name); + if (ok) { + *published = true; + *revision = next->revision; + temporary[0] = 0; + ok = !fault(PURE_BEFORE_DIR_SYNC) && sync_fd(directory); + } + if (*temporary) unlinkat(directory, temporary, 0); + return ok; +} + +static void collect_old_segments(int directory, const char *key, uint64_t current) { + char prefix[72], retained[96]; + snprintf(prefix, sizeof(prefix), "dat-%s-", key); + segment_name(key, current, retained); + int copy = dup(directory); + if (copy < 0) return; + DIR *entries = fdopendir(copy); + if (!entries) { close(copy); return; } + bool removed = false; + struct dirent *entry; + size_t prefix_size = strlen(prefix); + while ((entry = readdir(entries))) { + if (!strncmp(entry->d_name, ".pending-", 9)) { + if (!unlinkat(directory, entry->d_name, 0)) removed = true; + continue; + } + if (strncmp(entry->d_name, prefix, prefix_size) || + strlen(entry->d_name + prefix_size) != 16 || + !strcmp(entry->d_name, retained)) continue; + bool hexadecimal = true; + for (const char *p = entry->d_name + prefix_size; *p; ++p) + if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f'))) hexadecimal = false; + if (hexadecimal && !unlinkat(directory, entry->d_name, 0)) removed = true; + } + closedir(entries); + if (removed) sync_fd(directory); +} + +static PureResult mutate(const char *path, const char *key, int64_t expected, + PureSpan body, bool splice, size_t offset, size_t remove, + size_t max_size, int64_t *revision) { + *revision = 0; + char name[72], new_segment[96] = ""; + if (!file_name(key, name) || (body.size && !body.data)) return PURE_STORE_ERROR; + if (body.size > PURE_OBJECT_LIMIT || max_size > PURE_OBJECT_LIMIT || expected < 0) return PURE_LIMIT; + int directory = -1, lock = -1, head = -1, old_data = -1, new_data = -1; + Root old = {0}, next = {0}; + bool published = false, reused_segment = false, owned_segment = false; + struct stat info; + PureResult result = open_locked(path, true, &directory, &lock); + if (result != PURE_OK) goto finish; + result = PURE_STORE_ERROR; + head = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + if (head >= 0) { + if (!read_root(head, &old)) goto finish; + old_data = old.inline_data ? dup(head) : open_segment(directory, key, &old, O_RDWR); + if (old_data < 0) goto finish; + } else if (errno != ENOENT) goto finish; + *revision = old.revision; + if (expected != old.revision) { result = PURE_CONFLICT; goto finish; } + if (old.revision == INT64_MAX) { result = PURE_LIMIT; goto finish; } + size_t result_size = body.size; + if (splice) { + if (offset > old.size || remove > old.size - offset || + old.size - remove > max_size || + body.size > max_size - (old.size - remove)) { result = PURE_LIMIT; goto finish; } + result_size = old.size - remove + body.size; + } + next = old; + next.revision = old.revision + 1; + next.size = result_size; + next.inline_data = result_size <= PAGE; + if (next.inline_data) next.generation = 0; + bool patch = old.revision && !old.inline_data && !next.inline_data && + splice && remove == body.size && result_size == old.size; + /* A small value retains the existing PUREB001 24-byte header. */ + if (!next.inline_data && patch && fstat(old_data, &info) == 0 && + info.st_size < (off_t)segment_limit(old.size)) { + reused_segment = true; + if (!append_patch(old_data, &old, &next, offset, body)) goto finish; + } else if (!next.inline_data) { + uint64_t candidate = old.generation; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + if (candidate == UINT64_MAX) goto finish; + segment_name(key, ++candidate, new_segment); + new_data = openat(directory, new_segment, + O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + if (new_data >= 0) { owned_segment = true; break; } + if (errno != EEXIST) goto finish; + } + if (new_data < 0) goto finish; + next.generation = candidate; + if (!write_full_pages(old_data, &old, new_data, splice, offset, remove, body, &next) || + !sync_fd(new_data)) goto finish; + /* A new data file's directory entry must survive before the root + * can durably name it. */ + if (!sync_fd(directory)) goto finish; + } + if (!publish_root(directory, name, &next, revision, &published, + old_data, &old, splice, offset, remove, body)) { + result = published ? PURE_UNCERTAIN : PURE_STORE_ERROR; + goto finish; + } + result = PURE_OK; + /* Readers have mapped each old inode before releasing their shared lock. + * A failed cleanup leaks space but cannot invalidate an existing view. */ + /* An inline-only edit creates no segments. Avoid a directory-wide scan + * on the small-object hot path. */ + if (!reused_segment && (old.generation || next.generation)) + collect_old_segments(directory, key, next.generation); +finish: + if (new_data >= 0) close(new_data); + if (old_data >= 0) close(old_data); + if (head >= 0) close(head); + if (result != PURE_OK && result != PURE_UNCERTAIN && owned_segment) + unlinkat(directory, new_segment, 0); + if (lock >= 0) close(lock); + if (directory >= 0) 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, PURE_OBJECT_LIMIT, revision); +} + +PureResult pure_store_splice(const char *path, const char *key, int64_t expected, + size_t offset, size_t remove, PureSpan insert, + size_t max_size, int64_t *revision) { + return mutate(path, key, expected, insert, true, offset, remove, max_size, revision); +} diff --git a/prototypes/connections/store-port.c b/prototypes/connections/store-port.c new file mode 100644 index 0000000..7bbe682 --- /dev/null +++ b/prototypes/connections/store-port.c @@ -0,0 +1,51 @@ +#include "store-port.h" +#include "store.h" +#include +#include + +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 void *definition, const char *key, + size_t available, PureResource *out, int64_t *revision) { + const PureProgram *program = definition; + if (!program) return PURE_INVALID; + PureStoredView view = {0}; + PureRegion region = program->read; + PureResult result = pure_store_select(context, key, region.offset, region.length, + region.to_end, program->read_required, 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 void *definition, const char *key, + int64_t expected, PureSpan input, PureSpan *reply, + int64_t *revision) { + const PureProgram *program = definition; + if (!program) return PURE_INVALID; + PureSpan value; + if (!pure_program_input(program, input, &value, reply)) return PURE_LIMIT; + if (!program->splice) return pure_store_put(context, key, expected, value, revision); + size_t offset, remove; + if (!pure_program_splice(program, input, &offset, &remove)) return PURE_LIMIT; + return pure_store_splice(context, key, expected, offset, remove, value, + program->splice_limit, revision); +} + +PureHost pure_store_host(const char *path) { + return (PureHost){.context = (void *)path, .allocate = allocate, + .read = read_value, .write = write_value}; +} diff --git a/prototypes/connections/store-port.h b/prototypes/connections/store-port.h new file mode 100644 index 0000000..0b09403 --- /dev/null +++ b/prototypes/connections/store-port.h @@ -0,0 +1,7 @@ +#ifndef PURE_STORE_PORT_H +#define PURE_STORE_PORT_H +#include "runtime.h" +#include "program.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/store.c b/prototypes/connections/store.c new file mode 100644 index 0000000..d9b15a9 --- /dev/null +++ b/prototypes/connections/store.c @@ -0,0 +1,309 @@ +#if defined(PURE_STORE_CFR_COPY) && defined(__linux__) +#define _GNU_SOURCE +#else +#define _POSIX_C_SOURCE 200809L +#endif +#if defined(PURE_STORE_CFR_COPY) && !defined(__linux__) +#error PURE_STORE_CFR_COPY is a Linux-only selected storage experiment +#endif +/* Darwin's strict POSIX namespace hides open flags used by this adapter. */ +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE +#endif +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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) { +#if defined(PURE_STORE_CFR_COPY) && defined(__linux__) + /* Opt-in Linux filesystem copy experiment. Keep the POSIX path for + * reported unsupported operations and for every non-selected build. */ + if (length >= 16384) { + off_t from = (off_t)(HEADER_BYTES + offset); +#ifdef PURE_STORE_TEST + bool unsupported_after_partial = + pure_store_test_fault(PURE_COPY_RANGE_UNSUPPORTED_AFTER_PARTIAL); + bool first_copy = true; +#endif + while (length) { + ssize_t copied; +#ifdef PURE_STORE_TEST + if (unsupported_after_partial && !first_copy) { + errno = EOPNOTSUPP; + copied = -1; + } else { + size_t wanted = unsupported_after_partial && length > 4096 ? 4096 : length; + copied = copy_file_range(source, &from, destination, NULL, wanted, 0); + } + first_copy = false; +#else + copied = copy_file_range(source, &from, destination, NULL, length, 0); +#endif + if (copied > 0) { + offset += (size_t)copied; + length -= (size_t)copied; + continue; + } + if (copied == 0) return false; /* The immutable source was truncated. */ + if (errno == EINTR) continue; + if (errno != ENOSYS && errno != EOPNOTSUPP && errno != EXDEV && + errno != EINVAL) return false; + break; /* Unsupported on this filesystem; finish with read/write. */ + } + } +#endif + 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, + size_t max_size, 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 || max_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 || + old_size - remove > max_size || + body.size > max_size - (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, PURE_OBJECT_LIMIT, revision); +} + +PureResult pure_store_splice(const char *path, const char *key, int64_t expected, + size_t offset, size_t remove, PureSpan insert, + size_t max_size, int64_t *revision) { + return mutate(path, key, expected, insert, true, offset, remove, max_size, revision); +} diff --git a/prototypes/connections/store.h b/prototypes/connections/store.h new file mode 100644 index 0000000..6896360 --- /dev/null +++ b/prototypes/connections/store.h @@ -0,0 +1,48 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include "result.h" +#include + +#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). + * Retained bytes use bounded scratch space or a selected platform file copy; + * never assembles a whole new object in application memory. The immutable + * file is still rewritten. max_size is the final-body policy from the + * prepared app and is checked with the current size under the writer lock. */ +PureResult pure_store_splice(const char *path, const char *key, int64_t expected, + size_t offset, size_t remove, PureSpan insert, + size_t max_size, 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, + PURE_COPY_RANGE_UNSUPPORTED_AFTER_PARTIAL +} PureStoreStage; +#ifdef PURE_STORE_TEST +int pure_store_test_fault(PureStoreStage stage); +#endif +#endif diff --git a/prototypes/connections/test-async-server-gate.c b/prototypes/connections/test-async-server-gate.c new file mode 100644 index 0000000..1db9816 --- /dev/null +++ b/prototypes/connections/test-async-server-gate.c @@ -0,0 +1,21 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include + +/* Test-only gate at the file sync point. The production server never links + * this file or the PURE_STORE_TEST hook. Only the storage worker reaches it. */ +int pure_store_test_fault(PureStoreStage stage) { + static int entered; + if (stage != PURE_BEFORE_FILE_SYNC || entered) return 0; + entered = 1; + const char *signal_fd = getenv("PURE_TEST_GATE_SIGNAL_FD"); + const char *release_fd = getenv("PURE_TEST_GATE_RELEASE_FD"); + if (!signal_fd || !release_fd) return 0; + int signal = atoi(signal_fd), release = atoi(release_fd); + const char byte = 1; + if (write(signal, &byte, 1) != 1) _exit(91); + char released; + if (read(release, &released, 1) != 1) _exit(92); + return 0; +} diff --git a/prototypes/connections/test-async.c b/prototypes/connections/test-async.c new file mode 100644 index 0000000..6efaf0b --- /dev/null +++ b/prototypes/connections/test-async.c @@ -0,0 +1,330 @@ +#define _POSIX_C_SOURCE 200809L +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(test) do { if (!(test)) { fprintf(stderr, "line %d: %s\n", __LINE__, #test); exit(1); } } while (0) + +static const PureProgram identity = { + .read = {.to_end = true}, .write = {.to_end = true}, + .reply = {.to_end = true}, .reply_input = true +}; + +typedef struct { + PureEffect effect; + const PureProgram *definition; + const char *directory; + int start, done; + PureResult status; + PureSpan reply; + int64_t revision; +} Worker; + +static void *write_after_signal(void *context) { + Worker *worker = context; + char byte = 0; + if (read(worker->start, &byte, 1) != 1) worker->status = PURE_STORE_ERROR; + else { + PureHost store = pure_store_host(worker->directory); + worker->status = store.write(store.context, worker->definition, + worker->effect.key, worker->effect.expected, + worker->effect.value, &worker->reply, + &worker->revision); + } + (void)write(worker->done, &byte, 1); + return NULL; +} + +static PureHost async_host(const char *directory) { + PureHost host = pure_store_host(directory); + host.read = NULL; + host.write = NULL; + return host; +} + +static PureTicket submit(PureRuntime *runtime, const char *key, + const void *body, size_t size) { + PureTicket ticket; + CHECK(pure_runtime_accept(runtime, PURE_INPUT_WRITE, key, 0, size, &ticket) == PURE_OK); + if (size) memcpy(pure_runtime_ingress(ticket, NULL), body, size); + CHECK(pure_runtime_ready(ticket)); + return ticket; +} + +static void expect(const char *directory, const char *key, const void *body, size_t size) { + PureStoredView view; + CHECK(pure_store_get(directory, key, 64, &view) == PURE_OK); + CHECK(view.body.size == size && !memcmp(view.body.data, body, size)); + pure_store_release(&view); +} + +static void worker_and_forward(const char *directory) { + PureRuntime source, destination; + PureWork source_slots[2], destination_slot[1]; + CHECK(pure_runtime_init(&source, source_slots, 2, &identity, + async_host(directory), 64)); + PureTicket ignored = {0}; + CHECK(pure_runtime_accept(&source, PURE_INPUT_WRITE, "denied", 0, 1, + &ignored) == PURE_DENIED); + CHECK(pure_runtime_enable_async(&source)); + PureTicket slow = submit(&source, "slow", "alpha", 5); + PureTicket fast = submit(&source, "fast", "beta", 4); + CHECK(!pure_runtime_step(&source)); /* No synchronous host callback. */ + PureEffect deferred; + CHECK(pure_runtime_take(&source, &deferred) == PURE_TAKE_EFFECT); + CHECK(deferred.ticket.generation == slow.generation && + deferred.value.size == 5 && !memcmp(deferred.value.data, "alpha", 5)); + CHECK(!pure_runtime_cancel(slow)); + int start_pipe[2], done_pipe[2]; + CHECK(!pipe(start_pipe) && !pipe(done_pipe)); + Worker worker = {.effect = deferred, .definition = &identity, .directory = directory, + .start = start_pipe[0], .done = done_pipe[1]}; + pthread_t thread; + CHECK(!pthread_create(&thread, NULL, write_after_signal, &worker)); + CHECK(pure_runtime_release(slow)); + CHECK(source.active == 2 && source.retained == 9); + PureEffect immediate; + CHECK(pure_runtime_take(&source, &immediate) == PURE_TAKE_EFFECT); + int64_t revision = 0; + PureHost store = pure_store_host(directory); + PureSpan reply = {0}; + PureResult written = store.write(store.context, &identity, immediate.key, + immediate.expected, immediate.value, &reply, &revision); + CHECK(pure_runtime_complete(immediate.ticket, written, NULL, reply, revision)); + PureOutcome result; + CHECK(pure_runtime_result(fast, &result) && result.status == PURE_OK && + result.body.size == 4 && !memcmp(result.body.data, "beta", 4)); + CHECK(pure_runtime_release(fast)); + expect(directory, "fast", "beta", 4); + PureStoredView absent; + CHECK(pure_store_get(directory, "slow", 64, &absent) == PURE_NOT_FOUND); + pure_runtime_stop(&source); + CHECK(!pure_runtime_restart(&source, &identity, async_host(directory), 64)); + CHECK(pure_runtime_accept(&source, PURE_INPUT_READ, "fast", 0, 0, + &ignored) == PURE_CLOSED); + CHECK(write(start_pipe[1], "x", 1) == 1); + struct pollfd done = {.fd = done_pipe[0], .events = POLLIN}; + CHECK(poll(&done, 1, 5000) == 1 && (done.revents & POLLIN)); + CHECK(!pthread_join(thread, NULL)); + CHECK(pure_runtime_complete(deferred.ticket, worker.status, NULL, + worker.reply, worker.revision)); + CHECK(!pure_runtime_complete(deferred.ticket, PURE_OK, NULL, (PureSpan){0}, 1)); + CHECK(!pure_runtime_result(slow, &result)); /* Caller released before I/O ended. */ + CHECK(source.active == 0 && source.retained == 0 && source.commits == 2); + expect(directory, "slow", "alpha", 5); + CHECK(!close(start_pipe[0]) && !close(start_pipe[1]) && + !close(done_pipe[0]) && !close(done_pipe[1])); + CHECK(pure_runtime_restart(&source, &identity, async_host(directory), 64)); + + CHECK(pure_runtime_init(&destination, destination_slot, 1, &identity, + async_host(directory), 64)); + CHECK(pure_runtime_enable_async(&destination)); + PureTicket read_ticket; + CHECK(pure_runtime_accept(&source, PURE_INPUT_READ, "fast", 0, 0, + &read_ticket) == PURE_OK); + PureEffect read_effect; + CHECK(pure_runtime_take(&source, &read_effect) == PURE_TAKE_EFFECT && + read_effect.input == PURE_INPUT_READ); + PureResource mapping = {0}; + revision = 0; + PureResult read_status = store.read(store.context, &identity, read_effect.key, + read_effect.available, &mapping, &revision); + CHECK(pure_runtime_complete(read_effect.ticket, read_status, &mapping, + (PureSpan){0}, revision)); + CHECK(!mapping.allocation); + CHECK(pure_runtime_result(read_ticket, &result) && result.status == PURE_OK && + result.body.size == 4); + PureTicket forwarded; + CHECK(pure_runtime_forward(read_ticket, &destination, "copy", 0, + &forwarded) == PURE_OK); + const unsigned char *borrowed = result.body.data; + CHECK(pure_runtime_release(read_ticket)); + CHECK(source.active == 1 && source.retained == 4); + PureEffect output; + CHECK(pure_runtime_take(&destination, &output) == PURE_TAKE_EFFECT); + CHECK(output.value.data == borrowed && output.value.size == 4); + revision = 0; + reply = (PureSpan){0}; + written = store.write(store.context, &identity, output.key, output.expected, + output.value, &reply, &revision); + CHECK(pure_runtime_complete(output.ticket, written, NULL, reply, revision)); + CHECK(pure_runtime_result(forwarded, &result) && result.status == PURE_OK && + result.body.data == borrowed); + CHECK(pure_runtime_release(forwarded)); + CHECK(source.active == 0 && source.retained == 0 && + destination.active == 0 && destination.retained == 0); + expect(directory, "copy", "beta", 4); +} + +static void rejection_and_budget(const char *directory) { + PureProgram invalid = identity; + invalid.input_required = 4; + PureRuntime runtime; + PureWork slots[2]; + CHECK(pure_runtime_init(&runtime, slots, 2, &invalid, + async_host(directory), 6)); + CHECK(pure_runtime_enable_async(&runtime)); + PureTicket rejected = submit(&runtime, "bad", "x", 1); + PureEffect effect; + CHECK(pure_runtime_take(&runtime, &effect) == PURE_TAKE_EFFECT); + PureHost store = pure_store_host(directory); + PureSpan reply = {0}; + int64_t revision = 0; + PureResult status = store.write(store.context, &invalid, effect.key, + effect.expected, effect.value, &reply, &revision); + CHECK(status == PURE_LIMIT); + CHECK(pure_runtime_complete(effect.ticket, status, NULL, reply, revision)); + PureOutcome result; + CHECK(pure_runtime_result(rejected, &result) && result.status == PURE_LIMIT); + CHECK(!pure_runtime_complete(rejected, PURE_OK, NULL, (PureSpan){0}, 1)); + CHECK(pure_runtime_release(rejected)); + CHECK(runtime.active == 0 && runtime.retained == 0); + + CHECK(pure_runtime_restart(&runtime, &identity, async_host(directory), 6)); + PureTicket cancelled = submit(&runtime, "unused", "x", 1); + CHECK(pure_runtime_cancel(cancelled)); + CHECK(pure_runtime_result(cancelled, &result) && result.status == PURE_CANCELLED); + CHECK(pure_runtime_release(cancelled)); + PureTicket invalid_read; + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_READ, "fast", 0, 0, + &invalid_read) == PURE_OK); + PureEffect invalid_effect; + CHECK(pure_runtime_take(&runtime, &invalid_effect) == PURE_TAKE_EFFECT); + PureResource foreign = {.body = {(const unsigned char *)"x", 1}}; + CHECK(!pure_runtime_complete(invalid_effect.ticket, PURE_OK, &foreign, + (PureSpan){0}, 1)); + CHECK(foreign.body.size == 1 && runtime.active == 1); + CHECK(pure_runtime_complete(invalid_effect.ticket, PURE_STORE_ERROR, NULL, + (PureSpan){0}, 0)); + CHECK(pure_runtime_release(invalid_read)); + PureTicket bad_reply = submit(&runtime, "bad_reply", "x", 1); + PureEffect bad_effect; + CHECK(pure_runtime_take(&runtime, &bad_effect) == PURE_TAKE_EFFECT); + CHECK(!pure_runtime_complete(bad_effect.ticket, PURE_OK, NULL, + (PureSpan){(const unsigned char *)"outside", 7}, 1)); + CHECK(runtime.active == 1 && runtime.retained == 1); + CHECK(pure_runtime_complete(bad_effect.ticket, PURE_STORE_ERROR, NULL, + (PureSpan){0}, 0)); + CHECK(pure_runtime_release(bad_reply) && runtime.active == 0 && runtime.retained == 0); + PureTicket first, second; + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_READ, "fast", 0, 0, &first) == PURE_OK); + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_READ, "fast", 0, 0, &second) == PURE_OK); + PureEffect one, two; + CHECK(pure_runtime_take(&runtime, &one) == PURE_TAKE_EFFECT); + CHECK(pure_runtime_take(&runtime, &two) == PURE_TAKE_EFFECT); + CHECK(pure_runtime_take(&runtime, &effect) == PURE_TAKE_EMPTY); + PureResource a = {0}, b = {0}; + int64_t revision_a = 0, revision_b = 0; + CHECK(store.read(store.context, &identity, one.key, one.available, + &a, &revision_a) == PURE_OK); + CHECK(store.read(store.context, &identity, two.key, two.available, + &b, &revision_b) == PURE_OK); + CHECK(pure_runtime_complete(two.ticket, PURE_OK, &b, (PureSpan){0}, revision_b)); + CHECK(!b.allocation); + CHECK(pure_runtime_complete(one.ticket, PURE_OK, &a, (PureSpan){0}, revision_a)); + CHECK(!a.allocation); + CHECK(pure_runtime_result(first, &result) && result.status == PURE_BUDGET); + CHECK(pure_runtime_result(second, &result) && result.status == PURE_OK); + CHECK(runtime.budget_rejections == 1 && runtime.retained == 4); + CHECK(pure_runtime_release(first) && pure_runtime_release(second)); + CHECK(runtime.active == 0 && runtime.retained == 0); +} + +static void authored_document(const char *directory) { + PureProgram program; + char error[160]; + CHECK(pure_program_load("examples/editor.pure", &program, error, sizeof(error))); + PureRuntime runtime; + PureWork slot[1]; + CHECK(pure_runtime_init(&runtime, slot, 1, &program, + async_host(directory), 64)); + CHECK(pure_runtime_enable_async(&runtime)); + const unsigned char create[] = {0,0,0,0, 0,0,0,0, 'H','i'}; + PureTicket ticket = submit(&runtime, "document", create, sizeof(create)); + PureEffect effect; + CHECK(pure_runtime_take(&runtime, &effect) == PURE_TAKE_EFFECT && + effect.value.size == sizeof(create)); + PureSpan value, reply = {0}; + size_t offset, remove; + CHECK(pure_program_splice(&program, effect.value, &offset, &remove) && + pure_program_input(&program, effect.value, &value, &reply) && + offset == 0 && remove == 0 && value.size == 2 && + !memcmp(value.data, "Hi", 2)); + PureHost store = pure_store_host(directory); + int64_t revision = 0; + PureResult status = store.write(store.context, &program, effect.key, + effect.expected, effect.value, &reply, &revision); + CHECK(pure_runtime_complete(effect.ticket, status, NULL, reply, revision)); + PureOutcome result; + CHECK(pure_runtime_result(ticket, &result) && result.status == PURE_OK && + result.body.size == 2 && !memcmp(result.body.data, "Hi", 2)); + CHECK(pure_runtime_release(ticket)); + + const unsigned char append[] = {2,0,0,0, 0,0,0,0, '!'}; + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_WRITE, "document", 1, + sizeof(append), &ticket) == PURE_OK); + memcpy(pure_runtime_ingress(ticket, NULL), append, sizeof(append)); + CHECK(pure_runtime_ready(ticket)); + CHECK(pure_runtime_take(&runtime, &effect) == PURE_TAKE_EFFECT && + effect.value.size == sizeof(append)); + CHECK(pure_program_splice(&program, effect.value, &offset, &remove) && + offset == 2 && remove == 0); + reply = (PureSpan){0}; + status = store.write(store.context, &program, effect.key, effect.expected, + effect.value, &reply, &revision); + CHECK(pure_runtime_complete(effect.ticket, status, NULL, reply, revision)); + CHECK(pure_runtime_result(ticket, &result) && result.status == PURE_OK && + result.body.size == 1 && result.body.data[0] == '!'); + CHECK(pure_runtime_release(ticket)); + expect(directory, "document", "Hi!", 3); + + ticket = submit(&runtime, "document", append, sizeof(append)); + CHECK(pure_runtime_take(&runtime, &effect) == PURE_TAKE_EFFECT); + reply = (PureSpan){0}; + status = store.write(store.context, &program, effect.key, effect.expected, + effect.value, &reply, &revision); + CHECK(pure_runtime_complete(effect.ticket, status, NULL, reply, revision)); + CHECK(pure_runtime_result(ticket, &result) && result.status == PURE_CONFLICT && + !result.body.size); + CHECK(pure_runtime_release(ticket)); + expect(directory, "document", "Hi!", 3); + + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_READ, "document", 0, 0, + &ticket) == PURE_OK); + CHECK(pure_runtime_take(&runtime, &effect) == PURE_TAKE_EFFECT); + PureResource mapping = {0}; + status = store.read(store.context, &program, effect.key, + effect.available, &mapping, &revision); + CHECK(pure_runtime_complete(effect.ticket, status, &mapping, + (PureSpan){0}, revision)); + CHECK(pure_runtime_result(ticket, &result) && result.status == PURE_OK && + result.body.size == 3 && !memcmp(result.body.data, "Hi!", 3)); + CHECK(pure_runtime_release(ticket)); + CHECK(runtime.active == 0 && runtime.retained == 0 && + runtime.commits == 2 && runtime.conflicts == 1 && runtime.gets == 1); +} + +int main(void) { + char template[] = "/tmp/pure-async-effects-XXXXXX"; + char *directory = mkdtemp(template); + CHECK(directory && pure_store_init(directory) == PURE_OK); + worker_and_forward(directory); + rejection_and_budget(directory); + authored_document(directory); + DIR *opened = opendir(directory); + CHECK(opened); + struct dirent *entry; + while ((entry = readdir(opened))) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; + CHECK(unlinkat(dirfd(opened), entry->d_name, 0) == 0); + } + CHECK(!closedir(opened) && !rmdir(directory)); + puts("{\"async_effects\":\"authored-document-worker-overlap-release-forward-budget-recovery\"}"); +} diff --git a/prototypes/connections/test-fields.c b/prototypes/connections/test-fields.c new file mode 100644 index 0000000..63dfc3e --- /dev/null +++ b/prototypes/connections/test-fields.c @@ -0,0 +1,153 @@ +#include "program.h" +#include +#include +#include +#include +#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/test-flow.c b/prototypes/connections/test-flow.c new file mode 100644 index 0000000..5ff028d --- /dev/null +++ b/prototypes/connections/test-flow.c @@ -0,0 +1,183 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#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/test-paged-store.c b/prototypes/connections/test-paged-store.c new file mode 100644 index 0000000..aed0539 --- /dev/null +++ b/prototypes/connections/test-paged-store.c @@ -0,0 +1,270 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { PAGE = 16384, SIZE = 3 * PAGE + 23 }; +static int fail_stage = -1; +static int crash; +int pure_store_test_fault(PureStoreStage stage) { + if ((int)stage != fail_stage) return 0; + if (crash) _exit(77); + return 1; +} + +static void expect(const char *path, int64_t revision, const unsigned char *bytes) { + PureStoredView view; + assert(pure_store_get(path, "large", SIZE, &view) == PURE_OK); + assert(view.revision == revision && view.body.size == SIZE && + !memcmp(view.body.data, bytes, SIZE)); + pure_store_release(&view); +} + +static uint32_t random_value(uint32_t *state) { + *state ^= *state << 13; + *state ^= *state >> 17; + *state ^= *state << 5; + return *state; +} + +static void expect_root(const char *path, const char *key, + const char magic[8], off_t size) { + char filename[320]; + snprintf(filename, sizeof(filename), "%s/obj-%s", path, key); + struct stat info; + assert(stat(filename, &info) == 0 && info.st_size == size); + int fd = open(filename, O_RDONLY); + assert(fd >= 0); + char actual[8]; + assert(read(fd, actual, sizeof(actual)) == sizeof(actual)); + assert(!memcmp(actual, magic, sizeof(actual))); + assert(close(fd) == 0); +} + +static void format_transitions(const char *path) { + unsigned char bytes[PAGE + 1]; + memset(bytes, 'A', sizeof(bytes)); + int64_t revision; + assert(pure_store_put(path, "boundary", 0, (PureSpan){bytes, PAGE}, + &revision) == PURE_OK); + expect_root(path, "boundary", "PUREB001", 24 + PAGE); + PureStoredView old; + assert(pure_store_get(path, "boundary", PAGE + 1, &old) == PURE_OK); + assert(pure_store_splice(path, "boundary", revision, PAGE, 0, + (PureSpan){bytes, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + expect_root(path, "boundary", "PUREP001", 48 + 2 * 8); + assert(old.body.size == PAGE && old.body.data[PAGE - 1] == 'A'); + pure_store_release(&old); + PureStoredView large; + assert(pure_store_get(path, "boundary", PAGE + 1, &large) == PURE_OK); + assert(large.body.size == PAGE + 1 && large.body.data[PAGE] == 'A'); + assert(pure_store_splice(path, "boundary", revision, PAGE, 1, + (PureSpan){NULL, 0}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + expect_root(path, "boundary", "PUREB001", 24 + PAGE); + assert(large.body.size == PAGE + 1 && large.body.data[PAGE] == 'A'); + pure_store_release(&large); + PureStoredView current; + assert(pure_store_get(path, "boundary", PAGE, ¤t) == PURE_OK); + assert(current.revision == 3 && current.body.size == PAGE && + !memcmp(current.body.data, bytes, PAGE)); + pure_store_release(¤t); + + const unsigned char one = 'X', two = 'Y'; + assert(pure_store_put(path, "tiny", 0, (PureSpan){&one, 1}, &revision) == PURE_OK); + expect_root(path, "tiny", "PUREB001", 25); + assert(pure_store_splice(path, "tiny", revision, 1, 0, + (PureSpan){&two, 1}, 1, &revision) == PURE_LIMIT); + assert(revision == 1); + PureStoredView policy_view; + assert(pure_store_get(path, "tiny", 1, &policy_view) == PURE_OK); + assert(policy_view.revision == 1 && policy_view.body.size == 1 && policy_view.body.data[0] == one); + pure_store_release(&policy_view); + fail_stage = PURE_BEFORE_RENAME; + assert(pure_store_splice(path, "tiny", revision, 0, 1, + (PureSpan){&two, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_STORE_ERROR); + fail_stage = -1; + assert(pure_store_get(path, "tiny", 1, ¤t) == PURE_OK); + assert(current.revision == 1 && current.body.data[0] == one); + pure_store_release(¤t); + fail_stage = PURE_BEFORE_DIR_SYNC; + assert(pure_store_splice(path, "tiny", revision, 0, 1, + (PureSpan){&two, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_UNCERTAIN); + fail_stage = -1; + assert(pure_store_get(path, "tiny", 1, ¤t) == PURE_OK); + assert(current.revision == 2 && current.body.data[0] == two); + pure_store_release(¤t); + + /* An existing flat-store object has no data segment. Its first large + * edit must read the old inline body before publishing a page root. */ + char legacy_file[320]; + snprintf(legacy_file, sizeof(legacy_file), "%s/obj-legacy", path); + int fd = open(legacy_file, O_WRONLY | O_CREAT | O_EXCL, 0600); + assert(fd >= 0); + unsigned char header[24] = "PUREB001"; + header[8] = 1; + header[16] = 1; + header[17] = 0x40; /* 0x4001 = 16 KiB + 1 */ + assert(write(fd, header, sizeof(header)) == sizeof(header)); + assert(write(fd, bytes, sizeof(bytes)) == sizeof(bytes)); + assert(close(fd) == 0); + assert(pure_store_get(path, "legacy", PAGE + 1, ¤t) == PURE_OK); + assert(current.revision == 1 && current.body.size == PAGE + 1); + pure_store_release(¤t); + assert(pure_store_splice(path, "legacy", 1, PAGE, 1, + (PureSpan){&two, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + expect_root(path, "legacy", "PUREP001", 48 + 2 * 8); + assert(pure_store_get(path, "legacy", PAGE + 1, ¤t) == PURE_OK); + assert(current.revision == 2 && current.body.size == PAGE + 1 && + current.body.data[PAGE] == two); + pure_store_release(¤t); +} + +static void mixed_edits(const char *path) { + enum { CAPACITY = 6 * PAGE }; + unsigned char *oracle = malloc(CAPACITY), *before = malloc(CAPACITY); + unsigned char insert[4096]; + assert(oracle && before); + size_t length = 2 * PAGE + 17; + uint32_t state = 0xA17E532Du; + for (size_t i = 0; i < length; ++i) oracle[i] = (unsigned char)random_value(&state); + int64_t revision; + assert(pure_store_put(path, "mixed", 0, (PureSpan){oracle, length}, &revision) == PURE_OK); + for (unsigned step = 0; step < 128; ++step) { + size_t offset = random_value(&state) % (length + 1); + size_t remove = random_value(&state) % 4097; + if (remove > length - offset) remove = length - offset; + size_t count = step % 3 ? random_value(&state) % 4097 : remove; + if (count > CAPACITY - (length - remove)) count = CAPACITY - (length - remove); + for (size_t i = 0; i < count; ++i) insert[i] = (unsigned char)random_value(&state); + PureStoredView old; + assert(pure_store_get(path, "mixed", CAPACITY, &old) == PURE_OK); + if (length) memcpy(before, old.body.data, length); + assert(pure_store_splice(path, "mixed", revision, offset, remove, + (PureSpan){insert, count}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + assert(old.body.size == length && + (!length || !memcmp(old.body.data, before, length))); + pure_store_release(&old); + memmove(oracle + offset + count, oracle + offset + remove, length - offset - remove); + memcpy(oracle + offset, insert, count); + length = length - remove + count; + PureStoredView current; + assert(pure_store_get(path, "mixed", CAPACITY, ¤t) == PURE_OK); + assert(current.revision == (int64_t)step + 2 && current.body.size == length && + (!length || !memcmp(current.body.data, oracle, length))); + pure_store_release(¤t); + } + free(before); + free(oracle); +} + +int main(void) { + assert(sysconf(_SC_PAGESIZE) == PAGE); + char base[] = "/tmp/pure-page-store-XXXXXX"; + assert(mkdtemp(base)); + char path[256]; + snprintf(path, sizeof(path), "%s/store", base); + assert(pure_store_init(path) == PURE_OK); + unsigned char expected[SIZE]; + for (size_t i = 0; i < SIZE; ++i) expected[i] = (unsigned char)(i * 17u + i / PAGE); + int64_t revision; + assert(pure_store_put(path, "large", 0, (PureSpan){expected, SIZE}, &revision) == PURE_OK); + assert(revision == 1); + PureStoredView old; + assert(pure_store_get(path, "large", SIZE, &old) == PURE_OK); + unsigned char original = old.body.data[PAGE - 1]; + const unsigned char replacement = 0xC7; + assert(pure_store_splice(path, "large", revision, PAGE - 1, 1, + (PureSpan){&replacement, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + expected[PAGE - 1] = replacement; + assert(revision == 2 && old.body.data[PAGE - 1] == original); + expect(path, revision, expected); + PureStoredView range; + assert(pure_store_get_range(path, "large", PAGE - 3, 9, 9, &range) == PURE_OK); + assert(!memcmp(range.body.data, expected + PAGE - 3, 9)); + pure_store_release(&range); + char file[320]; + struct stat info; + snprintf(file, sizeof(file), "%s/dat-large-%016x", path, 1); + assert(stat(file, &info) == 0 && info.st_size == (off_t)(5 * PAGE)); + + /* The bounded append log compacts into a new generation. The old view + * remains valid even after the old generation has been unlinked. */ + for (unsigned i = 0; i < 3; ++i) { + unsigned char value = (unsigned char)(0xA0 + i); + assert(pure_store_splice(path, "large", revision, SIZE - 1, 1, + (PureSpan){&value, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + expected[SIZE - 1] = value; + expect(path, revision, expected); + } + assert(access(file, F_OK) != 0 && old.body.data[PAGE - 1] == original); + pure_store_release(&old); + + /* Unpublished crashes may leave an orphan generation. They never change + * the visible root; the next writer must skip the collided generation. */ + pid_t child = fork(); + assert(child >= 0); + if (!child) { + fail_stage = PURE_BEFORE_RENAME; + crash = 1; + pure_store_put(path, "large", revision, (PureSpan){expected, SIZE}, &revision); + _exit(78); + } + int status; + assert(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 77); + assert(pure_store_init(path) == PURE_OK); + expect(path, revision, expected); + unsigned char value = 0x5A; + assert(pure_store_splice(path, "large", revision, 0, 1, + (PureSpan){&value, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + expected[0] = value; + expect(path, revision, expected); + + fail_stage = PURE_BEFORE_DIR_SYNC; + value = 0x36; + assert(pure_store_splice(path, "large", revision, PAGE + 4, 1, + (PureSpan){&value, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_UNCERTAIN); + fail_stage = -1; + expected[PAGE + 4] = value; + expect(path, revision, expected); + + /* A whole replacement must skip the generation orphaned by the crash. */ + assert(pure_store_put(path, "large", revision, + (PureSpan){expected, SIZE}, &revision) == PURE_OK); + expect(path, revision, expected); + DIR *segments = opendir(path); + assert(segments); + unsigned generations = 0; + struct dirent *segment; + while ((segment = readdir(segments))) { + assert(strncmp(segment->d_name, ".pending-", 9)); + if (!strncmp(segment->d_name, "dat-large-", 10)) ++generations; + } + assert(closedir(segments) == 0 && generations == 1); + const unsigned char initial[] = "abcde", inserted[] = "XYZ"; + assert(pure_store_put(path, "variable", 0, + (PureSpan){initial, 5}, &revision) == PURE_OK); + assert(pure_store_splice(path, "variable", revision, 1, 2, + (PureSpan){inserted, 3}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + PureStoredView variable; + assert(pure_store_get(path, "variable", 6, &variable) == PURE_OK); + assert(variable.body.size == 6 && !memcmp(variable.body.data, "aXYZde", 6)); + pure_store_release(&variable); + format_transitions(path); + mixed_edits(path); + DIR *directory = opendir(path); + assert(directory); + struct dirent *entry; + while ((entry = readdir(directory))) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; + assert(unlinkat(dirfd(directory), entry->d_name, 0) == 0); + } + assert(closedir(directory) == 0 && rmdir(path) == 0 && rmdir(base) == 0); + puts("{\"hybrid_storage\":\"snapshot-patch-range-compaction-crash-conflict-uncertain-inline-transitions\",\"mixed_edits\":128}"); +} diff --git a/prototypes/connections/test-program.c b/prototypes/connections/test-program.c new file mode 100644 index 0000000..6a4a19d --- /dev/null +++ b/prototypes/connections/test-program.c @@ -0,0 +1,111 @@ +#include "program.h" +#include +#include +#include +#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/test-runtime.c b/prototypes/connections/test-runtime.c new file mode 100644 index 0000000..0883ec8 --- /dev/null +++ b/prototypes/connections/test-runtime.c @@ -0,0 +1,356 @@ +#define _POSIX_C_SOURCE 200809L +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include +#include +#include +#include +#include +#include + +static const PureProgram identity = { + .read = {.to_end = true}, .write = {.to_end = true}, + .reply = {.to_end = true}, .reply_input = true +}; + +static void remove_store(const char *path) { + DIR *directory = opendir(path); + assert(directory); + struct dirent *entry; + while ((entry = readdir(directory))) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; + assert(unlinkat(dirfd(directory), entry->d_name, 0) == 0); + } + assert(closedir(directory) == 0 && rmdir(path) == 0); +} +typedef struct { + size_t allocations, writes, frees, live; + bool allocation_failure, invalid_allocation, oversized_read, foreign_reply, reenter, reenter_release; + PureRuntime *runtime; + PureTicket release_ticket; + 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); + if (a->host->reenter_release) { + PureTicket ignored = {0}; + assert(!pure_runtime_release(a->host->release_ticket)); + assert(pure_runtime_accept(a->host->runtime, PURE_INPUT_WRITE, + "reenter", 0, 0, &ignored) == PURE_INVALID); + } + ++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 + (host->invalid_allocation ? 1u : 0u)}, .allocation = a, + .allocation_size = size, .release = free_input}; + return true; +} + +static PureResult write_value(void *context, const void *definition, const char *key, + int64_t expected, PureSpan input, PureSpan *reply, + int64_t *revision) { + Host *host = context; + (void)key; + PureSpan body; + if (!pure_program_input(definition, input, &body, reply)) return PURE_LIMIT; + if (host->foreign_reply) { + *reply = (PureSpan){(const unsigned char *)"outside", 7}; + *revision = 1; + return PURE_OK; + } + 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 void *definition, const char *key, + size_t available, PureResource *out, int64_t *revision) { + Host *host = context; + (void)definition; (void)key; + 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); + host.reenter_release = true; + host.release_ticket = a; + assert(pure_runtime_cancel(a)); + host.reenter_release = false; + 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)); + host.reenter_release = true; + host.release_ticket = next; + assert(pure_runtime_release(next)); /* queued input: no effect */ + host.reenter_release = false; + 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; + host.invalid_allocation = host.reenter_release = true; + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "bad_allocation", 0, 1, &out) == PURE_ALLOCATION); + assert(r.active == 0 && r.retained == 0 && host.live == 0); + host.invalid_allocation = host.reenter_release = 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)); + host.foreign_reply = true; + PureTicket invalid_reply = submit(&r, "bad_reply", "x", 1); + assert(completed(&r, invalid_reply).status == PURE_STORE_ERROR); + assert(r.retained == 0 && host.live == 0 && pure_runtime_release(invalid_reply)); + host.foreign_reply = false; + 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 idle_binding(void) { + Host a = {0}, b = {0}; + PureRuntime source, destination; + PureWork source_work[1], destination_work[1]; + PureTicket first = {0}; + assert(pure_runtime_init(&source, NULL, 0, &identity, port(&a), 8)); + assert(pure_runtime_init(&destination, destination_work, 1, &identity, port(&b), 8)); + assert(pure_runtime_accept(&source, PURE_INPUT_WRITE, "idle", 0, 1, &first) == PURE_BUSY); + assert(!first.runtime && !pure_runtime_bind(&source, NULL, 1)); + assert(pure_runtime_bind(&source, source_work, 1)); + first = submit(&source, "source", "abc", 3); + assert(!pure_runtime_bind(&source, NULL, 0)); + assert(completed(&source, first).status == PURE_OK); + PureTicket receiver; + assert(pure_runtime_forward(first, &destination, "receiver", 0, &receiver) == PURE_OK); + assert(pure_runtime_release(first)); + assert(!pure_runtime_bind(&source, NULL, 0)); /* The receiver still pins its backing. */ + assert(completed(&destination, receiver).status == PURE_OK); + assert(pure_runtime_release(receiver)); + assert(!source.active && !source.retained && !a.live); + assert(pure_runtime_bind(&source, NULL, 0)); + assert(!pure_runtime_result(first, &(PureOutcome){0})); + assert(pure_runtime_bind(&source, source_work, 1)); + PureTicket second = submit(&source, "again", "d", 1); + assert(second.generation > first.generation && !pure_runtime_ready(first)); + assert(completed(&source, second).status == PURE_OK); + assert(pure_runtime_release(second)); + assert(pure_runtime_bind(&source, NULL, 0)); + assert(source.accepted == 2 && source.releases == 2 && source.serial == second.generation); +} + +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]; + 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); + remove_store(source_path); + remove_store(target_path); + assert(rmdir(directory) == 0); +} + +int main(void) { + admission_and_lifecycle(); + forwarding(); + idle_binding(); + 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,\"idle_slot_binding\":true," + "\"release_reentrancy_guard\":true}\n", sizeof(PureRuntime), sizeof(PureWork), + sizeof(PureTicket), sizeof(PureProgram)); + return 0; +} diff --git a/prototypes/connections/test-splice.c b/prototypes/connections/test-splice.c new file mode 100644 index 0000000..bf3c0a9 --- /dev/null +++ b/prototypes/connections/test-splice.c @@ -0,0 +1,190 @@ +#define _POSIX_C_SOURCE 200809L +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include +#include +#include +#include +#include +#include + +#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); + CHECK(p.splice_limit == PURE_OBJECT_LIMIT); + 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); + snprintf(source, sizeof(source), "%s on put { let s = store.splice(request.key, request.revision, 0, 0, request.body, 8); reply(s); }", get); + CHECK(pure_program_parse(source, strlen(source), &p, error, sizeof(error)) && p.splice_limit == 8); + 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);", + "let s = store.splice(request.key, request.revision, 0, 0, e.insert, e.start); reply(s);", + "let s = store.splice(request.key, request.revision, 0, 0, e.insert, 1449985); reply(s);" + }; + 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}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + CHECK(revision == 1); +#if defined(PURE_STORE_CFR_COPY) && defined(__linux__) + /* A short kernel copy followed by an unsupported result must continue + * from exactly the remaining source and destination byte positions. */ + unsigned char replacement = 'Z'; + failed_stage = PURE_COPY_RANGE_UNSUPPORTED_AFTER_PARTIAL; + CHECK(pure_store_splice(path, "work", revision, 32768, 1, + (PureSpan){&replacement, 1}, PURE_OBJECT_LIMIT, &revision) == PURE_OK); + failed_stage = -1; + oracle[32768] = replacement; + expect(path, "work", revision, (PureSpan){oracle, length}); +#endif + /* 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}, PURE_OBJECT_LIMIT, &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}, PURE_OBJECT_LIMIT, &revision) == PURE_CONFLICT); + CHECK(revision == latest); + CHECK(pure_store_splice(path, "work", latest, SIZE_MAX, 0, (PureSpan){0}, PURE_OBJECT_LIMIT, &revision) == PURE_LIMIT); + CHECK(pure_store_splice(path, "work", latest, 0, SIZE_MAX, (PureSpan){0}, PURE_OBJECT_LIMIT, &revision) == PURE_LIMIT); + CHECK(pure_store_splice(path, "work", latest, length + 1, 0, (PureSpan){0}, PURE_OBJECT_LIMIT, &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}, PURE_OBJECT_LIMIT, &revision) == PURE_LIMIT); + CHECK(pure_store_splice(path, "work", latest, 0, 0, (PureSpan){insert, 1}, length, &revision) == PURE_LIMIT); + CHECK(revision == latest); + expect(path, "work", latest, (PureSpan){oracle, length}); + CHECK(pure_store_splice(path, "missing", 0, 1, 0, (PureSpan){0}, PURE_OBJECT_LIMIT, &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}, PURE_OBJECT_LIMIT, &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}, PURE_OBJECT_LIMIT, &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}, PURE_OBJECT_LIMIT, &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}, PURE_OBJECT_LIMIT, &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.write = NULL; /* no write effect grants no splice permission */ + 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/tui-budget.json b/prototypes/connections/tui-budget.json new file mode 100644 index 0000000..1ca06b0 --- /dev/null +++ b/prototypes/connections/tui-budget.json @@ -0,0 +1,12 @@ +{ + "profile": "linux-aarch64-gcc14-fixed-document-terminal-editor", + "binaries": { + "pure-edit-tui-fixed": { + "max_text_bytes": 32768, + "max_data_bytes": 1152, + "max_bss_bytes": 64, + "needed": ["libc.so.6"] + } + }, + "scope": "Release-linked optional POSIX terminal editor for one build-time prepared document app, including the bounded document browser. The raised text ceiling reviews its measured 2,541-byte addition; default CLI and core ceilings are unchanged. Excludes libc, terminal emulator, kernel TTY buffers, heap and stack. Whole-process RSS, including 1,024-key browsing, is checked by check_tui.py on the recorded Linux AArch64 host." +} diff --git a/prototypes/connections/window-budget.json b/prototypes/connections/window-budget.json new file mode 100644 index 0000000..511557e --- /dev/null +++ b/prototypes/connections/window-budget.json @@ -0,0 +1,12 @@ +{ + "profile": "linux-aarch64-gcc14-selected-native-document-authoring", + "binaries": { + "pure-edit-window": { + "max_text_bytes": 57344, + "max_data_bytes": 1800, + "max_bss_bytes": 64, + "needed": ["libgtk-4.so.1", "libgio-2.0.so.0", "libgobject-2.0.so.0", "libglib-2.0.so.0", "libc.so.6"] + } + }, + "scope": "Review ceiling for the selected GTK document-and-definition host on Linux AArch64/GCC 14. Linked code sections and direct dependencies only; the measured 44 MiB whole-process RSS and shared GTK stack remain separate costs." +} diff --git a/prototypes/forgetful-core/README.md b/prototypes/forgetful-core/README.md new file mode 100644 index 0000000..fde22e3 --- /dev/null +++ b/prototypes/forgetful-core/README.md @@ -0,0 +1,116 @@ +# Pure: 기록을 버릴 수 있는 실행 코어 + +2026-09-22. **자체 실행 계약의 첫 C17 구현. 정확성과 자원 한도를 확인했으며, +독창성·성능 우위·제품 통합은 아직 검증하지 않았다.** 새 문법이나 화면 데모는 없다. + +후속: [기존 작성물의 표시 계산에 실행 계약을 이식](../../docs/research/009-bounded-display-integration.md)했다. +아래 정수 모델의 결과와 별도로, Zig/Wasm의 소수점·상태 변경 의미를 보존해 검증했다. + +## 우리가 확인한 질문 + +> 재사용을 위한 값과 변경 기록을 모두 버릴 수 있게 하면서, 잘못된 결과의 +> 재사용을 막고 한 요청이 사용할 작업량까지 제한할 수 있는가? + +기억이 부족할 때 정확도를 낮추는 대신 재계산한다. 재계산도 지정한 작업량을 +넘으면 결과를 발행하지 않고 `PF_WORK_LIMIT`를 반환한다. 필수 입력·프로그램은 +보존한다. 이 계약을 만족하면 재사용 기록을 필수 상태에서 분리하는 구현을 +계속 검토할 수 있다. 작은 모델에서도 오래된 값이 발행되거나 한도를 넘으면 +이 설계를 폐기·수정하는 것이 이번 작업의 중단 기준이었다. + +## 선행기술과 우리 구현의 경계 + +필요한 출력만 계산하는 것은 이미 [Adapton](https://matthewhammer.org/adapton/)의 +연구 대상이다. [Salsa의 튜닝 문서](https://salsa-rs.github.io/salsa/tuning.html)는 +LRU가 계산 결과를 제거하며, 질의 키·의존성 메타데이터는 그 제거 대상이 아니라고 +설명한다. 이것이 Salsa 전체에 다른 메모리 회수 기능이 없다는 뜻은 아니다. + +우리의 이번 구현 선택은 **값·입력 의존성 요약을 담은 캐시와 변경 기록을 모두 +고정 크기로 제한하고, 남은 증거로 재사용을 정당화할 수 없으면 버리는 것**이다. +새 이름을 붙인 증분 계산을 발명이라고 주장하지 않는다. 증분 계산, 변경 일지, +보수적인 무효화의 기존 아이디어를 이용한 자체 코어이며, 이 조합의 신규성이나 +기존 시스템 대비 개선은 입증하지 않았다. 소수 자료의 확인은 중복 검토의 +출발점이며 포괄적인 선행연구 검토가 아니다. 위 자료는 2026-09-22 확인했다. + +## 실행 규칙 + +- 프로그램은 최대 64개 노드의 DAG, 입력은 32개의 `uint64_t`다. 덧셈·곱셈은 + 2^64 모듈러 연산이다. 상수·입력·덧셈·곱셈·XOR·조건 선택을 지원한다. +- 조건 선택은 선택한 가지와 조건만 읽는다. 각 결과에 실제 읽은 입력의 + 32비트 요약을 합성한다. 자식의 캐시를 써도 그 의존성을 부모에 전한다. +- 캐시는 결과·입력 요약·확인한 revision을 함께 보관한다. 캐시 교체 시 + 이 세 가지를 함께 버리며, 별도의 질의별 키 테이블이나 역방향 간선을 남기지 않는다. +- 변경 일지는 고정 길이의 순환 배열이다. 기록이 덮어써지거나 삭제되면 + `history_floor`를 올린다. 그보다 오래된 캐시는 재사용할 근거가 없으므로 재계산한다. +- 기록이 충분하면 캐시의 입력 요약과 이후 변경이 겹치지 않을 때만 재사용한다. + 같은 값의 입력 쓰기는 revision을 올리지 않는다. +- 프로그램 교체는 전체 정의를 검증한 뒤 적용하며 입력을 보존하고 재사용 기록은 + 폐기한다. 잘못된 교체는 기존 프로그램·입력·기록을 유지한다. +- revision 오버플로 때는 이전 기록을 폐기한다. 입력 변경·조회·기록 삭제는 + 한 스레드에서 순서대로 호출한다. 동시 호출·외부 I/O·이벤트 재실행은 지원하지 않는다. + +정확성의 핵심은 다음이다. 기록이 완전한 구간에서 이전 실행이 읽었던 입력이 +하나도 변하지 않았다면, 순수하고 결정적인 계산은 같은 분기와 결과를 갖는다. +조건의 의존성도 포함해야 이 논리가 성립한다. 기록이 불완전하면 재사용하지 않는다. +테스트는 이 논증의 구현 오류를 찾는 수단이며 일반적인 형식 증명은 아니다. + +## 한도의 의미와 비용 + +`pf_eval`의 작업 한도에는 실행 단계·캐시 검사·변경 기록 검사가 포함된다. +성공할 때만 호출자의 출력에 값을 쓴다. 중단된 조회에서 이미 완성한 중간 캐시는 +남을 수 있지만 입력을 바꾸거나 부분 결과를 발행하지 않는다. 이 한도는 CPU 명령 +수나 실제 응답 시간 보장이 아니다. 재시도는 처음부터 시작하며 완료를 보장하지 않는다. + +현재 Linux AArch64/GCC 14.2.0에서 `sizeof(PfCore)`는 **3,696바이트**다. + +| 고정 영역 | 바이트 | +|---|---:| +| 프로그램 64개 노드 | 1,536 | +| 입력 32개 | 256 | +| 결과·의존성·revision 캐시 8개 | 192 | +| 변경 기록 8개 | 128 | +| 비재귀 평가 스택 64칸 | 1,536 | +| 개수·revision·정책 위치·작업 카운터 | 48 | + +코어는 힙 할당을 하지 않는다. 호출자가 제공하는 `PfCore`에 위 영역이 모두 들어간다. +논리적 캐시·기록 용량을 0~8로 조절해도 예약된 3,696바이트는 줄지 않는다. +메모리 압력에 맞춰 실제 저장 공간을 반납하는 할당기는 이번 구현에 없다. +노드 번호가 자식으로 내려갈 때 반드시 작아지므로 평가 스택은 노드 수를 넘지 않는다. + +이 수치는 전체 실행 RAM이 아니다. 실행 코드·C 호출 스택·호스트·출력과 로드할 +프로그램을 담은 호출자 버퍼는 별도다. GCC의 이 빌드에서 `pf_eval` 호출 프레임 +추정은 64바이트였다. 이는 전체 프로세스 스택 측정이나 다른 타깃의 보장이 아니다. +API는 초기화된 코어를 요구하고, 코어 필드의 외부 변경과 입력·출력 인수의 코어 내부 +별칭을 허용하지 않는다. `pf_load`에 넘긴 노드 배열도 코어와 겹치면 안 된다. + +## 확인한 것과 채택 보류 이유 + +[검증 기록](verification.json)의 일반 빌드와 ASan·UBSan 빌드가 각각 다음을 확인했다. + +- 성공 조회 **116,143개**가 독립적인 순방향 전체 계산과 일치했다. +- 작업량 중단 **16,425회**에서 부분 출력을 발행하지 않았다. +- 각 동작 뒤 결과를 조회하는 조건에서, 여섯 동작의 길이 5 이하 모든 순서와 + 아홉 캐시·기록 용량 조합을 확인했다. 동작은 두 입력 변경, 값 삭제, + 기록 삭제, 둘 다 삭제, 한도 0 조회다. +- 고정 seed의 32,000개 추가 동작에서 분기·입력·프로그램 교체·작업 한도를 섞었다. +- 기록 덮어쓰기, 미선택 가지 변경, 31번 입력 비트, revision 오버플로, + 잘못된 정의 거절, 최장 평가 스택을 별도로 확인했다. + +역시 중요한 결과는 **캐시가 없으면 작은 DAG도 재계산량이 지수적으로 커질 수 +있다**는 것이다. 그런 사례에서는 한도 내 거절을 확인했다. 따라서 ‘항상 적은 +메모리와 빠른 응답으로 완료’한다는 가설은 이 계약이 보장하지 않는다. + +이번으로 기본 계약 확인을 닫는다. 합성 조건을 늘리는 벤치마크는 만들지 않는다. +제품 채택은 실제 Pure 작업에서 작은 캐시의 반복 중단과 기록 검사 비용이 +허용되는지로 판단해야 한다. 현재 작업물의 32개 상태 제한과는 맞지만 큰 문서나 +서버의 임의 개수 입력에 적용할 표현은 없다. 메모리 반납·float 오류·상태 전이· +입출력의 의미도 기존 workbench와 통합되지 않았다. 기존 코어를 교체하지 않았다. + +## 재실행 + +```sh +python3 prototypes/forgetful-core/check.py --output /tmp/pure-forgetful-verification.json +``` + +C17 컴파일러, Python 3, ASan·UBSan이 필요하다. 외부 의존성을 내려받지 않는다. +출력 파일은 새 경로만 허용하며 소스 해시·컴파일러·플래그·호스트를 기록한다. +이 명령은 성능 비교를 실행하지 않는다. macOS 성능은 측정하지 않았다. diff --git a/prototypes/forgetful-core/check.py b/prototypes/forgetful-core/check.py new file mode 100644 index 0000000..d26adca --- /dev/null +++ b/prototypes/forgetful-core/check.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Verify the contract; this runner deliberately does not benchmark speed.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import shlex +import subprocess +import tempfile +from datetime import datetime, timezone + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, help='new JSON evidence file') + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('output already exists') + root = Path(__file__).resolve().parent + cc = shlex.split(os.environ.get('CC', 'cc')) + common = ['-std=c17', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror'] + record = { + 'kind': 'correctness-and-bounds-only', + 'utc': datetime.now(timezone.utc).isoformat(), + 'host': platform.platform(), + 'machine': platform.machine(), + 'compiler': subprocess.check_output(cc + ['--version'], text=True).splitlines()[0], + 'source_sha256': { + name: hashlib.sha256((root / name).read_bytes()).hexdigest() + for name in ('core.h', 'core.c', 'test.c', 'check.py') + }, + 'runs': [], + } + with tempfile.TemporaryDirectory(prefix='pure-forgetful-') as temporary: + for name, extra in ( + ('native', ['-O2', '-fstack-usage']), + ('asan-ubsan', ['-O1', '-fsanitize=address,undefined', '-fno-omit-frame-pointer']), + ): + binary = Path(temporary) / name + flags = common + extra + subprocess.run(cc + flags + [str(root / 'core.c'), str(root / 'test.c'), + '-o', str(binary)], cwd=temporary, check=True) + completed = subprocess.run([str(binary)], text=True, capture_output=True) + if completed.returncode or completed.stderr: + raise RuntimeError(f'{name} failed ({completed.returncode}):\n' + f'{completed.stdout}\n{completed.stderr}') + result = json.loads(completed.stdout) + if record['runs'] and result != record['runs'][0]['result']: + raise RuntimeError('native and sanitized verification disagree') + run = {'name': name, 'flags': flags, 'result': result} + if name == 'native': + # Compiler estimates, not a measured process stack high-water mark. + run['compiler_stack_usage'] = { + path.name: path.read_text() + for path in sorted(Path(temporary).glob('*core.su')) + } + record['runs'].append(run) + output = json.dumps(record, 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/forgetful-core/core.c b/prototypes/forgetful-core/core.c new file mode 100644 index 0000000..53edff0 --- /dev/null +++ b/prototypes/forgetful-core/core.c @@ -0,0 +1,163 @@ +#include "core.h" + +#include + +int pf_init(PfCore *core, uint32_t cache_capacity, uint32_t history_capacity) { + if (!core || cache_capacity > PF_CACHE || history_capacity > PF_HISTORY) + return PF_INVALID; + memset(core, 0, sizeof(*core)); + core->cache_capacity = cache_capacity; + core->history_capacity = history_capacity; + core->revision = 1; + core->history_floor = 1; + return PF_OK; +} + +void pf_forget(PfCore *core, unsigned what) { + if (what & PF_FORGET_VALUES) { + memset(core->memo, 0, sizeof(core->memo)); + core->next_memo = 0; + } + if (what & PF_FORGET_HISTORY) { + memset(core->history, 0, sizeof(core->history)); + core->next_change = 0; + /* Records older than this floor can no longer justify reuse. */ + core->history_floor = core->revision; + } +} + +int pf_load(PfCore *core, const PfNode *nodes, uint32_t count) { + if (!nodes || !count || count > PF_NODES) return PF_INVALID; + for (uint32_t i = 0; i < count; ++i) { + const PfNode n = nodes[i]; + if (n.op > PF_SELECT) return PF_INVALID; + if (n.op == PF_INPUT && n.a >= PF_INPUTS) return PF_INVALID; + if (n.op >= PF_ADD && (n.a >= i || n.b >= i)) return PF_INVALID; + if (n.op == PF_SELECT && n.c >= i) return PF_INVALID; + } + memcpy(core->nodes, nodes, count * sizeof(*nodes)); + core->node_count = count; + pf_forget(core, PF_FORGET_VALUES | PF_FORGET_HISTORY); + return PF_OK; +} + +int pf_write(PfCore *core, uint32_t input, uint64_t value) { + if (input >= PF_INPUTS) return PF_INVALID; + if (core->inputs[input] == value) return PF_OK; + /* No old certificate is allowed to become current after epoch wrap. */ + if (core->revision == UINT64_MAX) { + core->revision = 1; + pf_forget(core, PF_FORGET_VALUES | PF_FORGET_HISTORY); + } + ++core->revision; + core->inputs[input] = value; + if (!core->history_capacity) { + core->history_floor = core->revision; + } else { + PfChange *change = &core->history[core->next_change]; + if (change->revision > core->history_floor) + core->history_floor = change->revision; + *change = (PfChange){core->revision, UINT32_C(1) << input}; + core->next_change = (core->next_change + 1) % core->history_capacity; + } + return PF_OK; +} + +static int step(PfCore *core, uint64_t limit) { + if (core->work == limit) return 0; + ++core->work; + return 1; +} + +/* Return 1 for a proven hit, 0 for a miss, -1 for work exhaustion. No reverse + * dependency edges or version tables grow when nodes are queried. */ +static int lookup(PfCore *core, uint32_t node, uint64_t limit, PfMemo *found) { + for (uint32_t i = 0; i < core->cache_capacity; ++i) { + if (!step(core, limit)) return -1; + const PfMemo memo = core->memo[i]; + if (memo.node_plus_one != node + 1) continue; + if (memo.revision < core->history_floor) return 0; + if (memo.revision != core->revision) { + for (uint32_t j = 0; j < core->history_capacity; ++j) { + if (!step(core, limit)) return -1; + const PfChange change = core->history[j]; + if (change.revision > memo.revision && + (change.changed & memo.dependencies)) return 0; + } + } + *found = memo; + /* Validation can extend the certificate without evaluating the node. */ + core->memo[i].revision = core->revision; + return 1; + } + return 0; +} + +static void remember(PfCore *core, uint32_t node, uint64_t value, uint32_t deps) { + if (!core->cache_capacity) return; + /* Round-robin keeps replacement state fixed. Duplicates are safe: every + * entry has its own certificate; the first matching entry may cause extra + * recomputation but cannot authorize a stale value. */ + core->memo[core->next_memo] = (PfMemo){value, core->revision, deps, node + 1}; + core->next_memo = (core->next_memo + 1) % core->cache_capacity; +} + +static void push(PfCore *core, uint32_t node) { + core->frames[core->depth++] = (PfFrame){.node = node}; +} + +int pf_eval(PfCore *core, uint32_t node, uint64_t work_limit, uint64_t *out) { + core->work = 0; + core->depth = 0; + if (!out || node >= core->node_count) return PF_INVALID; + push(core, node); + uint64_t value = 0; + uint32_t deps = 0; + while (core->depth) { + if (!step(core, work_limit)) goto exhausted; + PfFrame *frame = &core->frames[core->depth - 1]; + const PfNode n = core->nodes[frame->node]; + if (frame->stage == 0) { + PfMemo memo; + const int hit = lookup(core, frame->node, work_limit, &memo); + if (hit < 0) goto exhausted; + if (hit) { + value = memo.value; + deps = memo.dependencies; + --core->depth; + continue; + } + if (n.op == PF_CONST) { + value = n.literal; + deps = 0; + } else if (n.op == PF_INPUT) { + value = core->inputs[n.a]; + deps = UINT32_C(1) << n.a; + } else { + frame->stage = 1; + push(core, n.a); + continue; + } + } else if (frame->stage == 1) { + frame->first = value; + frame->dependencies = deps; + frame->stage = 2; + push(core, n.op == PF_SELECT && !value ? n.c : n.b); + continue; + } else { + deps |= frame->dependencies; + if (n.op == PF_ADD) value = frame->first + value; + else if (n.op == PF_MUL) value = frame->first * value; + else if (n.op == PF_XOR) value = frame->first ^ value; + /* SELECT preserves the chosen result and includes control deps. */ + } + remember(core, frame->node, value, deps); + --core->depth; + } + *out = value; + return PF_OK; + +exhausted: + core->depth = 0; + return PF_WORK_LIMIT; +} diff --git a/prototypes/forgetful-core/core.h b/prototypes/forgetful-core/core.h new file mode 100644 index 0000000..061afc8 --- /dev/null +++ b/prototypes/forgetful-core/core.h @@ -0,0 +1,58 @@ +#ifndef PURE_FORGETFUL_CORE_H +#define PURE_FORGETFUL_CORE_H + +#include +#include + +/* An executable contract, not a replacement for the workbench VM. */ +enum { PF_NODES = 64, PF_INPUTS = 32, PF_CACHE = 8, PF_HISTORY = 8 }; +enum { PF_CONST, PF_INPUT, PF_ADD, PF_MUL, PF_XOR, PF_SELECT }; +enum { PF_OK, PF_INVALID, PF_WORK_LIMIT }; +enum { PF_FORGET_VALUES = 1, PF_FORGET_HISTORY = 2 }; + +typedef struct { + uint32_t op, a, b, c; + uint64_t literal; +} PfNode; + +typedef struct { + uint64_t value, revision; + uint32_t dependencies, node_plus_one; +} PfMemo; + +typedef struct { + uint64_t revision; + uint32_t changed; +} PfChange; + +typedef struct { + uint64_t first; + uint32_t node, stage, dependencies; +} PfFrame; + +/* All persistent arrays and the nonrecursive evaluation stack live here. + * Callers must treat these fields as private; public exposure allows static + * allocation and sizeof accounting without a heap or a platform allocator. */ +typedef struct { + PfNode nodes[PF_NODES]; + uint64_t inputs[PF_INPUTS]; + PfMemo memo[PF_CACHE]; + PfChange history[PF_HISTORY]; + PfFrame frames[PF_NODES]; + uint64_t revision, history_floor, work; + uint32_t node_count, cache_capacity, history_capacity; + uint32_t next_memo, next_change, depth; +} PfCore; + +int pf_init(PfCore *core, uint32_t cache_capacity, uint32_t history_capacity); +/* Load validates every operand before replacement. Inputs survive replacement; + * cached results and history do not. Caller supplies nonoverlapping nodes. */ +int pf_load(PfCore *core, const PfNode *nodes, uint32_t count); +int pf_write(PfCore *core, uint32_t input, uint64_t value); +void pf_forget(PfCore *core, unsigned what); +/* Queries are pure. Only PF_OK publishes *out. A failed query may leave valid + * intermediate memos, but never changes inputs or publishes a partial result. + * work counts evaluator stages, memo probes and history probes, not CPU time. */ +int pf_eval(PfCore *core, uint32_t node, uint64_t work_limit, uint64_t *out); + +#endif diff --git a/prototypes/forgetful-core/test.c b/prototypes/forgetful-core/test.c new file mode 100644 index 0000000..11d7024 --- /dev/null +++ b/prototypes/forgetful-core/test.c @@ -0,0 +1,256 @@ +#include "core.h" + +#include +#include +#include +#include + +static uint64_t checks, queries, exhausted_queries; +static uint64_t rng = UINT64_C(20260922); + +static uint64_t random_word(void) { + rng ^= rng << 13; + rng ^= rng >> 7; + rng ^= rng << 17; + return rng; +} + +/* Independent forward evaluator: no memo, history, stack machine, or demand + * scheduling. All nodes are total/pure, so eager SELECT arms are safe here. */ +static uint64_t reference(const PfNode *nodes, uint32_t count, + const uint64_t *inputs, uint32_t target) { + uint64_t values[PF_NODES] = {0}; + for (uint32_t i = 0; i < count; ++i) { + const PfNode n = nodes[i]; + switch (n.op) { + case PF_CONST: values[i] = n.literal; break; + case PF_INPUT: values[i] = inputs[n.a]; break; + case PF_ADD: values[i] = values[n.a] + values[n.b]; break; + case PF_MUL: values[i] = values[n.a] * values[n.b]; break; + case PF_XOR: values[i] = values[n.a] ^ values[n.b]; break; + case PF_SELECT: values[i] = values[n.a] ? values[n.b] : values[n.c]; break; + default: assert(0); + } + } + return values[target]; +} + +static void check_query(PfCore *core, const PfNode *nodes, uint32_t count, + const uint64_t *inputs, uint32_t target, uint64_t limit) { + const uint64_t sentinel = UINT64_C(0xbaadc0de12345678); + uint64_t out = sentinel; + const int status = pf_eval(core, target, limit, &out); + assert(core->work <= limit && core->depth == 0); + assert(memcmp(core->inputs, inputs, sizeof(core->inputs)) == 0); + assert(status == PF_OK || status == PF_WORK_LIMIT); + if (status == PF_OK) { + assert(out == reference(nodes, count, inputs, target)); + ++queries; + } else { + assert(out == sentinel); + ++exhausted_queries; + /* Completion is also checked after the interrupted query's memos. */ + assert(pf_eval(core, target, UINT64_C(10000000), &out) == PF_OK); + assert(out == reference(nodes, count, inputs, target)); + ++queries; + } + ++checks; +} + +static const PfNode branch_program[] = { + {.op = PF_INPUT, .a = 0}, + {.op = PF_INPUT, .a = 1}, + {.op = PF_CONST, .literal = 7}, + {.op = PF_ADD, .a = 1, .b = 2}, + {.op = PF_XOR, .a = 1, .b = 2}, + {.op = PF_SELECT, .a = 0, .b = 3, .c = 4}, + {.op = PF_MUL, .a = 5, .b = 3}, + {.op = PF_SELECT, .a = 0, .b = 6, .c = 4}, +}; + +static void walk(const PfCore *parent, const uint64_t *inputs, unsigned depth) { + for (unsigned action = 0; action < 6; ++action) { + PfCore core = *parent; + uint64_t next[PF_INPUTS]; + memcpy(next, inputs, sizeof(next)); + if (action < 2) { + next[action] ^= 1; + assert(pf_write(&core, action, next[action]) == PF_OK); + } else if (action < 5) { + pf_forget(&core, action - 1); + } else { + uint64_t untouched = 123; + assert(pf_eval(&core, 7, 0, &untouched) == PF_WORK_LIMIT); + assert(untouched == 123); + ++exhausted_queries; + } + check_query(&core, branch_program, 8, next, 7, 10000); + if (depth > 1) walk(&core, next, depth - 1); + } +} + +static void exhaustive(void) { + const uint32_t capacities[] = {0, 1, 8}; + for (unsigned c = 0; c < 3; ++c) { + for (unsigned h = 0; h < 3; ++h) { + PfCore core; + uint64_t inputs[PF_INPUTS] = {0}; + assert(pf_init(&core, capacities[c], capacities[h]) == PF_OK); + assert(pf_load(&core, branch_program, 8) == PF_OK); + walk(&core, inputs, 5); + } + } +} + +static void directed(void) { + PfCore core, saved; + uint64_t out, inputs[PF_INPUTS] = {0}; + assert(pf_init(&core, 8, 8) == PF_OK); + assert(pf_load(&core, branch_program, 8) == PF_OK); + saved = core; + assert(pf_init(&core, 9, 8) == PF_INVALID); + assert(memcmp(&core, &saved, sizeof(core)) == 0); + assert(pf_load(&core, NULL, 0) == PF_INVALID); + const PfNode invalid[] = {{.op = PF_ADD, .a = 0, .b = 0}}; + assert(pf_load(&core, invalid, 1) == PF_INVALID); + assert(memcmp(&core, &saved, sizeof(core)) == 0); + assert(pf_write(&core, 32, 1) == PF_INVALID); + assert(memcmp(&core, &saved, sizeof(core)) == 0); + out = 456; + assert(pf_eval(&core, 8, 100, &out) == PF_INVALID && out == 456); + + check_query(&core, branch_program, 8, inputs, 7, 10000); + /* An unrelated write still permits a proven hit; forgetting its history + * must make the old memo unprovable even though its value happens to match. */ + inputs[31] = 99; + assert(pf_write(&core, 31, 99) == PF_OK); + saved = core; + assert(pf_write(&core, 31, 99) == PF_OK); + assert(memcmp(&core, &saved, sizeof(core)) == 0); + check_query(&core, branch_program, 8, inputs, 7, 10000); + assert(core.work <= 1 + PF_CACHE + PF_HISTORY); + inputs[31] = 100; + assert(pf_write(&core, 31, 100) == PF_OK); + pf_forget(&core, PF_FORGET_HISTORY); + check_query(&core, branch_program, 8, inputs, 7, 10000); + const uint64_t recompute_work = core.work; + check_query(&core, branch_program, 8, inputs, 7, 10000); + assert(core.work < recompute_work); + inputs[0] = 1; + assert(pf_write(&core, 0, 1) == PF_OK); + check_query(&core, branch_program, 8, inputs, 7, 10000); + + /* Overwrite the journal many times without querying. */ + for (unsigned i = 0; i < 40; ++i) { + inputs[1] = i; + assert(pf_write(&core, 1, i) == PF_OK); + } + check_query(&core, branch_program, 8, inputs, 7, 10000); + /* White-box setup of the otherwise impractical epoch rollover case. */ + core.revision = UINT64_MAX; + inputs[1] = UINT64_MAX; + assert(pf_write(&core, 1, inputs[1]) == PF_OK); + assert(core.revision == 2); + check_query(&core, branch_program, 8, inputs, 7, 10000); + for (uint64_t budget = 0; budget < 160; ++budget) { + pf_forget(&core, PF_FORGET_VALUES | PF_FORGET_HISTORY); + check_query(&core, branch_program, 8, inputs, 7, budget); + } + + const PfNode select[] = { + {.op = PF_INPUT, .a = 0}, {.op = PF_INPUT, .a = 1}, + {.op = PF_INPUT, .a = 31}, {.op = PF_SELECT, .a = 0, .b = 1, .c = 2}, + }; + assert(pf_load(&core, select, 4) == PF_OK); + inputs[0] = 0; + assert(pf_write(&core, 0, 0) == PF_OK); + check_query(&core, select, 4, inputs, 3, 10000); + int saw_dependencies = 0; + for (unsigned i = 0; i < PF_CACHE; ++i) { + if (core.memo[i].node_plus_one == 4) { + assert(core.memo[i].dependencies == UINT32_C(0x80000001)); + saw_dependencies = 1; + } + } + assert(saw_dependencies); + inputs[1] = 123; + assert(pf_write(&core, 1, 123) == PF_OK); + check_query(&core, select, 4, inputs, 3, 10000); + assert(core.work <= 1 + PF_CACHE + PF_HISTORY); + inputs[31] = 456; + assert(pf_write(&core, 31, 456) == PF_OK); + check_query(&core, select, 4, inputs, 3, 10000); + inputs[0] = 1; + assert(pf_write(&core, 0, 1) == PF_OK); + check_query(&core, select, 4, inputs, 3, 10000); + + PfNode chain[PF_NODES] = {{.op = PF_CONST, .literal = UINT64_MAX}}; + for (uint32_t i = 1; i < PF_NODES; ++i) + chain[i] = (PfNode){.op = PF_ADD, .a = i - 1, .b = 0}; + assert(pf_load(&core, chain, PF_NODES) == PF_OK); + check_query(&core, chain, PF_NODES, inputs, PF_NODES - 1, 10000); + chain[0].literal = 2; + assert(pf_load(&core, chain, PF_NODES) == PF_OK); + check_query(&core, chain, PF_NODES, inputs, PF_NODES - 1, 10000); + + /* A compact DAG can require exponential work without retained results. + * The core must reject within the budget, not promise bounded latency. */ + assert(pf_init(&core, 0, 0) == PF_OK); + for (uint32_t i = 1; i < PF_NODES; ++i) chain[i].b = i - 1; + assert(pf_load(&core, chain, PF_NODES) == PF_OK); + out = 456; + assert(pf_eval(&core, PF_NODES - 1, 100, &out) == PF_WORK_LIMIT); + assert(core.work == 100 && out == 456 && core.depth == 0); + ++exhausted_queries; +} + +static void random_trace(void) { + for (unsigned trial = 0; trial < 32; ++trial) { + PfCore core; + PfNode nodes[16] = {0}; + uint64_t inputs[PF_INPUTS] = {0}; + assert(pf_init(&core, trial % 9, (trial / 3) % 9) == PF_OK); + for (uint32_t i = 0; i < 16; ++i) { + nodes[i].op = i < 4 ? PF_INPUT : (uint32_t)(random_word() % 6); + if (nodes[i].op == PF_INPUT) nodes[i].a = (uint32_t)(random_word() % 32); + else if (nodes[i].op >= PF_ADD) { + nodes[i].a = (uint32_t)(random_word() % i); + nodes[i].b = (uint32_t)(random_word() % i); + nodes[i].c = (uint32_t)(random_word() % i); + } + nodes[i].literal = random_word(); + } + assert(pf_load(&core, nodes, 16) == PF_OK); + for (unsigned action = 0; action < 1000; ++action) { + const uint64_t choice = random_word(); + if (choice % 11 < 6) { + const uint32_t id = (uint32_t)(random_word() % 32); + inputs[id] = random_word(); + assert(pf_write(&core, id, inputs[id]) == PF_OK); + } else if (choice % 11 < 9) { + pf_forget(&core, (unsigned)(choice % 3 + 1)); + } else if (choice % 11 == 9) { + const uint32_t id = 4 + (uint32_t)(random_word() % 12); + nodes[id] = (PfNode){.op = PF_SELECT, + .a = (uint32_t)(random_word() % id), + .b = (uint32_t)(random_word() % id), + .c = (uint32_t)(random_word() % id)}; + assert(pf_load(&core, nodes, 16) == PF_OK); + } + check_query(&core, nodes, 16, inputs, (uint32_t)(choice % 16), choice % 300); + } + } +} + +int main(void) { + directed(); + exhaustive(); + random_trace(); + printf("{\"checks\":%" PRIu64 ",\"successful_queries\":%" PRIu64 + ",\"exhausted_queries\":%" PRIu64 ",\"core_bytes\":%zu," + "\"node_bytes\":%zu,\"memo_bytes\":%zu,\"history_entry_bytes\":%zu," + "\"frame_bytes\":%zu,\"seed\":20260922}\n", + checks, queries, exhausted_queries, sizeof(PfCore), sizeof(PfNode), + sizeof(PfMemo), sizeof(PfChange), sizeof(PfFrame)); + return 0; +} diff --git a/prototypes/forgetful-core/verification.json b/prototypes/forgetful-core/verification.json new file mode 100644 index 0000000..bf5e37a --- /dev/null +++ b/prototypes/forgetful-core/verification.json @@ -0,0 +1,67 @@ +{ + "kind": "correctness-and-bounds-only", + "utc": "2026-09-22T13:34:34.908273+00:00", + "host": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "source_sha256": { + "core.h": "cbc8cad736c72bf45f1e7d9d545b7ac40af44c4e79c9f7128ff15bc3de79e8f5", + "core.c": "6b98b56ab6d0802135d2b40877a3020224af687722d934911ba360bd9ba7f00e", + "test.c": "3d87a842ec15f9c3e35e7238165bc29652573bbe731fde348dd4500879bf7171", + "check.py": "2013f0530fbd06b19672da4ea4660cb44f00bf6a2154b32a3e8375b5befb2b71" + }, + "runs": [ + { + "name": "native", + "flags": [ + "-std=c17", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-O2", + "-fstack-usage" + ], + "result": { + "checks": 116143, + "successful_queries": 116143, + "exhausted_queries": 16425, + "core_bytes": 3696, + "node_bytes": 24, + "memo_bytes": 24, + "history_entry_bytes": 16, + "frame_bytes": 24, + "seed": 20260922 + }, + "compiler_stack_usage": { + "native-core.su": "/home/eunhhu/work/pure/prototypes/forgetful-core/core.c:5:5:pf_init\t48\tstatic\n/home/eunhhu/work/pure/prototypes/forgetful-core/core.c:16:6:pf_forget\t0\tstatic\n/home/eunhhu/work/pure/prototypes/forgetful-core/core.c:29:5:pf_load\t32\tstatic\n/home/eunhhu/work/pure/prototypes/forgetful-core/core.c:44:5:pf_write\t0\tstatic\n/home/eunhhu/work/pure/prototypes/forgetful-core/core.c:109:5:pf_eval\t64\tstatic\n" + } + }, + { + "name": "asan-ubsan", + "flags": [ + "-std=c17", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-O1", + "-fsanitize=address,undefined", + "-fno-omit-frame-pointer" + ], + "result": { + "checks": 116143, + "successful_queries": 116143, + "exhausted_queries": 16425, + "core_bytes": 3696, + "node_bytes": 24, + "memo_bytes": 24, + "history_entry_bytes": 16, + "frame_bytes": 24, + "seed": 20260922 + } + } + ] +} diff --git a/prototypes/live-rules/.gitignore b/prototypes/live-rules/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/prototypes/live-rules/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/prototypes/live-rules/README.md b/prototypes/live-rules/README.md new file mode 100644 index 0000000..a32d697 --- /dev/null +++ b/prototypes/live-rules/README.md @@ -0,0 +1,126 @@ +# Pure · 실행 중 규칙 바꾸기 + +공룡 연습 장면에서 점프 규칙을 바꾸고, 현재 상태와 입력이 허용·거절된 이유를 +확인하는 첫 사용 경험 실험이다. 부딪혀도 계속 진행되며, 규칙을 적용해도 위치·속도· +진행 상태는 유지된다. **처음부터**만 규칙과 상태를 함께 초기화한다. + +Zig 코어 하나를 WebAssembly로 빌드해 브라우저와 터미널에서 사용한다. +JavaScript에는 게임 물리나 별도의 규칙 실행기가 없다. 이 데모의 목적은 편집 의미와 +관찰 비용을 확인하는 것이다. 일반 런타임·전체 앱 편집기나 성능 우월성을 입증한 결과는 아니다. + +## 실행 + +저장소 루트에서 다음을 실행한다. 외부 JavaScript 패키지는 필요하지 않다. + +```sh +python3 prototypes/live-rules/manage.py serve --port 8765 +``` + +브라우저에서 를 연다. 포함된 `web/core.wasm`을 사용한다. +`file://`로 HTML을 직접 여는 대신 위 서버를 사용한다. 서버는 로컬 주소에만 바인딩한다. + +소스 수정 후에는 [Zig 0.15.2](https://ziglang.org/download/)로 다시 빌드한다. +새 버전과 구분해 재현하기 위해 이 버전으로 고정했다. 전체 재빌드에는 인터넷이 필요하지 않다. + +```sh +python3 prototypes/live-rules/manage.py build +# PATH에 없는 컴파일러를 지정할 때: +python3 prototypes/live-rules/manage.py build --zig /path/to/zig +``` + +빌드는 네이티브 테스트를 먼저 수행한다. 그 뒤 `wasm32-freestanding`, `ReleaseSafe`, +스택 예약 16 KiB, 초기·최대 선형 메모리 64 KiB로 빌드한다. 컴파일 캐시는 `build/`에 둔다. +버전·소스 해시·바이너리 해시·검사 출력은 `build/build.json`에 저장한다. +Wasm 파일 크기와 선형 메모리 용량은 서로 다른 값이다. + +## 직접 확인할 작업 + +1. 시작하고 **점프**를 누른다. 공중에서 다시 누르면 왜 거절됐는지 확인한다. +2. 공중에서 일시정지한다. 점프 조건을 **두 번 점프**로 바꾸고 **규칙 적용**을 누른다. +3. 위치와 진행 상태가 그대로인지 보고 다시 점프한다. 새 규칙의 판정이 표시되는지 확인한다. +4. 점프를 비활성화해 거절 이유를 확인한 뒤 다시 활성화한다. + +시간을 놓치면 정지 상태에서 점프하고 **한 프레임**씩 진행해도 된다. +정지 중 입력도 판정하지만, 실제 위치 이동은 프레임을 진행할 때 일어난다. +규칙을 바꾸기 전의 마지막 판정은 해당 규칙 버전의 기록으로 남는다. +입력과 규칙 변경은 한 프레임 처리 사이에서 순서대로 적용한다. + +## 같은 코어를 터미널에서 사용 + +Node.js 20 이상으로 실행한다. 검증 환경은 Node 24다. + +```sh +node prototypes/live-rules/cli.mjs +node prototypes/live-rules/cli.mjs --demo +node prototypes/live-rules/cli.mjs --demo --json +``` + +터미널에서는 시간이 저절로 흐르지 않는다. 아래 명령을 순서대로 입력하면 +공중에서의 거절과 규칙 수정 후 두 번째 점프를 정확히 같은 프레임에서 관찰할 수 있다. + +```text +jump +step 8 +jump +rules double 12 on +jump +inspect +memory +``` + +`help`는 명령 안내, `quit`은 종료다. 규칙 편집은 세 값을 한꺼번에 검증해 적용한다. +지원하지 않는 값이나 잘못된 명령은 이전 규칙과 상태를 보존한 채 거절한다. + +## 구조와 측정 범위 + +| 파일 | 역할 | +|---|---| +| `core.zig` | 실제 여섯 노드 판정 그래프, 고정 간격 물리, 규칙 변경, 상태·판정·기록 보관, 네이티브 테스트 | +| `web/` | 같은 코어에 연결하는 화면·키보드·버튼 어댑터 | +| `runtime.mjs`, `cli.mjs` | 같은 Wasm을 로드하는 터미널 어댑터 | +| `adapter.test.mjs` | 실제 Wasm과 입력 어댑터의 의미·메모리 한도 검증 | +| `measure.mjs` | 자료 구조 크기, 정확한 변경 기록, 소스·바이너리 해시 저장 | + +규칙은 점프 조건·힘·활성화 세 항목이다. 입력·활성 여부·접지 여부·남은 점프 수를 +읽고, 조건을 선택한 뒤 AND로 합류하는 실제 고정 그래프를 평가한다. +연산이나 연결을 자유롭게 추가하는 그래프 편집기는 아직 없다. + +자료 구조 크기는 Zig의 `@sizeOf`로 계산한다. 그래프 상수는 컴파일러가 코드로 +접을 수도 있으므로 이 합계를 물리 메모리 실측으로 해석하지 않는다. +Wasm 선형 메모리 64 KiB에는 코어 데이터와 스택 여유가 포함되며, 둘을 더하지 않는다. +브라우저/Node 엔진, DOM·렌더러, 코드와 프로세스 메모리는 별도이며 이번 수치에 포함하지 않는다. + +가장 최근 입력의 판정과 최근 사건 16개만 보관한다. 오래된 사건을 덮어쓰는 +이 기록은 완전한 실행 재생 로그가 아니다. 프레임 입력 큐를 만들어 몰래 버리는 방식도 아니다. +브라우저가 숨겨지면 일시정지하고, 프레임 처리가 밀린 경우의 시간 제한은 화면에 표시한다. +이 데모는 하드 실시간 동작을 보장하지 않는다. + +## 검증 및 기록 생성 + +```sh +python3 prototypes/live-rules/manage.py build +node prototypes/live-rules/adapter.test.mjs +node prototypes/live-rules/measure.mjs prototypes/live-rules/results/my-run +``` + +측정 출력 폴더의 상위 `results/`는 있어야 하며, 이미 있는 출력 폴더를 덮어쓰지 않는다. +측정 스크립트는 처리 속도·입력 지연을 측정하지 않는다. 브라우저를 통한 실제 조작 확인은 +이 자동 검사와 별도로 수행한다. 자세한 결과는 [연구 보고서 003](../../docs/research/003-live-rules.md)에 있다. + +브라우저 검증용 `qa-browser.js`는 Playwright CLI의 `run-code`에 전달하는 함수 표현식이다. +서버를 실행하고 CLI로 해당 페이지를 연 뒤 다음처럼 파일 내용을 인수로 전달할 수 있다. +검증에 사용한 CLI 버전은 0.1.21, 브라우저는 Chromium 151이며 런타임의 의존성은 아니다. +Playwright가 사용하는 브라우저의 설치 또는 실행 경로 설정은 별도로 필요하다. + +```sh +npx @playwright/cli -s=pure-live open http://127.0.0.1:8765 +python3 - <<'PY' +from pathlib import Path +import subprocess +Path('output/playwright').mkdir(parents=True, exist_ok=True) +script = Path('prototypes/live-rules/qa-browser.js').read_text() +subprocess.run(['npx', '@playwright/cli', '-s=pure-live', 'run-code', script], check=True) +PY +``` + +이 검사는 화면과 상태를 초기화하고 여러 시나리오를 조작한다. 개발용 페이지에서 실행한다. diff --git a/prototypes/live-rules/adapter.test.mjs b/prototypes/live-rules/adapter.test.mjs new file mode 100644 index 0000000..95af13b --- /dev/null +++ b/prototypes/live-rules/adapter.test.mjs @@ -0,0 +1,96 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { loadCore, snapshot, storage, parseCommand, execute } from './runtime.mjs'; + +test('the actual Wasm preserves airborne state through a rule change, then permits the second jump', async () => { + const c = await loadCore(); + execute(c, parseCommand('jump')); + execute(c, parseCommand('step 8')); + execute(c, parseCommand('jump')); + const before = snapshot(c); + assert.equal(before.state.lastReason, 3); + const after = execute(c, parseCommand('rules double 12 on')); + for (const field of ['tick', 'y', 'vy', 'distance', 'obstacleX', 'jumpsUsed', 'hits', 'passed']) { + assert.equal(after.state[field], before.state[field], field); + } + assert.deepEqual(after.decision, before.decision); + assert.equal(after.state.revision, 1); + execute(c, parseCommand('jump')); + assert.equal(c.get(10), 1); + assert.equal(c.get(2), 1200); + assert.equal(c.get(7), 2); + assert.equal(c.input(2), 0); + assert.equal(c.get(10), 4); +}); + +test('bad CLI edits and malformed numeric input cannot silently coerce into a different rule', async () => { + const c = await loadCore(); + const before = snapshot(c); + for (const command of ['rules double 8.5 on', 'rules double 4294967306 on', 'rules ground 5 on', + 'rules other 10 on', 'rules double 10 maybe', 'rules double 10 on extra', 'step 0', 'step 601', 'jump extra']) { + assert.throws(() => execute(c, parseCommand(command)), undefined, command); + assert.deepEqual(snapshot(c), before); + } + assert.equal(c.apply_rules(2, 10, 1), 1); + assert.deepEqual(snapshot(c), before); +}); + +test('keyboard, pointer and terminal requests give the same simulation result', async () => { + const states = []; + for (const source of [0, 1, 2]) { + const c = await loadCore(); + c.apply_rules(1, 12, 1); + c.input(source); + for (let i = 0; i < 8; i++) c.step(); + c.input(source); + const { lastSource, ...state } = snapshot(c).state; + assert.equal(lastSource, source); + states.push(state); + } + assert.deepEqual(states[0], states[1]); + assert.deepEqual(states[1], states[2]); +}); + +test('paused input has an inspectable decision; advance cannot move paused state', async () => { + const c = await loadCore(); + assert.equal(snapshot(c).decision, null); + assert.equal(c.input(0), 1); + assert.equal(c.get(1), 0); + assert.equal(c.get(17), 0); + const before = snapshot(c); + assert.equal(c.advance(60), 0); + assert.deepEqual(snapshot(c), before); + c.step(); + assert.equal(c.get(1), 945); + assert.equal(c.get(0), 1); +}); + +test('bounded history and fixed linear memory survive repeated play and edits', async () => { + const c = await loadCore(); + const before = storage(c); + c.set_running(1); + for (let i = 0; i < 5000; i++) { + c.apply_rules(i % 2, 6 + i % 11, 1); + c.input(i % 3); + c.advance(8); + } + assert.equal(c.history_count(), 16); + assert.equal(c.history_get(16, 0), -1); + assert.equal(c.get(0), 40000); + assert.deepEqual(storage(c), before); + assert.equal(before.wasmLinearMemoryBytes, 65536); + assert.throws(() => c.memory.grow(1), RangeError); + const b = before.layoutBytes; + assert.equal(b.logicalTotal, b.engine + b.graph); + assert.ok(b.engine >= b.rules + b.state + b.decision + b.history); +}); + +test('replaying the same CLI operations produces identical snapshots and journal', async () => { + const trace = ['jump', 'step 8', 'jump', 'rules double 10 on', 'jump', 'step 60', 'rules ground 8 off', 'jump']; + const snapshots = []; + for (let i = 0; i < 2; i++) { + const c = await loadCore(); + snapshots.push(trace.map((line) => execute(c, parseCommand(line)))); + } + assert.deepEqual(snapshots[0], snapshots[1]); +}); diff --git a/prototypes/live-rules/cli.mjs b/prototypes/live-rules/cli.mjs new file mode 100644 index 0000000..2c8a564 --- /dev/null +++ b/prototypes/live-rules/cli.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +import { createInterface } from 'node:readline'; +import { loadCore, storage, parseCommand, execute } from './runtime.mjs'; + +const help = `Pure · 규칙 실험실 (브라우저와 같은 Zig 코어) + jump 점프 입력 + step [1..600] 지정한 프레임만 진행 (기본 1) + rules ground|double 6..16 on|off + 점프 조건·힘·활성화를 함께 적용 + status / inspect / memory 현재 상태 / 마지막 판정·기록 / 공간 + reset 규칙과 상태 모두 처음으로 + help / quit +예: jump → step 8 → jump → rules double 10 on → jump +시간은 step으로만 진행합니다. 실행 규칙 변경은 진행 상태를 보존합니다.`; + +const reason = ['아직 입력 없음', '점프 허용', '점프 비활성', '공중: 땅에서만 가능', '두 번 모두 사용', '지원하지 않는 입력']; + +async function main() { + const args = process.argv.slice(2); + if (args.includes('--help')) { console.log(help); return; } + if (args.some((x) => !['--demo', '--json'].includes(x))) throw new Error('사용법: node cli.mjs [--demo] [--json]'); + const json = args.includes('--json'); + const core = await loadCore(); + function show(line) { + const command = parseCommand(line); + if (command.op === 'help') { console.log(help); return true; } + if (command.op === 'quit') return false; + const result = execute(core, command); + if (json) { + console.log(JSON.stringify({ command: line, ...result, ...(command.op === 'memory' ? { storage: storage(core) } : {}) })); + } else if (command.op === 'memory') { + console.log(JSON.stringify(storage(core), null, 2)); + } else if (command.op === 'inspect') { + console.log(JSON.stringify(result, null, 2)); + } else { + const s = result.state; + console.log(`t=${s.tick} · 높이 ${(s.y / 100).toFixed(2)} · 속도 ${(s.vy / 100).toFixed(2)} · 점프 ${s.jumpsUsed}/2 · 규칙 r${s.revision} (${s.mode ? 'double' : 'ground'}, ${s.impulse}, ${s.enabled ? 'on' : 'off'}) · ${reason[s.lastReason]}`); + } + return true; + } + if (args.includes('--demo')) { + for (const line of ['status', 'jump', 'step 8', 'jump', 'rules double 10 on', 'jump', 'step 60', 'memory']) show(line); + return; + } + if (!json && process.stdin.isTTY) console.log(help); + const terminal = createInterface({ input: process.stdin, output: process.stdout, + terminal: Boolean(process.stdin.isTTY) }); + terminal.setPrompt('pure> '); + if (process.stdin.isTTY) terminal.prompt(); + for await (const line of terminal) { + try { + if (!show(line)) { terminal.close(); break; } + } catch (error) { + console.error(error.message); + if (!process.stdin.isTTY) process.exitCode = 1; + } + if (process.stdin.isTTY) terminal.prompt(); + } +} + +main().catch((error) => { + console.error(`${error.message}\n빌드: python3 prototypes/live-rules/manage.py build`); + process.exitCode = 1; +}); diff --git a/prototypes/live-rules/core.zig b/prototypes/live-rules/core.zig new file mode 100644 index 0000000..bd709df --- /dev/null +++ b/prototypes/live-rules/core.zig @@ -0,0 +1,504 @@ +//! A bounded, deterministic practice-game experiment, not a general runtime. +//! The same source is tested natively and compiled to freestanding WebAssembly. +//! All persistent storage is static; functions perform no allocation. + +const std = @import("std"); + +const Rules = struct { + mode: u32 = 0, + impulse: u32 = 10, + enabled: u32 = 1, + revision: u32 = 0, +}; + +const State = struct { + tick: u32 = 0, + y: i32 = 0, + vy: i32 = 0, + distance: u32 = 0, + obstacle_x: i32 = 800, + passed: u32 = 0, + hits: u32 = 0, + jumps_used: u32 = 0, + running: u32 = 0, + last_reason: u32 = 0, + last_source: u32 = std.math.maxInt(u32), + total_inputs: u32 = 0, + accepted_jumps: u32 = 0, + obstacle_height: u32 = 48, + rng: u32 = 0x50555245, + obstacle_touched: u32 = 0, + obstacle_passed: u32 = 0, +}; + +const Decision = struct { + tick: u32 = 0, + revision: u32 = 0, + y: i32 = 0, + jumps: u32 = 0, + values: [6]u8 = @splat(0), + valid: bool = false, +}; + +const Event = struct { + tick: u32 = 0, + kind: u32 = 0, + detail: u32 = 0, + revision: u32 = 0, +}; + +const history_capacity: u32 = 16; + +const History = struct { + entries: [history_capacity]Event = @splat(.{}), + next: u32 = 0, + count: u32 = 0, +}; + +const Engine = struct { + rules: Rules = .{}, + state: State = .{}, + decision: Decision = .{}, + history: History = .{}, +}; + +const Op = enum(u8) { request, enabled, grounded, extra, select, all }; +const Node = struct { op: Op, a: u8 = 0, b: u8 = 0, c: u8 = 0 }; + +// Node indices are topologically ordered. These descriptors drive evaluation; +// the display is observing this actual graph, not a separate diagram model. +const graph = [_]Node{ + .{ .op = .request }, + .{ .op = .enabled }, + .{ .op = .grounded }, + .{ .op = .extra }, + .{ .op = .select, .a = 2, .b = 3 }, + .{ .op = .all, .a = 0, .b = 1, .c = 4 }, +}; + +var engine: Engine = .{}; + +fn asPublic(value: u32) i32 { + return @bitCast(value); +} + +fn grounded() bool { + // A jump accepted while paused already consumes the ground jump even though + // position will not be integrated until the next tick. + return engine.state.y == 0 and engine.state.vy == 0; +} + +fn record(kind: u32, detail: u32) void { + const h = &engine.history; + h.entries[h.next] = .{ + .tick = engine.state.tick, + .kind = kind, + .detail = detail, + .revision = engine.rules.revision, + }; + h.next = (h.next + 1) % history_capacity; + h.count = @min(h.count + 1, history_capacity); +} + +fn evaluate(source: u32) void { + var decision: Decision = .{ + .tick = engine.state.tick, + .revision = engine.rules.revision, + .y = engine.state.y, + .jumps = engine.state.jumps_used, + .valid = true, + }; + for (graph, 0..) |node, i| { + decision.values[i] = switch (node.op) { + .request => @intFromBool(source <= 2), + .enabled => @intFromBool(engine.rules.enabled == 1), + .grounded => @intFromBool(grounded()), + .extra => @intFromBool(engine.state.jumps_used < 2), + .select => if (engine.rules.mode == 0) decision.values[node.a] else decision.values[node.b], + .all => decision.values[node.a] & decision.values[node.b] & decision.values[node.c], + }; + } + engine.decision = decision; +} + +export fn reset() void { + engine = .{}; +} + +export fn set_running(flag: u32) void { + engine.state.running = @intFromBool(flag != 0); +} + +export fn apply_rules(mode: u32, impulse: u32, enabled: u32) u32 { + // Validate the complete candidate before mutating any part of the engine. + if (mode > 1 or impulse < 6 or impulse > 16 or enabled > 1) return 1; + const revision = engine.rules.revision +% 1; + engine.rules = .{ .mode = mode, .impulse = impulse, .enabled = enabled, .revision = revision }; + record(3, mode); + return 0; +} + +export fn input(source: u32) u32 { + evaluate(source); + const state = &engine.state; + state.total_inputs +%= 1; + state.last_source = source; + if (engine.decision.values[5] != 0) { + state.last_reason = 1; + state.vy = @as(i32, @intCast(engine.rules.impulse)) * 100; + state.jumps_used +%= 1; + state.accepted_jumps +%= 1; + record(1, source); + return 1; + } + state.last_reason = if (source > 2) 5 else if (engine.rules.enabled == 0) 2 else if (engine.rules.mode == 0) 3 else 4; + record(2, state.last_reason); + return 0; +} + +fn nextRandom() u32 { + engine.state.rng = engine.state.rng *% 1664525 +% 1013904223; + return engine.state.rng; +} + +fn tickOnce() void { + const state = &engine.state; + state.tick +%= 1; + state.distance +%= 4; + + if (!grounded()) { + state.vy -= 55; + state.y += state.vy; + if (state.y <= 0) { + state.y = 0; + state.vy = 0; + state.jumps_used = 0; + record(4, 0); + } + } + + state.obstacle_x -= 4; + const horizontal_overlap = state.obstacle_x < 156 and state.obstacle_x + 32 > 120; + if (horizontal_overlap and state.y < @as(i32, @intCast(state.obstacle_height)) * 100 and state.obstacle_touched == 0) { + state.hits +%= 1; + state.obstacle_touched = 1; + record(5, state.hits); + } + if (state.obstacle_x + 32 <= 120 and state.obstacle_passed == 0) { + state.passed +%= 1; + state.obstacle_passed = 1; + record(6, state.passed); + } + if (state.obstacle_x + 32 < 0) { + state.obstacle_x = 500 + @as(i32, @intCast(nextRandom() % 301)); + state.obstacle_height = 32 + (nextRandom() % 4) * 8; + state.obstacle_touched = 0; + state.obstacle_passed = 0; + } +} + +export fn advance(ticks: u32) u32 { + if (ticks > 600 or engine.state.running == 0) return 0; + for (0..ticks) |_| tickOnce(); + return ticks; +} + +export fn step() void { + tickOnce(); +} + +export fn get(field: u32) i32 { + const s = &engine.state; + return switch (field) { + 0 => asPublic(s.tick), + 1 => s.y, + 2 => s.vy, + 3 => asPublic(s.distance), + 4 => s.obstacle_x, + 5 => asPublic(s.passed), + 6 => asPublic(s.hits), + 7 => asPublic(s.jumps_used), + 8 => asPublic(s.running), + 9 => asPublic(engine.rules.revision), + 10 => asPublic(s.last_reason), + 11 => asPublic(s.last_source), + 12 => asPublic(s.total_inputs), + 13 => asPublic(s.accepted_jumps), + 14 => asPublic(engine.rules.mode), + 15 => asPublic(engine.rules.impulse), + 16 => asPublic(engine.rules.enabled), + 17 => @intFromBool(grounded()), + 18 => asPublic(s.obstacle_height), + else => -1, + }; +} + +export fn node_value(id: u32) i32 { + if (!engine.decision.valid or id >= engine.decision.values.len) return -1; + return engine.decision.values[id]; +} + +export fn decision_meta(field: u32) i32 { + if (!engine.decision.valid) return -1; + return switch (field) { + 0 => asPublic(engine.decision.tick), + 1 => asPublic(engine.decision.revision), + 2 => engine.decision.y, + 3 => asPublic(engine.decision.jumps), + else => -1, + }; +} + +export fn history_count() u32 { + return engine.history.count; +} + +export fn history_get(newest_index: u32, field: u32) i32 { + const h = &engine.history; + if (newest_index >= h.count) return -1; + const index = (h.next + history_capacity - 1 - newest_index) % history_capacity; + const event = &h.entries[index]; + return switch (field) { + 0 => asPublic(event.tick), + 1 => asPublic(event.kind), + 2 => asPublic(event.detail), + 3 => asPublic(event.revision), + else => -1, + }; +} + +export fn bytes(kind: u32) u32 { + return switch (kind) { + 0 => @sizeOf(Rules), + 1 => @sizeOf(State), + 2 => @sizeOf(Decision), + 3 => @sizeOf(History), + 4 => @sizeOf(Engine), + 5 => @sizeOf(@TypeOf(graph)), + 6 => @sizeOf(Engine) + @sizeOf(@TypeOf(graph)), + else => 0, + }; +} + +test "edits preserve complete simulation state and latest observation" { + reset(); + set_running(1); + try std.testing.expectEqual(@as(u32, 1), input(0)); + try std.testing.expectEqual(@as(u32, 8), advance(8)); + const before_state = engine.state; + const before_decision = engine.decision; + try std.testing.expectEqual(@as(u32, 0), apply_rules(1, 16, 0)); + try std.testing.expectEqualDeep(before_state, engine.state); + try std.testing.expectEqualDeep(before_decision, engine.decision); + try std.testing.expectEqual(@as(i32, 1), get(9)); + try std.testing.expectEqual(@as(i32, 3), history_get(0, 1)); + try std.testing.expectEqual(@as(i32, 1), history_get(0, 3)); +} + +test "invalid edits and oversized advance are atomic" { + reset(); + set_running(1); + _ = input(2); + step(); + const before = engine; + const invalid = [_][3]u32{ + .{ 2, 10, 1 }, .{ 0, 5, 1 }, .{ 0, 17, 1 }, + .{ 0, 10, 2 }, .{ std.math.maxInt(u32), 10, 1 }, + }; + for (invalid) |candidate| { + try std.testing.expectEqual(@as(u32, 1), apply_rules(candidate[0], candidate[1], candidate[2])); + try std.testing.expectEqualDeep(before, engine); + } + try std.testing.expectEqual(@as(u32, 0), advance(601)); + try std.testing.expectEqual(@as(u32, 0), advance(std.math.maxInt(u32))); + try std.testing.expectEqualDeep(before, engine); +} + +test "pause, step, and ground-only jump handle input before integration" { + reset(); + try std.testing.expectEqual(@as(i32, -1), node_value(0)); + try std.testing.expectEqual(@as(i32, -1), decision_meta(0)); + try std.testing.expectEqual(@as(u32, 0), advance(600)); + try std.testing.expectEqual(@as(u32, 1), input(0)); + try std.testing.expectEqual(@as(i32, 0), get(1)); + try std.testing.expectEqual(@as(i32, 1000), get(2)); + try std.testing.expectEqual(@as(i32, 0), get(17)); + try std.testing.expectEqual(@as(u32, 0), input(0)); + try std.testing.expectEqual(@as(i32, 3), get(10)); + step(); + try std.testing.expectEqual(@as(i32, 1), get(0)); + try std.testing.expectEqual(@as(i32, 945), get(1)); + try std.testing.expectEqual(@as(i32, 0), get(8)); + for (0..100) |_| step(); + try std.testing.expectEqual(@as(i32, 1), get(17)); + try std.testing.expectEqual(@as(i32, 0), get(7)); + try std.testing.expectEqual(@as(u32, 1), input(0)); +} + +test "double jump, disable, mode changes, and impulse apply at input boundaries" { + reset(); + _ = input(0); + for (0..5) |_| step(); + const old_velocity = get(2); + try std.testing.expectEqual(@as(u32, 0), apply_rules(1, 16, 1)); + try std.testing.expectEqual(old_velocity, get(2)); + try std.testing.expectEqual(@as(u32, 1), input(1)); + try std.testing.expectEqual(@as(i32, 1600), get(2)); + try std.testing.expectEqual(@as(i32, 2), get(7)); + try std.testing.expectEqual(@as(u32, 0), input(2)); + try std.testing.expectEqual(@as(i32, 4), get(10)); + _ = apply_rules(0, 6, 1); + try std.testing.expectEqual(@as(u32, 0), input(0)); + try std.testing.expectEqual(@as(i32, 3), get(10)); + for (0..150) |_| step(); + _ = apply_rules(1, 6, 0); + try std.testing.expectEqual(@as(u32, 0), input(0)); + try std.testing.expectEqual(@as(i32, 2), get(10)); + _ = apply_rules(1, 6, 1); + try std.testing.expectEqual(@as(u32, 1), input(0)); + try std.testing.expectEqual(@as(i32, 600), get(2)); + try std.testing.expectEqual(@as(u32, 1), input(0)); + try std.testing.expectEqual(@as(u32, 0), input(0)); +} + +test "all input sources share transition behavior and retain provenance" { + var reference: State = undefined; + for (0..3) |source| { + reset(); + try std.testing.expectEqual(@as(u32, 1), input(@intCast(source))); + for (0..7) |_| step(); + try std.testing.expectEqual(@as(i32, @intCast(source)), get(11)); + try std.testing.expectEqual(@as(i32, @intCast(source)), history_get(0, 2)); + var normalized = engine.state; + normalized.last_source = 0; + if (source == 0) reference = normalized else try std.testing.expectEqualDeep(reference, normalized); + } + reset(); + try std.testing.expectEqual(@as(u32, 0), input(std.math.maxInt(u32))); + try std.testing.expectEqual(@as(i32, 5), get(10)); + try std.testing.expectEqual(@as(i32, 0), get(2)); + try std.testing.expectEqual(@as(i32, 0), node_value(0)); + try std.testing.expectEqual(@as(i32, 5), history_get(0, 2)); +} + +test "fixed history overwrites oldest entry and handles invalid access" { + reset(); + for (0..40) |i| { + _ = apply_rules(@intCast(i % 2), 10, 1); + step(); + } + try std.testing.expectEqual(@as(u32, 16), history_count()); + for (0..16) |i| { + try std.testing.expectEqual(@as(i32, @intCast(40 - i)), history_get(@intCast(i), 3)); + try std.testing.expectEqual(@as(i32, @intCast(39 - i)), history_get(@intCast(i), 0)); + } + try std.testing.expectEqual(@as(i32, -1), history_get(16, 0)); + try std.testing.expectEqual(@as(i32, -1), history_get(0, 4)); + try std.testing.expectEqual(@as(i32, -1), history_get(std.math.maxInt(u32), 0)); +} + +test "decision is pre-input state and remains at its original revision" { + reset(); + _ = apply_rules(1, 10, 1); + step(); + _ = input(2); + try std.testing.expectEqual(@as(i32, 1), decision_meta(0)); + try std.testing.expectEqual(@as(i32, 1), decision_meta(1)); + try std.testing.expectEqual(@as(i32, 0), decision_meta(2)); + try std.testing.expectEqual(@as(i32, 0), decision_meta(3)); + try std.testing.expectEqual(@as(i32, 1), get(7)); + try std.testing.expectEqual(@as(i32, 1), node_value(2)); + try std.testing.expectEqual(@as(i32, 1), node_value(5)); + const snapshot = engine.decision; + _ = apply_rules(0, 16, 0); + step(); + try std.testing.expectEqualDeep(snapshot, engine.decision); + try std.testing.expectEqual(@as(i32, -1), node_value(6)); + try std.testing.expectEqual(@as(i32, -1), decision_meta(4)); +} + +test "graph truth table agrees with a direct permission predicate" { + for (0..2) |mode| { + for (0..2) |enabled| { + for (0..2) |airborne| { + for (0..2) |pending_jump| { + for (0..3) |jumps| { + for (0..4) |source| { + reset(); + engine.rules.mode = @intCast(mode); + engine.rules.enabled = @intCast(enabled); + engine.state.y = if (airborne == 1) 500 else 0; + engine.state.vy = if (pending_jump == 1) 1000 else 0; + engine.state.jumps_used = @intCast(jumps); + const on_ground = airborne == 0 and pending_jump == 0; + const permission = if (mode == 0) on_ground else jumps < 2; + const expected = source < 3 and enabled == 1 and permission; + const accepted = input(@intCast(source)); + try std.testing.expectEqual(@as(u32, @intFromBool(expected)), accepted); + try std.testing.expectEqual(@as(i32, @intFromBool(on_ground)), node_value(2)); + try std.testing.expectEqual(@as(i32, @intFromBool(jumps < 2)), node_value(3)); + try std.testing.expectEqual(@as(i32, @intFromBool(permission)), node_value(4)); + try std.testing.expectEqual(@as(i32, @intFromBool(expected)), node_value(5)); + } + } + } + } + } + } +} + +fn replay() Engine { + reset(); + set_running(1); + for (0..3000) |i| { + if (i % 127 == 0) _ = apply_rules(@intCast((i / 127) % 2), @intCast(6 + i % 11), @intFromBool(i % 254 != 0)); + if (i % 29 == 0) _ = input(@intCast((i / 29) % 3)); + _ = advance(1); + } + return engine; +} + +test "replay includes respawns and produces identical complete engine state" { + const first = replay(); + const second = replay(); + try std.testing.expectEqualDeep(first, second); + try std.testing.expect(first.state.passed > 5); + try std.testing.expect(first.state.hits > 0); + try std.testing.expectEqual(@as(u32, 3000), first.state.tick); + try std.testing.expectEqual(@as(u32, 12000), first.state.distance); +} + +test "collisions do not reset progress and each obstacle is counted once" { + reset(); + set_running(1); + _ = advance(180); + try std.testing.expectEqual(@as(i32, 1), get(6)); + try std.testing.expectEqual(@as(i32, 1), get(5)); + try std.testing.expectEqual(@as(i32, 180), get(0)); + try std.testing.expectEqual(@as(i32, 720), get(3)); + _ = advance(10); + try std.testing.expectEqual(@as(i32, 1), get(6)); + try std.testing.expectEqual(@as(i32, 1), get(5)); + try std.testing.expectEqual(@as(i32, 1), get(8)); +} + +test "counters wrap explicitly and memory accounting has no overlapping total" { + reset(); + engine.state.tick = std.math.maxInt(u32); + engine.state.distance = std.math.maxInt(u32) - 1; + engine.state.total_inputs = std.math.maxInt(u32); + engine.state.accepted_jumps = std.math.maxInt(u32); + engine.rules.revision = std.math.maxInt(u32); + _ = input(0); + _ = apply_rules(1, 10, 1); + step(); + try std.testing.expectEqual(@as(i32, 0), get(0)); + try std.testing.expectEqual(@as(i32, 2), get(3)); + try std.testing.expectEqual(@as(i32, 0), get(9)); + try std.testing.expectEqual(@as(i32, 0), get(12)); + try std.testing.expectEqual(@as(i32, 0), get(13)); + try std.testing.expectEqual(bytes(4) + bytes(5), bytes(6)); + try std.testing.expect(bytes(4) >= bytes(0) + bytes(1) + bytes(2) + bytes(3)); + try std.testing.expectEqual(@as(u32, 0), bytes(7)); + try std.testing.expectEqual(@as(i32, -1), get(19)); +} diff --git a/prototypes/live-rules/manage.py b/prototypes/live-rules/manage.py new file mode 100644 index 0000000..fbe047c --- /dev/null +++ b/prototypes/live-rules/manage.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Build/test the shared Zig core or serve the local experiment. No Python dependencies.""" +import argparse +from datetime import datetime, timezone +from functools import partial +import hashlib +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import shutil +import subprocess + +ROOT = Path(__file__).resolve().parent +VERSION = "0.15.2" + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def build(zig): + compiler = shutil.which(zig) + if compiler is None: + raise SystemExit("Zig 0.15.2가 필요합니다. ziglang.org/download 또는 --zig /path/to/zig를 사용하세요.") + version = subprocess.check_output([compiler, "version"], text=True).strip() + if version != VERSION: + raise SystemExit(f"이 실험은 Zig {VERSION}로 고정했습니다. 현재: {version}") + build_dir = ROOT / "build" + build_dir.mkdir(exist_ok=True) + cache = ["--cache-dir", str(build_dir / "cache"), + "--global-cache-dir", str(build_dir / "global-cache")] + source = ROOT / "core.zig" + native = [compiler, "test", str(source), *cache] + print("Checking native core…", flush=True) + checked = subprocess.run(native, capture_output=True, text=True) + print(checked.stdout + checked.stderr, end="", flush=True) + checked.check_returncode() + wasm = ROOT / "web" / "core.wasm" + wasm.parent.mkdir(exist_ok=True) + flags = ["-target", "wasm32-freestanding", "-O", "ReleaseSafe", "-fno-entry", + "-rdynamic", "--export-memory", "--stack", "16384", + "--initial-memory=65536", "--max-memory=65536"] + command = [compiler, "build-exe", str(source), *flags, f"-femit-bin={wasm}", *cache] + subprocess.run(command, check=True) + (build_dir / "build.json").write_text(json.dumps({ + "built_utc": datetime.now(timezone.utc).isoformat(), + "zig_version": version, "flags": flags, + "source_sha256": digest(source), "wasm_sha256": digest(wasm), + "wasm_file_bytes": wasm.stat().st_size, + "native_test_output": checked.stdout + checked.stderr, + }, ensure_ascii=False, indent=2) + "\n") + print(f"Built {wasm} ({wasm.stat().st_size:,} bytes)") + + +class Handler(SimpleHTTPRequestHandler): + extensions_map = {**SimpleHTTPRequestHandler.extensions_map, ".wasm": "application/wasm"} + + def end_headers(self): + self.send_header("Cache-Control", "no-store") + super().end_headers() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="action", required=True) + compile_cmd = sub.add_parser("build", help="native tests + WebAssembly build") + compile_cmd.add_argument("--zig", default="zig") + serve = sub.add_parser("serve", help="serve web/ on loopback only") + serve.add_argument("--port", type=int, default=8765) + args = parser.parse_args() + if args.action == "build": + build(args.zig) + return + if not 1 <= args.port <= 65535: + parser.error("port must be between 1 and 65535") + if not (ROOT / "web" / "core.wasm").is_file(): + parser.error("먼저 manage.py build를 실행하세요.") + handler = partial(Handler, directory=str(ROOT / "web")) + with ThreadingHTTPServer(("127.0.0.1", args.port), handler) as server: + print(f"Pure: http://127.0.0.1:{args.port} (Ctrl+C to stop)", flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/prototypes/live-rules/measure.mjs b/prototypes/live-rules/measure.mjs new file mode 100644 index 0000000..2f12815 --- /dev/null +++ b/prototypes/live-rules/measure.mjs @@ -0,0 +1,48 @@ +// Reproducible storage ledger + exact state transitions, not a speed benchmark. +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; +import { arch, platform, release } from 'node:os'; +import { loadCore, snapshot, storage, parseCommand, execute } from './runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +const destination = process.argv[2]; +if (!destination || process.argv.length !== 3) throw new Error('Usage: node measure.mjs NEW_OUTPUT_DIRECTORY'); +const core = await loadCore(); +const checked = spawnSync(process.execPath, [resolve(root, 'adapter.test.mjs')], { encoding: 'utf8' }); +if (checked.error || checked.status !== 0) throw new Error(checked.error?.message || checked.stdout + checked.stderr); +process.stdout.write(checked.stdout + checked.stderr); + +const trace = ['jump', 'step 8', 'jump', 'rules double 12 on', 'jump', 'step 60']; +const steps = trace.map((command) => ({ command, ...execute(core, parseCommand(command)) })); +const baseline = storage(core); +const imports = WebAssembly.Module.imports(await WebAssembly.compile(await readFile(resolve(root, 'web/core.wasm')))); +const hashes = {}; +for (const name of ['core.zig', 'web/core.wasm', 'runtime.mjs', 'cli.mjs', 'adapter.test.mjs', 'measure.mjs', 'manage.py', 'qa-browser.js', 'web/app.js', 'web/index.html', 'web/style.css']) { + const data = await readFile(resolve(root, name)); + hashes[name] = { bytes: data.length, sha256: createHash('sha256').update(data).digest('hex') }; +} +let build = null; +try { + build = JSON.parse(await readFile(resolve(root, 'build/build.json'), 'utf8')); + if (build.source_sha256 !== hashes['core.zig'].sha256 || build.wasm_sha256 !== hashes['web/core.wasm'].sha256) { + throw new Error('Build record and current source/binary differ. Rebuild first.'); + } +} catch (error) { + if (error.code !== 'ENOENT') throw error; +} + +const result = { + recordedUtc: new Date().toISOString(), + host: { platform: platform(), release: release(), arch: arch(), node: process.version }, + scope: 'Declared Zig type layouts and actual Wasm memory capacity; excludes JS/DOM/renderer, code pages and RSS. No latency or throughput measurement.', + imports, storage: baseline, trace: steps, + finalState: snapshot(core), build, files: hashes, + adapterTests: { command: 'node prototypes/live-rules/adapter.test.mjs', exitCode: checked.status, + output: checked.stdout + checked.stderr }, +}; +await mkdir(destination, { recursive: false }); +await writeFile(resolve(destination, 'storage-and-replay.json'), JSON.stringify(result, null, 2) + '\n'); +console.log(`Saved ${resolve(destination, 'storage-and-replay.json')}`); diff --git a/prototypes/live-rules/qa-browser.js b/prototypes/live-rules/qa-browser.js new file mode 100644 index 0000000..ee11c3a --- /dev/null +++ b/prototypes/live-rules/qa-browser.js @@ -0,0 +1,131 @@ +// Pass this expression to Playwright CLI's run-code after opening the local demo. +// Interactions use real UI controls; the read-only snapshot only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = (error) => errors.push(error.message); + const check = (condition, label) => { + if (!condition) throw new Error(label); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureDemo.snapshot()); + const button = (name) => page.getByRole('button', { name, exact: true }); + const step = async (count) => { for (let i = 0; i < count; i++) await button('한 프레임').click(); }; + page.on('pageerror', onError); + try { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureDemo?.snapshot().ready); + const initial = await state(); + check(initial.running === 0 && initial.tick === 0 && initial.revision === 0, 'initial state is paused'); + + await page.locator('#stage').focus(); + await page.keyboard.press('Space'); + let s = await state(); + check(s.accepted === 1 && s.inputs === 1 && s.source === 0 && s.y === 0, 'Space goes through keyboard input once while paused'); + await step(8); + await button('점프 Space').click(); + const before = await state(); + check(before.tick === 8 && before.y === 6020 && before.vy === 560 && before.reason === 3, 'ground rule rejects a second airborne jump'); + await page.getByRole('radio', { name: '두 번 점프 공중에서 한 번 더 뛸 수 있어요.', exact: true }).check(); + const draft = await state(); + check(draft.mode === 0 && draft.revision === 0 && draft.draftDirty, 'draft changes do not execute before Apply'); + await button('규칙 적용').click(); + const applied = await state(); + check(['tick', 'y', 'vy', 'distance', 'obstacleX', 'jumps'].every((key) => applied[key] === before[key]), 'applying rules preserves exact simulation progress'); + check(applied.mode === 1 && applied.revision === 1 && applied.decisionRevision === 0 && await page.locator('#stale-decision').isVisible(), 'old decision retains and displays its revision'); + await button('점프 Space').click(); + const second = await state(); + check(second.reason === 1 && second.jumps === 2 && second.source === 1 && second.decisionRevision === 1, 'pointer input uses new rule and accepts second jump'); + await button('점프 Space').click(); + check((await state()).reason === 4, 'third jump is rejected'); + + await page.getByRole('checkbox').uncheck(); + await button('규칙 적용').click(); + await button('점프 Space').click(); + check((await state()).reason === 2, 'disabled rule explains rejection'); + await button('처음부터').click(); + s = await state(); + check(s.tick === 0 && s.mode === 0 && s.impulse === 10 && s.enabled === 1 && s.inputs === 0 && s.running === 0, 'explicit reset restores rules and state'); + + await page.getByRole('slider', { name: '점프 세기' }).focus(); + await page.keyboard.press('Space'); + await page.getByRole('checkbox').focus(); + await page.keyboard.press('Space'); + check((await state()).inputs === 0, 'Space in form controls never dispatches a jump'); + await button('처음부터').click(); + await button('점프 Space').focus(); + await page.keyboard.press('Space'); + check((await state()).inputs === 1, 'Space on the jump button does not double dispatch'); + await button('처음부터').click(); + await page.locator('#stage').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).inputs === 1, 'keyboard repeat does not add a second request'); + + await button('처음부터').click(); + await button('실행').click(); + await page.waitForFunction(() => window.pureDemo.snapshot().tick >= 6); + await button('일시정지').click(); + const paused = await state(); + await page.waitForTimeout(120); + check(paused.tick >= 6 && paused.tick === (await state()).tick, 'running advances and pause freezes simulation'); + + await button('처음부터').click(); + await page.getByRole('radio', { name: '두 번 점프 공중에서 한 번 더 뛸 수 있어요.', exact: true }).check(); + await page.getByRole('slider', { name: '점프 세기' }).focus(); + await page.keyboard.press('End'); + await button('규칙 적용').click(); + await button('점프 Space').click(); + await step(28); + await button('점프 Space').click(); + await step(29); + const high = await state(); + const visible = await page.evaluate(() => { + const d = document.getElementById('dino').getBoundingClientRect(); + const w = document.getElementById('world').getBoundingClientRect(); + return d.top >= w.top && d.bottom <= w.bottom && d.left >= w.left && d.right <= w.right; + }); + check(high.y > 40000 && visible, 'maximum-height double jump remains inside the scene'); + + for (let i = 0; i < 25; i++) await button('점프 Space').click(); + check(await page.locator('#history-list li').count() === 8, 'visible history is bounded to eight rows'); + check((await state()).wasmMemoryBytes === 65536, 'browser instance retains fixed 64 KiB linear memory'); + await page.locator('.research-panel summary').click(); + check((await page.locator('#bytes-total').textContent()).includes('396'), 'research panel shows logical layout separately'); + + await page.setViewportSize({ width: 390, height: 844 }); + await page.locator('.research-panel summary').click(); + await button('처음부터').click(); + const width = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(width.content <= width.viewport, '390px viewport has no horizontal overflow'); + check(await page.locator('#dino').evaluate((el) => el.getBoundingClientRect().height) >= 28, 'narrow view keeps the dinosaur large enough to inspect'); + await button('점프 Space').click(); + await step(1); + check((await state()).y === 945, 'narrow viewport controls remain usable'); + await page.screenshot({ path: 'output/playwright/pure-live-mobile.png', fullPage: true }); + + await page.route('**/core.wasm', (route) => route.abort()); + await page.reload(); + await page.locator('#load-error').waitFor({ state: 'visible' }); + check(await button('실행').isDisabled(), 'missing Wasm has an actionable error and disabled controls'); + await page.unroute('**/core.wasm'); + await page.reload(); + await page.waitForFunction(() => window.pureDemo?.snapshot().ready); + await page.setViewportSize({ width: 1440, height: 1000 }); + await button('점프 Space').click(); + await step(8); + await button('점프 Space').click(); + await page.getByRole('radio', { name: '두 번 점프 공중에서 한 번 더 뛸 수 있어요.', exact: true }).check(); + await button('규칙 적용').click(); + await button('점프 Space').click(); + await page.screenshot({ path: 'output/playwright/pure-live-desktop.png', fullPage: true }); + check(errors.length === 0, 'no uncaught page errors'); + return { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, before, applied, second, high, mobile: width, errors, + screenshots: ['output/playwright/pure-live-desktop.png', 'output/playwright/pure-live-mobile.png'] }; + } finally { + page.off('pageerror', onError); + } +} diff --git a/prototypes/live-rules/results/2026-09-22-linux-aarch64/browser-checks.json b/prototypes/live-rules/results/2026-09-22-linux-aarch64/browser-checks.json new file mode 100644 index 0000000..efb9ec7 --- /dev/null +++ b/prototypes/live-rules/results/2026-09-22-linux-aarch64/browser-checks.json @@ -0,0 +1,215 @@ +{ + "recordedUtc": "2026-09-22T10:20:31.347Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial state is paused", + "Space goes through keyboard input once while paused", + "ground rule rejects a second airborne jump", + "draft changes do not execute before Apply", + "applying rules preserves exact simulation progress", + "old decision retains and displays its revision", + "pointer input uses new rule and accepts second jump", + "third jump is rejected", + "disabled rule explains rejection", + "explicit reset restores rules and state", + "Space in form controls never dispatches a jump", + "Space on the jump button does not double dispatch", + "keyboard repeat does not add a second request", + "running advances and pause freezes simulation", + "maximum-height double jump remains inside the scene", + "visible history is bounded to eight rows", + "browser instance retains fixed 64 KiB linear memory", + "research panel shows logical layout separately", + "390px viewport has no horizontal overflow", + "narrow view keeps the dinosaur large enough to inspect", + "narrow viewport controls remain usable", + "missing Wasm has an actionable error and disabled controls", + "no uncaught page errors" + ], + "before": { + "ready": true, + "tick": 8, + "y": 6020, + "vy": 560, + "distance": 32, + "obstacleX": 768, + "passed": 0, + "hits": 0, + "jumps": 1, + "running": 0, + "revision": 0, + "reason": 3, + "source": 1, + "inputs": 2, + "accepted": 1, + "mode": 0, + "impulse": 10, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48, + "draftDirty": false, + "droppedTicks": 0, + "renderCount": 11, + "wasmMemoryBytes": 65536, + "logicalBytes": 396, + "decisionTick": 8, + "decisionRevision": 0, + "decisionNodes": [ + 1, + 1, + 0, + 1, + 0, + 0 + ] + }, + "applied": { + "ready": true, + "tick": 8, + "y": 6020, + "vy": 560, + "distance": 32, + "obstacleX": 768, + "passed": 0, + "hits": 0, + "jumps": 1, + "running": 0, + "revision": 1, + "reason": 3, + "source": 1, + "inputs": 2, + "accepted": 1, + "mode": 1, + "impulse": 10, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48, + "draftDirty": false, + "droppedTicks": 0, + "renderCount": 12, + "wasmMemoryBytes": 65536, + "logicalBytes": 396, + "decisionTick": 8, + "decisionRevision": 0, + "decisionNodes": [ + 1, + 1, + 0, + 1, + 0, + 0 + ] + }, + "second": { + "ready": true, + "tick": 8, + "y": 6020, + "vy": 1000, + "distance": 32, + "obstacleX": 768, + "passed": 0, + "hits": 0, + "jumps": 2, + "running": 0, + "revision": 1, + "reason": 1, + "source": 1, + "inputs": 3, + "accepted": 2, + "mode": 1, + "impulse": 10, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48, + "draftDirty": false, + "droppedTicks": 0, + "renderCount": 13, + "wasmMemoryBytes": 65536, + "logicalBytes": 396, + "decisionTick": 8, + "decisionRevision": 1, + "decisionNodes": [ + 1, + 1, + 0, + 1, + 1, + 1 + ] + }, + "high": { + "ready": true, + "tick": 57, + "y": 44945, + "vy": 5, + "distance": 228, + "obstacleX": 572, + "passed": 0, + "hits": 0, + "jumps": 2, + "running": 0, + "revision": 1, + "reason": 1, + "source": 1, + "inputs": 2, + "accepted": 2, + "mode": 1, + "impulse": 16, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48, + "draftDirty": false, + "droppedTicks": 0, + "renderCount": 92, + "wasmMemoryBytes": 65536, + "logicalBytes": 396, + "decisionTick": 28, + "decisionRevision": 1, + "decisionNodes": [ + 1, + 1, + 0, + 1, + 1, + 1 + ] + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "errors": [], + "screenshots": [ + { + "file": "desktop.png", + "sha256": "ae02e473b472816dccf147e1d7b0a45135ffbc10fbef97002a7f0697303a7fbb" + }, + { + "file": "mobile.png", + "sha256": "a3295e68fdaff2064d9076c0e136a80062abd0dd58ef770aa9b7c7dff356926d" + } + ], + "playwrightCliVersion": "0.1.21", + "files": { + "qa-browser.js": { + "sha256": "50d4293d41d5aa63b404c3e22fdc1349008765365fac8cdafe01dfdf858893da", + "bytes": 7554 + }, + "web/app.js": { + "sha256": "b3dcd10fa4e4ab56dff200e9238c640e6dbea5fa7b624328d7d421ee8744e778", + "bytes": 14358 + }, + "web/index.html": { + "sha256": "28630577d3faa81b61fec95723d421e1cac9a576452d909342cfd054d3dea385", + "bytes": 13396 + }, + "web/style.css": { + "sha256": "b399be728d8f2bfb1964903e11e69a9bccbc83c9d90ecfd6783110c7a0702923", + "bytes": 16606 + }, + "web/core.wasm": { + "sha256": "ea0789dcbb0cc2d6a17e049f4568374d242b44dafe7f996673538f3bd6dafed5", + "bytes": 32544 + } + } +} diff --git a/prototypes/live-rules/results/2026-09-22-linux-aarch64/desktop.png b/prototypes/live-rules/results/2026-09-22-linux-aarch64/desktop.png new file mode 100644 index 0000000..5318b55 Binary files /dev/null and b/prototypes/live-rules/results/2026-09-22-linux-aarch64/desktop.png differ diff --git a/prototypes/live-rules/results/2026-09-22-linux-aarch64/mobile.png b/prototypes/live-rules/results/2026-09-22-linux-aarch64/mobile.png new file mode 100644 index 0000000..2f11c77 Binary files /dev/null and b/prototypes/live-rules/results/2026-09-22-linux-aarch64/mobile.png differ diff --git a/prototypes/live-rules/results/2026-09-22-linux-aarch64/storage-and-replay.json b/prototypes/live-rules/results/2026-09-22-linux-aarch64/storage-and-replay.json new file mode 100644 index 0000000..aab0d2a --- /dev/null +++ b/prototypes/live-rules/results/2026-09-22-linux-aarch64/storage-and-replay.json @@ -0,0 +1,503 @@ +{ + "recordedUtc": "2026-09-22T10:21:30.811Z", + "host": { + "platform": "linux", + "release": "6.18.39+rpt-rpi-2712", + "arch": "arm64", + "node": "v24.20.0" + }, + "scope": "Declared Zig type layouts and actual Wasm memory capacity; excludes JS/DOM/renderer, code pages and RSS. No latency or throughput measurement.", + "imports": [], + "storage": { + "layoutBytes": { + "rules": 16, + "state": 68, + "decision": 24, + "history": 264, + "engine": 372, + "graph": 24, + "logicalTotal": 396 + }, + "wasmLinearMemoryBytes": 65536, + "note": "Type-layout sizes; constant graph may be compiled into code. Not browser heap or RSS." + }, + "trace": [ + { + "command": "jump", + "state": { + "tick": 0, + "y": 0, + "vy": 1000, + "distance": 0, + "obstacleX": 800, + "passed": 0, + "hits": 0, + "jumpsUsed": 1, + "running": 0, + "revision": 0, + "lastReason": 1, + "lastSource": 2, + "totalInputs": 1, + "acceptedJumps": 1, + "mode": 0, + "impulse": 10, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48 + }, + "decision": { + "tick": 0, + "revision": 0, + "y": 0, + "jumps": 0, + "nodes": [ + 1, + 1, + 1, + 1, + 1, + 1 + ] + }, + "history": [ + { + "tick": 0, + "kind": 1, + "detail": 2, + "revision": 0 + } + ] + }, + { + "command": "step 8", + "state": { + "tick": 8, + "y": 6020, + "vy": 560, + "distance": 32, + "obstacleX": 768, + "passed": 0, + "hits": 0, + "jumpsUsed": 1, + "running": 0, + "revision": 0, + "lastReason": 1, + "lastSource": 2, + "totalInputs": 1, + "acceptedJumps": 1, + "mode": 0, + "impulse": 10, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48 + }, + "decision": { + "tick": 0, + "revision": 0, + "y": 0, + "jumps": 0, + "nodes": [ + 1, + 1, + 1, + 1, + 1, + 1 + ] + }, + "history": [ + { + "tick": 0, + "kind": 1, + "detail": 2, + "revision": 0 + } + ] + }, + { + "command": "jump", + "state": { + "tick": 8, + "y": 6020, + "vy": 560, + "distance": 32, + "obstacleX": 768, + "passed": 0, + "hits": 0, + "jumpsUsed": 1, + "running": 0, + "revision": 0, + "lastReason": 3, + "lastSource": 2, + "totalInputs": 2, + "acceptedJumps": 1, + "mode": 0, + "impulse": 10, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48 + }, + "decision": { + "tick": 8, + "revision": 0, + "y": 6020, + "jumps": 1, + "nodes": [ + 1, + 1, + 0, + 1, + 0, + 0 + ] + }, + "history": [ + { + "tick": 8, + "kind": 2, + "detail": 3, + "revision": 0 + }, + { + "tick": 0, + "kind": 1, + "detail": 2, + "revision": 0 + } + ] + }, + { + "command": "rules double 12 on", + "state": { + "tick": 8, + "y": 6020, + "vy": 560, + "distance": 32, + "obstacleX": 768, + "passed": 0, + "hits": 0, + "jumpsUsed": 1, + "running": 0, + "revision": 1, + "lastReason": 3, + "lastSource": 2, + "totalInputs": 2, + "acceptedJumps": 1, + "mode": 1, + "impulse": 12, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48 + }, + "decision": { + "tick": 8, + "revision": 0, + "y": 6020, + "jumps": 1, + "nodes": [ + 1, + 1, + 0, + 1, + 0, + 0 + ] + }, + "history": [ + { + "tick": 8, + "kind": 3, + "detail": 1, + "revision": 1 + }, + { + "tick": 8, + "kind": 2, + "detail": 3, + "revision": 0 + }, + { + "tick": 0, + "kind": 1, + "detail": 2, + "revision": 0 + } + ] + }, + { + "command": "jump", + "state": { + "tick": 8, + "y": 6020, + "vy": 1200, + "distance": 32, + "obstacleX": 768, + "passed": 0, + "hits": 0, + "jumpsUsed": 2, + "running": 0, + "revision": 1, + "lastReason": 1, + "lastSource": 2, + "totalInputs": 3, + "acceptedJumps": 2, + "mode": 1, + "impulse": 12, + "enabled": 1, + "grounded": 0, + "obstacleHeight": 48 + }, + "decision": { + "tick": 8, + "revision": 1, + "y": 6020, + "jumps": 1, + "nodes": [ + 1, + 1, + 0, + 1, + 1, + 1 + ] + }, + "history": [ + { + "tick": 8, + "kind": 1, + "detail": 2, + "revision": 1 + }, + { + "tick": 8, + "kind": 3, + "detail": 1, + "revision": 1 + }, + { + "tick": 8, + "kind": 2, + "detail": 3, + "revision": 0 + }, + { + "tick": 0, + "kind": 1, + "detail": 2, + "revision": 0 + } + ] + }, + { + "command": "step 60", + "state": { + "tick": 68, + "y": 0, + "vy": 0, + "distance": 272, + "obstacleX": 528, + "passed": 0, + "hits": 0, + "jumpsUsed": 0, + "running": 0, + "revision": 1, + "lastReason": 1, + "lastSource": 2, + "totalInputs": 3, + "acceptedJumps": 2, + "mode": 1, + "impulse": 12, + "enabled": 1, + "grounded": 1, + "obstacleHeight": 48 + }, + "decision": { + "tick": 8, + "revision": 1, + "y": 6020, + "jumps": 1, + "nodes": [ + 1, + 1, + 0, + 1, + 1, + 1 + ] + }, + "history": [ + { + "tick": 56, + "kind": 4, + "detail": 0, + "revision": 1 + }, + { + "tick": 8, + "kind": 1, + "detail": 2, + "revision": 1 + }, + { + "tick": 8, + "kind": 3, + "detail": 1, + "revision": 1 + }, + { + "tick": 8, + "kind": 2, + "detail": 3, + "revision": 0 + }, + { + "tick": 0, + "kind": 1, + "detail": 2, + "revision": 0 + } + ] + } + ], + "finalState": { + "state": { + "tick": 68, + "y": 0, + "vy": 0, + "distance": 272, + "obstacleX": 528, + "passed": 0, + "hits": 0, + "jumpsUsed": 0, + "running": 0, + "revision": 1, + "lastReason": 1, + "lastSource": 2, + "totalInputs": 3, + "acceptedJumps": 2, + "mode": 1, + "impulse": 12, + "enabled": 1, + "grounded": 1, + "obstacleHeight": 48 + }, + "decision": { + "tick": 8, + "revision": 1, + "y": 6020, + "jumps": 1, + "nodes": [ + 1, + 1, + 0, + 1, + 1, + 1 + ] + }, + "history": [ + { + "tick": 56, + "kind": 4, + "detail": 0, + "revision": 1 + }, + { + "tick": 8, + "kind": 1, + "detail": 2, + "revision": 1 + }, + { + "tick": 8, + "kind": 3, + "detail": 1, + "revision": 1 + }, + { + "tick": 8, + "kind": 2, + "detail": 3, + "revision": 0 + }, + { + "tick": 0, + "kind": 1, + "detail": 2, + "revision": 0 + } + ] + }, + "build": { + "built_utc": "2026-09-22T10:01:20.063882+00:00", + "zig_version": "0.15.2", + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "source_sha256": "d9fdba86e700605b053586d95ccdfb47cbe87070717182fb1c2ab7b45221ad2b", + "wasm_sha256": "ea0789dcbb0cc2d6a17e049f4568374d242b44dafe7f996673538f3bd6dafed5", + "wasm_file_bytes": 32544, + "native_test_output": "1/11 core.test.edits preserve complete simulation state and latest observation...OK\n2/11 core.test.invalid edits and oversized advance are atomic...OK\n3/11 core.test.pause, step, and ground-only jump handle input before integration...OK\n4/11 core.test.double jump, disable, mode changes, and impulse apply at input boundaries...OK\n5/11 core.test.all input sources share transition behavior and retain provenance...OK\n6/11 core.test.fixed history overwrites oldest entry and handles invalid access...OK\n7/11 core.test.decision is pre-input state and remains at its original revision...OK\n8/11 core.test.graph truth table agrees with a direct permission predicate...OK\n9/11 core.test.replay includes respawns and produces identical complete engine state...OK\n10/11 core.test.collisions do not reset progress and each obstacle is counted once...OK\n11/11 core.test.counters wrap explicitly and memory accounting has no overlapping total...OK\nAll 11 tests passed.\n" + }, + "files": { + "core.zig": { + "bytes": 17914, + "sha256": "d9fdba86e700605b053586d95ccdfb47cbe87070717182fb1c2ab7b45221ad2b" + }, + "web/core.wasm": { + "bytes": 32544, + "sha256": "ea0789dcbb0cc2d6a17e049f4568374d242b44dafe7f996673538f3bd6dafed5" + }, + "runtime.mjs": { + "bytes": 3939, + "sha256": "9d01626245d1e0c6261a28a9ecacb04c56c67dfcb901958526d3ccb234bef159" + }, + "cli.mjs": { + "bytes": 2998, + "sha256": "6b39eebef35b5c19db01dee4d254ad667e42a5633c840b29d84607f37e98ef8f" + }, + "adapter.test.mjs": { + "bytes": 3701, + "sha256": "6f81dc74cd2e2b38390e7bcca4c4757e8a50b58ef65194eab871192c4e86cf66" + }, + "measure.mjs": { + "bytes": 2810, + "sha256": "4125f3e98d3d941bd1a61c389a91d3a1e0cd7e1bc14c6fedd87094ae6f0948dd" + }, + "manage.py": { + "bytes": 3559, + "sha256": "d9f1b13e635e15dff526a2f082144570e13bc18e73d9d85da1c1804de82a3e32" + }, + "qa-browser.js": { + "bytes": 7554, + "sha256": "50d4293d41d5aa63b404c3e22fdc1349008765365fac8cdafe01dfdf858893da" + }, + "web/app.js": { + "bytes": 14358, + "sha256": "b3dcd10fa4e4ab56dff200e9238c640e6dbea5fa7b624328d7d421ee8744e778" + }, + "web/index.html": { + "bytes": 13396, + "sha256": "28630577d3faa81b61fec95723d421e1cac9a576452d909342cfd054d3dea385" + }, + "web/style.css": { + "bytes": 16606, + "sha256": "b399be728d8f2bfb1964903e11e69a9bccbc83c9d90ecfd6783110c7a0702923" + } + }, + "adapterTests": { + "command": "node prototypes/live-rules/adapter.test.mjs", + "exitCode": 0, + "output": "✔ the actual Wasm preserves airborne state through a rule change, then permits the second jump (9.168858ms)\n✔ bad CLI edits and malformed numeric input cannot silently coerce into a different rule (1.968798ms)\n✔ keyboard, pointer and terminal requests give the same simulation result (2.355835ms)\n✔ paused input has an inspectable decision; advance cannot move paused state (2.338743ms)\n✔ bounded history and fixed linear memory survive repeated play and edits (4.602115ms)\n✔ replaying the same CLI operations produces identical snapshots and journal (3.481058ms)\nℹ tests 6\nℹ suites 0\nℹ pass 6\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 32.277966\n" + } +} diff --git a/prototypes/live-rules/runtime.mjs b/prototypes/live-rules/runtime.mjs new file mode 100644 index 0000000..9c854ea --- /dev/null +++ b/prototypes/live-rules/runtime.mjs @@ -0,0 +1,100 @@ +// Node adapter only. All rule evaluation, physics and persistent state live in Zig. +import { readFile } from 'node:fs/promises'; + +export const fields = [ + 'tick', 'y', 'vy', 'distance', 'obstacleX', 'passed', 'hits', 'jumpsUsed', + 'running', 'revision', 'lastReason', 'lastSource', 'totalInputs', + 'acceptedJumps', 'mode', 'impulse', 'enabled', 'grounded', 'obstacleHeight', +]; +const signed = new Set(['y', 'vy', 'obstacleX']); + +export async function loadCore() { + const binary = await readFile(new URL('./web/core.wasm', import.meta.url)); + const module = await WebAssembly.compile(binary); + if (WebAssembly.Module.imports(module).length !== 0) throw new Error('Core must not import a host runtime.'); + const instance = await WebAssembly.instantiate(module, {}); + instance.exports.reset(); + return instance.exports; +} + +export function snapshot(core) { + const state = Object.fromEntries(fields.map((name, id) => { + const value = core.get(id); + return [name, signed.has(name) ? value : value >>> 0]; + })); + return { + state, + decision: core.node_value(0) === -1 ? null : { + tick: core.decision_meta(0) >>> 0, + revision: core.decision_meta(1) >>> 0, + y: core.decision_meta(2), + jumps: core.decision_meta(3) >>> 0, + nodes: Array.from({ length: 6 }, (_, i) => core.node_value(i)), + }, + history: Array.from({ length: core.history_count() }, (_, i) => ({ + tick: core.history_get(i, 0) >>> 0, + kind: core.history_get(i, 1), + detail: core.history_get(i, 2) >>> 0, + revision: core.history_get(i, 3) >>> 0, + })), + }; +} + +export function storage(core) { + const keys = ['rules', 'state', 'decision', 'history', 'engine', 'graph', 'logicalTotal']; + return { + layoutBytes: Object.fromEntries(keys.map((key, i) => [key, core.bytes(i)])), + wasmLinearMemoryBytes: core.memory.buffer.byteLength, + note: 'Type-layout sizes; constant graph may be compiled into code. Not browser heap or RSS.', + }; +} + +export function parseCommand(line) { + const tokens = line.trim().split(/\s+/); + const name = tokens[0]; + if (!name) return { op: 'status' }; + if (['status', 'inspect', 'memory', 'reset', 'jump', 'help', 'quit'].includes(name)) { + if (tokens.length !== 1) throw new Error(`${name}: 인수가 필요하지 않습니다.`); + return { op: name }; + } + if (name === 'step') { + if (tokens.length > 2) throw new Error('사용법: step [1..600]'); + const count = tokens.length === 1 ? 1 : integer(tokens[1], 1, 600); + return { op: 'step', count }; + } + if (name === 'rules') { + if (tokens.length !== 4 || !['ground', 'double'].includes(tokens[1]) || !['on', 'off'].includes(tokens[3])) { + throw new Error('사용법: rules ground|double 6..16 on|off'); + } + return { op: 'rules', mode: tokens[1] === 'double' ? 1 : 0, + impulse: integer(tokens[2], 6, 16), enabled: tokens[3] === 'on' ? 1 : 0 }; + } + throw new Error('알 수 없는 명령입니다. help를 입력하세요.'); +} + +function integer(text, min, max) { + if (!/^\d+$/.test(text)) throw new Error(`${min}..${max} 범위의 정수를 입력하세요.`); + const value = Number(text); + if (!Number.isSafeInteger(value) || value < min || value > max) { + throw new Error(`${min}..${max} 범위의 정수를 입력하세요.`); + } + return value; +} + +export function execute(core, command) { + switch (command.op) { + case 'jump': core.input(2); break; + case 'step': + for (let n = 0; n < command.count; n += 1) core.step(); + break; + case 'rules': + if (core.apply_rules(command.mode, command.impulse, command.enabled) !== 0) { + throw new Error('규칙 전체를 거절했습니다. 이전 규칙과 상태를 유지합니다.'); + } + break; + case 'reset': core.reset(); break; + case 'status': case 'inspect': case 'memory': break; + default: throw new Error('실행할 수 없는 명령입니다.'); + } + return snapshot(core); +} diff --git a/prototypes/live-rules/web/app.js b/prototypes/live-rules/web/app.js new file mode 100644 index 0000000..d8ca815 --- /dev/null +++ b/prototypes/live-rules/web/app.js @@ -0,0 +1,294 @@ +"use strict"; + +(() => { + const $ = (id) => document.getElementById(id); + const field = Object.freeze({ tick: 0, y: 1, vy: 2, distance: 3, obstacleX: 4, passed: 5, hits: 6, jumps: 7, running: 8, revision: 9, reason: 10, source: 11, inputs: 12, accepted: 13, mode: 14, impulse: 15, enabled: 16, grounded: 17, obstacleHeight: 18 }); + const reasonText = ["아직 입력 없음", "점프를 받아들였어요.", "점프 입력이 꺼져 있어요.", "땅에 닿아야 다시 뛸 수 있어요.", "공중에서 쓸 점프를 모두 썼어요.", "알 수 없는 입력이에요."]; + const sourceText = ["키보드", "점프 버튼", "CLI"]; + const frameMs = 1000 / 60; + let core = null; + let ready = false; + let applied = null; + let lastTime = null; + let accumulator = 0; + let droppedTicks = 0; + let renderCount = 0; + let historySignature = ""; + let notice = ""; + const nodes = [...document.querySelectorAll("[data-node]")]; + + function text(id, value) { + const element = $(id); + const next = String(value); + if (element.textContent !== next) element.textContent = next; + } + + function get(name) { return core.get(field[name]); } + function counter(name) { return get(name) >>> 0; } + function say(message) { text("live-status", message); } + function formatBytes(value) { return `${(value >>> 0).toLocaleString("ko-KR")} B`; } + function draft() { + return { mode: Number(document.querySelector('input[name="mode"]:checked').value), impulse: Number($("impulse").value), enabled: $("enabled").checked ? 1 : 0 }; + } + function isDirty() { + if (!applied) return false; + const next = draft(); + return next.mode !== applied.mode || next.impulse !== applied.impulse || next.enabled !== applied.enabled; + } + function draftChanged() { + text("impulse-output", $("impulse").value); + const dirty = isDirty(); + text("draft-status", dirty ? "아직 적용하지 않은 변경" : "현재 규칙과 같음"); + $("draft-status").parentElement.classList.toggle("dirty", dirty); + $("apply-button").disabled = !ready || !dirty; + if (dirty) text("apply-feedback", "아직 이전 규칙으로 움직입니다. 적용하면 위치와 시간은 그대로 이어집니다."); + else if (ready) text("apply-feedback", "바꾼 뒤 적용을 눌러주세요. 위치와 진행 시간은 이어집니다."); + } + function syncApplied() { + applied = { mode: get("mode"), impulse: get("impulse"), enabled: get("enabled") }; + document.querySelector(`input[name="mode"][value="${applied.mode}"]`).checked = true; + $("impulse").value = String(applied.impulse); + $("enabled").checked = Boolean(applied.enabled); + draftChanged(); + } + + function renderHistory() { + const entries = []; + for (let i = 0, count = Math.min(8, core.history_count()); i < count; i++) { + entries.push([0, 1, 2, 3].map((part) => core.history_get(i, part) >>> 0)); + } + const signature = JSON.stringify(entries); + if (signature === historySignature) return; + historySignature = signature; + const list = $("history-list"); + list.replaceChildren(); + if (entries.length === 0) { + const empty = document.createElement("li"); + empty.className = "empty-history"; + empty.textContent = "점프하거나 규칙을 적용하면 이곳에 기록됩니다."; + list.append(empty); + return; + } + for (const [tick, kind, detail, revision] of entries) { + const row = document.createElement("li"); + const time = document.createElement("span"); + time.className = "history-time"; + time.textContent = `${tick}t`; + const label = document.createElement("span"); + label.className = "history-label"; + const descriptions = { 1: "점프 허용", 2: reasonText[detail] || "점프 거절", 3: "규칙 적용", 4: "땅에 착지", 5: "장애물에 닿음", 6: "장애물을 지남" }; + label.textContent = `${descriptions[kind] || "상태 변경"} · v${revision}`; + row.append(time, label); + list.append(row); + } + } + + function renderDecision() { + const reason = get("reason"); + const result = $("decision-result"); + result.classList.toggle("accepted", reason === 1); + result.classList.toggle("rejected", reason > 1); + text("decision-result", reason === 0 ? "첫 점프를 기다리고 있어요." : reasonText[reason] || "입력을 확인해주세요."); + if (reason === 0) { + text("decision-explanation", "한 번 누르면 입력이 규칙을 통과하는 과정을 볼 수 있습니다."); + text("decision-meta", "아직 입력 없음"); + $("stale-decision").hidden = true; + } else { + const preY = core.decision_meta(2); + const preJumps = core.decision_meta(3); + const revision = core.decision_meta(1) >>> 0; + text("decision-explanation", `누른 순간: 높이 ${(preY / 100).toFixed(1)} px · 사용한 점프 ${preJumps}회. ${get("running") ? "이후에도 세계는 움직입니다." : "멈춘 상태에서 판단을 살펴보고 있어요."}`); + text("decision-meta", `${sourceText[get("source")] || "알 수 없는 입력"} · ${core.decision_meta(0) >>> 0} tick · v${revision}`); + $("stale-decision").hidden = revision === counter("revision"); + } + for (const node of nodes) { + const value = core.node_value(Number(node.dataset.node)); + if (value < 0) delete node.dataset.result; + else node.dataset.result = String(Boolean(value)); + const output = node.querySelector(".node-value"); + const next = value < 0 ? "대기" : value ? "예" : "아니요"; + if (output.textContent !== next) output.textContent = next; + } + } + + function render() { + if (!ready) return; + renderCount++; + const y = get("y") / 100; + const velocity = get("vy") / 100; + const tick = counter("tick"); + const running = Boolean(get("running")); + const obstacleHeight = get("obstacleHeight"); + const cameraY = Math.min(0, 180 - y); + const viewWidth = $("stage").clientWidth < 500 ? 500 : 960; + $("world").setAttribute("viewBox", `0 ${cameraY} ${viewWidth} 350`); + $("dino").setAttribute("transform", `translate(120 ${230 - y})`); + $("dino-shadow").setAttribute("rx", String(Math.max(10, 27 - y / 12))); + $("dino-shadow").setAttribute("opacity", String(Math.max(0.05, 0.15 - y / 1600))); + $("obstacle").setAttribute("transform", `translate(${get("obstacleX")} ${278 - obstacleHeight})`); + $("obstacle-body").setAttribute("height", String(obstacleHeight)); + $("obstacle-mark").setAttribute("d", `M11 10v${Math.max(0, obstacleHeight - 20)}m10-${Math.max(0, obstacleHeight - 27)}v${Math.max(0, obstacleHeight - 27)}`); + text("running-badge", running ? "실행 중" : "멈춰 있음"); + $("running-badge").classList.toggle("running", running); + text("run-button", running ? "일시정지" : "실행"); + $("run-button").setAttribute("aria-pressed", String(running)); + $("step-button").disabled = running; + text("stage-hint", cameraY < 0 ? "높이에 맞춰 시점이 공룡을 따라갑니다." : running ? "Space 또는 점프 버튼 · 멈춰서 규칙을 바꿔도 좋아요." : "일시정지 상태에서도 규칙과 입력을 확인할 수 있어요."); + text("state-height", `${y.toFixed(1)} px`); + text("state-velocity", `${velocity > 0 ? "+" : ""}${velocity.toFixed(1)} px/틱`); + text("state-ground", get("grounded") ? "땅 위" : "공중"); + text("state-jumps", `${counter("jumps")}회`); + text("state-distance", `${counter("distance").toLocaleString("ko-KR")} px`); + text("state-time", `${(tick / 60).toFixed(2)}초`); + text("state-tick", tick); + text("state-passed", counter("passed")); + text("state-hits", counter("hits")); + text("revision-badge", `v${counter("revision")}`); + text("applied-label", `적용 v${counter("revision")}`); + text("runtime-note", notice || (droppedTicks > 0 ? `화면 지연으로 ${droppedTicks.toLocaleString("ko-KR")}틱을 생략했습니다. 진행 시간은 실제 시간보다 느릴 수 있어요.` : "실행하면 시간이 흐릅니다. 멈춘 뒤 한 프레임씩 살펴볼 수도 있어요.")); + text("scheduling-detail", `고정 60Hz · 한 화면 갱신당 최대 6틱 · 생략한 틱 ${droppedTicks.toLocaleString("ko-KR")}`); + text("bytes-linear", core.memory ? formatBytes(core.memory.buffer.byteLength) : "메모리 내보내기 없음"); + renderDecision(); + renderHistory(); + } + + function jump(source) { + if (!ready) return; + core.input(source); + render(); + say(reasonText[get("reason")] || "입력 처리 완료"); + } + + function setRunning(next) { + core.set_running(next ? 1 : 0); + accumulator = 0; + lastTime = null; + notice = ""; + render(); + say(next ? "실행했습니다." : "멈췄습니다. 규칙을 바꾸거나 한 프레임씩 살펴볼 수 있어요."); + } + + function frame(now) { + if (ready) { + if (get("running") && !document.hidden) { + if (lastTime !== null) { + accumulator += Math.max(0, now - lastTime); + const due = Math.floor(accumulator / frameMs); + if (due > 0) { + const ticks = Math.min(6, due); + droppedTicks += due - ticks; + accumulator -= due * frameMs; + core.advance(ticks); + render(); + } + } + lastTime = now; + } else { + lastTime = null; + accumulator = 0; + } + } + window.requestAnimationFrame(frame); + } + + $("rules-form").addEventListener("input", draftChanged); + $("rules-form").addEventListener("change", draftChanged); + $("rules-form").addEventListener("submit", (event) => { + event.preventDefault(); + if (!ready || !isDirty()) return; + const next = draft(); + if (core.apply_rules(next.mode, next.impulse, next.enabled) !== 0) { + text("apply-feedback", "적용하지 못했습니다. 점프 세기는 6~16이어야 합니다."); + say("규칙을 적용하지 못했습니다."); + return; + } + applied = next; + draftChanged(); + const feedback = `v${counter("revision")} 적용 완료. 위치·속도·진행 시간은 그대로 이어집니다.`; + text("apply-feedback", feedback); + render(); + say(feedback); + }); + $("run-button").addEventListener("click", () => { if (ready) setRunning(!get("running")); }); + $("step-button").addEventListener("click", () => { + if (!ready || get("running")) return; + core.step(); + render(); + say(`한 프레임 진행. 높이 ${(get("y") / 100).toFixed(1)} 픽셀.`); + }); + $("jump-button").addEventListener("click", () => jump(1)); + $("reset-button").addEventListener("click", () => { + if (!ready) return; + core.reset(); + core.set_running(0); + accumulator = 0; + lastTime = null; + droppedTicks = 0; + notice = "처음 상태로 돌아왔습니다. 멈춘 상태에서 다시 시작할 수 있어요."; + syncApplied(); + text("apply-feedback", "위치·진행 시간·규칙·기록을 초기 상태로 되돌렸습니다."); + render(); + say("세계와 규칙을 초기 상태로 되돌렸습니다."); + }); + document.addEventListener("keydown", (event) => { + if (!ready || (event.code !== "Space" && event.key !== " ") || event.isComposing || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; + const target = event.target; + if (target instanceof Element && (target.isContentEditable || target.closest("input, textarea, select, button, a[href], summary, [contenteditable], [role=button]"))) return; + event.preventDefault(); + if (event.repeat) return; + jump(0); + }); + document.addEventListener("visibilitychange", () => { + if (!ready || !document.hidden) return; + const wasRunning = Boolean(get("running")); + core.set_running(0); + lastTime = null; + accumulator = 0; + if (wasRunning) { + notice = "다른 탭으로 이동해 자동으로 멈췄습니다. 실행을 누르면 이어집니다."; + render(); + say(notice); + } + }); + window.addEventListener("resize", render); + + Object.defineProperty(window, "pureDemo", { value: Object.freeze({ + snapshot() { + if (!ready) return Object.freeze({ ready: false }); + return Object.freeze({ ready: true, ...Object.fromEntries(Object.keys(field).map((name) => [name, get(name)])), draftDirty: isDirty(), droppedTicks, renderCount, wasmMemoryBytes: core.memory?.buffer.byteLength ?? null, logicalBytes: core.bytes(6), decisionTick: core.decision_meta(0), decisionRevision: core.decision_meta(1), decisionNodes: Object.freeze(nodes.map((node) => core.node_value(Number(node.dataset.node)))) }); + } + }), writable: false, configurable: false }); + + async function boot() { + try { + const response = await fetch("core.wasm", { cache: "no-store" }); + if (!response.ok) throw new Error(`core.wasm HTTP ${response.status}`); + const { instance } = await WebAssembly.instantiate(await response.arrayBuffer(), {}); + core = instance.exports; + for (const name of ["reset", "set_running", "advance", "step", "input", "apply_rules", "get", "node_value", "decision_meta", "history_count", "history_get", "bytes"]) { + if (typeof core[name] !== "function") throw new Error(`필요한 코어 함수가 없습니다: ${name}`); + } + core.reset(); + core.set_running(0); + ready = true; + $("rule-controls").disabled = false; + for (const id of ["run-button", "step-button", "jump-button", "reset-button"]) $(id).disabled = false; + $("workspace").setAttribute("aria-busy", "false"); + $("load-message").hidden = true; + syncApplied(); + ["rules", "state", "decision", "history", "engine", "graph", "total"].forEach((name, i) => text(`bytes-${name}`, formatBytes(core.bytes(i)))); + render(); + say("준비되었습니다. 멈춘 상태입니다. 점프 버튼을 눌러보세요."); + window.requestAnimationFrame(frame); + } catch (error) { + ready = false; + $("workspace").setAttribute("aria-busy", "false"); + $("load-message").hidden = true; + $("load-error").hidden = false; + text("running-badge", "실행기 없음"); + text("error-detail", error instanceof Error ? error.message : String(error)); + for (const control of document.querySelectorAll("button, #rule-controls")) control.disabled = true; + } + } + boot(); +})(); diff --git a/prototypes/live-rules/web/core.wasm b/prototypes/live-rules/web/core.wasm new file mode 100755 index 0000000..8c79903 Binary files /dev/null and b/prototypes/live-rules/web/core.wasm differ diff --git a/prototypes/live-rules/web/index.html b/prototypes/live-rules/web/index.html new file mode 100644 index 0000000..45ff700 --- /dev/null +++ b/prototypes/live-rules/web/index.html @@ -0,0 +1,101 @@ + + + + + + + Pure — 규칙 실험실 + + + + + + + +
+
+

작게 만들고, 직접 바꿔보기

뛰는 도중에, 규칙을 바꿔보세요.

같은 공룡, 같은 순간. 점프 규칙만 바꾸면 무엇이 달라질까요?

+

살아 있는 규칙
첫 번째 연습장

+
+ +
작은 실행기를 불러오고 있습니다…
+ + +
+
+

01연습장

불러오는 중
+
+ + 공룡 점프 연습장규칙을 적용하고 점프 버튼이나 스페이스 키로 움직여 보세요. 현재 상태는 연습장 아래에 표시됩니다. + + + + + + + + + + + + + + + PURE · RULE PLAYGROUND + +
일시정지 상태에서도 규칙과 입력을 확인할 수 있어요.
+
+
+
+ + +
+

실행하면 시간이 흐릅니다. 멈춘 뒤 한 프레임씩 살펴볼 수도 있어요.

+
+
높이—
세로 속도—
발 디딤—
사용한 점프—
진행 거리—
진행 시간—
+
+
지나간 장애물 0닿은 장애물 0tick 0
+
+ + +
+ +
+

03왜 그렇게 움직였나요?

마지막 점프 요청
+

첫 점프를 기다리고 있어요.

한 번 누르면 입력이 규칙을 통과하는 과정을 볼 수 있습니다.

아직 입력 없음
+ +
    +
  1. 01요청 도착대기
  2. 02입력 켜짐대기
  3. 03땅에 있음대기
  4. 04점프 여유대기
  5. 05방식에 맞음대기
  6. 06점프 허용대기
  7. +
+

요청과 입력 켜짐을 확인하고, 선택한 방식에 따라 ‘땅에 있음’ 또는 ‘점프 여유’를 판단합니다. 기록은 입력을 받은 순간의 상태입니다.

+

최근 변화 최대 8개 표시

  1. 점프하거나 규칙을 적용하면 이곳에 기록됩니다.
+
+ +
실험의 범위와 메모리 보기기술적인 내용은 여기에

무엇을 확인하는 실험인가요?

세계가 실행되는 동안 점프 규칙을 바꾸고, 상태를 유지하며, 입력 판단을 살펴보는 작은 시제품입니다. 물리 계산과 규칙 판단은 Zig로 작성한 동일한 WebAssembly 코어에서 처리합니다. 화면은 그 결과를 보여줍니다.

시간은 고정 60Hz 틱으로 진행합니다. 느린 화면에서는 한 번에 최대 6틱을 처리하며, 생략한 틱 수를 아래에 표시합니다. 실제 시간과 정확히 같다는 보장은 없습니다.

높이·속도·거리는 세계 좌표(px)로 표시합니다. 높은 점프에서는 시점이 공룡을 따라 위로 이동합니다. 화면 크기와 시점은 물리 계산을 바꾸지 않습니다.

아직 실행 전 · 생략한 틱 0

코어의 자료 구조 크기

코어가 보고한 논리 자료 크기 · 실제 프로세스 메모리와 다름
규칙—
세계 상태—
마지막 판단—
기록과 관리 정보—
엔진 전체 · 정렬 포함—
고정 규칙 그래프—
엔진 + 그래프 · 논리 합계—
WASM 선형 메모리 · 별도—

자료 구조 크기는 Zig 타입 배치 기준입니다. 고정 그래프는 컴파일 과정에서 코드에 통합될 수 있어, 이 수치를 실제 RAM 사용량으로 읽으면 안 됩니다. WASM 선형 메모리는 실제 할당된 별도 범위이며 위 합계와 더하지 않습니다. 브라우저·화면·코드·프로세스 전체 메모리(RSS)는 이 표에 포함되지 않습니다. M3 성능이나 최적화 우위를 입증하는 측정도 아닙니다.

+

+
+
Pure · 원하는 동작을 먼저, 근거는 남기면서.
+ + diff --git a/prototypes/live-rules/web/style.css b/prototypes/live-rules/web/style.css new file mode 100644 index 0000000..95207c8 --- /dev/null +++ b/prototypes/live-rules/web/style.css @@ -0,0 +1 @@ +:root{color-scheme:light;--page:#f5f4ee;--card:#fffefa;--ink:#27372b;--muted:#74806f;--line:#e3e6dc;--green:#356548;--green-light:#edf3e9;--amber:#a36a31;--amber-light:#faf0df;--radius:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:15px}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--page)}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.46}a{color:inherit}button,input,a,summary,[tabindex]{-webkit-tap-highlight-color:transparent}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:3px solid #b37c39;outline-offset:4px}button,input{accent-color:var(--green)}[hidden]{display:none!important}.site-header{height:94px;max-width:1256px;padding:0 36px;margin:auto;display:flex;align-items:center;justify-content:space-between}.brand{display:inline-flex;align-items:center;gap:13px;font-size:19px;font-weight:750;text-decoration:none;letter-spacing:-.5px}.brand-mark{display:grid;place-items:center;width:35px;height:35px;background:var(--ink);color:var(--page);border-radius:9px;font:700 27px Georgia,serif;padding-bottom:4px}.brand-separator{margin:0 10px;color:#b1b7a9;font-weight:400}.brand-subtitle{font-size:14px;font-weight:500;letter-spacing:-.3px}.experiment-tag{display:inline-flex;align-items:center;gap:7px;color:var(--muted);font-size:12px}.status-dot,.tiny-dot{display:inline-block;width:6px;height:6px;background:#82a06e;border-radius:50%}main{max-width:1256px;margin:0 auto;padding:28px 36px 0}.intro{display:flex;align-items:center;justify-content:space-between;gap:32px;margin-bottom:30px}.eyebrow{font-size:11px;letter-spacing:1.25px;font-weight:650;color:var(--muted);margin:0 0 11px}h1{font-size:clamp(25px,2.6vw,34px);font-weight:650;line-height:1.35;letter-spacing:-1.3px;margin:0 0 11px}.intro-copy{color:var(--muted);font-size:14px;line-height:1.7;margin:0}.prototype-label{flex-shrink:0;text-align:right;color:var(--green);font-size:12px;font-weight:600;line-height:1.85;margin:0;border-left:1px solid var(--line);padding-left:35px}.prototype-label span{color:var(--muted);font-size:11px;font-weight:400}.notice,.load-error{border:1px solid var(--line);padding:16px 20px;border-radius:10px;margin-bottom:20px;font-size:13px;line-height:1.7;background:var(--card)}.load-error{background:#fff0e8;border-color:#e9c6b7;color:#7b392b}.load-error h2{font-size:17px}.load-error pre{overflow:auto;padding:12px;background:#fff9f3;border-radius:7px;font-size:12px}.error-detail{font-family:ui-monospace,monospace;font-size:11px;overflow-wrap:anywhere}.workbench{display:grid;grid-template-columns:minmax(0,1fr) 322px;gap:20px;align-items:start}.panel{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);overflow:hidden}.panel-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:22px 24px 19px}.panel-heading h2{font-size:14px;font-weight:650;margin:0;letter-spacing:-.3px;display:flex;align-items:center;gap:9px}.section-number{color:#9aa28f;font:11px ui-monospace,monospace;font-weight:400}.badge{font-size:10px;line-height:1;padding:6px 8px;background:#f0f1eb;color:#6e7967;border-radius:5px;font-variant-numeric:tabular-nums;white-space:nowrap}.badge.running{background:var(--green-light);color:var(--green)}.stage{position:relative;margin:0 14px;background:#f5f7f0;border:1px solid #e7ebdf;border-radius:10px;overflow:hidden;line-height:0}#world{display:block;width:100%;height:auto;min-height:230px}.stage-hint{position:absolute;top:16px;left:17px;right:15px;display:flex;align-items:center;gap:7px;color:#7a866f;font-size:10px;line-height:1.5}.tiny-dot{width:4px;height:4px;flex-shrink:0;background:#a5b496}.transport{display:flex;align-items:center;gap:9px;padding:20px 24px 0;flex-wrap:wrap}.transport-primary{display:flex;gap:7px}.button{border:1px solid transparent;border-radius:7px;line-height:1.25;font-size:12px;font-weight:550;padding:11px 15px;transition:background .15s,border-color .15s,transform .15s}.button:active:not(:disabled){transform:translateY(1px)}.button-dark{background:var(--ink);color:white;min-width:78px}.button-dark:hover:not(:disabled){background:#425544}.button-quiet{background:transparent;border-color:var(--line);color:var(--ink)}.button-quiet:hover:not(:disabled),.button-reset:hover:not(:disabled){background:#f2f3ec}.button-jump{margin-left:auto;background:var(--green-light);border-color:#dce6d5;color:var(--green);display:flex;align-items:center;gap:20px}.button-jump:hover:not(:disabled){background:#deead7}.button-jump kbd{font:9px ui-monospace,monospace;padding:2px 4px;border:1px solid #c7d5be;border-radius:3px;color:#6d865e}.button-reset{background:transparent;color:var(--muted);font-size:11px;padding-left:9px;padding-right:9px}.runtime-note{font-size:11px;color:var(--muted);line-height:1.7;margin:12px 24px 21px;min-height:19px}.state-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:23px 18px;padding:22px 24px;border-top:1px solid var(--line)}.state-grid>div{display:flex;flex-direction:column;gap:7px}.state-grid span{color:var(--muted);font-size:10px}.state-grid strong{font-size:16px;font-weight:550;font-variant-numeric:tabular-nums;letter-spacing:-.5px}.world-summary{display:flex;gap:22px;padding:14px 24px;border-top:1px solid var(--line);font-size:10px;color:var(--muted);background:#fcfcf7}.world-summary b{margin-left:5px;font-weight:550;color:#58694f;font-variant-numeric:tabular-nums}.tick-label{margin-left:auto;font-family:ui-monospace,monospace}.rules-panel .panel-heading{padding-bottom:22px}#rules-form{padding:0 23px}fieldset{border:0;padding:0;margin:0;min-width:0}.field-label{font-size:11px;font-weight:600;display:block;margin:0 0 12px}.mode-options{display:flex;flex-direction:column;gap:8px;margin-bottom:25px}.mode-option{position:relative;display:flex;align-items:center;gap:10px;padding:14px 12px;border:1px solid var(--line);border-radius:8px;cursor:pointer;background:var(--card);min-width:0}.mode-option:has(input:checked){border-color:#8fa783;background:#f1f5ec}.mode-option:has(input:focus-visible){outline:3px solid #b37c39;outline-offset:3px}.mode-option input{position:absolute;opacity:0;width:1px;height:1px}.mode-option>span:first-of-type{flex:1}.mode-option b,.enabled-option b{display:block;font-size:12px;font-weight:550}.mode-option small,.enabled-option small{display:block;font-size:10px;line-height:1.6;color:var(--muted);margin-top:5px;word-break:keep-all}.radio-mark{width:13px;height:13px;border:1px solid #c2cbb9;border-radius:50%;display:grid;place-items:center;flex-shrink:0}.mode-option input:checked~.radio-mark{border-color:var(--green)}.mode-option input:checked~.radio-mark:after{content:"";background:var(--green);width:5px;height:5px;border-radius:50%}.force-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.force-heading .field-label{margin:0}.force-heading output{background:#f1f3ec;font-size:12px;font-variant-numeric:tabular-nums;padding:4px 9px;border:1px solid var(--line);border-radius:5px;min-width:34px;text-align:center}input[type=range]{display:block;width:100%;margin:14px 0 9px;cursor:pointer;height:16px}.range-labels{display:flex;justify-content:space-between;font-size:9px;color:#8a937f}.field-help{font-size:10px;color:var(--muted);margin:10px 0 21px;line-height:1.6}.enabled-option{display:flex;align-items:center;position:relative;cursor:pointer;gap:15px;border-top:1px solid var(--line);padding-top:19px}.enabled-option>span:first-child{flex:1}.enabled-option input{position:absolute;right:4px;opacity:0;width:28px;height:20px}.switch{width:31px;height:19px;background:#ced4c7;border:1px solid #c5cdbe;border-radius:20px;padding:2px;flex-shrink:0;transition:background .15s}.switch:after{content:"";display:block;width:13px;height:13px;background:white;border-radius:50%;box-shadow:0 1px 2px #0002;transition:transform .15s}.enabled-option input:checked+.switch{background:var(--green);border-color:var(--green)}.enabled-option input:checked+.switch:after{transform:translateX(12px)}.enabled-option input:focus-visible+.switch{outline:3px solid #b37c39;outline-offset:4px}.apply-area{margin-top:23px}.draft-line{display:flex;justify-content:space-between;gap:10px;font-size:10px;color:var(--muted);margin-bottom:9px}.draft-line.dirty #draft-status{color:var(--amber);font-weight:600}.button-apply{display:block;width:100%;background:var(--green);color:white;font-size:12px;padding:12px}.button-apply:hover:not(:disabled){background:#284e36}#apply-feedback{font-size:10px;line-height:1.7;min-height:35px;margin:10px 0 22px;color:var(--muted)}.try-card{background:#f4f5ee;border-top:1px solid var(--line);padding:20px 23px}.try-card .eyebrow{font-size:10px;letter-spacing:.8px;display:block;margin-bottom:11px}.try-card ol{font-size:11px;line-height:1.8;padding-left:17px;margin:0;color:#627058}.try-card li+li{margin-top:5px}.try-card b{font-weight:600}.try-card p{font-size:10px;color:#859079;line-height:1.65;margin:12px 0 0}.decision-panel{margin-top:20px}.muted-small{font-size:10px;color:var(--muted)}.decision-summary{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:0 24px 19px}.decision-result{margin:0 0 7px;font-size:18px;letter-spacing:-.6px;font-weight:550}.decision-result.accepted{color:var(--green)}.decision-result.rejected{color:var(--amber)}.decision-explanation{font-size:11px;color:var(--muted);line-height:1.7;margin:0}.decision-meta{font-size:10px;color:var(--muted);text-align:right;line-height:1.9;white-space:nowrap;font-variant-numeric:tabular-nums}.stale-note{font-size:11px;color:var(--amber);background:var(--amber-light);margin:0 24px 17px;padding:10px 12px;border-radius:6px;line-height:1.7}.decision-path{list-style:none;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin:0;padding:0 24px}.decision-path li{position:relative;border:1px solid var(--line);background:#fafbf6;border-radius:8px;padding:12px;min-width:0}.decision-path li:not(:last-child):after{position:absolute;right:-10px;top:50%;content:"›";color:#a4af9a;font-size:12px;line-height:0}.decision-path li[data-result=true]{background:#f0f5eb;border-color:#d4e2ca}.decision-path li[data-result=false]{background:#fbf6ed;border-color:#ebdfc9}.node-step{display:block;font:9px ui-monospace,monospace;color:#a2aa97;margin-bottom:13px}.decision-path strong{display:block;font-size:11px;font-weight:550;white-space:nowrap}.node-value{display:block;font-size:10px;color:#8b9580;margin-top:6px}.decision-path li[data-result=true] .node-value{color:var(--green)}.decision-path li[data-result=false] .node-value{color:var(--amber)}.path-caption{font-size:10px;line-height:1.7;color:#85907b;padding:0 24px;margin:12px 0 21px}.history-section{border-top:1px solid var(--line);padding:19px 24px 20px;display:grid;grid-template-columns:145px minmax(0,1fr);gap:25px}.history-section h3{font-size:11px;font-weight:550;margin:3px 0;line-height:1.6}.history-section h3 span{display:block;font-size:9px;color:#939d88;font-weight:400;margin-top:4px}.history-list{list-style:none;padding:0;margin:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 25px}.history-list li{display:flex;gap:10px;align-items:baseline;font-size:10px;line-height:1.7;color:#69755e;min-width:0}.history-list .history-time{font:9px ui-monospace,monospace;color:#9aa38f;flex-shrink:0}.history-list .history-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-list .empty-history{grid-column:1/-1;color:#8d9781}.research-panel{margin-top:20px;border:1px solid var(--line);border-radius:12px;background:#f0f1e9;overflow:hidden}.research-panel summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:12px;padding:19px 23px;font-size:12px;font-weight:500}.research-panel summary::-webkit-details-marker{display:none}.research-panel summary:before{content:"+";font:16px ui-monospace,monospace;color:#8a947e}.research-panel[open] summary:before{content:"−"}.summary-caption{margin-left:auto;font-size:10px;font-weight:400;color:#8d9781}.research-body{border-top:1px solid var(--line);display:grid;grid-template-columns:1fr 1fr;gap:60px;padding:24px}.research-body h2{font-size:12px;margin:0 0 14px;font-weight:600}.research-body p{font-size:11px;line-height:1.85;color:#74806a;margin:0 0 13px}.memory-table{width:100%;border-collapse:collapse;font-size:10px;line-height:1.6;font-variant-numeric:tabular-nums}.memory-table caption{text-align:left;font-size:9px;color:#8c967f;padding-bottom:9px}.memory-table th{text-align:left;font-weight:400;padding:5px 0;color:#6b7762}.memory-table td{text-align:right;color:#4f6249;padding:5px 0;font-family:ui-monospace,monospace}.memory-table .total-row th,.memory-table .total-row td{border-top:1px solid #d6ddcd;font-weight:600;padding-top:9px}.research-body .metric-note{font-size:10px;margin-top:12px;color:#8a947f}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}footer{max-width:1256px;margin:0 auto;padding:30px 36px 35px;font-size:10px;color:#9ba48f;display:flex;align-items:center;gap:9px}footer span{color:#bdc4b4}@media(min-width:1100px){.state-grid{grid-template-columns:repeat(6,minmax(0,1fr));gap:10px}.state-grid strong{font-size:15px}.stage-hint{font-size:11px}}@media(max-width:900px){.site-header{height:77px;padding:0 24px}main{padding:22px 24px 0}.workbench{grid-template-columns:minmax(0,1fr) 286px;gap:15px}.panel-heading{padding:20px 19px 17px}#rules-form{padding:0 18px}.try-card{padding:18px}.transport{padding:17px 19px 0;gap:7px}.button{padding:10px 11px}.button-jump{gap:12px}.runtime-note{margin-left:19px;margin-right:19px}.state-grid{padding:19px}.world-summary{padding:13px 19px;gap:12px}.decision-path{gap:9px;padding:0 19px}.decision-path li{padding:10px 8px}.decision-path li:not(:last-child):after{right:-8px}.decision-path strong{font-size:10px}.decision-summary{padding-left:19px;padding-right:19px}.history-section{grid-template-columns:110px 1fr;padding-left:19px;padding-right:19px;gap:15px}.history-list{column-gap:14px}.intro{gap:20px}.prototype-label{display:none}.research-body{gap:32px}footer{padding-left:24px;padding-right:24px}}@media(max-width:700px){.site-header{height:70px;padding:0 18px}.brand{gap:9px;font-size:18px}.brand-separator{margin:0 6px}.brand-subtitle{font-size:12px}.experiment-tag{font-size:10px}main{padding:18px 16px 0}.intro{margin-bottom:24px}.eyebrow{font-size:10px}h1{font-size:25px;letter-spacing:-1px}.intro-copy{font-size:12px;word-break:keep-all}.workbench{grid-template-columns:1fr;gap:16px}.stage{margin:0 11px}#world{min-height:200px}.stage-hint{font-size:9px;left:11px;top:12px;max-width:280px}.transport{padding-left:17px;padding-right:17px;flex-wrap:nowrap;gap:6px}.button{font-size:11px;padding:11px 10px}.button-jump{gap:10px}.button-jump kbd{font-size:8px}.button-reset{font-size:10px;padding:10px 5px}.button-dark{min-width:69px}.state-grid{grid-template-columns:repeat(3,1fr);gap:20px}.state-grid strong{font-size:16px}.rules-panel .panel-heading{padding-bottom:19px}.mode-options{flex-direction:row;gap:9px;margin-bottom:21px}.mode-option{flex:1;padding:13px 11px;gap:7px}.mode-option b{font-size:11px}.mode-option small{font-size:9px}.radio-mark{width:12px;height:12px}.force-heading{margin-top:4px}.enabled-option{padding-top:15px}.apply-area{margin-top:21px}.try-card{padding:19px}.try-card li+li{margin-top:2px}.decision-panel{margin-top:16px}.decision-summary{align-items:flex-start;gap:15px}.decision-result{font-size:16px}.decision-explanation{font-size:10px}.decision-meta{font-size:9px}.decision-path{grid-template-columns:repeat(3,1fr);gap:9px}.decision-path li{padding:12px}.decision-path strong{font-size:11px}.decision-path li:nth-child(3):after{display:none}.node-step{margin-bottom:9px}.path-caption{padding:0 19px;font-size:9px}.history-section{display:block}.history-section h3{display:flex;justify-content:space-between;margin:0 0 13px}.history-section h3 span{margin:0}.history-list{grid-template-columns:1fr;gap:5px}.history-list li{font-size:10px}.stale-note{margin-left:19px;margin-right:19px;font-size:10px}.research-panel{margin-top:16px}.research-panel summary{padding:17px;font-size:11px}.summary-caption{font-size:9px}.research-body{grid-template-columns:1fr;gap:19px;padding:19px}.research-body>div+div{border-top:1px solid var(--line);padding-top:20px}footer{padding:24px 18px;font-size:9px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{transition:none!important}} diff --git a/prototypes/workbench/.gitignore b/prototypes/workbench/.gitignore new file mode 100644 index 0000000..cab7be7 --- /dev/null +++ b/prototypes/workbench/.gitignore @@ -0,0 +1,2 @@ +/build/ +/web/examples/ diff --git a/prototypes/workbench/CONTRACT.md b/prototypes/workbench/CONTRACT.md new file mode 100644 index 0000000..498129e --- /dev/null +++ b/prototypes/workbench/CONTRACT.md @@ -0,0 +1,575 @@ +# Workbench experiment contract + +An author starts with empty source. There is no built-in player, physics, jump, +score, or timer behavior. All behavior is declared by the author. This is a +bounded scalar interaction language experiment, not a general language claim. + +The implementation now lives in explicit `Engine` instances in `engine.zig`. +`core.zig` retains the browser ABI and tests; `native.zig` exposes caller-owned +instances through the selected C module. Native prepared definitions, host input +ownership and conditional snapshots are documented in [native/README.md](native/README.md) +and [024](../../docs/research/024-native-app-instances.md). Existing scalar limits +remain. This does not make the selected scalar VM mandatory for every Pure domain. + +## Source language + +```text +state count = 0 +input tap = Space +box tile(80, 150, 60, 60, "#356548") +circle dot(200, 180, 20, "#c78638") +text label(80, 100, count, "Count: ") +on tap.down { + count = count + 1 +} +on frame { + if count > 20 { + count = 0 + } +} +``` + +Declarations and statements end at a newline or semicolon; `#` comments outside +strings. ASCII identifiers, finite numeric literals, state initializers must be +numeric constants (optional unary sign). Input key codes accepted: +Space, Enter, ArrowLeft/Right/Up/Down, KeyA..KeyZ, Digit0..Digit9. Input names +must be unique, key codes unique, names cannot shadow `frame`, `dt`, `min`, `max`, +`clamp`, `event_x`, `event_y`. State and input names use separate namespaces, entity IDs unique. + +Expressions: parentheses, state references, dt (seconds on frame, zero on input), +unary - and !, + - * /, < <= > >= == !=, && ||, min(a,b), max(a,b), clamp(x,lo,hi). +Logical operators evaluate both sides. Division by zero/non-finite results fail +the whole event without committing state changes. No loops, recursion or eval. +An if without else executes its condition once; assignment statements are ordered. +Multiple handlers for the same event execute in source order within one atomic event. +`dt` is allowed in handlers, not view bindings. A frame always uses 1/60 seconds. + +box: x,y,width,height expressions + literal hex color; circle: x,y,radius + color; +text: x,y,value expression + optional literal prefix. Values are world units in a +640x360 view. No implicit clipping/collision/physics. Render expressions are pure. +Unknown identifiers and invalid syntax include 1-based line+column diagnostics. + +Initial scene is empty. Optional examples are explicit insertions, never auto-run. +Successful apply preserves scalar states by exact name; new names use initializers, +removed names disappear. Apply/reset are separate. Failed parse/load changes nothing. +Apply is refused while any input is held; release first. This avoids inventing +release events across changed mappings. Focus loss sends .up to ordinary buttons +and .cancel to pointer inputs. + +## Compiler interface (web/compiler.mjs, browser and Node) + +`compile(source)` returns: +``` +{ + states:[{name,initial,line}], inputs:[{name,key,line} | {name,target,line} | {name,type,routes,line}], + instructions:[{op,arg,value,line}], handlers:[{event,start,length,line}], + views:[{id,type,line,color?,prefix?,bindings:{x:{start,length},...}}], + pointerTargets:[{input,base,count}], metrics:{...} +} +``` +Bindings: box x/y/width/height; circle x/y/radius; text x/y/value. +Compiler throws an Error with .line and .column. Empty source is a valid empty program. +Handlers and binding expressions all occupy instructions (maximum 512 total), but +only handlers execute on events; bindings are evaluated on demand by VM eval_at. +Limits: 32 states, 8 inputs, 24 views, 64 handlers, 512 instructions, stack 64, +source 32768 UTF-8 bytes. State IDs follow declaration order. Input event IDs: +frame=0; input i down=1+2*i, up=2+2*i, move=17+i, cancel=25+i. +Move/cancel handlers require a pointer input in source. States may be referenced before declaration; +symbols must be resolved after gathering top-level declarations. `metrics` can +record source bytes/lines and instruction/handler/view/state counts. + +## VM opcodes + +| op | operation | +|---:|---| +|0| push constant `value` | +|1| push state `arg` | +|2| push dt | +|3..6| add, subtract, multiply, divide | +|7| negate | +|8..13| less, less-equal, greater, greater-equal, equal, not-equal | +|14..16| AND, OR, NOT (nonzero true) | +|17..19| min, max, clamp (push x,lo,hi; invalid lo>hi fails) | +|20| store state `arg` (pop) | +|21| jump-if-zero to absolute instruction index `arg` (pop), forward only | +|22,23| push current pointer event x,y; fail without point context | +|24,25| load/store field at handler parameter_base + arg; handler-bound only | + +Rules start with an empty stack and must end with empty stack. Bindings must finish +with exactly one value and cannot STORE, branch, or read dt/event coordinates. Branch targets may be handler end. + +## Zig exported ABI + +`reset_all()` clears programs and state. `begin_program(state_count,input_count)` +starts staging, returns0 success; `put_state(id,initial,old_id)` old_id=-1 means init; +`put_instruction(op,arg,value)` append; `put_handler(event,start,length)` append; +`commit_program()` all0 success otherwise error code, atomic; candidate is separate +from the active program, but shares its workspace with optional evaluation records. +Preserved IDs read active values only at successful commit. Validate limits, +indices, finite values and forward branch ranges; runtime stack errors are checked. +`reset_state()` restores active initializers, resets tick/event diagnostics. +`dispatch(event,dt)` accepts0..16 and finite dt; event0 dt must equal1/60, input dt0; +returns0 success, other code failure with no state mutation. Tick increments for +successful frame; one event runs all matching handlers in source order. +`dispatch_point(event,x,y)` accepts existing input down/up IDs plus move/cancel IDs +within input_count, never frame0. Coordinates must be finite; dt is implicitly0. +The core only knows input count, not host device kinds. The compiler and host enforce +pointer ownership and device mapping. Calling legacy dispatch on a coordinate-reading +handler fails with bad_event9. Point operands are ephemeral execution context, not +extra persistent state slots. Both dispatch paths use the same atomic commit/history. +The host requires this new export; older Wasm must be rebuilt. +`eval_at(start,length)` returns f64 or NaN on invalid/pure-expression failure; +must not mutate state or event diagnostics. `get_state(id)` returns f64 orNaN. +`info(id)` u32: 0 tick,1 last_error,2 error_pc (0xffffffff none),3 last_executed_ops, +4 revision,5 state_count,6 instruction_count,7 handler_count,8 input_count, +9 successful_event_count. `layout(id)` u32: 0 Engine reserved @sizeOf, +1 Program reserved @sizeOf,2 Instruction @sizeOf,3 Handler @sizeOf, +4 active used IR bytes (instructions+handlers+pointer targets),5 active state-value bytes, +6 staging reserved bytes (subset of Engine),7 evaluation stack capacity bytes,8 pointer target entry bytes,9 active pointer target count. +`error_message` belongs to host map; core error codes documented in core.zig. +No allocation or imported runtime in Wasm. Fixed64KiB linear memory target with +16KiB stack; increase only if actual linker evidence requires and document it. + +## Host interface (web/runtime.mjs) + +`await createRuntime(wasmBytes?, evaluationOptions?)` fetches core.wasm in browser if omitted. +Returns object with apply(source), reset(), frame(count=1), down(name,source), +up(name,source), point(name,phase,x,y,source,target?), releaseAll(), snapshot(), exportProject(), currentProgram. +Multiple sources per input share one logical held state; down emits only first +source, up only last. Source strings identify keyboard vs onscreen pointer. +`apply` returns compiled program or throws with diagnostic. It must fully validate +in host and stage+commit in core. It preserves by name. `reset` keeps active program. +`snapshot` returns {states:{name:value}, views:[{id,type,x,y,width?,height?,radius?, +value?,prefix?,color?}], tick,revision,lastError,errorLine,lastOps,memory:{...},held:[]} +`currentProgram` is the latest successfully applied compiled program or null. +The program and all nested metadata are frozen so readers cannot corrupt state +name mappings or binding ranges. `lastFailure` records the last failed event; +its `lastError`/line remain visible through successful release or focus cleanup +until successful apply or explicit reset. `lastEvent` still describes the latest +event. A view evaluation failure uses host error100 and does not roll back events. +UI draft and applied code are distinct. Browser/CLI share compiler/runtime/wasm; +instances are separate, not a live collaborative session. No arbitrary JS evaluation. + +## Bounded display evaluation (2026-09-22) + +The existing event and float semantics above are unchanged. The forgetful C +prototype's contract is adapted into `evaluation.zig`; its uint64 DAG is not +used as a substitute for this VM's float instructions. + +Each snapshot starts one display budget, shared by all bindings. Default options +are `{cacheEntries:0, historyEntries:8, workLimit:2048}`. A unit is a binding +request, a candidate-cache probe, a change-record probe, or an executed VM +instruction. State extraction, host object creation, event dispatch and DOM work +are outside this budget. It is not a time limit or a bound on whole-app work. +For the current compiler limits, 96 bindings * 10 units plus 512 instructions +is at most 1,472 units; the default admits even an uncached valid scene with +cache lookup/validation enabled. Malformed bindings may fail sooner. + +`configureEvaluation({cacheEntries?,historyEntries?,workLimit?})` accepts integers +0..32, 0..8, and 0..4294967295 respectively. Missing fields retain prior settings. +Valid configuration clears reuse records; invalid configuration changes nothing. +`forgetEvaluation('values'|'history'|'all')` drops optional records without +changing authored state, event diagnostics or program identity. +`evaluationStats()` returns the options plus work, instructions, cacheHits for +the last snapshot, and reservedBytes for the complete fixed evaluation structure. +Statistics are separate from snapshots so repeated observations remain equivalent. + +Memo entries contain the value, instruction range, input dependency bit mask +and validated revision. Direct mapping uses one candidate probe, with collisions +causing recomputation. Only successful computations are cached. Constant results +need no input-change history. All other reuse requires a complete retained +history since validation and no intersection with changed states. Dependencies +are collected from actual VM LOADs, not trusted compiler-supplied annotations. +History loss causes recomputation. All history and memo storage is fixed size; +setting capacities to zero does not return reserved linear memory. + +Only bitwise-different committed states advance change history (including signed +zero). Failed events and net-zero updates do not. Successful apply and reset +discard all reuse records. Parse/host validation failure before staging preserves +records. Beginning core staging discards them, including if that load is later +rejected or aborted. Epoch wrap also clears records. No inputs or effectful event +handlers are replayed to rebuild a binding. + +Budget exhaustion returns an empty views array for the entire snapshot, with +displayError `{code:101,line,message}`. Successfully committed event state remains. +There is no automatic retry or silent return of an old scene. Raising the budget +allows a fresh attempt. Arithmetic/size errors retain the earlier per-view error100 +behavior. `displayError` is null on display success; a latched execution error +still takes precedence in lastError/errorMessage, while displayError independently +reports display failure. Partial successful binding memos may survive a failed +snapshot but no partial scene is published. + +New core exports: `configure_evaluation(cache,history)` returns 0 or capacity1; +`forget_evaluation(flags)` bits 1=values, 2=history; `begin_evaluation(limit)`; +`eval_bounded(start,length)` returns f64 or NaN and consumes that shared budget. +`evaluation_info(id)` returns 0 status (10=work limit), 1 work, 2 instructions, +3 cache hits, 4 cache capacity, 5 history capacity, 6 reserved bytes. These +diagnostics do not replace event info. The unbounded pure `eval_at` remains for +reference checks and is not the host snapshot path. + +## Shared load/reuse workspace (2026-09-23) + +The pending definition/restore values and evaluation Tables occupy one untagged union. The active +Program, current state and evaluation control fields remain separate. All core +calls are single-threaded and synchronous; no callback can reenter a phase change. +No pointer to the reused tables is retained between calls. + +`begin_program` switches the workspace to staging and drops optional records. +Until commit/abort, old events and bindings still run against active. Binding +evaluation bypasses reuse and recomputes within its budget; cache configuration, +forget and reset must not write the staging payload. Old-id state preservation +reads the current values at successful commit, including events/reset during load. + +Successful commit publishes the new Program and then initializes empty Tables in +the shared workspace. `abort_program()` discards a pending definition and restores +empty Tables without changing active program/state/event diagnostics. Inactive +abort is a no-op. A failed commit leaves staging open for explicit abort or the +next begin. An invalid begin closes any previous pending load. Host apply aborts +after a core loading failure. Lost reuse may make a previously warm tiny budget +insufficient; neither a preserved cache nor completion at that budget is promised. + +`evaluation_info` additionally reports 7 dedicated control bytes, 8 shared Tables +bytes, 9 reuse available (false during load), and 10 union safety-tag overhead. +The host exposes these as dedicatedBytes, sharedBytes, reuseAvailable and +workspaceTagBytes. reservedBytes is control + Tables capacity, not incremental +Engine storage. The Tables capacity overlaps stagingReservedBytes and must not +be added to it. Before document restoration was added, the Wasm control was 48 B, shared Tables +1,152 B, union overhead 8 B. That Engine was 19,128 B, only 56 B larger than the +19,072 B pre-evaluation core; linear memory remains 65,536 B. No OS pages are +returned and this is not a measurement of browser/process memory. + +## Direct output publication (2026-09-23) + +`present(target)` evaluates the same bindings, in the same order and shared +budget as `snapshot()`, then calls a synchronous trusted output adapter: + +- `begin(program)`: the frozen current definition, or null at startup. +- `state(index, value)`: each current authored state, without a host state map. +- `view(index, valid)`: every declared view's publication status. +- `value(viewIndex, bindingName, value)`: each binding of a valid view only. +- `end(visibleCount)`: number of valid views. + +It returns the snapshot's diagnostics, held inputs, event information and memory +report, without `states` or `views`. Evaluation finishes before the first adapter +callback. On budget error101 every view is invalid and no binding values are +published; committed state values are still published. Error100 invalidates only +the affected views, as before. No stale value is silently substituted. + +Runtime mutations, `snapshot()` and nested `present()` are rejected during +callbacks. Read-only program/export/statistics access remains available. Adapters +must be synchronous and must not throw. If one does throw, the exception +propagates and the runtime unlocks in a finally block; arbitrary external adapter +effects cannot be rolled back. A later call republishes every value. This is not +a transaction across arbitrary output devices or an untrusted plugin boundary. + +Evaluation uses a private reusable Float64Array(96) and Uint8Array(24), allocated +on first snapshot/presentation. Their payload totals 792 host bytes, outside the +64 KiB Wasm memory. Typed-array object overhead, binding-entry arrays built at +apply, and DOM destination references are additional host memory. The numeric +buffer is a publication barrier, not another authoritative state or public +mutable scene. `snapshot()` remains an independent object representation for +CLI/export/debugging, built only when explicitly requested. + +The browser calls `present()` for normal refreshes. Its output adapter binds DOM +destinations once per successful definition change and compares values with the +existing DOM before writing. Input refreshes reuse scene, state and cost elements. +Invalid views remain allocated but are hidden using SVG `display="none"`; recovery +reveals them. A definition change can replace all scene/state destinations. This +does not skip unchanged binding evaluation, eliminate all host allocations, or +provide measured process-memory/paint-latency savings. + +## Authored direct manipulation (2026-09-23) + +`input drag = pointer(tile)` binds a logical input to a declared view, including a +forward declaration. One input per target; it shares the existing 8-input limit +with keyboard inputs. `on drag.down/move/up/cancel` handlers may read `event_x` +and `event_y` in the same 640x360 world coordinates as output. They are unavailable +in keyboard/frame handlers and display bindings; copy to authored state to retain +them. There is no built-in drag constraint, snap, grab offset, or gesture rollback. +The direct.pure example authors these using ordinary assignments and expressions. + +Host `point` requires a pointer input, valid phase, finite coordinates and nonempty +source string. Each logical input has one owning source; different targets may +have independent active contacts. Same-owner repeated down is a no-op; another +owner's down throws. Move/up/cancel without that owner are no-ops. Down establishes +ownership before dispatch; up/cancel release it before dispatch, even if authored +arithmetic fails. An event failure rolls back that event's state writes, not the +whole gesture. `releaseAll` cancels owned pointers at their last delivered position, +then releases ordinary buttons. `reset` directly clears ownership and resets state. +Last point events include x,y. A failed cancel can leave authored state unchanged, +but cannot strand host ownership; the failure remains visible until reset/apply. + +Browser hit testing selects the declared target on primary-button pointerdown. +The stable SVG root captures that pointer, including movement/release outside the +scene. Client coordinates are transformed through the inverse SVG screen matrix. +Pointerup exposes its own final coordinates to authored handlers. The direct.pure +example updates position only on move and ends dragging on up, preserving the +last displayed position even if a host repeats stale coordinates on release. Termination is observed in the window capture phase, including releases outside +the SVG. A primary mouseup can finish an owned mouse gesture if pointerup was +not delivered; it cannot finish a touch gesture. Duplicate termination is ignored. +Only capture loss of the SVG root ends its owner, not a bubbled descendant event. +Pointercancel, lostpointercapture and +focus loss use the last delivered position and dispatch cancel, never fake up. +Missing/non-invertible coordinate transforms cancel an active gesture. A second +contact on the same target is ignored; contacts on different targets are independent. +Touch scenes disable native panning on the SVG. Mouse/touch share this adapter; +pen-specific pressure, tilt and button meanings are not implemented or validated. + +Every delivered pointermove executes synchronously; this layer adds no event queue, +coalescing, frame gating or newest-only drop policy. This is not a guarantee that +browsers expose every hardware sample. Keyboard commands and pointer coordinates +enter the same authored state/commit/output path. CLI `point NAME PHASE X Y` uses +that host API and core with source `terminal`; it does not replay browser hit testing. + + +## Placement documents (2026-09-23) + +Code-only version1 JSON and .pure files keep their earlier behavior. Version2 +stores a restorable authored document, not a suspended execution image: + +```json +{"format":"pure-workbench","version":2,"source":"state x=80","draft":"state x=80", + "states":[{"name":"x","value":350}]} +``` + +`source` is the applied definition; optional `draft` is separate unapplied editor +text (default source). Each is at most 32768 UTF-8 bytes. Draft syntax may be +unfinished. State records must contain exactly one finite value for every declared +state name, with no extra, missing or duplicate names. Record order is irrelevant. +JSON numbers preserve finite f64 values, except negative zero which is encoded as +the exact string `"-0"`. No other numeric string is accepted. Input latches, running +clock, diagnostics, caches, DOM, compiled IR and external resources are not saved. +The serialized input file limit is 1449984 bytes, including escaped source/draft +and up to 32 generated item.field names (which can repeat a long schema name). + +`exportDocument(draft?)` returns an independent version2 object. It refuses an +unapplied runtime, held input or latched execution failure. It reads core state, +without constructing an output snapshot. `restoreDocument(document)` validates +and compiles before staging, refuses held input, then commits the definition and +all saved values together. It returns `{source,draft}`. Failed validation leaves +active program/values/diagnostics intact. A failed core load is aborted; optional +reuse records may be lost as with ordinary apply. Successful restore discards old +reuse, clears diagnostics, resets tick/event count and starts with no held inputs. +The original source initializers remain intact: explicit reset restores those, +not saved values. Existing `apply` continues to preserve current values by name. +Both new document APIs reject reentry during output publication. + +Core exports `put_restored_value(id,value)` and `commit_restored_program()` extend +the existing begin/put/commit loader. Every declared state needs exactly one finite +restored value; duplicate/out-of-range/nonfinite input poisons staging. Restored +commit checks completeness and program validity before touching active state. +Ordinary commit rejects staged restored values. Pending values live alongside the +pending Program in the same union as optional evaluation Tables; active Program +layout does not grow. `layout(6)` now includes Pending, rather than Program only. +Before shared rule bindings, the Wasm Pending was 9656 B (264 B added for 32 values, mask and alignment), +Engine was 19392 B; linear memory remains fixed at 65536 B. These are build-layout +facts, not a browser RAM or performance improvement claim. `evaluation_info(10)` +continues to report only union tag overhead, excluding restoration payload. + +Browser **배치 저장** stores applied source, all current values and editor draft. +It requires a stopped clock. **파일 열기** restores version2 after replacement +confirmation when existing work is present; it starts stopped. Version1/plain +source still only replaces the editor draft. Async reading checks draft generation, +program identity and successful event count so intervening edits/inputs cannot be +overwritten by a stale read. Invalid files do not alter the working document. +**.pure 저장 / 코드 JSON 저장** remain draft-only exports. + +CLI `save-project PATH` writes version2 with exclusive creation; `restore PATH` +restores it. Loading a .json path (including at startup) distinguishes versions. +A browser draft is retained through CLI save-project, but never applied implicitly. +Existing `save PATH` remains applied-source-only. No filesystem overwrite is added. + + +## Shared rules and named records (2026-09-23) + +```text +record Position { x=0; y=0 } +item green = Position(x=80,y=120) +item blue = Position(x=280,y=160) +rule move(t: Position) { t.x=event_x; t.y=event_y } +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +on drag_green.move use move(green) +on drag_blue.move use move(blue) +``` + +A record defines 1..8 named finite numeric initializers. An item has its own values; +optional literal field overrides replace defaults. Its fields occupy contiguous +slots in the existing scalar state array, with public names `item.field`. They +count toward the same 32-state limit. Schemas and items may be forward-referenced. +Unknown/duplicate fields, types or items fail compilation. Scalar and item names +cannot collide; type and rule names have separate namespaces. There are no runtime +record objects, nested records, record copying, dynamic item creation or deletion. + +A rule takes exactly one named record parameter with an exact schema name match. +Its body uses the existing expressions, ordered assignments and conditional blocks. +Parameter fields read/write the connected item's current slots; globals and other +qualified state names remain accessible. The parameter shadows the same qualified +prefix only inside its body. No rule calls from expressions or rule bodies, recursion, +return values, stack frames, or implicit event loop are introduced. Frame handlers +may use rules; coordinate-reading rules may only bind to pointer events. +Unused rules are name/type checked without emitting a body into the VM program. + +`on input.edge use rule(item)` is one event handler. Each used rule's instructions +are emitted once at its first use. Every use adds handler metadata referencing that +same exact instruction range, with parameterBase/parameterCount. Bindings run in +source order, including repeated uses of one item. All matching bindings and +ordinary handlers participate in the same atomic event: a failure in any rolls +back every state write in that event. Error PCs refer to the shared source body. + +Core `bind_handler(handler_index,base,count)` sets a previously unbound staged +handler's contiguous state range. Counts are 1..8; the entire range must fit current +staged state_count. A duplicate binding poisons staging. All owners of shared code +must have the same nonzero field count and exactly the same range. Partial overlap +remains invalid. Parameter opcodes must belong to a bound handler and use an index +below every owner's field count. Pure display evaluation rejects parameter ops; +view bindings use ordinary resolved state loads and their normal cache dependencies. + +Handler metadata grows from 12 to 16 B (base/count plus alignment). There is no +per-field indirection list or per-event parameter-value copy. At the shared-rules step, Wasm Program +was 9648 B, Pending 9912 B, Engine 19904 B: 512 B more fixed reservation across active +and pending handler arrays. The linear-memory limit stays 64 KiB. The required +bind_handler export makes the host reject older incompatible Wasm. + +Apply preserves each qualified field by name. Reordering schema fields or item +instances rebuilds physical bindings without changing their named values. Removing +an item removes its fields; adding it again initializes new fields. An active +input still blocks definition changes. Version2 documents save qualified field +names, not physical offsets; restoration recompiles all bindings and validates all +values before commit. This is static named-field identity, not a general lifetime +handle for dynamically deleted/reused objects. Example `id` and `selected` are +ordinary author-defined numbers; uniqueness/selection policy is not enforced by +an implicit object system. + + +## Target-routed pointer groups (2026-09-23) + +```text +input drag = pointer { green_box: green; blue_box: blue } +on drag.move use move(drag.target) +``` + +A group resolves named views to named items of exactly one record type. Forward +references work. Every view may be connected only once across single-target inputs +and groups. Every item may occur in only one group route, even across groups; +this prevents two group captures from writing aliased field ranges. Explicit +ordinary handlers and statically bound rules may still write other states as authored. +Groups must be nonempty. There are at most 24 routes in the program, independently +of the existing eight logical inputs. State/view/instruction limits are unchanged. +`input.target` is a contextual rule argument for the current group's event only, +not a scalar expression or persistent object handle. Wrong groups/types are rejected. + +A group handler is emitted once per authored `on`, independent of target count. +It can share its exact body with statically bound handlers. One route consists of +three bytes: logical input index, field base, field count. The compiler's routes +also retain source view/item names for the host. The host builds one bounded view +lookup Map per successful load; target-name lookup does not scan all group targets +on each event. JS Map/metadata allocations are outside the core memory accounting. + +`put_pointer_target(input,base,count)` appends a staged route, with count 1..8 and +an in-bounds, nonoverlapping state range. Same-input routes must have equal counts. +`bind_target_handler(handler,count)` binds a previously unbound staged handler to +the event target, encoded as parameter_base=255. Commit requires a non-frame event +with matching routes/counts. Existing `bind_handler` accepts only concrete bases. +`dispatch_target(event,route,x,y)` validates the active route's membership in the +event's logical input, then supplies its base to the matching handlers. Missing, +foreign, and out-of-range targets fail before any authored writes. Ordinary dispatch +and dispatch_point reject inputs that require a route. All matching static, routed, +and inline handlers still run in source order within one atomic event. +The VM indexes the route directly; it still scans the bounded handler list to match +event IDs. No claim of constant-time complete dispatch is made. + +Host `point(name,phase,x,y,source,target)` requires a group view name in the final +argument. Legacy single-target calls keep the old form and reject an extra target. +Capture/held ownership is `(input,view)`, so two views in a group can be manipulated +concurrently; a second source cannot steal the same view. Continuations from a +non-owner are ignored. Browser capture records retain the initial view across +movement, release outside the view, and cancellation. `releaseAll` cancels every +owned view once. `held` uses `input:view` for groups and the old input names for +legacy inputs; lastEvent/lastFailure add `target` for group events. + +Route indices live only in the active definition. Apply/restore remain blocked +while inputs are held. Reset clears owners; load rebuilds routes by source names. +Documents still store source and named scalar fields, never physical route indices. +Deletion/recreation at runtime and generation-checked dynamic identity are not added. +CLI uses `point-target NAME VIEW PHASE X Y`; existing `point` is unchanged. + +Each Program adds 80 B (24 three-byte routes, mask/count and alignment). Current +Program=9728 B, Pending=9992 B, Engine=20064 B, an additional 160 B of fixed engine +reservation over the shared-rules step. Handler remains 16 B, Instruction 16 B. +The active IR byte count now includes used routes. Linear memory remains 65536 B. +The host requires put_pointer_target, bind_target_handler, and dispatch_target. + +## Numeric host inputs and native document layout (2026-09-23) + +```text +state ratio = 50 +input split = value +on split.change { ratio = clamp(event_value,20,80) } +``` + +`value` is a numeric input kind, not a state initializer. Its only edge is +`.change`, event ID `33 + input_index` (33..40). `event_value` is reserved and +available only in these handlers; view expressions and pointer/keyboard/frame +bindings cannot read it. Shared rules may use it only when bound to a value input. +A rule requiring both pointer coordinates and a scalar value has no compatible +binding. Values are ephemeral event operands, not hidden state slots. + +Opcode26 pushes the event's numeric value. Core `dispatch_value(event,value)` +requires the scalar event range and a finite value. Ordinary dispatch/dispatch_point +reject scalar event IDs; scalar dispatch rejects other event IDs. Pure evaluation +rejects opcode26. The same ordered, atomic state commit and display-cache invalidation +apply. A later handler failure rolls back all earlier writes in this event. +No new persistent core tables are added: Engine remains 20064 B; linear memory is +65536 B. The existing event context's x payload holds the scalar under an explicit +scalar tag, separate from pointer context. Host requires dispatch_value export. + +Host `change(name,value,source='external')` validates the input kind and value, +returns the dispatch status, and records `.change` and `value` in the last event. +It does not acquire a held-input owner. Repeated equal requests still dispatch; +no authored side effects are dropped. Reentrant output callbacks cannot invoke it. +`down/up/point` reject numeric inputs. Source forms can submit finite numbers through +native number controls. CLI command is `change NAME VALUE`. + +`document.html` is a concrete host, not a generic UI declaration language. It uses +CSS Grid/media queries and native textarea editing, scrolling and text rendering. +The authored policy contains one scalar ratio and no output views. With no declared +views, display evaluation resets its budget then returns without allocating the +96-number/24-flag display publication buffers. No browser layout tree, text buffer, +scroll offset or viewport dimensions are copied into VM state. + +The document host projects ratio to CSS and accessible controls. Values outside +20..80 are rejected at publication, leaving the last valid visual ratio in place; +this host-output error is separate from VM arithmetic rollback. A later valid value +or corrected rule can restore valid output. Policy apply during its own splitter +capture is refused. The generic numeric port itself has no capture semantics. + +The splitter owns one pointer and an original ratio for cancel. Mouse, touch and +keyboard feed the same policy. Moves are coalesced to the latest coordinate once +per animation frame; release flushes a pending move and does not replace it with a +possibly stale release coordinate. Cancel/Escape/blur/capture loss restore the +original ratio. Crossing into narrow stacked layout cancels. Geometry is read at +gesture start and invalidated by resize observation, window resize and scroll; +observer callbacks do not write layout. No geometry reads occur in inactive resize, +scroll or text-edit handlers. A single geometry cache remains in the host between +gestures and is remeasured on every new gesture; it is not a layout-tree mirror. + +The textarea is the editable document authority. The reading DOM contains a second +text projection. There is no additional persistent JS string model, AST or document +copy in Wasm. This does NOT mean a zero-copy editor: native undo/layout storage, +DOM text and transient reads/decoding/encoding are outside core memory accounting. +Text edits schedule one reading publication per frame, deferred through composition. +Publication currently replaces the entire reading text; no incremental text format +or delta editor is claimed. Resize/split/scroll do not trigger text publication. + +Open/save use plain UTF-8 .txt, up to 2 MiB per imported/exported file. Invalid UTF-8 +and oversize files are refused without changing the current document. File reads +use generation/request guards against stale replacement. Native textarea normalizes +line endings; saved text uses LF and no BOM, not byte-identical preservation of +all original files. Files contain text only, not panel ratios or Pure rules. An +unavailable Wasm disables split policy controls while text editing/preview/save stay +available. Native undo is retained for ordinary editing; file/sample replacement +starts a new document value. No complete accessibility certification or physical +mobile/IME test is claimed by the browser automation. diff --git a/prototypes/workbench/README.md b/prototypes/workbench/README.md new file mode 100644 index 0000000..e55f6a2 --- /dev/null +++ b/prototypes/workbench/README.md @@ -0,0 +1,354 @@ +# 빈 작업물에서 시작하는 Pure + +자신이 만들고 싶은 작은 동작을 작성하는 실험이다. 처음에는 코드·장면·입력이 +모두 비어 있다. 상태, 입력, 도형과 동작을 추가하는 데 실행기 소스를 고칠 필요가 없다. + +이 디렉터리는 브라우저·CLI 검증 호스트, 선택 가능한 네이티브 계산 모듈과 Linux GTK 작성 창이다. +Pure의 최종 목표는 앱 로직·네트워크·RPC·DB·UI를 다루는 독립 네이티브 애플리케이션 런타임이다. +UI는 그중 하나의 하위 체계이며, 현재 호스트는 전체 런타임이나 일반 네이티브 UI 스택이 아니다. +네이티브 코어 검사와 Wasm 크기도 네이티브 UI 완성이나 전체 메모리 효율의 근거가 아니다. +현재 개발 방향은 [로드맵](../../docs/research/010-roadmap.md)을 따른다. + +CLI에는 기존 작업물을 실제 네이티브 서버·바이트 저장소에 저장하고 새 실행기에서 복원하는 +`connect`·`push`·`pull`이 추가됐다. [서버 실행과 사용법](../connections/README.md)을 따른다. +브라우저 저장 버튼은 이 서버에 자동 연결되지 않는다. + +같은 `.pure`를 **Node·브라우저 없이 실행하는 네이티브 프로그램**으로 빌드하는 경로도 있다. +빌드 시 기존 컴파일러를 재사용하며, 입력·상태 스냅샷·저장·복원을 C 실행 코어에 연결했다. +실행 전용 터미널 프로그램, 선택형 GTK 작성 창, 선택형 TLS 피어 호스트가 있다. +[빌드와 사용법](native/README.md), [작성 창의 비용과 검증](../../docs/research/025-native-authoring-shell.md), +[피어 연결과 전체 비용](../../docs/research/026-secure-peer-link.md). + +## 열기 + +저장소 루트에서 다음 명령을 실행한다. Python 3가 필요하며, 포함된 Wasm으로 바로 열린다. + +```sh +python3 prototypes/workbench/manage.py serve --port 8766 +``` + +[제작 화면](http://127.0.0.1:8766)을 연다. 기존 Dino 실험은 8765 포트를 사용한다. +서버는 로컬 컴퓨터에서만 접속할 수 있다. + +## 직접 만드는 흐름 + +1. 먼저 만들고 싶은 작은 동작을 정한다. 무엇을 기억하고, 어떤 입력에 어떻게 바뀔지 정하면 된다. +2. 빈 편집기에 작성한다. 화면 아래 **언어 안내**의 조각을 조합해도 된다. +3. **코드 적용**을 누르면 도형과 선언한 입력 버튼이 생긴다. +4. 버튼을 눌러보거나 장면을 클릭한 뒤 지정한 키를 누른다. `on frame`이 있다면 **실행** 또는 **한 프레임**으로 시간을 진행한다. +5. 필요한 상태·입력·도형·반응을 더 작성하고 다시 적용한다. 같은 이름의 상태 값은 이어진다. +6. **.pure 저장**으로 코드를, **배치 저장**으로 코드와 현재 배치를 보관한다. 새로고침으로 복원되지 않으므로 저장이 필요하다. + +문법이 막힐 때 볼 수 있는 최소 예시다. 이 예시를 실행하는 것 자체가 제작 경험 평가를 뜻하지는 않는다. + +```text +state x = 80 +input right = ArrowRight +circle seed(x, 180, 20, "#7ed9ae") +on right.down { + x = x + 20 +} +``` + +새 반응을 추가할 때 원하는 표현을 찾을 수 있는지, 어디서 막히는지가 이번에 확인할 점이다. +관찰은 **하려던 일 / 실제로 일어난 일 / 기대했던 일** 정도면 된다. +완성된 카운터·충전·전환·직접 조작·세 도형 배치 예제는 별도로 선택해 불러오며 자동 적용하지 않는다. + +## 적용·정지·오류의 의미 + +- 편집 중인 코드와 적용한 코드는 별개다. 저장은 **미적용 변경까지 포함한 편집기 내용**을 저장한다. +- 적용은 같은 이름의 숫자 상태를 보존한다. 새 이름은 초깃값으로 시작하고 삭제한 이름은 사라진다. + 이름 변경을 자동으로 추적하지 않는다. 초깃값만 바꿨다면 **초깃값으로**를 눌러야 현재 값도 바뀐다. +- 문법·이름·용량 오류가 있는 수정은 기존 작업을 유지한 채 거절한다. **문제 위치로**에서 해당 행으로 이동한다. +- 누르는 중에는 적용할 수 없다. 모든 입력을 놓은 뒤 적용한다. +- 정지는 시간 진행만 멈춘다. 입력 버튼은 정지 중에도 작동한다. +- 계산 실패는 해당 이벤트의 모든 상태 변경을 취소한다. 마지막 실패 안내는 놓기 동작으로 사라지지 않으며, + 수정 적용 또는 초기화로 지워진다. 잘못된 도형 크기·표시 계산은 별도의 표시 오류다. +- 편집기나 버튼에 포커스가 있으면 키 입력을 장면에 전달하지 않는다. 장면을 클릭하면 키를 사용할 수 있다. + 창에서 포커스가 벗어나면 시간을 멈추고 누른 입력을 놓는다. 키 입력의 `.up`, 직접 조작 입력의 `.cancel` 동작이 실행될 수 있다. +- 실행은 프레임당 `dt = 1/60`이다. 화면 갱신마다 최대 6프레임을 진행하며, 밀린 나머지는 건너뛰고 횟수를 표시한다. + +## 표현할 수 있는 범위 + +상태는 숫자다. 입력은 키의 누름·놓음과 대상에 연결한 포인터 좌표, 시간은 `on frame`으로 선언한다. +출력은 사각형·원·숫자 글자이며 좌표·크기·표시 값을 상태 표현식에 연결한다. +색상은 `"#rrggbb"` 상수다. 화면 좌표는 640×360 기준이다. + +```text +on right.up { x = 80 } +on frame { + x = x + 30 * dt + if x > 600 { x = 0 } +} +box tile(x, 220, 40, 40, "#356548") +text label(40, 60, x, "x: ") +``` + +연산: `+ - * /`, 비교, `&& || !`, `min`, `max`, `clamp`. +논리 연산은 양쪽을 모두 계산한다. `if`의 조건은 한 번만 계산하고 대입은 위에서 아래로 실행한다. +같은 이벤트에 여러 반응을 선언하면 작성 순서대로 실행하며 한 이벤트 전체를 함께 성공·실패 처리한다. +이벤트의 `dt`는 시간 진행에서 1/60, 입력에서는 0이다. 표시 표현식에서는 `dt`를 쓰지 않는다. + +키: Space, Enter, 방향키, KeyA~KeyZ, Digit0~Digit9. 한 키는 하나의 입력에 연결한다. +주석은 `#`, 문장 구분은 줄바꿈 또는 세미콜론이다. + +한도: 상태 32개, 입력 8개, 도형/글자 24개, 반응 64개, 중간 명령 512개, +계산 스택 64칸, 소스 32,768 UTF-8 바이트. 반복문·일반 함수 호출·문자열 상태·동적 배열은 없다. 매개변수 하나를 받는 공유 반응 규칙은 지원한다. +충돌·중력·점프 등의 동작은 자동으로 제공되지 않는다. +정확한 인터페이스와 명령 형식은 [CONTRACT.md](CONTRACT.md)에 있다. + +## 도형을 직접 잡아 조작하기 + +예제 선택에서 **직접 잡아 이동 → 예제 불러오기 → 코드 적용**을 누른다. +[direct.pure](examples/direct.pure)는 잡은 위치를 유지한 이동, 영역 제한, +연결된 숫자 출력, 취소 시 복귀를 작성한 코드다. 좌우 방향키로도 위치를 바꿀 수 있다. + +```text +input drag = pointer(tile) +on drag.move { + x = clamp(event_x - grab_x, 20, 564) + y = clamp(event_y - grab_y, 100, 276) +} +``` + +전체 예제에서 `.down`은 잡은 위치와 시작 위치를 기억하고, `.up`은 마지막으로 표시한 위치를 +확정하며, `.cancel`은 시작 위치로 되돌린다. 이 동작은 자동 제공되는 물리가 아니다. +제한 값이나 취소 규칙을 바꾸고 다시 적용하면 동작도 바뀐다. +`event_x/event_y`는 포인터 반응 안에서만 쓸 수 있는 장면 좌표다. +서로 다른 대상은 동시에 조작할 수 있고, 같은 대상의 두 번째 접촉은 무시한다. +초점·캡처를 잃으면 `.cancel`로 끝난다. 하나의 이벤트가 실패하면 그 이벤트만 취소된다. + +터미널에서도 같은 정의에 `point drag down 90 170`, `point drag move 360 220`, +`point drag up 410 240`을 보내 재현할 수 있다. +기능 검증과 범위는 [직접 조작 보고서](../../docs/research/013-authored-pointer-input.md)에 기록했다. + +## 배치를 저장하고 다시 열기 + +**세 도형 배치 → 예제 불러오기 → 코드 적용**으로 시작한다. +세 도형을 따로 움직일 수 있고, 마지막으로 잡은 도형은 방향키로 10씩 조정한다. +시간 진행과 조작을 끝낸 뒤 **배치 저장**을 누르면 현재 상태까지 포함된 JSON을 내려받는다. +**파일 열기**로 이 파일을 선택하면 새 페이지에서도 같은 배치가 복원된다. + +- 배치 파일에는 적용된 코드, 현재 숫자 상태, 아직 적용하지 않은 편집 초안이 들어 있다. +- 복원은 저장된 코드를 실행 정의로 사용한다. 미완성 초안은 편집기에 남기며 자동 적용하지 않는다. +- 조작 중인 입력과 실행 시계는 복원하지 않는다. 열린 작업은 정지 상태로 시작한다. +- **초깃값으로**는 코드의 원래 초깃값으로 돌아간다. 저장 당시 배치로 돌아가려면 파일을 다시 연다. +- **.pure 저장 / 코드 JSON 저장**은 기존처럼 초안만 저장한다. 배치 저장과 구분한다. +- 잘못된 파일이나 읽는 동안 작업이 바뀐 파일은 기존 작업을 유지한 채 거절한다. + +터미널에서도 같은 파일을 쓸 수 있다. + +```sh +node prototypes/workbench/cli.mjs my-layout.pure.json +``` + +명령 `save-project new-layout.json`으로 현재 배치를 저장하고 `restore saved-layout.json`으로 +복원한다. 기존 파일을 덮어쓰지 않는다. 브라우저에서 저장한 미적용 초안도 다시 저장할 때 보존한다. + +## 같은 규칙을 여러 대상에 연결하기 + +**공유 규칙으로 배치** 예제는 앞의 세 도형 배치와 같은 작업을 공통 규칙으로 작성한다. +`record`는 필드의 기본값, `item`은 대상별 실제 값, `rule`은 재사용할 반응이다. + +```text +record Position { x=0; y=0 } +item green = Position(x=80,y=120) +rule move(t: Position) { t.x=event_x; t.y=event_y } +input drag = pointer(tile) +box tile(green.x,green.y,56,44,"#c9f08b") +on drag.move use move(green) +``` + +여러 입력을 같은 규칙에 연결해도 규칙의 실행 명령은 한 벌만 저장한다. +매개변수의 필드는 연결된 대상의 값을 직접 바꾼다. 같은 사건의 여러 반응 중 하나가 +실패하면 앞의 변경도 함께 취소한다. 대상·필드 선언 순서를 바꾸어 적용해도 +`green.x` 같은 이름으로 값을 유지하고, 배치 파일도 그 이름을 저장한다. + +규칙은 레코드 매개변수 하나를 받는다. 레코드당 필드는 1~8개이며 모든 대상의 필드는 +기존 상태 32개 한도에 포함된다. 실행 중 대상 생성·삭제는 아직 없다. +전체 작성물은 [shared-layout.pure](examples/shared-layout.pure), 결과와 한계는 +[공유 규칙 보고서](../../docs/research/015-shared-rules.md)에 있다. + +## 여러 도형을 하나의 포인터 입력으로 연결하기 + +**입력 묶어서 배치 → 예제 불러오기 → 코드 적용**으로 시작한다. + +```text +input drag = pointer { green_box: green; blue_box: blue } +on drag.down use grab(drag.target) +on drag.move use move(drag.target) +on drag.up use drop(drag.target) +on drag.cancel use cancel(drag.target) +``` + +같은 record 타입의 대상을 묶고, 현재 잡은 대상의 필드를 공통 규칙에 전달한다. +도형을 늘려도 이 네 반응을 대상마다 복제하지 않는다. 서로 다른 도형은 동시에 +조작할 수 있고 하나를 놓거나 취소해도 다른 도형의 조작은 이어진다. +같은 대상은 한 그룹 연결만 가질 수 있다. 전체 연결 대상은 최대 24개이며 +상태 32개 등 기존 한도는 그대로다. 현재 예제의 키보드 선택 연결은 기존 방식이다. + +배치 파일은 이전과 같은 방식으로 저장·복원한다. 마지막 입력에는 대상 도형도 표시한다. +CLI에서는 `point-target drag green_box down 90 130`을 사용한다. +[전체 작성물](examples/routed-layout.pure), [구현 결과와 한계](../../docs/research/016-pointer-routing.md). + +## 일반 코드와 비교하기 + +[일반 JavaScript 비교 앱](http://127.0.0.1:8766/baseline.html)은 +[충전 예제](examples/charge.pure)와 같은 상태·출력 계산을 +[독립 JavaScript 모델](web/baseline.mjs)로 작성한 것이다. +Pure 컴파일러나 실행기를 가져오지 않는다. + +JS 모델은 고정된 앱이다. 일반 프로그램의 실행 중 교체나 상태 이전은 제공하지 않는다. +두 파일의 길이만으로 개발 생산성을 비교하지 않는다. 자세한 실행 비용과 범위는 +[연구 보고서 004](../../docs/research/004-authoring.md)에 있다. + +## 터미널에서 같은 작업 실행하기 + +Node.js가 필요하다. 검증에 사용한 버전은 24.20.0이다. + +```sh +node prototypes/workbench/cli.mjs my-work.pure +``` + +```text +down right +up right +frame 10 +show +load my-work.pure +reset +save saved-work.pure +quit +``` + +`load`는 같은 이름의 상태를 보존한다. `save`는 적용한 소스를 저장하고 기존 파일은 덮어쓰지 않는다. +경로에 공백이 있으면 명령 뒤에 경로 전체를 입력한다. 별도 따옴표는 붙이지 않는다. +브라우저와 터미널은 동일한 컴파일러·Wasm을 쓰지만 **각자 별도의 실행 상태**를 갖는다. + +## 계산 기록과 작업량 제한 + +표시 계산에 [기록을 버릴 수 있는 실행 계약](../../docs/research/009-bounded-display-integration.md)을 +연결했다. 모든 도형에 하나의 작업 한도를 적용한다. 한도를 넘으면 장면 전체를 +표시하지 않고 오류를 알린다. 그 전에 성공한 입력 이벤트의 상태는 유지된다. +기본값은 작업량 2,048, 캐시 꺼짐이다. 터미널에서 다음처럼 사용할 수 있다. + +```text +limits 32 8 2048 +show +costs +forget all +show +limits 32 8 1 +limits 32 8 2048 +``` + +`limits`의 숫자는 결과 캐시 개수·변경 기록 개수·표시 작업량이다. +`forget`은 `all`, `values`, `history`를 받는다. `costs`는 마지막 표시 계산의 +작업량·명령 수·캐시 적중을 보여준다. 한도가 부족하면 101 표시 오류가 나고, +충분히 늘리면 다시 계산한다. 파이프로 실행한 CLI는 중간 실패도 종료 코드 1에 남긴다. + +임베딩 API는 `createRuntime(wasmBytes, {cacheEntries:32, historyEntries:8, workLimit:2048})`, +`configureEvaluation`, `forgetEvaluation`, `evaluationStats`다. 브라우저 작성 화면에는 +이 설정을 위한 새 UI를 추가하지 않았다. 저장 공간 반납이나 앱 전체의 메모리·시간 +상한을 제공하는 기능은 아니다. 자세한 범위는 [계약](CONTRACT.md)을 따른다. + +## 빌드·검증 재현 + +Zig 0.15.2를 설치하고 저장소 루트에서 실행한다. 다른 경로의 Zig는 `--zig /path/to/zig`로 지정한다. + +```sh +python3 prototypes/workbench/manage.py build +node prototypes/workbench/compiler.test.mjs +node prototypes/workbench/runtime.test.mjs +node prototypes/workbench/baseline.test.mjs +node prototypes/workbench/cli.test.mjs +node prototypes/workbench/evaluation.test.mjs +node prototypes/workbench/workspace.test.mjs +node prototypes/workbench/presentation.test.mjs +node prototypes/workbench/routing.test.mjs +node prototypes/workbench/evaluation-check.mjs --output /tmp/pure-evaluation-new.json +node prototypes/workbench/workspace-check.mjs --output /tmp/pure-workspace-new.json +node prototypes/workbench/compare.mjs --output /tmp/pure-comparison-new.json +``` + +현재 빌드 명령은 네이티브 Zig 검사 37개도 실행한다. 전체 JS 검사는 +`node --test prototypes/workbench/*.test.mjs`로 실행하며 현재 87개다. +비교 파일은 덮어쓰지 않으므로 매번 새 출력 경로를 지정한다. +빌드는 `ReleaseSafe`, 디버그 정보 제거, 16 KiB 스택, 초기·최대 선형 메모리 64 KiB다. +산출물과 빌드 기록의 해시로 재현 대상을 확인할 수 있다. + +실제 브라우저 조작 시나리오는 [qa-browser.js](qa-browser.js)에 있다. +Playwright CLI 0.1.21의 `run-code`에 함수 전체를 전달하고, +마지막의 `window.__pureWorkbenchQAEvidence`를 후속 읽기로 수집했다. +대화상자·다운로드에서 CLI가 일찍 반환할 수 있으므로 반환 자체를 완료로 간주하지 않는다. +저장된 [브라우저 결과](results/2026-09-22-linux-aarch64/browser-checks.json)는 44개 확인을 포함한다. + +이 결과는 Linux aarch64에서 얻었다. M3 macOS, 네이티브 GUI·Metal, 센서 입력, +프로세스 전체 메모리와 실제 사용자의 제작 시간은 아직 측정하지 않았다. + +표시 계산의 첫 통합 기록은 +[2026-09-22-evaluation](results/2026-09-22-evaluation/verification.json)에 있다. +기존 44개 브라우저 확인은 이전 빌드의 기록이다. 이번 변경의 실제 브라우저 +확인 절차는 [qa-evaluation.js](qa-evaluation.js)다. +최신 빌드의 [브라우저 결과](results/2026-09-23-workspace/browser.json)는 12개 확인을 포함한다. +이 절차의 결과는 `window.__pureEvaluationEvidence`에서 읽는다. + +현재 빌드는 코드 교체 준비 공간과 계산 기록을 공유한다. 교체 준비 중에도 +기존 이벤트와 표시 계산은 실행할 수 있으며, 표시 계산은 기록 없이 재계산한다. +검사·비교 결과는 [2026-09-23-workspace](results/2026-09-23-workspace/verification.json)에 있다. +코어 고정 구조는 19,128 B로, 기록 기능을 넣기 전보다 증가한 전용 공간은 +56 B다. 공유 영역을 메모리 합계에 중복해서 더하지 않는다. + +현재 화면 갱신은 `present(target)`으로 계산 결과를 기존 DOM 요소에 전달한다. +매 입력마다 전체 장면 스냅샷이나 도형·상태·비용 요소를 다시 만들지 않는다. +CLI·검사용 `snapshot()`은 유지한다. [입력→출력 구현 보고서](../../docs/research/011-direct-presentation.md)에 +구조 변경, 추가 공간, 속도 측정의 한계를 기록했다. +직접 출력 통합 당시의 [52개 JS/CLI/Wasm 검사](results/2026-09-23-presentation/verification.json), +[브라우저 경로 비교](results/2026-09-23-presentation/browser.json), +[작성 흐름 검사](results/2026-09-23-presentation/browser-authoring.json), +[표시 한도 검사](results/2026-09-23-presentation/browser-limit.json)를 함께 보존했다. + +브라우저 비교는 Playwright CLI 0.1.21로 로컬 페이지를 연 세션에서 다음처럼 재실행한다. +이전 구현은 테스트 전용 응답 교체로 불러오며 현재 제품 파일은 바꾸지 않는다. + +```sh +python3 prototypes/workbench/qa-presentation.py --cli /path/to/playwright-cli.js --session pure-presentation --output /tmp/pure-presentation-new.json +``` + +후속으로 실행 입력과 코드 편집의 갱신 경로를 분리했다. 실행 중인 값이 바뀌어도 소스의 +줄 번호·크기·커서 정보를 다시 계산하지 않는다. 실제 소스 변경과 적용 상태는 계속 갱신한다. +[결과 보고서](../../docs/research/012-refresh-ownership.md), +[브라우저 비교](results/2026-09-23-refresh/browser.json), +[작성 흐름](results/2026-09-23-refresh/browser-authoring.json), +[표시 한도](results/2026-09-23-refresh/browser-limit.json)를 보존했다. +이 변경에서 런타임·컴파일러·Wasm은 그대로이며, 이전 단위 검사를 새 검사로 세지 않는다. + +```sh +python3 prototypes/workbench/qa-refresh.py --cli /path/to/playwright-cli.js --session pure-path --output /tmp/pure-refresh-new.json +``` + +## 실제 웹 레이아웃과 문서 편집 + +[문서 작업 화면](http://127.0.0.1:8766/document.html)은 일반 UTF-8 텍스트를 열고, +수정하고, 옆에서 읽고, `.txt`로 저장하는 작업이다. 기존 작업 화면에도 링크가 있다. + +- 넓은 화면에서는 경계를 잡거나 방향키·슬라이더로 두 패널의 비율을 조절한다. +- Esc·접촉 취소는 시작 비율로 돌아간다. 창 크기가 바뀌어도 조작 좌표를 다시 맞춘다. +- 좁은 화면에서는 작성·읽기가 위아래로 놓인다. 각 패널은 브라우저의 스크롤을 사용한다. +- **분할 규칙 수정**에서 `clamp(event_value,20,80)`의 제한을 더 좁혀 적용할 수 있다. +- 파일은 텍스트만 저장한다. 패널 비율과 규칙을 저장하는 별도 문서 형식은 만들지 않았다. + +이 화면은 직접 작성한 HTML 호스트다. Pure의 현재 DSL만으로 일반 웹 UI 전체를 +작성하게 된 것은 아니다. 레이아웃·텍스트·IME·선택을 VM에 복제하지 않고, 필요한 +숫자 조작만 전달한다. 파일은 2 MiB까지, UTF-8을 사용하며 저장 시 줄바꿈은 LF다. +현재 읽기 화면은 문서 편집 시 텍스트 전체를 갱신한다. 전체 앱의 메모리 절감이나 +큰 문서 성능 우위는 주장하지 않는다. [결과와 한계](../../docs/research/017-web-layout-boundary.md). + +```text +state ratio = 50 +input split = value +on split.change { ratio = clamp(event_value,20,80) } +``` + +웹 호스트 API는 `runtime.change('split',65,'range')`, CLI는 `change split 65`다. +숫자 값은 유한해야 하고 상태 배열에 자동 보관되지 않는다. 필요한 값만 규칙에서 저장한다. diff --git a/prototypes/workbench/baseline.test.mjs b/prototypes/workbench/baseline.test.mjs new file mode 100644 index 0000000..c9ef2de --- /dev/null +++ b/prototypes/workbench/baseline.test.mjs @@ -0,0 +1,78 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChargeModel } from './web/baseline.mjs'; + +test('reference begins with the authored states and matching scene', () => { + const model = createChargeModel(); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); + assert.deepEqual(model.snapshot().held, []); + assert.equal(model.snapshot().tick, 0); + assert.equal(model.snapshot().views.find(view => view.id === 'floor').y, 261); + assert.equal(model.snapshot().views.find(view => view.id === 'actor').radius, 20); +}); + +test('press, hold, release, and frame order produce exact expected values', () => { + const model = createChargeModel(); + model.down(); model.frame(15); + let expectedCharge = 0; + for (let frame = 0; frame < 15; frame++) expectedCharge = Math.min(Math.max(expectedCharge + 1 / 60, 0), 1); + assert.deepEqual(model.snapshot().states, { holding: 1, charge: expectedCharge, y: 240, velocity: 0 }); + model.up(); + const initialVelocity = -(160 + expectedCharge * 300); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: initialVelocity }); + model.frame(); + const nextVelocity = initialVelocity + 600 * (1 / 60); + assert.deepEqual(model.snapshot().states, { + holding: 0, charge: 0, y: 240 + nextVelocity * (1 / 60), velocity: nextVelocity, + }); +}); + +test('charge clamps at one and explicit floor restores rest', () => { + const model = createChargeModel(); + model.down(); model.frame(120); + assert.equal(model.snapshot().states.charge, 1); + model.up(); assert.equal(model.snapshot().states.velocity, -460); + model.frame(); assert.equal(model.snapshot().states.y, 232.5); + model.frame(120); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); +}); + +test('repeat and concurrent physical sources produce only logical edges', () => { + const model = createChargeModel(); + model.down('jump', 'keyboard'); model.frame(6); + const charge = model.snapshot().states.charge; + model.down('jump', 'keyboard'); model.down('jump', 'pointer'); + assert.equal(model.snapshot().states.charge, charge); + model.up('jump', 'keyboard'); + assert.equal(model.snapshot().states.holding, 1); + model.up('jump', 'unknown'); assert.equal(model.snapshot().states.holding, 1); + model.up('jump', 'pointer'); assert.equal(model.snapshot().states.velocity, -(160 + charge * 300)); + const released = model.snapshot(); model.up('jump', 'pointer'); + assert.deepEqual(model.snapshot(), released); +}); + +test('airborne presses cannot charge or cause another jump', () => { + const model = createChargeModel(); model.down(); model.up(); model.frame(); + const airborne = model.snapshot().states; + model.down(); model.up(); + assert.deepEqual(model.snapshot().states, airborne); +}); + +test('releaseAll emits release and reset clears latches without a jump', () => { + const model = createChargeModel(); + model.down('jump', 'keyboard'); model.down('jump', 'pointer'); model.frame(10); + model.releaseAll(); assert.equal(model.snapshot().states.holding, 0); + assert.ok(model.snapshot().states.velocity < 0); assert.deepEqual(model.snapshot().held, []); + model.reset(); assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); + assert.equal(model.snapshot().tick, 0); + model.down(); model.reset(); model.up(); assert.equal(model.snapshot().states.velocity, 0); +}); + +test('unknown inputs and invalid frame counts reject without mutation', () => { + const model = createChargeModel(), initial = model.snapshot(); + for (const method of ['down', 'up']) assert.throws(() => model[method]('other'), /Unknown input/); + for (const count of [-1, .5, 601, Infinity, NaN, '1']) assert.throws(() => model.frame(count), /integer/); + model.frame(0); assert.deepEqual(model.snapshot(), initial); + const snapshot = model.snapshot(); snapshot.states.y = -999; + assert.equal(model.snapshot().states.y, 240); +}); diff --git a/prototypes/workbench/cli.mjs b/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/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/workbench/cli.test.mjs b/prototypes/workbench/cli.test.mjs new file mode 100644 index 0000000..f355d2d --- /dev/null +++ b/prototypes/workbench/cli.test.mjs @@ -0,0 +1,121 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => { + const file = join(root, 'examples/direct.pure'); + const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]]) + runtime.point('drag', phase, x, y, 'terminal'); + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert.equal(result.snapshots.at(-1).states.x, 350); + assert.equal(result.snapshots.at(-1).states.y, 210); + assert.deepEqual(result.snapshots.at(-1).held, []); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); + +test('terminal saves and restores placement independently of reset and preserves a browser draft', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-layout-cli-')); + t.after(() => rm(directory, {recursive:true,force:true})); + const saved=join(directory,'layout.json'), copied=join(directory,'copy.json'); + const first=run(`point drag down 90 170\npoint drag move 360 220\npoint drag up 90 170\nsave-project ${saved}\nreset\nrestore ${saved}\nshow\nquit\n`,join(root,'examples/direct.pure')); + assert.equal(first.status,0,first.stderr);assert.equal(first.snapshots.at(-1).states.x,350); + const document=JSON.parse(await readFile(saved,'utf8'));document.draft='state incomplete ='; + await writeFile(saved,JSON.stringify(document)); + const second=run(`save-project ${copied}\nreset\nshow\nquit\n`,saved); + assert.equal(second.status,0,second.stderr);assert.equal(second.snapshots.at(-1).states.x,80); + const copy=JSON.parse(await readFile(copied,'utf8'));assert.equal(copy.draft,document.draft);assert.deepEqual(copy.states,document.states); + const rejected=run(`save-project ${saved}\nquit\n`,saved); + assert.equal(rejected.status,1);assert.match(rejected.stderr,/EEXIST/); +}); + +test('invalid restoration leaves terminal code and state unchanged', async t => { + const directory=await mkdtemp(join(tmpdir(),'pure-invalid-layout-')); + t.after(()=>rm(directory,{recursive:true,force:true})); + const file=join(directory,'bad.json'); + await writeFile(file,JSON.stringify({format:'pure-workbench',version:2,source:'state x=1',states:[]})); + const result=run(`show\nrestore ${file}\nshow\nquit\n`,join(root,'examples/counter.pure')); + assert.equal(result.status,1);assert.deepEqual(result.snapshots[0],result.snapshots[1]); +}); diff --git a/prototypes/workbench/compare.mjs b/prototypes/workbench/compare.mjs new file mode 100644 index 0000000..ceadb0c --- /dev/null +++ b/prototypes/workbench/compare.mjs @@ -0,0 +1,231 @@ +// Reproduce the fixed charge workload in the Wasm-host runtime and handwritten JS. +// This compares full host calls, not VM instructions or feature-equivalent products. +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { cpus, platform, release, arch, availableParallelism } from 'node:os'; +import { performance } from 'node:perf_hooks'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; +import { createChargeModel } from './web/baseline.mjs'; + +const args = process.argv.slice(2); +if (args.length && !(args.length === 2 && args[0] === '--output')) { + throw new Error('Usage: node compare.mjs [--output report.json]'); +} +const startedAt = new Date().toISOString(); +const seed = 20260922; +const trials = 7; +const warmupTraces = 5; +const targetTrialMs = 12; +const absoluteTolerance = 1e-11; +const relativeTolerance = 1e-12; +const files = { + charge: new URL('./examples/charge.pure', import.meta.url), + baseline: new URL('./web/baseline.mjs', import.meta.url), + runtime: new URL('./web/runtime.mjs', import.meta.url), + compiler: new URL('./web/compiler.mjs', import.meta.url), + wasm: new URL('./web/core.wasm', import.meta.url), + harness: new URL('./compare.mjs', import.meta.url), +}; +const contents = Object.fromEntries(await Promise.all( + Object.entries(files).map(async ([name, path]) => [name, await readFile(path)]), +)); +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +const sourceInfo = name => { + const text = contents[name].toString('utf8'); + return { + bytes: contents[name].length, + physicalLines: text === '' ? 0 : text.split('\n').length - Number(text.endsWith('\n')), + nonblankLines: text.split('\n').filter(line => line.trim().length).length, + sha256: hash(contents[name]), + }; +}; + +function makeTrace() { + const trace = []; + const action = (method, source = undefined) => trace.push({ method, ...(source === undefined ? {} : { source }) }); + const frames = count => { + // Individual calls let correctness checks inspect every frame, not just a batch end. + for (let i = 0; i < count; i++) action('frame'); + }; + action('reset'); action('up', 'unknown'); + action('down', 'keyboard'); action('down', 'keyboard'); frames(1); + action('down', 'pointer:1'); frames(59); action('up', 'keyboard'); frames(10); + action('up', 'pointer:1'); frames(6); + action('down', 'keyboard'); frames(2); action('up', 'keyboard'); frames(120); + action('down', 'pointer:1'); frames(600); action('releaseAll'); frames(120); + action('down', 'keyboard'); action('reset'); action('up', 'keyboard'); frames(1); + let random = seed >>> 0; + const next = limit => { + random = (Math.imul(random, 1664525) + 1013904223) >>> 0; + return random % limit; + }; + for (let cycle = 0; cycle < 48; cycle++) { + const source = next(2) ? 'keyboard' : 'pointer:1'; + action('down', source); frames(next(91)); action('down', source); + if (cycle % 3 === 0) action('down', 'pointer:2'); + frames(next(11)); action('up', source); + if (cycle % 3 === 0) { frames(next(17)); action('up', 'pointer:2'); } + if (cycle % 7 === 0) action('reset'); + else if (cycle % 5 === 0) action('releaseAll'); + frames(next(121)); + } + action('releaseAll'); frames(180); + return trace; +} + +const trace = makeTrace(); +function traceCounts() { + const counts = { hostActions: trace.length, frames: 0, logicalDown: 0, + logicalUp: 0, resets: 0, releaseAllCalls: 0, downCalls: 0, upCalls: 0 }; + const held = new Set(); + for (const action of trace) { + if (action.method === 'frame') counts.frames++; + else if (action.method === 'reset') { counts.resets++; held.clear(); } + else if (action.method === 'releaseAll') { + counts.releaseAllCalls++; + if (held.size) counts.logicalUp++; + held.clear(); + } else if (action.method === 'down') { + counts.downCalls++; + if (!held.size) counts.logicalDown++; + held.add(action.source); + } else { + counts.upCalls++; + if (held.delete(action.source) && !held.size) counts.logicalUp++; + } + } + counts.dispatchedEvents = counts.frames + counts.logicalDown + counts.logicalUp; + return counts; +} +const counts = traceCounts(); +const pure = await createRuntime(contents.wasm); +const program = pure.apply(contents.charge.toString('utf8')); +const baseline = createChargeModel(); +function perform(model, action) { + switch (action.method) { + case 'frame': return model.frame(1); + case 'down': return model.down('jump', action.source); + case 'up': return model.up('jump', action.source); + case 'reset': model.reset(); return 0; + case 'releaseAll': return model.releaseAll(); + default: throw new Error(`Unknown trace action ${action.method}`); + } +} + +let checkedNumbers = 0, maxAbsoluteDifference = 0; +function compare(actual, expected, path) { + if (typeof expected === 'number') { + if (!Number.isFinite(actual) || !Number.isFinite(expected)) throw new Error(`${path}: non-finite value`); + const difference = Math.abs(actual - expected); + maxAbsoluteDifference = Math.max(maxAbsoluteDifference, difference); + checkedNumbers++; + if (difference > absoluteTolerance + relativeTolerance * Math.max(Math.abs(actual), Math.abs(expected))) { + throw new Error(`${path}: ${actual} != ${expected}`); + } + } else if (Array.isArray(expected)) { + if (!Array.isArray(actual) || actual.length !== expected.length) throw new Error(`${path}: array shape mismatch`); + expected.forEach((item, index) => compare(actual[index], item, `${path}[${index}]`)); + } else if (expected && typeof expected === 'object') { + if (!actual || typeof actual !== 'object') throw new Error(`${path}: object missing`); + const actualKeys = Object.keys(actual).filter(key => actual[key] !== undefined).sort(); + const expectedKeys = Object.keys(expected).filter(key => expected[key] !== undefined).sort(); + if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) throw new Error(`${path}: object keys mismatch`); + for (const key of expectedKeys) compare(actual[key], expected[key], `${path}.${key}`); + } else if (actual !== expected) throw new Error(`${path}: ${JSON.stringify(actual)} != ${JSON.stringify(expected)}`); +} +function checkSnapshots(index) { + const actual = pure.snapshot(), expected = baseline.snapshot(); + if (actual.lastError) throw new Error(`Action ${index}: ${actual.errorMessage}`); + compare(actual.states, expected.states, `action[${index}].states`); + compare(actual.views, expected.views, `action[${index}].views`); + compare(actual.tick, expected.tick, `action[${index}].tick`); + compare(actual.held, expected.held, `action[${index}].held`); +} +checkSnapshots(-1); +for (let index = 0; index < trace.length; index++) { + for (const model of [pure, baseline]) { + const code = perform(model, trace[index]); + if (code !== 0) throw new Error(`Action ${index}: execution status ${code}`); + } + checkSnapshots(index); +} +const correctness = { passed: true, checkedSnapshots: trace.length + 1, + checkedNumbers, maxAbsoluteDifference, absoluteTolerance, relativeTolerance, + checkedFields: ['states', 'all view fields', 'tick', 'held input names'], + traceSha256: hash(JSON.stringify(trace)), counts }; +const memory = pure.snapshot().memory; + +// Snapshot construction, binding evaluation, compilation, Wasm loading, and DOM +// rendering are deliberately outside these timed loops. Host dispatch is inside. +function run(model, repetitions) { + for (let repeat = 0; repeat < repetitions; repeat++) { + for (const action of trace) { + const code = perform(model, action); + if (code !== 0) throw new Error(`Timing run failed with execution status ${code}`); + } + } +} +const models = { pureHostWasm: pure, handwrittenJavaScript: baseline }; +for (const model of Object.values(models)) run(model, warmupTraces); +const repetitions = {}; +const calibration = {}; +for (const [name, model] of Object.entries(models)) { + const start = performance.now(); run(model, 3); + const elapsedMs = performance.now() - start; + repetitions[name] = Math.max(1, Math.min(2048, Math.ceil(targetTrialMs / Math.max(elapsedMs / 3, .001)))); + calibration[name] = { elapsedMs, traces: 3, selectedRepetitions: repetitions[name] }; +} +const samples = []; +for (let trial = 0; trial < trials; trial++) { + const order = trial % 2 === 0 ? Object.keys(models) : Object.keys(models).reverse(); + for (const [position, name] of order.entries()) { + const start = performance.now(); run(models[name], repetitions[name]); + const elapsedMs = performance.now() - start; + const nsPerTrace = elapsedMs * 1e6 / repetitions[name]; + samples.push({ trial: trial + 1, position: position + 1, implementation: name, + repetitions: repetitions[name], elapsedMs, nsPerTrace, + nsPerHostAction: nsPerTrace / counts.hostActions, + nsPerDispatchedEvent: nsPerTrace / counts.dispatchedEvents }); + } +} +// Each trace begins with reset. Both timing implementations must still agree. +checkSnapshots('after timing'); +const summary = {}; +for (const name of Object.keys(models)) { + const values = samples.filter(sample => sample.implementation === name) + .map(sample => sample.nsPerHostAction).sort((a, b) => a - b); + summary[name] = { medianNsPerHostAction: values[Math.floor(values.length / 2)], + minNsPerHostAction: values[0], maxNsPerHostAction: values.at(-1) }; +} +summary.pureOverJavaScriptMedianRatio = summary.pureHostWasm.medianNsPerHostAction + / summary.handwrittenJavaScript.medianNsPerHostAction; + +const report = { + experiment: 'Pure Workbench charge / handwritten JavaScript reference', + environment: { startedAt, completedAt: new Date().toISOString(), + node: process.version, v8: process.versions.v8, platform: platform(), osRelease: release(), + architecture: arch(), cpuModel: cpus()[0]?.model ?? 'unknown', + logicalCPUs: cpus().length, availableParallelism: availableParallelism(), + affinityControlled: false, powerControlled: false, + command: `node ${fileURLToPath(import.meta.url)}${args.length ? ` --output ${args[1]}` : ''}` }, + artifacts: Object.fromEntries(Object.keys(files).map(name => [name, + { bytes: contents[name].length, sha256: hash(contents[name]) }])), + correctness, + sources: { chargePure: sourceInfo('charge'), fixedJavaScriptModel: sourceInfo('baseline'), + note: 'Raw file size and line counts only; excludes HTML/host/compiler and does not establish authoring effort or DX quality.' }, + compilerMetrics: program.metrics, + memory: { pure: memory, + note: 'Wasm linear capacity and core layout counters; not process RSS or browser memory. No equivalent JS heap measurement collected. Do not add overlapping counters.' }, + timing: { seed, warmupTracesPerImplementation: warmupTraces, + trialsPerImplementation: trials, targetTrialMs, calibration, traceCounts: counts, + order: 'Alternated each trial; Pure first on odd trials, JavaScript first on even trials.', + included: 'Host method dispatch, source-latch aggregation, state logic, and resets in the fixed trace. Pure additionally maintains generic VM/event diagnostics.', + excluded: 'Compilation, loading, snapshots, binding evaluation, DOM rendering, scene editing, and live apply.', + normalization: 'nsPerHostAction divides total time by calls including repeats, releaseAll and reset; nsPerDispatchedEvent also amortizes that host overhead.', + limitations: 'Single-machine batch throughput; no CPU pinning or energy/thermal control. Seven trial averages are not event latency percentiles. Fixed JS has no live program replacement, general compiler, or state migration; not feature-equivalent whole-product timing.', + samples, summary }, +}; +const json = JSON.stringify(report, null, 2) + '\n'; +if (args.length) await writeFile(args[1], json, { flag: 'wx' }); +process.stdout.write(json); diff --git a/prototypes/workbench/compiler.test.mjs b/prototypes/workbench/compiler.test.mjs new file mode 100644 index 0000000..89b8c6c --- /dev/null +++ b/prototypes/workbench/compiler.test.mjs @@ -0,0 +1,240 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile, LIMITS } from './web/compiler.mjs'; + +function diagnostic(source, pattern, line, column) { + assert.throws(() => compile(source), error => { + assert.match(error.message, pattern); + assert.ok(Number.isInteger(error.line) && error.line > 0); + assert.ok(Number.isInteger(error.column) && error.column > 0); + if (line !== undefined) assert.equal(error.line, line); + if (column !== undefined) assert.equal(error.column, column); + return true; + }); +} + +// A small independent evaluator checks compiler lowering. The product executes +// these instructions in Zig; the VM has its own native and integration checks. +function evaluate(program, start, length, values, dt = 0) { + const stack = []; + for (let pc = start; pc < start + length; pc++) { + const instruction = program.instructions[pc]; + const { op, arg, value } = instruction; + if (op === 0) stack.push(value); + else if (op === 1) stack.push(values[arg]); + else if (op === 2) stack.push(dt); + else if (op === 7) stack.push(-stack.pop()); + else if (op === 16) stack.push(Number(!stack.pop())); + else if (op === 19) { + const high = stack.pop(), low = stack.pop(), x = stack.pop(); + assert.ok(low <= high); stack.push(Math.min(Math.max(x, low), high)); + } else if (op === 20) values[arg] = stack.pop(); + else if (op === 21) { if (!stack.pop()) pc = arg - 1; } + else { + const right = stack.pop(), left = stack.pop(); + const operations = { + 3: () => left + right, 4: () => left - right, 5: () => left * right, + 6: () => left / right, 8: () => Number(left < right), + 9: () => Number(left <= right), 10: () => Number(left > right), + 11: () => Number(left >= right), 12: () => Number(left === right), + 13: () => Number(left !== right), 14: () => Number(Boolean(left) && Boolean(right)), + 15: () => Number(Boolean(left) || Boolean(right)), + 17: () => Math.min(left, right), 18: () => Math.max(left, right), + }; + assert.ok(operations[op], `Unknown opcode ${op}`); + stack.push(operations[op]()); + } + assert.ok(stack.every(Number.isFinite)); + } + return stack; +} + +function dispatch(program, event, values, dt = 0) { + for (const handler of program.handlers.filter(handler => handler.event === event)) { + assert.deepEqual(evaluate(program, handler.start, handler.length, values, dt), []); + } +} + +test('a blank document is an empty program with no implied behavior', () => { + const result = compile(''); + for (const field of ['states', 'inputs', 'views', 'instructions', 'handlers']) assert.deepEqual(result[field], []); + assert.equal(result.metrics.sourceBytes, 0); + assert.equal(result.metrics.maxStack, 0); + assert.deepEqual(compile('# comment only\n;').instructions, []); +}); + +test('forward references and separate state/input namespaces preserve declaration IDs', () => { + const program = compile('on tap.down { second = tap + first }\ntext label(0,0,second)\nstate first = -2\nstate tap = +3\ninput tap = Space\nstate second = .5e2'); + assert.deepEqual(program.states.map(state => state.name), ['first', 'tap', 'second']); + assert.equal(program.handlers[0].event, 1); + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + assert.deepEqual(values, [-2, 3, 1]); + const binding = program.views[0].bindings.value; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [1]); +}); + +test('ordered assignments, repeated handlers, and condition evaluation once', () => { + const program = compile(`state x = 1 +input tap = Enter +on tap.down { + if x == 1 { + x = 0 + x = x + 5 + } +} +on tap.down { x = x * 2 } +on tap.up { x = x - 1 }`); + const values = [1]; + dispatch(program, 1, values); + assert.equal(values[0], 10); + dispatch(program, 2, values); + assert.equal(values[0], 9); + const branch = program.instructions.find(instruction => instruction.op === 21); + assert.equal(branch.arg, program.handlers[0].start + program.handlers[0].length); +}); + +test('nested branches use absolute forward targets and skip correct bodies', () => { + const program = compile(`state x = 0 +text label(0, 0, x) +on frame { + if x > 0 { + x = 8 + if x == 8 { x = 9 } + } + x = x + 1 +}`); + const values = [0]; + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 1); + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 10); + const handler = program.handlers[0]; + program.instructions.forEach((instruction, pc) => { + if (instruction.op === 21) assert.ok(instruction.arg > pc && instruction.arg <= handler.start + handler.length); + }); +}); + +test('operator precedence, associativity, unary and builtin function lowering', () => { + const expressions = new Map([ + ['2 + 3 * 4', 14], ['8 - 3 - 2', 3], ['12 / 3 / 2', 2], + ['-2 * -3', 6], ['!0 + !4', 1], ['1 || 0 && 0', 1], + ['1 < 2 && 2 <= 2 && 4 > 3 && 4 >= 4', 1], + ['3 == 3 && 3 != 4', 1], ['clamp(max(3, 9) - min(4, 7), 0, 4)', 4], + ]); + for (const [source, expected] of expressions) { + const program = compile(`state x = 0\non frame { x = ${source} }`); + const values = [0]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], expected, source); + } + const eager = compile('state x = 0\non frame { x = 1 || x / 0 }'); + assert.ok(eager.instructions.some(instruction => instruction.op === 6)); + assert.ok(!eager.instructions.some(instruction => instruction.op === 21)); +}); + +test('multiline parentheses, scientific literals, comments, and string escapes', () => { + const program = compile(`state x = 1e-2 # comment +box tile( + (x + + 2), 0, max( + 1, 2), 4, + "#AABBCC") +text label(0, 0, x, "# count: \\\"value\\\" \\n") +on frame { x = x + dt }`); + assert.equal(program.views[0].color, '#AABBCC'); + assert.equal(program.views[1].prefix, '# count: "value" \n'); + const values = [.01]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], .01 + 1 / 60); + const binding = program.views[0].bindings.x; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [values[0] + 2]); +}); + +test('all instruction fields are numeric and carry original source line', () => { + const program = compile('state x = 0\non frame {\n x = x + dt\n}'); + for (const instruction of program.instructions) { + assert.deepEqual(Object.keys(instruction), ['op', 'arg', 'value', 'line']); + assert.ok(Object.values(instruction).every(Number.isFinite)); + assert.equal(instruction.line, 3); + } +}); + +test('contextual declaration words remain valid names outside reserved builtins', () => { + const program = compile('state if = 1\nstate state = 2\non frame { if = if + state }'); + const values = [1, 2]; dispatch(program, 0, values, 1 / 60); + assert.deepEqual(values, [3, 2]); +}); + +test('unknown and reserved symbols have useful one-based locations', () => { + diagnostic('state x = 0\non frame {\n x = missing\n}', /Unknown state missing/, 3, 7); + diagnostic('on missing.down {}', /Unknown input missing/, 1, 4); + diagnostic('on frame { absent = 1 }', /Unknown state absent/, 1, 12); + for (const name of ['frame', 'dt', 'min', 'max', 'clamp']) { + diagnostic(`state ${name} = 0`, /Reserved name/, 1, 7); + diagnostic(`input ${name} = Space`, /Reserved name/); + } + diagnostic('text clock(0, 0, dt)', /dt is only available in handlers/); +}); + +test('duplicates, unsupported keys, initializers, and invalid syntax are rejected', () => { + const invalid = [ + ['state x=0\nstate x=1', /Duplicate state/], + ['input tap=Space\ninput tap=Enter', /Duplicate input/], + ['input tap=Space\ninput other=Space', /Duplicate keyboard/], + ['box a(0,0,1,1,"#000000")\ncircle a(0,0,1,"#000000")', /Duplicate view/], + ['input tap=Escape', /Unsupported keyboard/], + ['state x=1+2', /Expected newline/], + ['state x=1e999', /finite/], + ['state x=NaN', /numeric state initializer/], + ['on tap.click {}', /Expected down or up/], + ['on frame.down {}', /Expected {/], + ['state x=0\non frame { x = min(1) }', /min expects 2/], + ['state x=0\non frame { x = clamp(1,2) }', /clamp expects 3/], + ['state x=0\non frame { x = 1 +\n2 }', /Expected a number/], + ['text a(0,0,1,"broken)', /Unterminated string/], + ['text a(0,0,1,"bad\\q")', /Unsupported string escape/], + ['box a(0,0,1,1,"blue")', /Color must use/], + ['state x=0 @', /Unexpected character/], + ['on frame {', /assignment, if, or closing brace/], + ['state x = 0 state y = 1', /Expected newline/], + ]; + for (const [source, pattern] of invalid) diagnostic(source, pattern); +}); + +test('document, declaration, instruction, and nesting limits reject overflow', () => { + diagnostic('#' + '한'.repeat(11000), /32768 UTF-8 bytes/); + diagnostic(Array.from({ length: 33 }, (_, i) => `state s${i}=0`).join('\n'), /32 states/); + diagnostic(Array.from({ length: 9 }, (_, i) => `input i${i}=Digit${i}`).join('\n'), /8 inputs/); + diagnostic(Array.from({ length: 25 }, (_, i) => `text t${i}(0,0,0)`).join('\n'), /24 views/); + diagnostic('on frame {}\n'.repeat(65), /64 handlers/); + diagnostic(`state x=0\non frame {\n${'x=1\n'.repeat(257)}}`, /512 instructions/); + assert.equal(compile(`state x=0\non frame {\n${'x=1\n'.repeat(256)}}`).instructions.length, LIMITS.instructions); + diagnostic(`state x=0\non frame { x=${'('.repeat(65)}1${')'.repeat(65)} }`, /nesting exceeds 64/); + diagnostic(`on frame { ${'if 1 { '.repeat(65)}${'}\n'.repeat(66)}`, /nesting exceeds 64/); + // Right-nested clamp's two waiting operands exceed the stack before its depth limit. + let deep = '0'; + for (let i = 0; i < 33; i++) deep = `clamp(0,0,${deep})`; + diagnostic(`text stack(0,0,${deep})`, /stack exceeds 64/); +}); + +test('every supported input maps to a bounded ordered event ID', () => { + const keys = ['Space', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyZ', 'Digit9']; + const program = compile(keys.map((key, i) => `input i${i}=${key}\non i${i}.down {}\non i${i}.up {}`).join('\n')); + assert.deepEqual(program.handlers.map(handler => handler.event), Array.from({ length: 16 }, (_, i) => i + 1)); +}); + +test('optional examples compile and charge behavior is entirely authored', async () => { + for (const name of ['counter', 'charge', 'toggle']) { + const program = compile(await readFile(new URL(`./examples/${name}.pure`, import.meta.url), 'utf8')); + assert.ok(program.instructions.length <= LIMITS.instructions); + assert.ok(program.views.length > 0); + if (name === 'charge') { + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + for (let frame = 0; frame < 90; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], 1); assert.equal(values[1], 1); assert.equal(values[2], 240); + dispatch(program, 2, values); assert.deepEqual(values, [0, 0, 240, -460]); + dispatch(program, 0, values, 1 / 60); assert.ok(values[2] < 240); + for (let frame = 0; frame < 120; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[2], 240); assert.equal(values[3], 0); + } + } +}); diff --git a/prototypes/workbench/core.zig b/prototypes/workbench/core.zig new file mode 100644 index 0000000..9b5e213 --- /dev/null +++ b/prototypes/workbench/core.zig @@ -0,0 +1,999 @@ +//! Browser ABI and regression tests for the explicit-instance scalar engine. +const std = @import("std"); +const impl = @import("engine.zig"); +const Engine = impl.Engine; +const Code = impl.Code; +const max_states = impl.max_states; +const max_instructions = impl.max_instructions; +const max_stack = impl.max_stack; +const no_pc = impl.no_pc; +const frame_dt = impl.frame_dt; +const Pending = impl.Pending; +const Workspace = impl.Workspace; +const Tables = impl.Tables; + +var engine: Engine = .{}; + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +export fn reset_all() void { + engine.reset_all(); +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + return engine.begin_program(state_count, input_count); +} + +export fn abort_program() void { + engine.abort_program(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + return engine.put_state(id, initial, old_id); +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + return engine.put_instruction(op, arg, value); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + return engine.put_handler(event, start, length); +} + +export fn bind_handler(handler: u32, base: u32, count: u32) u32 { + return engine.bind_handler(handler, base, count); +} + +export fn put_pointer_target(input: u32, base: u32, count: u32) u32 { + return engine.put_pointer_target(input, base, count); +} + +export fn bind_target_handler(handler: u32, count: u32) u32 { + return engine.bind_target_handler(handler, count); +} + +export fn put_restored_value(id: u32, value: f64) u32 { + return engine.put_restored_value(id, value); +} + +export fn commit_program() u32 { + return engine.commit_program(); +} + +export fn commit_restored_program() u32 { + return engine.commit_restored_program(); +} + +export fn reset_state() void { + engine.reset_state(); +} + +export fn dispatch(event: u32, dt: f64) u32 { + return engine.dispatch(event, dt); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return engine.dispatch_point(event, x, y); +} + +export fn dispatch_target(event: u32, target: u32, x: f64, y: f64) u32 { + return engine.dispatch_target(event, target, x, y); +} + +export fn dispatch_value(event: u32, value: f64) u32 { + return engine.dispatch_value(event, value); +} + +export fn eval_at(start: u32, length: u32) f64 { + return engine.eval_at(start, length); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return engine.configure_evaluation(cache, history); +} + +export fn forget_evaluation(flags: u32) void { + engine.forget_evaluation(flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.begin_evaluation(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + return engine.eval_bounded(start, length); +} + +export fn evaluation_info(id: u32) u32 { + return engine.evaluation_info(id); +} + +export fn get_state(id: u32) f64 { + return engine.get_state(id); +} + +export fn info(id: u32) u32 { + return engine.info(id); +} + +export fn layout(id: u32) u32 { + return engine.layout(id); +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 16), layout(3)); + try std.testing.expectEqual(@as(u32, 80), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "shared handler bodies access their own bound state slots in source order" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try emit(24, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(1, 1, 1)); + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(2, 0, 1)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(f64, 21), get_state(1)); + try std.testing.expectEqual(@as(u32, 4), info(6)); // Three bindings, one body. + try std.testing.expectEqual(@as(u32, 12), info(3)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); +} + +test "failure in a later shared binding rolls back all earlier bindings" { + reset_all(); + try ok(begin_program(4, 1)); + for ([_]f64{ 8, 2, 12, 0 }, 0..) |value, i| try ok(put_state(@intCast(i), value, -1)); + try emit(24, 0, 0); + try emit(24, 1, 0); + try emit(6, 0, 0); + try emit(25, 0, 0); + for (0..2) |i| { + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(@intCast(i), @intCast(i * 2), 2)); + } + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(2)); + try std.testing.expectEqual(@as(u32, 0), info(9)); +} + +test "shared bindings reject missing arguments partial overlaps and invalid slot mappings" { + reset_all(); + try counterProgram(); + const revision = info(4); + for (0..5) |mode| { + try ok(begin_program(1, 1)); + try ok(put_state(0, 9, -1)); + try emit(24, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(1, 0, 2)); + switch (mode) { + 0 => {}, // Parameter op without a binding. + 1 => { + try ok(bind_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), bind_handler(0, 0, 1)); + }, + 2 => { + try std.testing.expectEqual(public(.bad_state), bind_handler(0, 0, 2)); + }, + 3 => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 1, 1)); + try ok(bind_handler(1, 0, 1)); + }, + else => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 2)); // Another owner missing its mapping. + }, + } + try std.testing.expect(commit_program() != 0); + abort_program(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + } +} + +test "one routed handler changes only its event target and rejects missing or foreign routes" { + reset_all(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try ok(put_pointer_target(0, 0, 1)); + try ok(put_pointer_target(0, 1, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try ok(commit_program()); + try ok(dispatch_target(17, 1, 77, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(3)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(17, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, 2, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, no_pc, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(18, 0, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(0, 0, 99, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_target(17, 0, std.math.inf(f64), 0)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 3), layout(8)); + try std.testing.expectEqual(@as(u32, 2), layout(9)); + try std.testing.expectEqual(@as(u32, 54), layout(4)); +} + +test "routes reject invalid mappings and bindings without replacing active work" { + for (0..7) |mode| { + reset_all(); + try counterProgram(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + switch (mode) { + 0 => try std.testing.expectEqual(public(.bad_state), put_pointer_target(0, 1, 2)), + 1 => try std.testing.expectEqual(public(.bad_event), put_pointer_target(2, 0, 1)), + 2 => { + try ok(put_pointer_target(0, 0, 2)); + try std.testing.expectEqual(public(.invalid), put_pointer_target(1, 1, 1)); + }, + else => { + if (mode != 3) try ok(put_pointer_target(0, 0, 1)); + try emit(0, 0, 2); + try emit(25, 0, 0); + try ok(put_handler(if (mode == 4) 0 else 1, 0, 2)); + try ok(bind_target_handler(0, if (mode == 5) 2 else 1)); + if (mode == 6) try std.testing.expectEqual(public(.invalid), bind_target_handler(0, 1)); + }, + } + try std.testing.expect(commit_program() != 0); + // Active program still executes while the failed candidate occupies staging. + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 0), layout(9)); + } + reset_all(); + try ok(begin_program(32, 1)); + for (0..32) |i| try ok(put_state(@intCast(i), 0, -1)); + for (0..24) |i| try ok(put_pointer_target(0, @intCast(i), 1)); + try std.testing.expectEqual(public(.capacity), put_pointer_target(0, 24, 1)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "routed event rollback includes globals and earlier handlers and survives staged route replacement" { + reset_all(); + try ok(begin_program(3, 1)); + for (0..3) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + try ok(put_pointer_target(0, 1, 1)); + try ok(put_pointer_target(0, 2, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try emit(0, 0, 1); + try emit(23, 0, 0); + try emit(6, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 2, 6)); + try ok(bind_target_handler(1, 1)); + try ok(commit_program()); + try ok(begin_program(1, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_pointer_target(0, 0, 1)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_target(17, 1, 88, 0)); + try std.testing.expectEqual(@as(f64, 0), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 2), get_state(2)); + abort_program(); + try ok(dispatch_target(17, 1, 88, 2)); + try std.testing.expectEqual(@as(f64, 99), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(2)); +} + +test "scalar inputs carry a finite ephemeral value without adding state or pointer ownership" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 50, -1)); + try emit(26, 0, 0); + try emit(0, 0, 20); + try emit(0, 0, 80); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(33, 0, 5)); + try ok(commit_program()); + try ok(dispatch_value(33, 200)); + try std.testing.expectEqual(@as(f64, 80), get_state(0)); + try ok(dispatch_value(33, -200)); + try std.testing.expectEqual(@as(f64, 20), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), info(5)); + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqual(@as(u32, 2), info(9)); + for ([_]u32{ 0, 1, 17, 25, 34, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_value(event, 60)); + try std.testing.expectEqual(public(.bad_event), dispatch(33, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(33, 60, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_value(33, std.math.nan(f64))); + try std.testing.expectEqual(@as(f64, 20), get_state(0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "scalar input failure rolls back earlier writes and old scalar events survive staging" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(26, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(33, 0, 2)); + try emit(0, 0, 1); + try emit(26, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(33, 2, 4)); + try ok(commit_program()); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_value(33, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch_value(33, 2)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 1), info(9)); +} diff --git a/prototypes/workbench/engine.zig b/prototypes/workbench/engine.zig new file mode 100644 index 0000000..16907ef --- /dev/null +++ b/prototypes/workbench/engine.zig @@ -0,0 +1,700 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +pub const std = @import("std"); +const root = @import("root"); +const fixed_definition = @hasDecl(root, "pure_definition"); +pub const Evaluation = @import("evaluation.zig").Evaluation; +pub const Tables = @import("evaluation.zig").Tables; + +// A fixed native definition may request exact capacities from its generated +// build root. Browser/authoring builds retain the full language limits. +pub const max_states = if (@hasDecl(root, "pure_max_states")) root.pure_max_states else 32; +pub const max_inputs = if (@hasDecl(root, "pure_max_inputs")) root.pure_max_inputs else 8; +pub const max_instructions = if (@hasDecl(root, "pure_max_instructions")) root.pure_max_instructions else 512; +pub const max_handlers = if (@hasDecl(root, "pure_max_handlers")) root.pure_max_handlers else 64; +pub const max_stack = 64; +pub const max_pointer_targets = if (@hasDecl(root, "pure_max_pointer_targets")) root.pure_max_pointer_targets else 24; +comptime { + if (max_states > 32 or max_inputs > 8 or max_instructions > 512 or + max_handlers > 64 or max_pointer_targets > 24) + @compileError("native profile exceeds the language limits"); +} +pub const event_target = 255; +pub const no_pc = std.math.maxInt(u32); +pub const frame_dt: f64 = 1.0 / 60.0; + +pub const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +pub const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +pub const Handler = struct { + event: u32 = 0, + start: u32 = 0, + length: u32 = 0, + parameter_base: u8 = 0, + parameter_count: u8 = 0, +}; +pub const PointerTarget = struct { input: u8, base: u8, count: u8 }; +pub const Program = struct { + pointer_targets: [max_pointer_targets]PointerTarget = undefined, + pointer_target_count: u8 = 0, + routed_inputs: u8 = 0, + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +const shared_program: Program = if (fixed_definition) blk: { + const definition = root.pure_definition; + var program: Program = .{}; + program.state_count = definition.states_count; + program.input_count = definition.inputs_count; + program.instruction_count = definition.instructions_count; + program.handler_count = definition.handlers_count; + program.pointer_target_count = definition.pointer_targets_count; + program.routed_inputs = definition.routed_inputs; + for (definition.initials, 0..) |value, i| program.initials[i] = value; + for (definition.code, 0..) |inst, i| + program.instructions[i] = .{ .value = inst.value, .op = inst.op, .arg = inst.arg }; + for (definition.handlers_data, 0..) |handler, i| + program.handlers[i] = .{ .event = handler.event, .start = handler.start, + .length = handler.length, .parameter_base = handler.base, + .parameter_count = handler.count }; + for (definition.targets_data, 0..) |target, i| + program.pointer_targets[i] = .{ .input = target.input, .base = target.base, .count = target.count }; + break :blk program; +} else .{}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +pub const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +pub const Workspace = union { staging: Pending, reuse: Tables }; +pub const Engine = struct { + active: if (fixed_definition) bool else Program = if (fixed_definition) false else .{}, + workspace: if (fixed_definition) void else Workspace = if (fixed_definition) {} else .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, + + fn program(self: *Engine) *const Program { + return if (comptime fixed_definition) &shared_program else &self.active; + } + + pub fn reuseTables(self: *Engine) ?*Tables { + if (comptime fixed_definition) return null; + return if (self.staging_open) null else &self.workspace.reuse; + } + + pub fn finishStaging(self: *Engine) void { + if (comptime fixed_definition) return; + self.workspace = .{ .reuse = .{} }; + self.staging_open = false; + self.evaluation.forget(null, 3); + } + + pub fn public(_: *Engine, code: Code) u32 { + return @intFromEnum(code); + } + + pub fn stagingFail(self: *Engine, code: Code) u32 { + if (self.staging_error == .ok) self.staging_error = code; + return self.public(code); + } + + pub fn reset_all(self: *Engine) void { + self.* = .{}; + } + + pub fn installFixed(self: *Engine, identity: *const [32]u8, + state_count: u32, input_count: u32, instruction_count: u32, + handler_count: u32, target_count: u32, restored: ?[*]const f64) u32 { + if (comptime !fixed_definition) return self.public(.invalid); + const definition = root.pure_definition; + if (!std.mem.eql(u8, identity, &definition.identity) or + state_count != definition.states_count or input_count != definition.inputs_count or + instruction_count != definition.instructions_count or + handler_count != definition.handlers_count or target_count != definition.pointer_targets_count) + return self.public(.invalid); + var next: [max_states]f64 = @splat(0); + for (0..state_count) |i| { + const value = if (restored) |values| values[i] else shared_program.initials[i]; + if (!std.math.isFinite(value)) return self.public(.non_finite); + next[i] = value; + } + self.active = true; + self.values = next; + self.staging_open = false; + self.staging_error = .ok; + self.evaluation.forget(null, 3); + if (restored != null) { + self.tick = 0; + self.successful_events = 0; + } + self.revision +%= 1; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + return 0; + } + + pub fn begin_program(self: *Engine, state_count: u32, input_count: u32) u32 { + self.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (self.staging_open) self.finishStaging(); + return self.stagingFail(.capacity); + } + self.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + self.staging_open = true; + self.evaluation.forget(null, 3); + return 0; + } + + pub fn abort_program(self: *Engine) void { + if (self.staging_open) self.finishStaging(); + } + + pub fn put_state(self: *Engine, id: u32, initial: f64, old_id: i32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + if (id >= self.workspace.staging.program.state_count or old_id < -1) return self.stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= self.program().state_count) return self.stagingFail(.bad_state); + if (self.workspace.staging.program.declared[id]) return self.stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return self.stagingFail(.non_finite); + self.workspace.staging.program.initials[id] = initial; + self.workspace.staging.program.old_ids[id] = old_id; + self.workspace.staging.program.declared[id] = true; + return 0; + } + + pub fn put_instruction(self: *Engine, op: u32, arg: u32, value: f64) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.instruction_count == max_instructions) return self.stagingFail(.capacity); + if (op > 26) return self.stagingFail(.invalid); + if (!std.math.isFinite(value)) return self.stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; + } + + pub fn validEvent(_: *Engine, event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs) or + (event >= 33 and event - 33 < inputs); + } + + pub fn put_handler(self: *Engine, event: u32, start: u32, length: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.handler_count == max_handlers) return self.stagingFail(.capacity); + if (!self.validEvent(event, p.input_count)) return self.stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; + } + + pub fn bind_handler(self: *Engine, handler: u32, base: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return self.stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return self.stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return self.stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; + } + + // A route is a static program-local index, never a persistent object identity. + pub fn put_pointer_target(self: *Engine, input: u32, base: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.pointer_target_count == max_pointer_targets) return self.stagingFail(.capacity); + if (input >= p.input_count) return self.stagingFail(.bad_event); + if (base >= p.state_count or count == 0 or count > 8 or count > p.state_count - base) return self.stagingFail(.bad_state); + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (base < @as(u32, t.base) + t.count and t.base < base + count) return self.stagingFail(.invalid); + if (t.input == input and t.count != count) return self.stagingFail(.bad_state); + } + p.pointer_targets[p.pointer_target_count] = .{ .input = @intCast(input), .base = @intCast(base), .count = @intCast(count) }; + p.pointer_target_count += 1; + p.routed_inputs |= @as(u8, 1) << @as(u3, @intCast(input)); + return 0; + } + + pub fn bind_target_handler(self: *Engine, handler: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (handler >= p.handler_count or count == 0 or count > 8) return self.stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return self.stagingFail(.invalid); + h.parameter_base = event_target; + h.parameter_count = @intCast(count); + return 0; + } + + pub fn eventInput(_: *Engine, event: u32) u32 { + return if (event >= 33) event - 33 else if (event >= 25) event - 25 else if (event >= 17) event - 17 else (event - 1) / 2; + } + + pub fn validate(self: *Engine, p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= self.program().state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!self.validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_base == event_target) { + if (h.event == 0 or h.event >= 33 or h.parameter_count == 0 or h.parameter_count > 8) return .bad_event; + var found = false; + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (t.input != self.eventInput(h.event)) continue; + if (t.count != h.parameter_count) return .bad_state; + found = true; + } + if (!found) return .bad_event; + } else if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 26) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; + } + + pub fn put_restored_value(self: *Engine, id: u32, value: f64) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const pending = &self.workspace.staging; + if (id >= pending.program.state_count) return self.stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return self.stagingFail(.bad_state); + if (!std.math.isFinite(value)) return self.stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; + } + + pub fn commit_program(self: *Engine) u32 { + return self.commit(false); + } + + pub fn commit_restored_program(self: *Engine) u32 { + return self.commit(true); + } + + pub fn commit(self: *Engine, restoring: bool) u32 { + if (!self.staging_open) return self.public(.invalid); + if (self.staging_error != .ok) return self.public(self.staging_error); + const count = self.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (self.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return self.stagingFail(.bad_state); + const code = self.validate(&self.workspace.staging.program); + if (code != .ok) return self.stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..self.workspace.staging.program.state_count) |i| { + const old_id = self.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) self.workspace.staging.values[i] else if (old_id < 0) self.workspace.staging.program.initials[i] else self.values[@intCast(old_id)]; + } + self.active = self.workspace.staging.program; + self.values = next; + self.finishStaging(); + if (restoring) { + self.tick = 0; + self.successful_events = 0; + } + self.revision +%= 1; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + return 0; + } + + pub fn reset_state(self: *Engine) void { + self.values = if (fixed_definition and !self.active) @splat(0) else self.program().initials; + self.evaluation.forget(self.reuseTables(), 3); + self.tick = 0; + self.successful_events = 0; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + } + + const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, + }; + + pub fn executionFail(_: *Engine, result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; + } + + const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false, scalar: bool = false, target: ?u32 = null }; + + pub fn execute(self: *Engine, values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + var result: Execution = .{}; + if (comptime fixed_definition) { + if (!self.active) { + if (start != 0 or length != 0) return self.executionFail(result, .invalid, no_pc); + if (pure) return self.executionFail(result, .stack, 0); + return result; + } + } + const p = self.program(); + if (start > p.instruction_count or length > p.instruction_count - start) return self.executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return self.executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return self.executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24, 26 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return self.executionFail(result, .invalid, pc), + }; + if (sp < need) return self.executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24 or inst.op == 26) { + if (sp == max_stack) return self.executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return self.executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return self.executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return self.executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 26 => if (context.scalar) context.x else return self.executionFail(result, .bad_event, pc), + 2 => context.dt, + 22, 23 => if (!context.point) return self.executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return self.executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return self.executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return self.executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return self.executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return self.executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return self.executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return self.executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return self.executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; + } + + pub fn dispatch(self: *Engine, event: u32, dt: f64) u32 { + return self.dispatchContext(event, .{ .dt = dt }); + } + + pub fn dispatch_point(self: *Engine, event: u32, x: f64, y: f64) u32 { + return self.dispatchContext(event, .{ .x = x, .y = y, .point = true }); + } + + pub fn dispatch_target(self: *Engine, event: u32, target: u32, x: f64, y: f64) u32 { + return self.dispatchContext(event, .{ .x = x, .y = y, .point = true, .target = target }); + } + + pub fn dispatch_value(self: *Engine, event: u32, value: f64) u32 { + return self.dispatchContext(event, .{ .x = value, .scalar = true }); + } + + pub fn dispatchContext(self: *Engine, event: u32, context: EventContext) u32 { + const dt = context.dt; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + if (comptime fixed_definition) { + if (!self.active) { + if (event != 0 or context.point or context.scalar) { + self.last_error = .bad_event; + return self.public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + self.last_error = .non_finite; + return self.public(.non_finite); + } + if (dt != frame_dt) { + self.last_error = .bad_frame; + return self.public(.bad_frame); + } + self.tick +%= 1; + self.successful_events +%= 1; + return 0; + } + } + if (!self.validEvent(event, self.program().input_count) or + ((context.point or context.scalar) and event == 0) or + (context.scalar != (event >= 33)) or + (!context.point and !context.scalar and event > self.program().input_count * 2)) + { + self.last_error = .bad_event; + return self.public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + self.last_error = .non_finite; + return self.public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + self.last_error = .bad_frame; + return self.public(.bad_frame); + } + var route: ?PointerTarget = null; + const routed = event != 0 and self.program().routed_inputs & (@as(u8, 1) << @as(u3, @intCast(self.eventInput(event)))) != 0; + if (context.target) |target| { + if (!routed or target >= self.program().pointer_target_count or + self.program().pointer_targets[target].input != self.eventInput(event)) + { + self.last_error = .bad_event; + return self.public(.bad_event); + } + route = self.program().pointer_targets[target]; + } else if (routed) { + self.last_error = .bad_event; + return self.public(.bad_event); + } + var pending = self.values; + for (self.program().handlers[0..self.program().handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = if (h.parameter_base == event_target) route.?.base else h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = self.execute(&pending, h.start, h.length, bound, false, null); + self.last_ops += result.ops; + if (result.code != .ok) { + self.last_error = result.code; + self.error_pc = result.pc; + return self.public(result.code); + } + } + if (self.evaluation.cache_count != 0 and !self.staging_open) { + var changed: u32 = 0; + for (0..self.program().state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(self.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + self.evaluation.changed(self.reuseTables(), changed); + } + self.values = pending; + if (event == 0) self.tick +%= 1; + self.successful_events +%= 1; + return 0; + } + + pub fn eval_at(self: *Engine, start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = self.values; + const result = self.execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); + } + + pub fn configure_evaluation(self: *Engine, cache: u32, history: u32) u32 { + if (comptime fixed_definition) if (cache != 0) return self.public(.capacity); + return if (self.evaluation.configure(self.reuseTables(), cache, history)) 0 else self.public(.capacity); + } + + pub fn forget_evaluation(self: *Engine, flags: u32) void { + self.evaluation.forget(self.reuseTables(), flags); + } + + pub fn begin_evaluation(self: *Engine, limit: u32) void { + self.evaluation.begin(limit); + } + + pub fn eval_bounded(self: *Engine, start: u32, length: u32) f64 { + const e = &self.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(self.reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = self.values; + const result = self.execute(&values, start, length, .{}, true, e); + e.status = self.public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(self.reuseTables(), start, length, result.value, result.dependencies); + return result.value; + } + + pub fn evaluation_info(self: *Engine, id: u32) u32 { + const e = &self.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + if (fixed_definition) @as(u32, 0) else @as(u32, @sizeOf(Tables)), + 7 => @sizeOf(Evaluation), + 8 => if (fixed_definition) 0 else @sizeOf(Tables), + 9 => @intFromBool(!self.staging_open), + 10 => if (fixed_definition) 0 else @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; + } + + pub fn get_state(self: *Engine, id: u32) f64 { + if (comptime fixed_definition) if (!self.active) return std.math.nan(f64); + return if (id < self.program().state_count) self.values[id] else std.math.nan(f64); + } + + pub fn info(self: *Engine, id: u32) u32 { + const loaded = if (fixed_definition) self.active else true; + return switch (id) { + 0 => self.tick, + 1 => self.public(self.last_error), + 2 => self.error_pc, + 3 => self.last_ops, + 4 => self.revision, + 5 => if (loaded) self.program().state_count else 0, + 6 => if (loaded) self.program().instruction_count else 0, + 7 => if (loaded) self.program().handler_count else 0, + 8 => if (loaded) self.program().input_count else 0, + 9 => self.successful_events, + else => 0, + }; + } + + pub fn layout(self: *Engine, id: u32) u32 { + if (comptime fixed_definition) { + if (!self.active and (id == 4 or id == 5 or id == 9)) return 0; + } + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => self.program().instruction_count * @sizeOf(Instruction) + self.program().handler_count * @sizeOf(Handler) + @as(u32, self.program().pointer_target_count) * @sizeOf(PointerTarget), + 5 => self.program().state_count * @sizeOf(f64), + 6 => if (fixed_definition) 0 else @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + 8 => @sizeOf(PointerTarget), + 9 => self.program().pointer_target_count, + else => 0, + }; + } +}; diff --git a/prototypes/workbench/evaluation-check.mjs b/prototypes/workbench/evaluation-check.mjs new file mode 100644 index 0000000..bb3d616 --- /dev/null +++ b/prototypes/workbench/evaluation-check.mjs @@ -0,0 +1,76 @@ +// A bounded adoption check on existing authored files, not a platform benchmark. +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { platform, release, arch, cpus } from 'node:os'; +import { performance } from 'node:perf_hooks'; +import { createRuntime } from './web/runtime.mjs'; + +const args = process.argv.slice(2); +if (args.length !== 2 || args[0] !== '--output') throw new Error('Use --output NEW.json'); +const root = new URL('.', import.meta.url); +const wasm = await readFile(new URL('web/core.wasm', root)); +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +const evidence = { purpose: 'Choose whether to enable binding reuse by default', + utc: new Date().toISOString(), host: { platform: platform(), release: release(), + arch: arch(), cpu: cpus()[0]?.model, node: process.version, affinityControlled: false }, + sourceSha256: {}, cases: [], + timingScope: 'Event dispatch plus snapshot construction. Excludes loading, compilation, DOM, equality checks and statistics reads. Five batch samples are not latency percentiles.', +}; +for (const path of ['web/core.wasm', 'core.zig', 'evaluation.zig', 'web/runtime.mjs', + 'web/compiler.mjs', 'evaluation-check.mjs', 'evaluation.test.mjs']) + evidence.sourceSha256[path] = hash(await readFile(new URL(path, root))); + +for (const name of ['counter', 'toggle', 'charge']) { + const path = `examples/${name}.pure`; + const source = await readFile(new URL(path, root), 'utf8'); + evidence.sourceSha256[path] = hash(source); + const plain = await createRuntime(wasm), cached = await createRuntime(wasm, {cacheEntries:32}); + const program = plain.apply(source); cached.apply(source); + const input = program.inputs[0]?.name; + const trace = Array.from({length:100}, () => 'show'); + for (let i = 0; i < 600; i++) { + if (input && i % 60 === 0) trace.push('down'); + if (input && i % 60 === 15) trace.push('up'); + trace.push('frame'); + } + function action(runtime, kind) { + if (kind === 'frame') assert.equal(runtime.frame(), 0); + else if (kind !== 'show') assert.equal(runtime[kind](input, 'replay'), 0); + return runtime.snapshot(); + } + const totals = {plain:{work:0,instructions:0,cacheHits:0},cached:{work:0,instructions:0,cacheHits:0}}; + for (const kind of trace) { + const left = action(plain, kind), right = action(cached, kind); + assert.equal(left.lastError, 0); assert.deepEqual(right, left); + for (const [key, runtime] of Object.entries({plain,cached})) { + const stats = runtime.evaluationStats(); + for (const field of Object.keys(totals[key])) totals[key][field] += stats[field]; + } + } + function replay(runtime) { + runtime.reset(); + for (const kind of trace) { + if (kind === 'frame') runtime.frame(); + else if (kind !== 'show') runtime[kind](input, 'replay'); + runtime.snapshot(); + } + } + for (let warm = 0; warm < 3; warm++) { replay(plain); replay(cached); } + const samples = []; + for (let trial = 0; trial < 5; trial++) { + for (const key of trial % 2 ? ['cached','plain'] : ['plain','cached']) { + const runtime = key === 'plain' ? plain : cached; + const start = performance.now(); replay(runtime); + samples.push({trial,key,elapsedMs:performance.now()-start}); + } + assert.deepEqual(cached.snapshot(), plain.snapshot()); + } + const median = key => samples.filter(s=>s.key===key).map(s=>s.elapsedMs).sort((a,b)=>a-b)[2]; + evidence.cases.push({name,checkedSnapshots:trace.length, traceSha256:hash(JSON.stringify(trace)), + totals, memory:cached.snapshot().memory, evaluation:cached.evaluationStats(), samples, + medianMs:{plain:median('plain'),cached:median('cached')}, cachedOverPlain:median('cached')/median('plain')}); +} +await writeFile(args[1], JSON.stringify(evidence,null,2)+'\n', {flag:'wx'}); +for (const c of evidence.cases) console.log(JSON.stringify({name:c.name,checkedSnapshots:c.checkedSnapshots, + totals:c.totals,medianMs:c.medianMs,cachedOverPlain:c.cachedOverPlain})); diff --git a/prototypes/workbench/evaluation.test.mjs b/prototypes/workbench/evaluation.test.mjs new file mode 100644 index 0000000..21a9590 --- /dev/null +++ b/prototypes/workbench/evaluation.test.mjs @@ -0,0 +1,133 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const make = (options = {}) => createRuntime(wasm, options); +const source = `state x = 3 +state spare = 0 +input next = KeyN +input other = KeyO +input fail = KeyF +text value(0, 0, (x + 1) * (x + 2) / 2) +on next.down { x = x + 0.25 } +on other.down { spare = spare + 1 } +on fail.down { x = 900; spare = 1 / 0 } +`; + +test('cache reuse, unrelated changes and discarded records preserve authored values', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + cached.apply(source); plain.apply(source); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(cached.evaluationStats().cacheHits > 0); + assert(cached.evaluationStats().instructions < plain.evaluationStats().instructions); + for (let i = 0; i < 100; i++) { + const name = i % 3 ? 'other' : 'next'; + for (const runtime of [cached, plain]) { runtime.down(name); runtime.up(name); } + if (i % 7 === 0) cached.forgetEvaluation('history'); + if (i % 11 === 0) cached.forgetEvaluation('values'); + if (i % 13 === 0) cached.forgetEvaluation('all'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + } + // Omit observations long enough to overwrite every change record. + for (let i = 0; i < 20; i++) for (const runtime of [cached, plain]) { + runtime.down('next'); runtime.up('next'); + } + assert.deepEqual(cached.snapshot(), plain.snapshot()); +}); + +test('committed changes only: failed events and failed edits preserve reusable output', async () => { + const cached = await make({ cacheEntries: 32 }); cached.apply(source); + const before = cached.snapshot(); + cached.down('fail'); cached.up('fail'); + const failed = cached.snapshot(); + assert.deepEqual(failed.states, before.states); + assert.deepEqual(failed.views, before.views); + assert.equal(failed.lastError, 4); + assert.equal(cached.evaluationStats().instructions, 0); + assert.throws(() => cached.apply('state x = missing')); + assert.deepEqual(cached.snapshot(), failed); + assert.equal(cached.evaluationStats().instructions, 0); +}); + +test('program edits, state reordering and reset cannot reuse old definitions', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + for (const r of [cached, plain]) { r.apply(source); r.down('next'); r.up('next'); r.snapshot(); } + const changed = source.replace('state x = 3\nstate spare = 0', 'state spare = 9\nstate x = 300') + .replace('(x + 1) * (x + 2) / 2', '(x + 10) * (x + 20) / 2'); + for (const r of [cached, plain]) r.apply(changed); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.reset(); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().states.x, 300); +}); + +test('finite float semantics include signed zero, high state bit and arithmetic errors', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + const program = `${Array.from({ length: 32 }, (_, i) => `state s${i} = 0`).join('\n')} +input flip = KeyF +text sign(0, 0, s31) +text error(0, 0, 1 / s31) +on flip.down { s31 = -s31 } +on flip.up { s31 = 2 } +`; + for (const r of [cached, plain]) r.apply(program); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.down('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(Object.is(cached.snapshot().views[0].value, -0)); + assert.equal(cached.snapshot().displayError.code, 100); + for (const r of [cached, plain]) r.up('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().displayError, null); +}); + +test('one budget covers the whole scene; failures publish no partial scene or state rollback', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + r.down('next'); r.up('next'); + const good = r.snapshot(); + r.configureEvaluation({ workLimit: 5 }); + const limited = r.snapshot(); + assert.equal(limited.lastError, 101); + assert.deepEqual(limited.views, []); + assert.deepEqual(limited.states, good.states); + assert.equal(limited.successfulEvents, good.successfulEvents); + assert(r.evaluationStats().work <= 5); + r.down('fail'); r.up('fail'); + assert.equal(r.snapshot().lastError, 4); + assert.equal(r.snapshot().displayError.code, 101); + r.configureEvaluation({ workLimit: 2048 }); + assert.deepEqual(r.snapshot().views, good.views); + assert.equal(r.snapshot().lastError, 4); +}); + +test('small caches, no history and every small work limit give exact results or explicit failure', async () => { + const plain = await make(); plain.apply(source); + for (const cacheEntries of [0, 1, 32]) for (const historyEntries of [0, 1, 8]) { + const r = await make({ cacheEntries, historyEntries }); r.apply(source); + for (let workLimit = 0; workLimit < 100; workLimit++) { + r.configureEvaluation({ workLimit }); + const actual = r.snapshot(); + assert(r.evaluationStats().work <= workLimit); + if (actual.lastError) { + assert.equal(actual.lastError, 101); + assert.deepEqual(actual.views, []); + } else assert.deepEqual(actual, plain.snapshot()); + } + } +}); + +test('invalid settings and forget names preserve policy and state', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + const before = r.snapshot(), policy = r.evaluationStats(); + for (const option of [{cacheEntries:33}, {historyEntries:9}, {workLimit:-1}, + {workLimit:0x100000000}, {workLimit:NaN}, {cacheEntries:0.5}]) { + assert.throws(() => r.configureEvaluation(option)); + assert.deepEqual(r.evaluationStats(), policy); + } + for (const name of ['unknown', '__proto__', 'constructor']) assert.throws(() => r.forgetEvaluation(name)); + assert.deepEqual(r.snapshot(), before); + assert.equal(r.snapshot().memory.linearBytes, 65536); +}); diff --git a/prototypes/workbench/evaluation.zig b/prototypes/workbench/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/examples/charge.pure b/prototypes/workbench/examples/charge.pure new file mode 100644 index 0000000..eac55c4 --- /dev/null +++ b/prototypes/workbench/examples/charge.pure @@ -0,0 +1,34 @@ +# All holding, charging, gravity, jumping, and floor behavior is authored here. +state holding = 0 +state charge = 0 +state y = 240 +state velocity = 0 +input jump = Space +box floor(40, 261, 560, 4, "#356548") +circle actor(180, y, 20, "#c78638") +box meter(80, 80, charge * 200, 12, "#c78638") +text amount(80, 60, charge, "Charge: ") +on jump.down { + if y >= 240 { + holding = 1 + charge = 0 + } +} +on jump.up { + if holding { + velocity = -(160 + charge * 300) + holding = 0 + charge = 0 + } +} +on frame { + if holding { + charge = clamp(charge + dt, 0, 1) + } + velocity = velocity + 600 * dt + y = y + velocity * dt + if y >= 240 { + y = 240 + velocity = 0 + } +} diff --git a/prototypes/workbench/examples/counter.pure b/prototypes/workbench/examples/counter.pure new file mode 100644 index 0000000..ca5f18e --- /dev/null +++ b/prototypes/workbench/examples/counter.pure @@ -0,0 +1,8 @@ +# Space or the tap control increments a state that is displayed and drawn. +state count = 0 +input tap = Space +text label(80, 90, count, "Count: ") +box tile(80, 150, 40 + min(count, 20) * 8, 60, "#356548") +on tap.down { + count = count + 1 +} diff --git a/prototypes/workbench/examples/direct.pure b/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/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/workbench/examples/document.pure b/prototypes/workbench/examples/document.pure new file mode 100644 index 0000000..6d78f8c --- /dev/null +++ b/prototypes/workbench/examples/document.pure @@ -0,0 +1,6 @@ +# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다. +state ratio = 50 +input split = value +input reset = KeyR +on split.change { ratio = clamp(event_value, 20, 80) } +on reset.down { ratio = 50 } diff --git a/prototypes/workbench/examples/layout.pure b/prototypes/workbench/examples/layout.pure new file mode 100644 index 0000000..41cf149 --- /dev/null +++ b/prototypes/workbench/examples/layout.pure @@ -0,0 +1,92 @@ +# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요. +# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다. +state selected = 0 +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +state x1 = 80 +state y1 = 120 +state ox1 = 0 +state oy1 = 0 +state sx1 = 80 +state sy1 = 120 +state held1 = 0 +input drag1 = pointer(tile1) +state x2 = 280 +state y2 = 160 +state ox2 = 0 +state oy2 = 0 +state sx2 = 280 +state sy2 = 160 +state held2 = 0 +input drag2 = pointer(tile2) +state x3 = 450 +state y3 = 220 +state ox3 = 0 +state oy3 = 0 +state sx3 = 450 +state sy3 = 220 +state held3 = 0 +input drag3 = pointer(tile3) +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box tile1(x1,y1,56,44,"#c9f08b") +box tile2(x2,y2,56,44,"#74c4d4") +box tile3(x3,y3,56,44,"#dfb184") +on drag1.down { + selected = 1 + sx1 = x1; sy1 = y1 + ox1 = event_x - x1; oy1 = event_y - y1 + held1 = 1 +} +on drag1.move { + x1 = clamp(event_x - ox1, 20, 564) + y1 = clamp(event_y - oy1, 90, 286) +} +on drag1.up { held1 = 0 } +on drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 } +on drag2.down { + selected = 2 + sx2 = x2; sy2 = y2 + ox2 = event_x - x2; oy2 = event_y - y2 + held2 = 1 +} +on drag2.move { + x2 = clamp(event_x - ox2, 20, 564) + y2 = clamp(event_y - oy2, 90, 286) +} +on drag2.up { held2 = 0 } +on drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 } +on drag3.down { + selected = 3 + sx3 = x3; sy3 = y3 + ox3 = event_x - x3; oy3 = event_y - y3 + held3 = 1 +} +on drag3.move { + x3 = clamp(event_x - ox3, 20, 564) + y3 = clamp(event_y - oy3, 90, 286) +} +on drag3.up { held3 = 0 } +on drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 } +on left.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) } +} +on right.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) } +} +on up.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) } +} +on down.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) } +} diff --git a/prototypes/workbench/examples/routed-layout.pure b/prototypes/workbench/examples/routed-layout.pure new file mode 100644 index 0000000..8933c70 --- /dev/null +++ b/prototypes/workbench/examples/routed-layout.pure @@ -0,0 +1,70 @@ +# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다. +state selected = 0 +record Tile { + x = 0; y = 0 + ox = 0; oy = 0 + sx = 0; sy = 0 + held = 0; id = 0 +} +item green = Tile(x=80, y=120, sx=80, sy=120, id=1) +item blue = Tile(x=280, y=160, sx=280, sy=160, id=2) +item amber = Tile(x=450, y=220, sx=450, sy=220, id=3) +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +input drag = pointer { + green_box: green + blue_box: blue + amber_box: amber +} +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +box amber_box(amber.x,amber.y,56,44,"#dfb184") + +rule grab(t: Tile) { + selected = t.id + t.sx = t.x; t.sy = t.y + t.ox = event_x - t.x; t.oy = event_y - t.y + t.held = 1 +} +rule move(t: Tile) { + t.x = clamp(event_x - t.ox, 20, 564) + t.y = clamp(event_y - t.oy, 90, 286) +} +rule drop(t: Tile) { t.held = 0 } +rule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 } +rule go_left(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) } +} +rule go_right(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) } +} +rule go_up(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) } +} +rule go_down(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) } +} + +on drag.down use grab(drag.target) +on drag.move use move(drag.target) +on drag.up use drop(drag.target) +on drag.cancel use cancel(drag.target) + +on left.down use go_left(green) +on right.down use go_right(green) +on up.down use go_up(green) +on down.down use go_down(green) + +on left.down use go_left(blue) +on right.down use go_right(blue) +on up.down use go_up(blue) +on down.down use go_down(blue) + +on left.down use go_left(amber) +on right.down use go_right(amber) +on up.down use go_up(amber) +on down.down use go_down(amber) diff --git a/prototypes/workbench/examples/shared-layout.pure b/prototypes/workbench/examples/shared-layout.pure new file mode 100644 index 0000000..38c001d --- /dev/null +++ b/prototypes/workbench/examples/shared-layout.pure @@ -0,0 +1,75 @@ +# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다. +state selected = 0 +record Tile { + x = 0; y = 0 + ox = 0; oy = 0 + sx = 0; sy = 0 + held = 0; id = 0 +} +item green = Tile(x=80, y=120, sx=80, sy=120, id=1) +item blue = Tile(x=280, y=160, sx=280, sy=160, id=2) +item amber = Tile(x=450, y=220, sx=450, sy=220, id=3) +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +input drag_amber = pointer(amber_box) +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +box amber_box(amber.x,amber.y,56,44,"#dfb184") + +rule grab(t: Tile) { + selected = t.id + t.sx = t.x; t.sy = t.y + t.ox = event_x - t.x; t.oy = event_y - t.y + t.held = 1 +} +rule move(t: Tile) { + t.x = clamp(event_x - t.ox, 20, 564) + t.y = clamp(event_y - t.oy, 90, 286) +} +rule drop(t: Tile) { t.held = 0 } +rule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 } +rule go_left(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) } +} +rule go_right(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) } +} +rule go_up(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) } +} +rule go_down(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) } +} + +on drag_green.down use grab(green) +on drag_green.move use move(green) +on drag_green.up use drop(green) +on drag_green.cancel use cancel(green) +on left.down use go_left(green) +on right.down use go_right(green) +on up.down use go_up(green) +on down.down use go_down(green) + +on drag_blue.down use grab(blue) +on drag_blue.move use move(blue) +on drag_blue.up use drop(blue) +on drag_blue.cancel use cancel(blue) +on left.down use go_left(blue) +on right.down use go_right(blue) +on up.down use go_up(blue) +on down.down use go_down(blue) + +on drag_amber.down use grab(amber) +on drag_amber.move use move(amber) +on drag_amber.up use drop(amber) +on drag_amber.cancel use cancel(amber) +on left.down use go_left(amber) +on right.down use go_right(amber) +on up.down use go_up(amber) +on down.down use go_down(amber) diff --git a/prototypes/workbench/examples/toggle.pure b/prototypes/workbench/examples/toggle.pure new file mode 100644 index 0000000..a29f38f --- /dev/null +++ b/prototypes/workbench/examples/toggle.pure @@ -0,0 +1,17 @@ +# Space toggles the circle. Right arrow moves the tile. +state visible = 1 +state position = 120 +input toggle = Space +input move = ArrowRight +circle marker(position, 140, visible * 24, "#c78638") +box tile(position - 24, 190, 48, 48, "#356548") +text value(80, 80, visible, "Visible: ") +on toggle.down { + visible = !visible +} +on move.down { + position = position + 40 + if position > 560 { + position = 80 + } +} diff --git a/prototypes/workbench/manage.py b/prototypes/workbench/manage.py new file mode 100644 index 0000000..5bbc542 --- /dev/null +++ b/prototypes/workbench/manage.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Build the authoring VM or serve its local browser adapter.""" +import argparse +from datetime import datetime, timezone +from functools import partial +import hashlib +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import shutil +import shlex +import subprocess + +ROOT = Path(__file__).resolve().parent + + +def sync_examples(): + target = ROOT / 'web/examples' + target.mkdir(exist_ok=True) + for source in (ROOT / 'examples').glob('*.pure'): + shutil.copyfile(source, target / source.name) + + +def build(zig): + compiler = shutil.which(zig) + if not compiler: + raise SystemExit('Zig 0.15.2가 필요합니다. --zig /path/to/zig로 지정할 수 있습니다.') + version = subprocess.check_output([compiler, 'version'], text=True).strip() + if version != '0.15.2': + raise SystemExit(f'이 실험은 Zig 0.15.2로 고정했습니다. 현재 {version}') + folder = ROOT / 'build' + folder.mkdir(exist_ok=True) + cache = ['--cache-dir', str(folder / 'cache'), '--global-cache-dir', str(folder / 'global-cache')] + source = ROOT / 'core.zig' + result = subprocess.run([compiler, 'test', str(source), *cache], capture_output=True, text=True) + print(result.stdout + result.stderr, end='', flush=True) + result.check_returncode() + flags = ['-target', 'wasm32-freestanding', '-O', 'ReleaseSafe', '-fstrip', '-fno-entry', + '-rdynamic', '--export-memory', '--stack', '16384', + '--initial-memory=65536', '--max-memory=65536'] + artifact = ROOT / 'web/core.wasm' + subprocess.run([compiler, 'build-exe', str(source), *flags, f'-femit-bin={artifact}', *cache], check=True) + sync_examples() + sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() + (folder / 'build.json').write_text(json.dumps({ + 'builtUtc': datetime.now(timezone.utc).isoformat(), 'zigVersion': version, + 'sourceSha256': sha(source), 'wasmSha256': sha(artifact), + 'evaluationSourceSha256': sha(ROOT / 'evaluation.zig'), + 'engineSourceSha256': sha(ROOT / 'engine.zig'), + 'wasmFileBytes': artifact.stat().st_size, 'flags': flags, + 'nativeTests': result.stdout + result.stderr, + }, indent=2) + '\n') + print(f'Built {artifact} ({artifact.stat().st_size:,} bytes)') + + +def native(zig, source, output, cc, desktop=False, peer=False): + if desktop and peer: + raise SystemExit('--desktop and --peer are separate optional hosts') + compiler = shutil.which(zig) + if not compiler or subprocess.check_output([compiler, 'version'], text=True).strip() != '0.15.2': + raise SystemExit('Zig 0.15.2가 필요합니다. --zig /path/to/zig로 지정하세요.') + folder = ROOT / 'build/native' + folder.mkdir(parents=True, exist_ok=True) + cache = ['--cache-dir', str(ROOT / 'build/cache'), '--global-cache-dir', str(ROOT / 'build/global-cache')] + prepared = folder / 'application.c' + profile = folder / 'fixed-profile.zig' + prepare = ['node', str(ROOT / 'native/prepare.mjs'), str(source.resolve()), str(prepared)] + if not desktop: + prepare.append(str(profile)) + subprocess.run(prepare, check=True) + library = folder / ('libpure-scalar.a' if desktop else 'libpure-scalar-fixed.a') + if desktop: + subprocess.run([compiler, 'build-lib', str(ROOT / 'native.zig'), '-O', 'ReleaseSafe', + '-fno-compiler-rt', f'-femit-bin={library}', *cache], check=True) + else: + subprocess.run([compiler, 'build-lib', '-O', 'ReleaseSafe', '-fno-compiler-rt', + '--dep', 'profile', '-Mroot=' + str(ROOT / 'native-fixed.zig'), + '-Mprofile=' + str(profile), f'-femit-bin={library}', *cache], check=True) + result = output.resolve() if output else folder / ('pure-desktop' if desktop else 'pure-peer' if peer else 'pure-native') + result.parent.mkdir(parents=True, exist_ok=True) + connections = ROOT.parent / 'connections' + inputs = [prepared, ROOT / ('native/desktop.c' if desktop else 'native/peer.c' if peer else 'native/main.c'), + *([ROOT / 'native/peer-wire.c', ROOT.parent / 'common/tls-link.c'] if peer else []), + ROOT / 'native/app.c', ROOT / 'native/plan.c', + *(connections / name for name in ('runtime.c', 'evaluate.c', 'store.c', 'store-port.c')), library] + flags = ['-std=c17', '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror'] + if not desktop: + flags.append('-DPURE_FIXED_DEFINITION=1') + desktop_flags = [] + peer_flags = [] + if desktop: + desktop_flags = shlex.split(subprocess.check_output(['pkg-config', '--cflags', '--libs', 'gtk4'], text=True)) + flags.extend(['-DPURE_NATIVE_AUTHORING=1', f'-DPURE_PREPARE_PATH="{ROOT / "native/prepare.mjs"}"', + f'-DPURE_NATIVE_HEADERS="{ROOT / "native"}"', f'-DPURE_CC="{cc}"']) + if peer: + peer_flags = shlex.split(subprocess.check_output(['pkg-config', '--cflags', '--libs', 'openssl'], text=True)) + subprocess.run([cc, *flags, '-I' + str(ROOT / 'native'), *(str(p) for p in inputs), + '-lm', *desktop_flags, *peer_flags, *(['-ldl'] if desktop else []), + '-o', str(result)], check=True) + sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() + (folder / 'build.json').write_text(json.dumps({'source': str(source.resolve()), 'sourceSha256': sha(source), + 'zigVersion': '0.15.2', 'zigFlags': ['-O', 'ReleaseSafe', '-fno-compiler-rt'], + 'scalarProfile': 'general-authoring' if desktop else 'fixed-definition', + 'zigRoot': str(ROOT / ('native.zig' if desktop else 'native-fixed.zig')), + 'cFlags': flags, + 'binary': str(result), 'binarySha256': sha(result), 'librarySha256': sha(library), + 'fixedProfileSha256': sha(profile) if not desktop else None, + 'preparedSha256': sha(prepared), 'desktop': desktop, 'desktopFlags': desktop_flags, + 'peer': peer, 'peerFlags': peer_flags, + 'implementationSha256': {str(p.relative_to(ROOT.parent.parent)): sha(p) for p in [ + ROOT / 'engine.zig', ROOT / 'evaluation.zig', ROOT / 'native.zig', + ROOT / 'native-fixed.zig', ROOT / 'native/prepare.mjs', + ROOT / 'native/scalar.h', ROOT / 'native/plan.h', ROOT / 'native/app.h', ROOT / 'web/compiler.mjs', + *(p for p in inputs if p.suffix == '.c' and p != prepared)]}}, indent=2) + '\n') + print(f'Built native application: {result}') + + +class Handler(SimpleHTTPRequestHandler): + extensions_map = {**SimpleHTTPRequestHandler.extensions_map, '.wasm': 'application/wasm', '.mjs': 'text/javascript', '.pure': 'text/plain'} + + def end_headers(self): + self.send_header('Cache-Control', 'no-store') + super().end_headers() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='action', required=True) + b = sub.add_parser('build') + b.add_argument('--zig', default='zig') + n = sub.add_parser('native', help='prepare an existing .pure definition as a native CLI, authoring desktop, or peer app') + n.add_argument('source', type=Path) + n.add_argument('--zig', default='zig') + n.add_argument('--cc', default='cc') + n.add_argument('--output', type=Path) + n.add_argument('--desktop', action='store_true', help='build the optional Linux GTK4 authoring shell') + n.add_argument('--peer', action='store_true', help='build the optional TLS 1.3 Pure-peer host') + s = sub.add_parser('serve') + s.add_argument('--port', type=int, default=8766) + args = parser.parse_args() + if args.action == 'build': + build(args.zig) + return + if args.action == 'native': + native(args.zig, args.source, args.output, args.cc, args.desktop, args.peer) + return + if not 1 <= args.port <= 65535: + parser.error('port must be 1..65535') + if not (ROOT / 'web/core.wasm').is_file(): + parser.error('먼저 manage.py build를 실행하세요.') + sync_examples() + with ThreadingHTTPServer(('127.0.0.1', args.port), partial(Handler, directory=str(ROOT / 'web'))) as server: + print(f'Pure workbench: http://127.0.0.1:{args.port}', flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + main() diff --git a/prototypes/workbench/native-fixed.zig b/prototypes/workbench/native-fixed.zig new file mode 100644 index 0000000..8ddd0df --- /dev/null +++ b/prototypes/workbench/native-fixed.zig @@ -0,0 +1,11 @@ +//! Fixed-definition native build root. The profile is generated from a +//! validated .pure definition; native.zig supplies the unchanged C ABI. +const profile = @import("profile"); +pub const pure_definition = profile; +pub const panic = @import("std").debug.no_panic; +pub const pure_max_states = profile.states; +pub const pure_max_inputs = profile.inputs; +pub const pure_max_instructions = profile.instructions; +pub const pure_max_handlers = profile.handlers; +pub const pure_max_pointer_targets = profile.pointer_targets; +comptime { _ = @import("native.zig"); } diff --git a/prototypes/workbench/native.zig b/prototypes/workbench/native.zig new file mode 100644 index 0000000..36a99e4 --- /dev/null +++ b/prototypes/workbench/native.zig @@ -0,0 +1,146 @@ +//! Native C ABI. The caller supplies aligned instance storage; no global VM, +//! allocator, OS call or browser dependency is part of this selected module. +const Engine = @import("engine.zig").Engine; +const fixed_definition = @hasDecl(@import("root"), "pure_definition"); + +// Keep ReleaseSafe bounds/overflow checks. Unexpected internal invariant +// failures trap without importing a process-wide stack unwinder or I/O layer. +// Expected authored errors still return the ordinary scalar status codes. +pub const panic = @import("std").debug.no_panic; + +export fn pure_scalar_bytes() usize { + return @sizeOf(Engine); +} +export fn pure_scalar_alignment() usize { + return @alignOf(Engine); +} + +export fn pure_scalar_initialize(memory: ?*anyopaque, size: usize) ?*Engine { + const pointer = memory orelse return null; + if (size < @sizeOf(Engine) or @intFromPtr(pointer) % @alignOf(Engine) != 0) return null; + const instance: *Engine = @ptrCast(@alignCast(pointer)); + instance.* = .{}; + return instance; +} + +export fn pure_scalar_reset_all(instance: *Engine) void { + instance.reset_all(); +} + +export fn pure_scalar_install_fixed(instance: *Engine, identity: *const [32]u8, + state_count: u32, input_count: u32, instruction_count: u32, + handler_count: u32, target_count: u32, restored: ?[*]const f64) u32 { + return instance.installFixed(identity, state_count, input_count, instruction_count, + handler_count, target_count, restored); +} + +export fn pure_scalar_begin_program(instance: *Engine, state_count: u32, input_count: u32) u32 { + if (comptime fixed_definition) return 1; + return instance.begin_program(state_count, input_count); +} + +export fn pure_scalar_abort_program(instance: *Engine) void { + if (comptime fixed_definition) return; + instance.abort_program(); +} + +export fn pure_scalar_put_state(instance: *Engine, id: u32, initial: f64, old_id: i32) u32 { + if (comptime fixed_definition) return 1; + return instance.put_state(id, initial, old_id); +} + +export fn pure_scalar_put_instruction(instance: *Engine, op: u32, arg: u32, value: f64) u32 { + if (comptime fixed_definition) return 1; + return instance.put_instruction(op, arg, value); +} + +export fn pure_scalar_put_handler(instance: *Engine, event: u32, start: u32, length: u32) u32 { + if (comptime fixed_definition) return 1; + return instance.put_handler(event, start, length); +} + +export fn pure_scalar_bind_handler(instance: *Engine, handler: u32, base: u32, count: u32) u32 { + if (comptime fixed_definition) return 1; + return instance.bind_handler(handler, base, count); +} + +export fn pure_scalar_put_pointer_target(instance: *Engine, input: u32, base: u32, count: u32) u32 { + if (comptime fixed_definition) return 1; + return instance.put_pointer_target(input, base, count); +} + +export fn pure_scalar_bind_target_handler(instance: *Engine, handler: u32, count: u32) u32 { + if (comptime fixed_definition) return 1; + return instance.bind_target_handler(handler, count); +} + +export fn pure_scalar_put_restored_value(instance: *Engine, id: u32, value: f64) u32 { + if (comptime fixed_definition) return 1; + return instance.put_restored_value(id, value); +} + +export fn pure_scalar_commit_program(instance: *Engine) u32 { + if (comptime fixed_definition) return 1; + return instance.commit_program(); +} + +export fn pure_scalar_commit_restored_program(instance: *Engine) u32 { + if (comptime fixed_definition) return 1; + return instance.commit_restored_program(); +} + +export fn pure_scalar_reset_state(instance: *Engine) void { + instance.reset_state(); +} + +export fn pure_scalar_dispatch(instance: *Engine, event: u32, dt: f64) u32 { + return instance.dispatch(event, dt); +} + +export fn pure_scalar_dispatch_point(instance: *Engine, event: u32, x: f64, y: f64) u32 { + return instance.dispatch_point(event, x, y); +} + +export fn pure_scalar_dispatch_target(instance: *Engine, event: u32, target: u32, x: f64, y: f64) u32 { + return instance.dispatch_target(event, target, x, y); +} + +export fn pure_scalar_dispatch_value(instance: *Engine, event: u32, value: f64) u32 { + return instance.dispatch_value(event, value); +} + +export fn pure_scalar_eval_at(instance: *Engine, start: u32, length: u32) f64 { + return instance.eval_at(start, length); +} + +export fn pure_scalar_configure_evaluation(instance: *Engine, cache: u32, history: u32) u32 { + return instance.configure_evaluation(cache, history); +} + +export fn pure_scalar_forget_evaluation(instance: *Engine, flags: u32) void { + instance.forget_evaluation(flags); +} + +export fn pure_scalar_begin_evaluation(instance: *Engine, limit: u32) void { + instance.begin_evaluation(limit); +} + +export fn pure_scalar_eval_bounded(instance: *Engine, start: u32, length: u32) f64 { + return instance.eval_bounded(start, length); +} + +export fn pure_scalar_evaluation_info(instance: *Engine, id: u32) u32 { + return instance.evaluation_info(id); +} + +export fn pure_scalar_get_state(instance: *Engine, id: u32) f64 { + return instance.get_state(id); +} + +export fn pure_scalar_info(instance: *Engine, id: u32) u32 { + return instance.info(id); +} + +export fn pure_scalar_layout(instance: *Engine, id: u32) u32 { + return instance.layout(id); +} diff --git a/prototypes/workbench/native/README.md b/prototypes/workbench/native/README.md new file mode 100644 index 0000000..2485a2d --- /dev/null +++ b/prototypes/workbench/native/README.md @@ -0,0 +1,136 @@ +# 기존 .pure 작성물을 네이티브로 실행 + +기존 Workbench 문법을 그대로 준비해 C ABI의 독립 계산 인스턴스에서 실행한다. +입력과 저장·복원은 `prototypes/connections`의 실행 코어를 통과한다. 실행 전용 CLI와 +선택형 Linux GTK4 작성 창과 인증된 피어 연결 호스트가 있다. +빌드에는 Node·Zig 0.15.2·C17 컴파일러가 필요하다. 실행 전용 CLI의 실행에는 필요하지 않다. +선택형 작성 창은 소스를 적용할 때 Node·C 컴파일러가 필요하다. +현재 검증 환경은 Linux AArch64다. GTK 창은 실행 전용 프로그램에 들어가지 않는다. +고정 정의 CLI·피어는 검증된 `.pure`의 불변 프로그램을 한 번 링크하고 인스턴스마다 +가변 상태만 둔다. 새 정의를 사용하려면 다시 빌드한다. 작성 창과 브라우저는 정의 교체가 +가능한 전체 작성 용량을 유지한다. [반복 메모리와 실행물 전체 비용](../../../docs/research/028-shared-fixed-program.md)을 +구분해서 확인했다. +고정 실행 앱은 유휴 중 작업 슬롯을 예약하지 않는다. 입력·저장·복원 호출 동안만 +코어 슬롯을 스택에서 대여하고, 1,000개 실제 앱의 반복 RSS 절감과 추가 스택·코드 +비용을 [측정했다](../../../docs/research/049-idle-app-work-slots.md). + +저장소 루트에서: + +```sh +python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure --zig /path/to/zig +prototypes/workbench/build/native/pure-native /tmp/pure-native-work scene +``` + +프로그램에 입력할 명령: + +```text +point drag down green_box 90 130 +point drag move green_box 230 210 +point drag up green_box 230 210 +save +quit +``` + +같은 실행 파일을 같은 디렉터리·이름으로 다시 시작하고 `load`를 입력하면 저장한 상태를 +복원한다. 위 예제에서 `green.x=220`, `green.y=200`, `green.held=0`이다. 시작만으로 기존 상태를 +자동 채택하거나 덮어쓰지 않는다. 저장 성공 때 채택한 버전을 기준으로 다음 저장을 조건부 실행한다. +다른 실행기가 먼저 저장했으면 충돌 상태를 출력하고 로컬 값을 유지한다. + +다른 작성물도 `manage.py native MY.pure --output MY_APP`으로 빌드한다. 지원 명령은 +`show`, `key INPUT down|up`, `value INPUT NUMBER`, `point INPUT down|move|up|cancel TARGET X Y`, +`frame`, `reset`, `save`, `load`, `quit`이다. 입력 한 줄은 510 B까지다. +출력 JSON의 `status=0`은 성공이며 `scalar_error`는 작성된 계산의 실패 코드다. +명령 중 오류가 있었으면 종료 코드는 1, 시작 실패는 2다. 포인터 TARGET은 단일 입력에도 명시한다. +명령 인터페이스는 한 출처를 사용하고 C 호스트 API는 대상별 소유자·키별 출처를 구분한다. + +## 선택형 네이티브 작성 창 + +Linux에서 GTK4 개발 헤더·라이브러리와 `pkg-config`를 설치한 뒤 빌드한다. 작성 창의 +실시간 소스 적용에는 Node와 C 컴파일러도 실행 시 필요하다. CLI 실행에는 둘 다 필요 없다. + +```sh +python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure \ + --desktop --zig /path/to/zig +prototypes/workbench/build/native/pure-desktop /tmp/pure-native-work scene \ + prototypes/workbench/examples/routed-layout.pure +``` + +왼쪽에서 `.pure`를 편집하고 **Apply source**를 누른다. 성공하면 같은 이름의 숫자 상태를 +유지하고 소스 파일을 저장한다. 실패하면 편집 중인 텍스트는 남지만 이전 실행 정의와 저장된 +소스는 유지된다. 창을 다시 열면 소스를 준비·적용한다. 오른쪽 화면을 눌러 키를 전달하고 +도형을 잡아 이동한다. **Save state**와 **Load state**는 소스 파일과 별개의 조건부 스냅샷이다. +소스를 바꾸면 이전 정의의 스냅샷은 자동 복원되지 않는다. 새 정의를 적용한 뒤 상태를 다시 +저장하면 새 정의의 스냅샷이 된다. 수치 입력은 오른쪽 아래 입력창에서 숫자를 입력하고 Enter를 누른다. + +GPU가 없는 Xvfb 검증에서는 GTK의 Cairo 렌더러와 GPU 탐색 비활성화를 사용했다. + +```sh +GSK_RENDERER=cairo GDK_DISABLE=gl,vulkan \ + prototypes/workbench/build/native/pure-desktop /tmp/pure-native-work scene \ + prototypes/workbench/examples/routed-layout.pure +``` + +이 설정은 모든 호스트의 기본값이 아니다. 이 환경의 유휴 GTK 작성 창은 약 36 MiB RSS, +기본 소프트웨어 GPU 경로는 약 174 MiB였으며 CLI는 약 1.5 MiB였다. **편집 도구 전체를 +마이크로 런타임 크기라고 부르지 않는다.** [실제 창 검증·전체 비용](../../../docs/research/025-native-authoring-shell.md). + +`scalar.h`는 호출자가 제공한 메모리의 명시적인 인스턴스 API다. `plan.h`는 빌드 시 준비한 +읽기 전용 데이터, `app.h`는 같은 코어에서 입력·스냅샷·저장을 연결하는 호스트다. +고정 CLI·피어에서는 `plan.h`의 명령·반응 포인터가 비어 있으며, 해당 데이터는 Zig의 +공유 프로그램에서 읽는다. 고정 실행물의 정의 staging 및 선택형 평가 캐시는 사용할 수 없다. +`prepare.mjs`는 기존 컴파일러를 재사용하며 새 런타임 파서를 추가하지 않는다. +준비된 명령을 해석하는 계산 모듈이며, 모든 앱 연산을 기계어로 직접 생성하는 컴파일러는 아니다. +작성 창의 새 정의 설치도 실행 코어의 작업 접수·완료를 거친다. `PURE_NATIVE_AUTHORING`은 +작성 창에만 링크되어 CLI의 코드 크기와 동적 의존성을 늘리지 않는다. + +## 선택형 인증 피어 연결 + +OpenSSL 개발 패키지와 `pkg-config`가 있는 Linux에서 빌드한다. 실행 전용 CLI에는 +OpenSSL을 연결하지 않는다. 같은 `.pure` 정의로 만든 피어 서버와 클라이언트가 필요하다. + +```sh +python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure \ + --peer --zig /path/to/zig +prototypes/workbench/build/native/pure-peer serve 127.0.0.1 8443 /tmp/pure-peer-state scene \ + CA.pem server.pem server.key pure-client +prototypes/workbench/build/native/pure-peer call 127.0.0.1 8443 localhost \ + CA.pem client.pem client.key +``` + +서버 인증서는 `localhost` DNS SAN과 `serverAuth`, 클라이언트 인증서는 `pure-client` +DNS SAN과 `clientAuth`가 필요하다. 양쪽 인증서는 지정한 CA의 서명을 받아야 한다. +실제 검증용 일회성 인증서를 만들고 Python 표준 TLS 클라이언트와 상호운용성·실패 경로를 +검사하는 재현 명령은 다음과 같다. 인증서는 임시 디렉터리에서만 사용하고 삭제한다. + +```sh +python3 prototypes/workbench/native/peer-check.py prototypes/workbench/build/native/pure-peer +``` + +클라이언트 명령은 `state`, `save`, `load`, `frame`, `reset`, `key NAME down|up`, +`value NAME NUMBER`, `point NAME down|move|up|cancel TARGET X Y`, `quit`이다. +서버는 시작 시 기존 저장 상태를 복원하고, 각 요청의 기대 버전을 확인한다. 연결이 끊긴 뒤 +입력 소유권을 해제한다. 응답을 잃은 부작용 작업은 자동 재시도하지 않는다. +정의 identity가 다르면 Pure 클라이언트가 연결 직후 거절한다. 연결은 현재 한 번에 한 +클라이언트만 처리한다. 서버와 클라이언트 모두 인증 실패 시 평문으로 낮추지 않는다. +프로토콜 길이·크기·전체 호스트 RSS와 한계는 [026 보고서](../../../docs/research/026-secure-peer-link.md)에 있다. + +현재 저장 스냅샷은 이 실행물의 준비 정의에 묶여 있다. 정의를 바꾼 뒤 다른 정의의 스냅샷을 +묵시적으로 적용하지 않는다. 작성 창에는 실시간 정의 교체가 있지만 CLI의 실시간 소스 편집과 +기존 JSON 프로젝트 변환은 미구현이다. 현재 보기는 고정된 640×360 논리 화면을 확대·축소하며 +범용 네이티브 레이아웃 엔진이나 macOS 창 백엔드는 아니다. + +일반·C 호스트 sanitizer 검증 (`prototypes/workbench` 기준): + +```sh +node native/check.mjs +node native/check.mjs --sanitize +node native/check.mjs --fixed --sanitize --zig /path/to/zig +node --test *.test.mjs +``` + +첫 세 명령은 현재 소스에서 라이브러리를 직접 빌드하고 기존 Wasm을 사용하며, 실제 자식 +프로세스와 파일 저장이 필요하다. Zig 0.15.2가 `PATH`에 없다면 각 명령에 +`--zig /path/to/zig`를 준다. `--fixed`는 작성물별 공유 불변 프로그램을 빌드해 같은 동작을 +대조한다. Zig 오브젝트는 ReleaseSafe이며 C 호스트만 ASan/UBSan으로 계측한다. +[전체 용량 기준](../../../docs/research/024-native-app-instances.md), +[공유 고정 정의 결과](../../../docs/research/028-shared-fixed-program.md). diff --git a/prototypes/workbench/native/app-memory.c b/prototypes/workbench/native/app-memory.c new file mode 100644 index 0000000..a1fd0f0 --- /dev/null +++ b/prototypes/workbench/native/app-memory.c @@ -0,0 +1,87 @@ +#define _POSIX_C_SOURCE 200809L +/* Linux-only whole-app evidence: many independent native app instances, each + * exercising the execution core, scalar program and durable state path. */ +#include "app.h" +#include +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; + +static long rss_kib(void) { + FILE *file = fopen("/proc/self/status", "r"); + if (!file) return -1; + char line[256]; + long result = -1; + while (fgets(line, sizeof(line), file)) + if (strncmp(line, "VmRSS:", 6) == 0) { + if (sscanf(line + 6, "%ld", &result) != 1) result = -1; + break; + } + fclose(file); + return result; +} + +int main(void) { + enum { COUNT = 1000 }; + char directory[] = "/tmp/pure-app-memory-XXXXXX"; + assert(mkdtemp(directory)); + long before = rss_kib(); + assert(before >= 0); + PureNativeApp *apps = calloc(COUNT, sizeof(*apps)); + void **scalar_memory = calloc(COUNT, sizeof(*scalar_memory)); + assert(apps && scalar_memory); + size_t scalar_bytes = pure_scalar_bytes(); + size_t completed_actions = 0, completed_transfers = 0; + for (size_t i = 0; i < COUNT; ++i) { + scalar_memory[i] = malloc(scalar_bytes); + assert(scalar_memory[i]); + char key[32]; + snprintf(key, sizeof(key), "doc%04zu", i); + assert(pure_native_app_init(&apps[i], &pure_app_plan, scalar_memory[i], + scalar_bytes, directory, key, 4096) == PURE_OK); + assert(pure_native_app_action(&apps[i], (PureAction){.phase = PURE_RESET}).status == PURE_OK); + ++completed_actions; +#ifndef PURE_APP_MEMORY_PERSISTENT_WORK + assert(!apps[i].execution.work && !apps[i].execution.capacity); +#endif + } + long initialized = rss_kib(); + assert(initialized >= before); + for (size_t i = 0; i < COUNT; ++i) { + assert(pure_native_app_save(&apps[i]).status == PURE_OK); + ++completed_transfers; + assert(apps[i].stored_revision == 1); + assert(!apps[i].execution.active && !apps[i].execution.retained); +#ifndef PURE_APP_MEMORY_PERSISTENT_WORK + assert(!apps[i].execution.work && !apps[i].execution.capacity); +#endif + } + long saved = rss_kib(); + assert(saved >= before); + for (size_t i = 0; i < COUNT; i += 249) { + assert(pure_native_app_restore(&apps[i]).status == PURE_OK); + ++completed_transfers; + } + for (size_t i = 0; i < COUNT; ++i) { + assert(!apps[i].execution.active && !apps[i].execution.retained); + char name[128]; + snprintf(name, sizeof(name), "%s/obj-%s", directory, apps[i].document); + assert(unlink(name) == 0); + free(scalar_memory[i]); + } + char lock[128]; + snprintf(lock, sizeof(lock), "%s/.writer.lock", directory); + assert(unlink(lock) == 0 && rmdir(directory) == 0); + printf("{\"apps\":%u,\"app_controller_bytes\":%zu,\"scalar_bytes\":%zu," + "\"rss_before_kib\":%ld,\"rss_initialized_kib\":%ld,\"rss_saved_kib\":%ld," + "\"rss_initialized_delta_kib\":%ld,\"rss_saved_delta_kib\":%ld," + "\"completed_actions\":%zu,\"completed_transfers\":%zu}\n", + COUNT, sizeof(*apps), scalar_bytes, before, initialized, saved, + initialized - before, saved - before, completed_actions, completed_transfers); + free(scalar_memory); + free(apps); + return 0; +} diff --git a/prototypes/workbench/native/app.c b/prototypes/workbench/native/app.c new file mode 100644 index 0000000..e53b8a5 --- /dev/null +++ b/prototypes/workbench/native/app.c @@ -0,0 +1,260 @@ +#include "app.h" +#include "../../connections/store-port.h" +#include "../../connections/store.h" +#include +#include +#include + +_Static_assert(sizeof(double) == 8 && FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024, + "the current snapshot boundary requires IEEE binary64"); + +static const PureProgram connections = {.read = {.to_end = true}, .write = {.to_end = true}}; +static const unsigned char marker[8] = {'P','U','R','E','S','0','0','1'}; +enum { SNAPSHOT_HEADER = 44 }; + +static void release(void *memory, size_t size) { (void)size; free(memory); } +static bool allocate(void *context, size_t size, PureResource *out) { + (void)context; + void *memory = malloc(size); + if (!memory) return false; + *out = (PureResource){.body = {memory, size}, .allocation = memory, + .allocation_size = size, .release = release}; + return true; +} +static uint64_t decode(const unsigned char *bytes, unsigned count) { + uint64_t value = 0; + for (unsigned i = 0; i < count; ++i) value |= (uint64_t)bytes[i] << (8 * i); + return value; +} +static void encode(unsigned char *bytes, uint64_t value, unsigned count) { + for (unsigned i = 0; i < count; ++i) bytes[i] = (unsigned char)(value >> (8 * i)); +} + +static PureResult read_value(void *context, const void *definition, const char *key, + size_t available, PureResource *out, int64_t *revision) { + PureNativeApp *app = context; + (void)definition; + if (!strcmp(key, "load")) { + PureHost store = pure_store_host(app->directory); + return store.read(store.context, &connections, app->document, available, out, revision); + } + if (strcmp(key, "snapshot")) return PURE_INVALID; + if (app->held) return PURE_BUSY; + size_t size = SNAPSHOT_HEADER + 8u * app->plan->state_count; + /* This host contract currently exports the complete typed snapshot. */ + if (size > available) return PURE_BUDGET; + if (!allocate(NULL, size, out)) return PURE_ALLOCATION; + unsigned char *bytes = out->allocation; + memcpy(bytes, marker, sizeof(marker)); + memcpy(bytes + 8, app->plan->identity, 32); + encode(bytes + 40, app->plan->state_count, 4); + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + double value = pure_scalar_get_state(app->scalar, i); + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + encode(bytes + SNAPSHOT_HEADER + i * 8, bits, 8); + } + *revision = app->revision; + return PURE_OK; +} + +static PureResult action(PureNativeApp *app, PureAction a) { + if (a.phase == PURE_RESET) { + if (app->held) return PURE_BUSY; + pure_scalar_reset_state(app->scalar); + return PURE_OK; + } + if (a.phase == PURE_FRAME) { + app->last_scalar_error = pure_scalar_dispatch(app->scalar, 0, 1.0 / 60.0); + return app->last_scalar_error ? PURE_INVALID : PURE_OK; + } + if (a.input >= app->plan->input_count || a.input >= 8) return PURE_INVALID; + PureScalarInputKind kind = app->plan->inputs[a.input].kind; + uint32_t bit = 1u << a.input, event; + if (kind == PURE_VALUE) { + if (a.phase != PURE_CHANGE) return PURE_INVALID; + app->last_scalar_error = pure_scalar_dispatch_value(app->scalar, 33 + a.input, a.x); + return app->last_scalar_error ? PURE_INVALID : PURE_OK; + } + if (kind == PURE_KEY) { + if ((a.phase != PURE_DOWN && a.phase != PURE_UP) || a.source >= 32) return PURE_INVALID; + uint32_t source = 1u << a.source, before = app->key_sources[a.input]; + if (a.phase == PURE_DOWN) { + app->key_sources[a.input] |= source; + app->held |= bit; + if (before) return PURE_OK; + } else { + if (!(before & source)) return PURE_OK; + app->key_sources[a.input] &= ~source; + if (app->key_sources[a.input]) return PURE_OK; + app->held &= ~bit; + } + event = a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u); + app->last_scalar_error = pure_scalar_dispatch(app->scalar, event, 0); + } else { + if (a.phase > PURE_CANCEL) return PURE_INVALID; + uint32_t slot = a.input; + if (kind == PURE_ROUTED) { + if (a.target >= app->plan->target_count || a.target >= 24 || app->plan->targets[a.target].input != a.input) return PURE_INVALID; + slot = 8 + a.target; + } + bit = 1u << slot; + if (a.phase == PURE_DOWN) { + if (app->held & bit) return app->pointer_owners[slot] == a.source ? PURE_OK : PURE_BUSY; + app->held |= bit; + app->pointer_owners[slot] = a.source; + } else { + if (!(app->held & bit) || app->pointer_owners[slot] != a.source) return PURE_INVALID; + if (a.phase == PURE_UP || a.phase == PURE_CANCEL) app->held &= ~bit; + } + event = a.phase == PURE_MOVE ? 17 + a.input : a.phase == PURE_CANCEL ? 25 + a.input + : a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u); + app->last_scalar_error = kind == PURE_ROUTED + ? pure_scalar_dispatch_target(app->scalar, event, a.target, a.x, a.y) + : pure_scalar_dispatch_point(app->scalar, event, a.x, a.y); + } + /* Input ownership follows the physical phases even if an authored handler + * fails; its scalar writes remain atomic. Each routed target has its owner. */ + return app->last_scalar_error ? PURE_INVALID : PURE_OK; +} + +static PureResult write_value(void *context, const void *definition, const char *key, + int64_t expected, PureSpan bytes, PureSpan *reply, + int64_t *revision) { + PureNativeApp *app = context; + (void)definition; + *reply = (PureSpan){0}; + if (!strcmp(key, "save")) return pure_store_put(app->directory, app->document, expected, bytes, revision); + *revision = app->revision; + if (expected != app->revision) return PURE_CONFLICT; + if (app->revision == INT64_MAX) return PURE_LIMIT; + app->last_scalar_error = 0; + if (!strcmp(key, "event")) { + if (bytes.size != sizeof(PureAction)) return PURE_LIMIT; + PureAction input; + memcpy(&input, bytes.data, sizeof(input)); + PureResult status = action(app, input); + if (status != PURE_OK) return status; +#ifdef PURE_NATIVE_AUTHORING + } else if (!strcmp(key, "plan")) { + if (bytes.size != sizeof(const PureScalarPlan *)) return PURE_LIMIT; + if (app->held) return PURE_BUSY; + const PureScalarPlan *candidate; + memcpy(&candidate, bytes.data, sizeof(candidate)); + if (!candidate || !candidate->states || !candidate->inputs || !candidate->views || + candidate->state_count > 32 || candidate->input_count > 8 || + candidate->target_count > 24 || candidate->view_count > 24) return PURE_INVALID; + app->last_scalar_error = pure_scalar_load(app->scalar, candidate, app->plan, NULL); + if (app->last_scalar_error) return PURE_INVALID; + app->plan = candidate; + memset(app->pointer_owners, 0, sizeof(app->pointer_owners)); + memset(app->key_sources, 0, sizeof(app->key_sources)); +#endif + } else if (!strcmp(key, "restore")) { + if (app->held) return PURE_BUSY; + if (bytes.size != SNAPSHOT_HEADER + 8u * app->plan->state_count || + memcmp(bytes.data, marker, sizeof(marker)) || memcmp(bytes.data + 8, app->plan->identity, 32) || + decode(bytes.data + 40, 4) != app->plan->state_count) return PURE_INVALID; + double values[32]; + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + uint64_t bits = decode(bytes.data + SNAPSHOT_HEADER + 8 * i, 8); + memcpy(values + i, &bits, sizeof(bits)); + } + app->last_scalar_error = pure_scalar_load(app->scalar, app->plan, NULL, values); + if (app->last_scalar_error) return PURE_INVALID; + } else return PURE_INVALID; + *revision = ++app->revision; + return PURE_OK; +} + +PureResult pure_native_app_init(PureNativeApp *app, const PureScalarPlan *plan, void *memory, + size_t size, const char *directory, const char *key, size_t budget) { + if (!app || !plan || !directory || !key || !*key || strlen(key) > 64 || plan->state_count > 32) return PURE_INVALID; + for (const char *p = key; *p; ++p) + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')) return PURE_INVALID; + PureScalar *scalar = pure_scalar_initialize(memory, size); + if (!scalar) return PURE_INVALID; + if (pure_scalar_load(scalar, plan, NULL, NULL)) return PURE_INVALID; + PureResult status = pure_store_init(directory); + if (status != PURE_OK) return status; + *app = (PureNativeApp){.scalar = scalar, .plan = plan, .directory = directory}; + strcpy(app->document, key); + PureHost host = {.context = app, .allocate = allocate, .read = read_value, .write = write_value}; + return pure_runtime_init(&app->execution, NULL, 0, NULL, host, budget) ? PURE_OK : PURE_INVALID; +} + +static PureOutcome finish(PureNativeApp *app, PureTicket ticket) { + PureOutcome result; + while (!pure_runtime_result(ticket, &result)) { + if (!pure_runtime_step(&app->execution)) return (PureOutcome){.status = PURE_INVALID}; + } + return result; +} + +PureOutcome pure_native_app_action(PureNativeApp *app, PureAction input) { + PureWork work[1]; + if (!pure_runtime_bind(&app->execution, work, 1)) + return (PureOutcome){.status = PURE_BUSY}; + PureTicket ticket; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_WRITE, "event", app->revision, sizeof(input), &ticket); + PureOutcome result = {.status = status}; + if (status == PURE_OK) { + memcpy(pure_runtime_ingress(ticket, NULL), &input, sizeof(input)); + pure_runtime_ready(ticket); + result = finish(app, ticket); + pure_runtime_release(ticket); + } + result.body = (PureSpan){0}; + if (!pure_runtime_bind(&app->execution, NULL, 0)) abort(); + return result; +} + +static PureOutcome transfer(PureNativeApp *app, bool restore) { + PureWork work[2]; + if (!pure_runtime_bind(&app->execution, work, 2)) + return (PureOutcome){.status = PURE_BUSY}; + PureTicket source, destination; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_READ, restore ? "load" : "snapshot", 0, 0, &source); + PureOutcome result = {.status = status}; + if (status != PURE_OK) goto done; + result = finish(app, source); + int64_t stored = result.revision; + if (result.status == PURE_OK) { + status = pure_runtime_forward(source, &app->execution, restore ? "restore" : "save", + restore ? app->revision : app->stored_revision, &destination); + pure_runtime_release(source); + if (status != PURE_OK) { result = (PureOutcome){.status = status}; goto done; } + result = finish(app, destination); + pure_runtime_release(destination); + if (result.status == PURE_OK) app->stored_revision = restore ? stored : result.revision; + } else pure_runtime_release(source); +done: + /* Bodies are consumed internally before this API returns. */ + result.body = (PureSpan){0}; + if (!pure_runtime_bind(&app->execution, NULL, 0)) abort(); + return result; +} +PureOutcome pure_native_app_save(PureNativeApp *app) { return transfer(app, false); } +PureOutcome pure_native_app_restore(PureNativeApp *app) { return transfer(app, true); } + +#ifdef PURE_NATIVE_AUTHORING +PureResult pure_native_app_replace_plan(PureNativeApp *app, const PureScalarPlan *candidate) { + if (!app || !candidate) return PURE_INVALID; + if (app->held || app->execution.active || app->execution.retained) return PURE_BUSY; + PureWork work[1]; + if (!pure_runtime_bind(&app->execution, work, 1)) return PURE_BUSY; + PureTicket ticket; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_WRITE, + "plan", app->revision, sizeof(candidate), &ticket); + if (status == PURE_OK) { + memcpy(pure_runtime_ingress(ticket, NULL), &candidate, sizeof(candidate)); + pure_runtime_ready(ticket); + PureOutcome result = finish(app, ticket); + pure_runtime_release(ticket); + status = result.status; + } + if (!pure_runtime_bind(&app->execution, NULL, 0)) abort(); + return status; +} +#endif diff --git a/prototypes/workbench/native/app.h b/prototypes/workbench/native/app.h new file mode 100644 index 0000000..dd498cc --- /dev/null +++ b/prototypes/workbench/native/app.h @@ -0,0 +1,32 @@ +#ifndef PURE_NATIVE_APP_H +#define PURE_NATIVE_APP_H +#include "plan.h" +#include "../../connections/runtime.h" + +typedef enum { PURE_DOWN, PURE_UP, PURE_MOVE, PURE_CANCEL, PURE_CHANGE, PURE_FRAME, PURE_RESET } PureActionPhase; +/* In-process event value, never a persistent or network representation. */ +typedef struct { uint32_t input, phase, target, source; double x, y; } PureAction; +typedef struct { + PureScalar *scalar; + const PureScalarPlan *plan; + const char *directory; + char document[65]; + PureRuntime execution; + int64_t revision, stored_revision; + uint32_t last_scalar_error, held, pointer_owners[32], key_sources[8]; +} PureNativeApp; + +/* Caller supplies instance memory and keeps plan/directory alive. This selected + * host uses the existing SQL-free store. Single owner; effects are synchronous. + * Idle instances retain no work slots; operations bind caller-stack slots. */ +PureResult pure_native_app_init(PureNativeApp *, const PureScalarPlan *, void *memory, + size_t size, const char *directory, const char *key, size_t budget); +PureOutcome pure_native_app_action(PureNativeApp *, PureAction); +PureOutcome pure_native_app_save(PureNativeApp *); +PureOutcome pure_native_app_restore(PureNativeApp *); +/* Authoring host only. Candidate plan stays alive on success; previous plan + * stays alive until this call returns. A failed reload retains the old app. */ +#ifdef PURE_NATIVE_AUTHORING +PureResult pure_native_app_replace_plan(PureNativeApp *, const PureScalarPlan *); +#endif +#endif diff --git a/prototypes/workbench/native/check.mjs b/prototypes/workbench/native/check.mjs new file mode 100644 index 0000000..3b021f1 --- /dev/null +++ b/prototypes/workbench/native/check.mjs @@ -0,0 +1,178 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { resolve, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { compile } from '../web/compiler.mjs'; +import { createRuntime } from '../web/runtime.mjs'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const repo = resolve(root, '../..'); +const connections = resolve(root, '../connections'); +const args = process.argv.slice(2); +const sanitized = args.includes('--sanitize'); +const fixed = args.includes('--fixed'); +const zigIndex = args.indexOf('--zig'); +const zig = zigIndex >= 0 ? args[zigIndex + 1] : process.env.ZIG || 'zig'; +const index = args.indexOf('--output'); +const output = index >= 0 ? resolve(args[index + 1]) : null; +const temporary = await mkdtemp(join(tmpdir(), 'pure-native-parity-')); +const library = join(temporary, 'general.a'); +const wasm = await readFile(resolve(root, 'web/core.wasm')); +const hash = data => createHash('sha256').update(data).digest('hex'); +const compiler = process.env.CC || 'cc'; +const flags = ['-std=c17', sanitized ? '-O1' : '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror', + '-DPURE_RUNTIME_METRICS', + ...(sanitized ? ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] : []), + ...(fixed ? ['-DPURE_FIXED_DEFINITION=1'] : []), '-I' + resolve(root, 'native')]; +function run(command, arguments_, options = {}) { + const p = spawnSync(command, arguments_, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, timeout: 60000, ...options }); + assert.ifError(p.error); + return p; +} +function successful(command, arguments_, options) { + const p = run(command, arguments_, options); + assert.equal(p.status, 0, p.stdout + p.stderr); + return p.stdout; +} +const units = [resolve(root, 'native/app.c'), resolve(root, 'native/plan.c'), + ...['runtime.c', 'evaluate.c', 'store.c', 'store-port.c'].map(name => resolve(connections, name))]; +const objects = []; +const cases = []; +let unitResult; +try { + successful(zig, ['build-lib', resolve(root, 'native.zig'), '-O', 'ReleaseSafe', + '-fno-compiler-rt', '-femit-bin=' + library, '--cache-dir', resolve(root, 'build/cache'), + '--global-cache-dir', resolve(root, 'build/global-cache')]); + for (const [i, path] of units.entries()) { + const object = join(temporary, `unit-${i}.o`); + successful(compiler, [...flags, '-c', path, '-o', object]); + objects.push(object); + } + const main = join(temporary, 'main.o'); + successful(compiler, [...flags, '-c', resolve(root, 'native/main.c'), '-o', main]); + const sizeSource = join(temporary, 'size.c'); + await writeFile(sizeSource, '#include "scalar.h"\n#include \nint main(void) { printf("%zu\\n", pure_scalar_bytes()); }\n'); + const generalSizeBinary = join(temporary, 'general-size'); + successful(compiler, [...flags, sizeSource, library, '-lm', '-o', generalSizeBinary]); + const generalInstanceBytes = Number(successful(generalSizeBinary, []).trim()); + const definition = name => readFile(resolve(root, 'examples', name + '.pure'), 'utf8'); + const repeat = (line, n) => Array(n).fill(line); + const definitions = [ + ['counter', await definition('counter'), ['key tap down', 'key tap down', 'key tap up', 'key tap down', 'key tap up']], + ['toggle', await definition('toggle'), ['key toggle down', 'key toggle up', ...Array.from({length: 13}, () => ['key move down', 'key move up']).flat()]], + ['charge', await definition('charge'), ['key jump down', ...repeat('frame', 12), 'key jump up', ...repeat('frame', 90)]], + ['direct', await definition('direct'), ['point drag down tile 90 180', 'point drag move tile 250 220', 'point drag up tile 250 220']], + ['routed', await definition('routed-layout'), ['point drag down green_box 90 130', 'point drag down blue_box 290 170', + 'point drag move green_box 200 210', 'point drag move blue_box 500 270', 'point drag cancel green_box 200 210', + 'point drag up blue_box 500 270', 'key left down', 'key left up']], + ['layout', await definition('layout'), ['point drag1 down tile1 90 130', 'point drag1 move tile1 230 210', + 'point drag1 up tile1 230 210', 'key right down', 'key right up', + 'point drag3 down tile3 460 230', 'point drag3 cancel tile3 510 270']], + ['shared-layout', await definition('shared-layout'), ['point drag_green down green_box 90 130', + 'point drag_green move green_box 230 210', 'point drag_green up green_box 230 210', + 'key right down', 'key right up', 'point drag_amber down amber_box 460 230', + 'point drag_amber cancel amber_box 510 270']], + ['document', await definition('document'), ['value split 99', 'value split -10', 'value split 61.5', 'key reset down', 'key reset up']], + ['atomic-error', 'state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,"value: ")', + ['value n 0', 'value n 4']], + ['blank', '', []], + ['literal-boundary', 'state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,"한글 \\\" \\\\ \\n")', ['value n -0']], + ]; + for (const [name, source, commands] of definitions) { + const sourcePath = join(temporary, name + '.pure'), prepared = join(temporary, name + '.c'), binary = join(temporary, name); + await writeFile(sourcePath, source); + const profile = join(temporary, name + '.zig'); + successful(process.execPath, [resolve(root, 'native/prepare.mjs'), sourcePath, prepared, + ...(fixed ? [profile] : [])]); + let caseLibrary = library; + if (fixed) { + caseLibrary = join(temporary, name + '.a'); + successful(zig, ['build-lib', '-O', 'ReleaseSafe', '-fno-compiler-rt', '--dep', 'profile', + '-Mroot=' + resolve(root, 'native-fixed.zig'), '-Mprofile=' + profile, + '-femit-bin=' + caseLibrary, '--cache-dir', resolve(root, 'build/cache'), + '--global-cache-dir', resolve(root, 'build/global-cache')]); + } + successful(compiler, [...flags, prepared, main, ...objects, caseLibrary, '-lm', '-o', binary]); + const sizeBinary = join(temporary, name + '-size'); + successful(compiler, [...flags, sizeSource, caseLibrary, '-lm', '-o', sizeBinary]); + const instanceBytes = Number(successful(sizeBinary, []).trim()); + assert(Number.isSafeInteger(instanceBytes) && instanceBytes > 0 && instanceBytes <= generalInstanceBytes); + const native = run(binary, [join(temporary, name + '-store'), 'project'], { input: commands.concat(['save', 'quit']).join('\n') + '\n' }); + const rows = native.stdout.trim().split('\n').map(line => JSON.parse(line)); + const runtime = await createRuntime(wasm); + const plan = compile(source); + runtime.apply(source); + function equivalent(row) { + const snapshot = runtime.snapshot(); + assert.deepEqual(row.states, snapshot.states, name); + const views = Object.fromEntries(snapshot.views.map(view => [view.id, Object.fromEntries(Object.keys(plan.views.find(v => v.id === view.id).bindings).map(key => [key, view[key]]))])); + assert.deepEqual(row.views, views, name); + } + equivalent(rows[0]); + let failures = 0; + commands.forEach((command, i) => { + const [verb, input, phase, target, x, y] = command.split(' '); + let code = 0; + if (verb === 'key') code = phase === 'down' ? runtime.down(input) : runtime.up(input); + else if (verb === 'frame') code = runtime.frame(1); + else if (verb === 'value') code = runtime.change(input, Number(phase)); + else if (verb === 'point') { + const routed = plan.inputs.find(p => p.name === input).routes; + code = runtime.point(input, phase, Number(x), Number(y), target, routed ? target : undefined); + } + if (code) ++failures; + assert.equal(rows[i + 1].status === 0, code === 0, command); + if (code) assert.equal(rows[i + 1].scalar_error, code, command); + equivalent(rows[i + 1]); + }); + assert.equal(rows.length, commands.length + 2); + assert.equal(rows.at(-1).status, 0); + assert.equal(native.status, failures ? 1 : 0, native.stderr); + const restored = JSON.parse(successful(binary, [join(temporary, name + '-store'), 'project'], {input: 'load\nquit\n'}).trim().split('\n').at(-1)); + equivalent(restored); + assert.equal(restored.stored_revision, '1'); + if (name === 'routed') { + if (fixed) { + const capacityUnit = join(temporary, 'fixed-capacity'); + successful(compiler, [...flags, prepared, resolve(root, 'native/test-fixed.c'), + resolve(root, 'native/plan.c'), caseLibrary, '-lm', '-o', capacityUnit]); + unitResult = JSON.parse(successful(capacityUnit, [])); + } else { + const unit = join(temporary, 'test-native'); + const authoring = join(temporary, 'app-author.o'); + successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', '-c', units[0], '-o', authoring]); + successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', prepared, resolve(root, 'native/test.c'), + authoring, ...objects.slice(1), caseLibrary, '-lm', '-o', unit]); + unitResult = JSON.parse(successful(unit, [])); + } + } + cases.push({name, commands: commands.length, failed_events: failures, states_and_views_match: true, + instance_bytes: instanceBytes, fixed_profile_sha256: fixed ? hash(await readFile(profile)) : null, + fresh_process_restore: true, source, source_sha256: hash(source), prepared_sha256: hash(await readFile(prepared)), + native_binary_sha256: hash(await readFile(binary)), native_binary_bytes: (await readFile(binary)).length}); + } + const paths = [resolve(root, 'core.zig'), resolve(root, 'engine.zig'), resolve(root, 'evaluation.zig'), resolve(root, 'native.zig'), resolve(root, 'native-fixed.zig'), + resolve(root, 'manage.py'), ...['scalar.h','plan.h','plan.c','app.h','app.c','main.c','prepare.mjs','test.c','test-fixed.c','check.mjs'].map(name => resolve(root, 'native', name)), + ...['compiler.mjs','runtime.mjs','project.mjs','core.wasm'].map(name => resolve(root, 'web', name)), + ...['runtime.c','runtime.h','evaluate.c','program.h','flow.h','result.h','store.c','store.h','store-port.c','store-port.h'].map(name => resolve(connections, name))]; + const sourceHashes = Object.fromEntries(await Promise.all(paths.map(async path => [relative(repo, path), hash(await readFile(path))]))); + const result = {kind:'native-instance-and-authored-behavior-correctness-not-performance', sanitized_c_host: sanitized, + profile: fixed ? 'fixed-definition' : 'general-authoring', general_instance_bytes: generalInstanceBytes, + zig_mode:'ReleaseSafe; Zig object is not ASan-instrumented', compiler: successful(compiler,['--version']).split('\n')[0], + cases, units: unitResult, source_sha256: sourceHashes, scalar_archive_sha256: hash(await readFile(library)), + scalar_archive_size: successful('size',[library]), scalar_archive_undefined: successful('nm',['-u',library])}; + if (output) { + await mkdir(resolve(output, '..'), {recursive: true}); + await writeFile(output, JSON.stringify(result,null,2)+'\n', {flag:'wx'}); + } + console.log(JSON.stringify({cases:cases.length, event_comparisons:cases.reduce((n,c)=>n+c.commands,0), + general_instance_bytes: generalInstanceBytes, + instance_bytes: Object.fromEntries(cases.map(c => [c.name, c.instance_bytes])), + units:unitResult, sanitized_c_host:sanitized, profile:result.profile})); +} finally { + await rm(temporary, {recursive:true, force:true}); +} diff --git a/prototypes/workbench/native/check_app_memory.py b/prototypes/workbench/native/check_app_memory.py new file mode 100644 index 0000000..713978e --- /dev/null +++ b/prototypes/workbench/native/check_app_memory.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Compare complete 1,000-instance native app memory, with real actions and saves.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import statistics +import subprocess +import tempfile + +WORKBENCH = Path(__file__).resolve().parents[1] +REPO = WORKBENCH.parents[1] +NATIVE = WORKBENCH / 'native' +CONNECTIONS = REPO / 'prototypes/connections' +PREPARED = WORKBENCH / 'build/native/application.c' +PROFILE = WORKBENCH / 'build/native/fixed-profile.zig' +LIBRARY = WORKBENCH / 'build/native/libpure-scalar-fixed.a' +BASELINE = WORKBENCH / 'results/2026-09-24-native-desktop/sources/native' + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def build(cc, directory, name, app, header, legacy, metrics=False): + binary = directory / name + command = [cc, '-std=c17', '-O2', + '-ffile-prefix-map=' + str(directory) + '=/pure-app-memory', + '-Wall', '-Wextra', + '-Wpedantic', '-Werror', '-DPURE_FIXED_DEFINITION=1'] + if legacy: + command.append('-DPURE_APP_MEMORY_PERSISTENT_WORK=1') + if metrics: + command.append('-DPURE_RUNTIME_METRICS') + command += ['-I' + str(header), '-I' + str(NATIVE), str(PREPARED), + str(directory / 'app-memory.c'), str(app), str(NATIVE / 'plan.c')] + command += [str(CONNECTIONS / name) for name in + ('runtime.c', 'evaluate.c', 'store.c', 'store-port.c')] + command += [str(LIBRARY), '-lm', '-o', str(binary)] + subprocess.run(command, check=True) + sections = subprocess.check_output(['size', str(binary)], text=True).splitlines()[1].split() + samples = [] + for _ in range(3): + sample = json.loads(subprocess.check_output([str(binary)], text=True, timeout=30)) + assert sample['apps'] == sample['completed_actions'] == 1000 + assert sample['completed_transfers'] == 1005 + samples.append(sample) + return {'binary_sha256': digest(binary), 'app_source_sha256': digest(app), + 'app_header_sha256': digest(header / 'app.h'), + 'text_bytes': int(sections[0]), 'data_bytes': int(sections[1]), + 'app_controller_bytes': samples[0]['app_controller_bytes'], + 'scalar_bytes': samples[0]['scalar_bytes'], 'samples': samples, + 'median_initialized_delta_kib': statistics.median( + sample['rss_initialized_delta_kib'] for sample in samples), + 'median_saved_delta_kib': statistics.median( + sample['rss_saved_delta_kib'] for sample in samples)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cc', default=os.environ.get('CC', 'cc')) + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + with tempfile.TemporaryDirectory(prefix='pure-app-memory-') as root: + directory = Path(root) + shutil.copy2(NATIVE / 'app-memory.c', directory / 'app-memory.c') + generated = directory / 'application.c' + generated_profile = directory / 'fixed-profile.zig' + subprocess.run(['node', str(NATIVE / 'prepare.mjs'), + str(WORKBENCH / 'examples/routed-layout.pure'), + str(generated), str(generated_profile)], check=True) + assert generated.read_bytes() == PREPARED.read_bytes(), 'prepared app is stale' + assert generated_profile.read_bytes() == PROFILE.read_bytes(), 'scalar profile is stale' + baseline = build(args.cc, directory, 'baseline', BASELINE / 'app.c', BASELINE, True) + elastic = build(args.cc, directory, 'elastic', NATIVE / 'app.c', NATIVE, False) + elastic_metrics = build(args.cc, directory, 'elastic-metrics', NATIVE / 'app.c', NATIVE, False, True) + assert baseline['scalar_bytes'] == elastic['scalar_bytes'] == elastic_metrics['scalar_bytes'] + assert elastic['app_controller_bytes'] < baseline['app_controller_bytes'] + assert elastic_metrics['app_controller_bytes'] - elastic['app_controller_bytes'] == 72 + result = {'profile': 'whole-native-app-idle-work-storage', + 'prepared_sha256': digest(PREPARED), 'scalar_archive_sha256': digest(LIBRARY), + 'harness_sha256': digest(NATIVE / 'app-memory.c'), + 'checker_sha256': digest(Path(__file__)), + 'baseline': baseline, 'elastic': elastic, 'elastic_metrics': elastic_metrics, + 'controller_saved_per_app_bytes': baseline['app_controller_bytes'] - elastic['app_controller_bytes'], + 'metrics_storage_saved_per_app_bytes': elastic_metrics['app_controller_bytes'] - elastic['app_controller_bytes'], + 'metrics_initialized_rss_median_saved_kib': elastic_metrics['median_initialized_delta_kib'] - elastic['median_initialized_delta_kib'], + 'metrics_saved_rss_median_saved_kib': elastic_metrics['median_saved_delta_kib'] - elastic['median_saved_delta_kib'], + 'initialized_rss_median_saved_kib': baseline['median_initialized_delta_kib'] - elastic['median_initialized_delta_kib'], + 'saved_rss_median_saved_kib': baseline['median_saved_delta_kib'] - elastic['median_saved_delta_kib']} + output = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.open('x').write(output) + print(output, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/workbench/native/desktop.c b/prototypes/workbench/native/desktop.c new file mode 100644 index 0000000..064b181 --- /dev/null +++ b/prototypes/workbench/native/desktop.c @@ -0,0 +1,503 @@ +/* Linux authoring shell. GTK, Cairo, the compiler and module loader stay outside + * the application/execution core; the standalone CLI links none of them. */ +#include "app.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PURE_PREPARE_PATH +#error PURE_PREPARE_PATH must name the existing build-time compiler adapter +#endif +#ifndef PURE_NATIVE_HEADERS +#error PURE_NATIVE_HEADERS must name the native plan headers +#endif +#ifndef PURE_CC +#define PURE_CC "cc" +#endif + +extern const PureScalarPlan pure_app_plan; + +typedef struct { + PureNativeApp app; + void *scalar_memory, *module; + const char *source_path; + char *initial_source; + gsize initial_source_bytes; + char *scratch; + unsigned generation; + GtkWidget *window, *canvas, *status, *values, *source; + bool pointer_active; + uint32_t pointer_input, pointer_target; + double pointer_x, pointer_y, pointer_px, pointer_py; + uint32_t keys_down; +} Desktop; + +typedef struct { double field[4]; bool valid; } SceneView; +typedef struct { Desktop *desktop; uint32_t input; } ValueControl; + +static void free_value_control(gpointer data, GClosure *closure) { + (void)closure; + g_free(data); +} + +static void report(Desktop *d, const char *name, PureOutcome result) { + char message[200]; + snprintf(message, sizeof(message), "%s · status %u · scalar %u · local %" PRId64 " · stored %" PRId64, + name, (unsigned)result.status, d->app.last_scalar_error, d->app.revision, d->app.stored_revision); + gtk_label_set_text(GTK_LABEL(d->status), message); + gtk_widget_queue_draw(d->canvas); +} + +static bool scene(Desktop *d, SceneView result[24]) { + if (d->app.plan->view_count > 24) return false; + pure_scalar_begin_evaluation(d->app.scalar, 2048); + for (uint32_t i = 0; i < d->app.plan->view_count; ++i) { + const PureScalarView *view = &d->app.plan->views[i]; + result[i].valid = view->count <= 4; + for (uint32_t j = 0; j < view->count && j < 4; ++j) { + result[i].field[j] = pure_scalar_eval_bounded(d->app.scalar, view->fields[j].start, view->fields[j].length); + if (pure_scalar_evaluation_info(d->app.scalar, 0) || !isfinite(result[i].field[j])) result[i].valid = false; + } + if (view->count >= 3 && strcmp(view->type, "text") && result[i].field[2] < 0) result[i].valid = false; + if (view->count == 4 && result[i].field[3] < 0) result[i].valid = false; + } + return true; +} + +static void viewport(int width, int height, double *scale, double *left, double *top) { + *scale = fmin((double)width / 640.0, (double)height / 360.0); + if (*scale <= 0) *scale = 1; + *left = (width - 640 * *scale) / 2; + *top = (height - 360 * *scale) / 2; +} + +static void set_color(cairo_t *cr, const char *text) { + GdkRGBA color; + if (!text || !gdk_rgba_parse(&color, text)) gdk_rgba_parse(&color, "#79a7a1"); + cairo_set_source_rgba(cr, color.red, color.green, color.blue, color.alpha); +} + +static void draw(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data) { + Desktop *d = data; + cairo_set_source_rgb(cr, .055, .075, .09); + cairo_paint(cr); + double scale, left, top; + viewport(width, height, &scale, &left, &top); + cairo_save(cr); + cairo_translate(cr, left, top); + cairo_scale(cr, scale, scale); + cairo_set_source_rgb(cr, .08, .115, .14); + cairo_rectangle(cr, 0, 0, 640, 360); + cairo_fill(cr); + SceneView values[24] = {0}; + if (scene(d, values)) for (uint32_t i = 0; i < d->app.plan->view_count; ++i) { + const PureScalarView *v = &d->app.plan->views[i]; + const double *f = values[i].field; + if (!values[i].valid) continue; + if (!strcmp(v->type, "box") && v->count == 4) { + set_color(cr, v->decoration); + cairo_rectangle(cr, f[0], f[1], f[2], f[3]); + cairo_fill(cr); + } else if (!strcmp(v->type, "circle") && v->count == 3) { + set_color(cr, v->decoration); + cairo_arc(cr, f[0], f[1], f[2], 0, 2 * G_PI); + cairo_fill(cr); + } else if (!strcmp(v->type, "text") && v->count == 3) { + char number[64]; + snprintf(number, sizeof(number), "%.6g", f[2]); + char *label = g_strconcat(v->decoration ? v->decoration : "", number, NULL); + PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(area), label); + PangoFontDescription *font = pango_font_description_from_string("Sans 15"); + pango_layout_set_font_description(layout, font); + cairo_set_source_rgb(cr, .88, .92, .92); + cairo_move_to(cr, f[0], f[1]); + pango_cairo_show_layout(cr, layout); + pango_font_description_free(font); + g_object_unref(layout); + g_free(label); + } + } + cairo_restore(cr); +} + +static bool hit(Desktop *d, double px, double py, uint32_t *input, uint32_t *target) { + double scale, left, top; + viewport(gtk_widget_get_width(d->canvas), gtk_widget_get_height(d->canvas), &scale, &left, &top); + double x = (px - left) / scale, y = (py - top) / scale; + if (x < 0 || y < 0 || x >= 640 || y >= 360) return false; + SceneView views[24] = {0}; + if (!scene(d, views)) return false; + for (uint32_t ri = d->app.plan->view_count; ri; --ri) { + uint32_t i = ri - 1; + const PureScalarView *v = &d->app.plan->views[i]; + const double *f = views[i].field; + if (!views[i].valid) continue; + bool inside = (!strcmp(v->type, "box") && v->count == 4 && + x >= f[0] && x <= f[0] + f[2] && y >= f[1] && y <= f[1] + f[3]) || + (!strcmp(v->type, "circle") && v->count == 3 && + (x - f[0]) * (x - f[0]) + (y - f[1]) * (y - f[1]) <= f[2] * f[2]); + if (!inside) continue; + for (uint32_t j = 0; j < d->app.plan->target_count; ++j) + if (!strcmp(v->id, d->app.plan->targets[j].view)) { + *input = d->app.plan->targets[j].input; *target = j; return true; + } + for (uint32_t j = 0; j < d->app.plan->input_count; ++j) + if (d->app.plan->inputs[j].kind == PURE_POINT && + !strcmp(v->id, d->app.plan->inputs[j].target)) { + *input = j; *target = UINT32_MAX; return true; + } + } + return false; +} + +static void pointer(Desktop *d, PureActionPhase phase, double px, double py) { + double scale, left, top; + viewport(gtk_widget_get_width(d->canvas), gtk_widget_get_height(d->canvas), &scale, &left, &top); + d->pointer_px = px; + d->pointer_py = py; + d->pointer_x = (px - left) / scale; + d->pointer_y = (py - top) / scale; + PureAction a = {.input = d->pointer_input, .phase = phase, .target = d->pointer_target, + .source = 0, .x = d->pointer_x, .y = d->pointer_y}; + report(d, "pointer", pure_native_app_action(&d->app, a)); +} +static void drag_begin(GtkGestureDrag *gesture, double x, double y, gpointer data) { + (void)gesture; + Desktop *d = data; + if (d->pointer_active || !hit(d, x, y, &d->pointer_input, &d->pointer_target)) return; + gtk_widget_grab_focus(d->canvas); + pointer(d, PURE_DOWN, x, y); + uint32_t slot = d->pointer_target == UINT32_MAX ? d->pointer_input : 8 + d->pointer_target; + d->pointer_active = (d->app.held & (1u << slot)) != 0; +} +static void drag_update(GtkGestureDrag *gesture, double dx, double dy, gpointer data) { + Desktop *d = data; + if (!d->pointer_active) return; + double x, y; + if (gtk_gesture_drag_get_start_point(gesture, &x, &y)) pointer(d, PURE_MOVE, x + dx, y + dy); +} +static void drag_end(GtkGestureDrag *gesture, double dx, double dy, gpointer data) { + Desktop *d = data; + if (!d->pointer_active) return; + double x, y; + if (gtk_gesture_drag_get_start_point(gesture, &x, &y)) pointer(d, PURE_UP, x + dx, y + dy); + else pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py); + d->pointer_active = false; +} +static void drag_cancel(GtkGesture *gesture, GdkEventSequence *sequence, gpointer data) { + (void)gesture; (void)sequence; + Desktop *d = data; + if (d->pointer_active) { + pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py); + d->pointer_active = false; + } +} + +static bool key_matches(const char *key, guint value) { + if (!strcmp(key, "ArrowLeft")) return value == GDK_KEY_Left; + if (!strcmp(key, "ArrowRight")) return value == GDK_KEY_Right; + if (!strcmp(key, "ArrowUp")) return value == GDK_KEY_Up; + if (!strcmp(key, "ArrowDown")) return value == GDK_KEY_Down; + if (!strcmp(key, "Space")) return value == GDK_KEY_space; + if (strlen(key) == 4 && !memcmp(key, "Key", 3) && value < 128) + return g_ascii_toupper((char)value) == key[3]; + return false; +} +static gboolean key_event(GtkEventControllerKey *controller, guint keyval, guint keycode, + GdkModifierType modifiers, gpointer data) { + (void)keycode; (void)modifiers; + Desktop *d = data; + bool pressed = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(controller), "pressed")); + bool handled = false; + for (uint32_t i = 0; i < d->app.plan->input_count; ++i) { + const PureScalarInput *input = &d->app.plan->inputs[i]; + if (input->kind != PURE_KEY || !key_matches(input->key, keyval)) continue; + handled = true; + if (pressed) d->keys_down |= 1u << i; else d->keys_down &= ~(1u << i); + report(d, input->name, pure_native_app_action(&d->app, (PureAction){.input = i, + .phase = pressed ? PURE_DOWN : PURE_UP, .source = 0})); + } + return handled; +} +static gboolean key_down(GtkEventControllerKey *c, guint key, guint code, GdkModifierType mods, gpointer d) { + g_object_set_data(G_OBJECT(c), "pressed", GINT_TO_POINTER(1)); + return key_event(c, key, code, mods, d); +} +static void key_up(GtkEventControllerKey *c, guint key, guint code, GdkModifierType mods, gpointer d) { + g_object_set_data(G_OBJECT(c), "pressed", NULL); + (void)key_event(c, key, code, mods, d); +} +static void focus_left(GtkEventControllerFocus *controller, gpointer data) { + (void)controller; + Desktop *d = data; + if (d->pointer_active) { + pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py); + d->pointer_active = false; + } + for (uint32_t i = 0; i < d->app.plan->input_count; ++i) + if (d->keys_down & (1u << i)) + report(d, "key release", pure_native_app_action(&d->app, (PureAction){.input = i, .phase = PURE_UP})); + d->keys_down = 0; +} + +static void value_enter(GtkEntry *entry, gpointer data) { + ValueControl *control = data; + const char *s = gtk_editable_get_text(GTK_EDITABLE(entry)); + char *end; + double n = g_ascii_strtod(s, &end); + if (!*s || *end || !isfinite(n)) { + gtk_label_set_text(GTK_LABEL(control->desktop->status), "Value must be a finite number"); + return; + } + report(control->desktop, "value", pure_native_app_action(&control->desktop->app, + (PureAction){.input = control->input, .phase = PURE_CHANGE, .x = n})); +} +static void rebuild_values(Desktop *d) { + GtkWidget *child; + while ((child = gtk_widget_get_first_child(d->values))) gtk_box_remove(GTK_BOX(d->values), child); + for (uint32_t i = 0; i < d->app.plan->input_count; ++i) { + if (d->app.plan->inputs[i].kind != PURE_VALUE) continue; + GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + GtkWidget *label = gtk_label_new(d->app.plan->inputs[i].name); + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "number ↵"); + gtk_widget_set_hexpand(entry, TRUE); + gtk_box_append(GTK_BOX(row), label); + gtk_box_append(GTK_BOX(row), entry); + gtk_box_append(GTK_BOX(d->values), row); + ValueControl *control = g_new(ValueControl, 1); + *control = (ValueControl){d, i}; + g_signal_connect_data(entry, "activate", G_CALLBACK(value_enter), control, free_value_control, 0); + } +} + +static bool run(char **arguments, char **failure) { + GError *error = NULL; + char *output = NULL, *err = NULL; + int status = 0; + bool launched = g_spawn_sync(NULL, arguments, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, + &output, &err, &status, &error); + bool ok = launched && g_spawn_check_wait_status(status, &error); + if (!ok) { + if (err && *err) { + const char *begin = strstr(err, "Error: "); + if (!begin) begin = err; + const char *end = strchr(begin, '\n'); + *failure = g_strndup(begin, end ? (size_t)(end - begin) : 300); + } else *failure = g_strdup(error ? error->message : "tool failed"); + } + g_clear_error(&error); + g_free(output); g_free(err); + return ok; +} + +static void apply_source(Desktop *d, bool save_source) { + if (d->app.held) { + gtk_label_set_text(GTK_LABEL(d->status), "Release held inputs before Apply"); + return; + } + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer, &start, &end); + char *source = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); + size_t bytes = strlen(source); + if (bytes > 32768) { + gtk_label_set_text(GTK_LABEL(d->status), "Source exceeds the 32 KiB compiler limit"); + g_free(source); return; + } + char *input = g_build_filename(d->scratch, "source.pure", NULL); + char *output = g_strdup_printf("%s/plan-%u.c", d->scratch, ++d->generation); + char *module = g_strdup_printf("%s/plan-%u.so", d->scratch, d->generation); + char *failure = NULL; + GError *error = NULL; + if (!g_file_set_contents(input, source, bytes, &error)) { + failure = g_strdup(error->message); g_clear_error(&error); goto done; + } + char *prepare_argv[] = {"node", PURE_PREPARE_PATH, input, output, NULL}; + if (!run(prepare_argv, &failure)) goto done; + char *headers = g_strconcat("-I", PURE_NATIVE_HEADERS, NULL); + char *compile_argv[] = {PURE_CC, "-std=c17", "-O2", "-fPIC", "-shared", "-Wall", "-Wextra", "-Werror", + headers, output, "-o", module, NULL}; + bool built = run(compile_argv, &failure); + g_free(headers); + if (!built) goto done; + void *handle = dlopen(module, RTLD_NOW | RTLD_LOCAL); + if (!handle) { failure = g_strdup(dlerror()); goto done; } + const PureScalarPlan *candidate = dlsym(handle, "pure_app_plan"); + bool same = candidate && !memcmp(candidate->identity, d->app.plan->identity, sizeof(candidate->identity)); + if (!same) { + PureResult status = pure_native_app_replace_plan(&d->app, candidate); + if (status != PURE_OK) { + failure = g_strdup_printf("plan rejected: status %u, scalar %u", status, d->app.last_scalar_error); + dlclose(handle); goto done; + } + if (d->module) dlclose(d->module); + d->module = handle; + rebuild_values(d); + } else { + dlclose(handle); + } + if (save_source && !g_file_set_contents(d->source_path, source, bytes, &error)) { + failure = g_strdup_printf("Applied in memory; source save failed: %s", error->message); + g_clear_error(&error); + } else { + gtk_text_buffer_set_modified(buffer, FALSE); + report(d, "Applied source", (PureOutcome){.status = PURE_OK}); + } +done: + if (failure) { + char message[350]; + g_snprintf(message, sizeof(message), "Apply: %.335s", failure); + gtk_label_set_text(GTK_LABEL(d->status), message); + g_free(failure); + } + g_remove(input); g_remove(output); g_remove(module); + g_free(input); g_free(output); g_free(module); g_free(source); + gtk_widget_queue_draw(d->canvas); +} +static void apply(GtkButton *button, gpointer data) { + (void)button; + apply_source(data, true); +} + +static void save_state(GtkButton *button, gpointer data) { + (void)button; + Desktop *d = data; + report(d, "Save state", pure_native_app_save(&d->app)); +} +static void load_state(GtkButton *button, gpointer data) { + (void)button; + Desktop *d = data; + report(d, "Load state", pure_native_app_restore(&d->app)); +} +static void reset_state(GtkButton *button, gpointer data) { + (void)button; + Desktop *d = data; + report(d, "Reset", pure_native_app_action(&d->app, (PureAction){.phase = PURE_RESET})); +} + +static GtkWidget *button(GtkWidget *parent, const char *title, GCallback callback, Desktop *d) { + GtkWidget *item = gtk_button_new_with_label(title); + gtk_box_append(GTK_BOX(parent), item); + g_signal_connect(item, "clicked", callback, d); + return item; +} +static void activate(GtkApplication *application, gpointer data) { + Desktop *d = data; + d->window = gtk_application_window_new(application); + gtk_window_set_title(GTK_WINDOW(d->window), "Pure · native authoring"); + gtk_window_set_default_size(GTK_WINDOW(d->window), 1320, 720); + GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_margin_start(root, 12); gtk_widget_set_margin_end(root, 12); + gtk_widget_set_margin_top(root, 12); gtk_widget_set_margin_bottom(root, 12); + gtk_window_set_child(GTK_WINDOW(d->window), root); + GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_append(GTK_BOX(root), bar); + button(bar, "Apply source", G_CALLBACK(apply), d); + button(bar, "Save state", G_CALLBACK(save_state), d); + button(bar, "Load state", G_CALLBACK(load_state), d); + button(bar, "Reset", G_CALLBACK(reset_state), d); + d->status = gtk_label_new("Edit source and Apply; click the scene for keyboard input"); + gtk_label_set_xalign(GTK_LABEL(d->status), 0); + gtk_label_set_single_line_mode(GTK_LABEL(d->status), TRUE); + gtk_label_set_ellipsize(GTK_LABEL(d->status), PANGO_ELLIPSIZE_END); + gtk_widget_set_hexpand(d->status, TRUE); + gtk_box_append(GTK_BOX(bar), d->status); + GtkWidget *panes = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_vexpand(panes, TRUE); + gtk_box_append(GTK_BOX(root), panes); + GtkWidget *left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_paned_set_start_child(GTK_PANED(panes), left); + GtkWidget *source_title = gtk_label_new(d->source_path); + gtk_label_set_xalign(GTK_LABEL(source_title), 0); + gtk_box_append(GTK_BOX(left), source_title); + GtkWidget *scroll = gtk_scrolled_window_new(); + gtk_widget_set_hexpand(scroll, TRUE); gtk_widget_set_vexpand(scroll, TRUE); + gtk_box_append(GTK_BOX(left), scroll); + d->source = gtk_text_view_new(); + gtk_text_view_set_monospace(GTK_TEXT_VIEW(d->source), TRUE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(d->source), GTK_WRAP_NONE); + gtk_text_buffer_set_enable_undo(gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)), TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), d->source); + gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)), + d->initial_source, (int)d->initial_source_bytes); + GtkWidget *right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_paned_set_end_child(GTK_PANED(panes), right); + d->canvas = gtk_drawing_area_new(); + gtk_widget_set_hexpand(d->canvas, TRUE); gtk_widget_set_vexpand(d->canvas, TRUE); + gtk_widget_set_focusable(d->canvas, TRUE); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(d->canvas), draw, d, NULL); + gtk_box_append(GTK_BOX(right), d->canvas); + d->values = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_append(GTK_BOX(right), d->values); + rebuild_values(d); + GtkGesture *drag = gtk_gesture_drag_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY); + g_signal_connect(drag, "drag-begin", G_CALLBACK(drag_begin), d); + g_signal_connect(drag, "drag-update", G_CALLBACK(drag_update), d); + g_signal_connect(drag, "drag-end", G_CALLBACK(drag_end), d); + g_signal_connect(drag, "cancel", G_CALLBACK(drag_cancel), d); + gtk_widget_add_controller(d->canvas, GTK_EVENT_CONTROLLER(drag)); + GtkEventController *keys = gtk_event_controller_key_new(); + g_signal_connect(keys, "key-pressed", G_CALLBACK(key_down), d); + g_signal_connect(keys, "key-released", G_CALLBACK(key_up), d); + gtk_widget_add_controller(d->canvas, keys); + GtkEventController *focus = gtk_event_controller_focus_new(); + g_signal_connect(focus, "leave", G_CALLBACK(focus_left), d); + gtk_widget_add_controller(d->canvas, focus); + gtk_paned_set_position(GTK_PANED(panes), 560); + apply_source(d, false); + gtk_window_present(GTK_WINDOW(d->window)); +} + +int main(int argc, char **argv) { + if (argc != 4) { + fprintf(stderr, "pure-desktop STORE_DIRECTORY DOCUMENT_KEY SOURCE.pure\n"); + return 2; + } + Desktop d = {.source_path = argv[3]}; + GError *error = NULL; + if (!g_file_get_contents(d.source_path, &d.initial_source, &d.initial_source_bytes, &error)) { + fprintf(stderr, "source read failed: %s\n", error->message); + g_clear_error(&error); + return 2; + } + if (d.initial_source_bytes > 32768) { + fprintf(stderr, "source exceeds the 32 KiB compiler limit\n"); + g_free(d.initial_source); return 2; + } + if (!g_utf8_validate(d.initial_source, (gssize)d.initial_source_bytes, NULL)) { + fprintf(stderr, "source must be valid UTF-8 text\n"); + g_free(d.initial_source); return 2; + } + d.scalar_memory = malloc(pure_scalar_bytes()); + if (!d.scalar_memory) { g_free(d.initial_source); return 2; } + PureResult status = pure_native_app_init(&d.app, &pure_app_plan, d.scalar_memory, + pure_scalar_bytes(), argv[1], argv[2], 4096); + if (status != PURE_OK) { + fprintf(stderr, "initialization failed: %u\n", status); + free(d.scalar_memory); g_free(d.initial_source); return 2; + } + d.scratch = g_dir_make_tmp("pure-author-XXXXXX", &error); + if (!d.scratch) { + fprintf(stderr, "authoring scratch: %s\n", error->message); + g_clear_error(&error); free(d.scalar_memory); g_free(d.initial_source); return 2; + } + GtkApplication *application = gtk_application_new("dev.pure.authoring", G_APPLICATION_NON_UNIQUE); + g_signal_connect(application, "activate", G_CALLBACK(activate), &d); + int result = g_application_run(G_APPLICATION(application), 0, NULL); + g_object_unref(application); + pure_runtime_stop(&d.app.execution); + if (d.module) dlclose(d.module); + g_rmdir(d.scratch); g_free(d.scratch); + free(d.scalar_memory); + g_free(d.initial_source); + return result; +} diff --git a/prototypes/workbench/native/instance-memory.c b/prototypes/workbench/native/instance-memory.c new file mode 100644 index 0000000..dc771b2 --- /dev/null +++ b/prototypes/workbench/native/instance-memory.c @@ -0,0 +1,49 @@ +#define _POSIX_C_SOURCE 200809L +/* Linux-only evidence harness: 1000 real initialized scalar instances of the + * same prepared app. It measures their process RSS delta, not a full service. */ +#include "plan.h" +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; + +static long rss_kib(void) { + FILE *file = fopen("/proc/self/status", "r"); + if (!file) return -1; + char line[256]; + long result = -1; + while (fgets(line, sizeof(line), file)) { + if (strncmp(line, "VmRSS:", 6) == 0) { + if (sscanf(line + 6, "%ld", &result) != 1) result = -1; + break; + } + } + fclose(file); + return result; +} + +int main(void) { + enum { COUNT = 1000 }; + PureScalar **instances = calloc(COUNT, sizeof(*instances)); + if (!instances) return 2; + long before = rss_kib(); + size_t bytes = pure_scalar_bytes(); + uint64_t checksum = 0; + for (size_t i = 0; i < COUNT; ++i) { + void *memory = malloc(bytes); + if (!memory) return 2; + instances[i] = pure_scalar_initialize(memory, bytes); + if (!instances[i] || pure_scalar_load(instances[i], &pure_app_plan, NULL, NULL)) return 2; + checksum += pure_scalar_info(instances[i], 4); + } + long after = rss_kib(); + if (before < 0 || after < before || checksum != COUNT) return 2; + printf("{\"instances\":%u,\"instance_bytes\":%zu,\"rss_before_kib\":%ld," + "\"rss_after_kib\":%ld,\"rss_delta_kib\":%ld,\"checksum\":%" PRIu64 "}\n", + COUNT, bytes, before, after, after - before, checksum); + for (size_t i = 0; i < COUNT; ++i) free(instances[i]); + free(instances); + return 0; +} diff --git a/prototypes/workbench/native/main.c b/prototypes/workbench/native/main.c new file mode 100644 index 0000000..3756297 --- /dev/null +++ b/prototypes/workbench/native/main.c @@ -0,0 +1,135 @@ +#include "app.h" +#include +#include +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; + +static void string(const char *value) { + putchar('"'); + for (const unsigned char *p = (const unsigned char *)value; *p; ++p) { + if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); } + else if (*p < 32) printf("\\u%04x", *p); + else putchar(*p); + } + putchar('"'); +} +static void number(double value) { + if (isfinite(value)) printf("%.17g", value); else fputs("null", stdout); +} +static void show(PureNativeApp *app, PureOutcome result) { + printf("{\"status\":%u,\"scalar_error\":%u,\"revision\":\"%" PRId64 "\",\"stored_revision\":\"%" PRId64 + "\",\"result_revision\":\"%" PRId64 "\",\"held\":%u,\"states\":{", + (unsigned)result.status, app->last_scalar_error, app->revision, app->stored_revision, result.revision, app->held); + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + if (i) putchar(','); + string(app->plan->states[i].name); putchar(':'); number(pure_scalar_get_state(app->scalar, i)); + } + fputs("},\"views\":{", stdout); + pure_scalar_begin_evaluation(app->scalar, 2048); + uint32_t display_error = 0; + for (uint32_t i = 0; i < app->plan->view_count; ++i) { + const PureScalarView *view = &app->plan->views[i]; + if (i) putchar(','); + string(view->id); fputs(":{", stdout); + const char *fields[] = {"x", "y", !strcmp(view->type, "text") ? "value" : !strcmp(view->type, "circle") ? "radius" : "width", "height"}; + for (uint32_t j = 0; j < view->count; ++j) { + if (j) putchar(','); + string(fields[j]); putchar(':'); + double value = pure_scalar_eval_bounded(app->scalar, view->fields[j].start, view->fields[j].length); + uint32_t code = pure_scalar_evaluation_info(app->scalar, 0); + if (code && !display_error) display_error = code; + if (j >= 2 && strcmp(view->type, "text") && value < 0) { display_error = 100; value = NAN; } + number(value); + } + putchar('}'); + } + printf("},\"display_error\":%u}\n", display_error); + fflush(stdout); +} +static bool real(const char *text, double *out) { + if (!text || !*text) return false; + char *end; + errno = 0; + double value = strtod(text, &end); + if (errno || *end || !isfinite(value)) return false; + *out = value; + return true; +} + +int main(int argc, char **argv) { + if (argc != 3) { + fputs("pure-native STORE_DIRECTORY DOCUMENT_KEY\n" + "Commands: show, key INPUT down|up, value INPUT NUMBER,\n" + "point INPUT down|move|up|cancel TARGET X Y, frame, reset, save, load, quit.\n", stderr); + return 2; + } + size_t bytes = pure_scalar_bytes(); + void *memory = malloc(bytes); + if (!memory) return 2; + PureNativeApp app; + PureResult status = pure_native_app_init(&app, &pure_app_plan, memory, bytes, argv[1], argv[2], 4096); + if (status != PURE_OK) { fprintf(stderr, "initialization failed: %u\n", status); free(memory); return 2; } + show(&app, (PureOutcome){.status = PURE_OK}); + int failed = 0; + char line[512]; + while (fgets(line, sizeof(line), stdin)) { + if (!strchr(line, '\n') && !feof(stdin)) { + int c; + while ((c = getchar()) != '\n' && c != EOF) {} + fputs("command exceeds 510 bytes\n", stderr); failed = 1; continue; + } + char *words[8]; + size_t count = 0; + for (char *token = strtok(line, " \t\r\n"); token; token = strtok(NULL, " \t\r\n")) { + if (count == 8) break; + words[count++] = token; + } + if (!count) continue; + PureOutcome result = {.status = PURE_INVALID}; + if (count == 1) { + if (!strcmp(words[0], "quit")) break; + if (!strcmp(words[0], "show")) result.status = PURE_OK; + else if (!strcmp(words[0], "save")) result = pure_native_app_save(&app); + else if (!strcmp(words[0], "load")) result = pure_native_app_restore(&app); + else if (!strcmp(words[0], "frame") || !strcmp(words[0], "reset")) + result = pure_native_app_action(&app, (PureAction){.phase = !strcmp(words[0], "frame") ? PURE_FRAME : PURE_RESET}); + } else { + uint32_t input = app.plan->input_count; + for (uint32_t i = 0; i < app.plan->input_count; ++i) + if (!strcmp(words[1], app.plan->inputs[i].name)) { input = i; break; } + if (input < app.plan->input_count) { + PureScalarInputKind kind = app.plan->inputs[input].kind; + PureAction action = {.input = input, .phase = UINT32_MAX, .target = UINT32_MAX}; + if (count == 3 && !strcmp(words[0], "key") && kind == PURE_KEY) { + if (!strcmp(words[2], "down")) action.phase = PURE_DOWN; + if (!strcmp(words[2], "up")) action.phase = PURE_UP; + } else if (count == 3 && !strcmp(words[0], "value") && kind == PURE_VALUE) { + if (real(words[2], &action.x)) action.phase = PURE_CHANGE; + } else if (count == 6 && !strcmp(words[0], "point") && (kind == PURE_POINT || kind == PURE_ROUTED)) { + bool target = kind == PURE_POINT && !strcmp(words[3], app.plan->inputs[input].target); + if (kind == PURE_ROUTED) + for (uint32_t i = 0; i < app.plan->target_count; ++i) + if (app.plan->targets[i].input == input && !strcmp(words[3], app.plan->targets[i].view)) { + target = true; action.target = i; break; + } + if (target && real(words[4], &action.x) && real(words[5], &action.y)) { + const char *phases[] = {"down", "up", "move", "cancel"}; + for (uint32_t i = 0; i < 4; ++i) if (!strcmp(words[2], phases[i])) action.phase = i; + } + } + if (action.phase != UINT32_MAX) result = pure_native_app_action(&app, action); + } + } + if (result.status != PURE_OK) failed = 1; + show(&app, result); + } + if (ferror(stdin) || ferror(stdout)) failed = 1; + pure_runtime_stop(&app.execution); + if (app.execution.active || app.execution.retained) failed = 1; + free(memory); + return failed; +} diff --git a/prototypes/workbench/native/peer-check.py b/prototypes/workbench/native/peer-check.py new file mode 100644 index 0000000..6a6b49c --- /dev/null +++ b/prototypes/workbench/native/peer-check.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Loopback integration of the optional TLS peer host and an ordinary Python client.""" +import argparse +import json +from pathlib import Path +import select +import socket +import ssl +import struct +import subprocess +import tempfile + + +def run(*args): + subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def certificates(root): + ca = root / 'ca.pem' + run('openssl', 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', + '-nodes', '-days', '1', '-subj', '/CN=Pure test CA', + '-addext', 'basicConstraints=critical,CA:TRUE', + '-addext', 'keyUsage=critical,keyCertSign,cRLSign', + '-keyout', root / 'ca.key', '-out', ca) + for role, san, eku in [('server', 'localhost', 'serverAuth'), + ('client', 'pure-client', 'clientAuth'), + ('other', 'wrong-client', 'clientAuth')]: + config = root / (role + '.ext') + config.write_text(f'basicConstraints=critical,CA:FALSE\n' + f'keyUsage=critical,digitalSignature\n' + f'subjectAltName=DNS:{san}\nextendedKeyUsage={eku}\n') + run('openssl', 'req', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', + '-nodes', '-subj', '/CN=' + san, '-keyout', root / (role + '.key'), + '-out', root / (role + '.csr')) + run('openssl', 'x509', '-req', '-in', root / (role + '.csr'), '-CA', ca, + '-CAkey', root / 'ca.key', '-CAcreateserial', '-days', '1', + '-extfile', config, '-out', root / (role + '.pem')) + return ca + + +def frame(payload): + return struct.pack('!H', len(payload)) + payload + + +def recv_exact(sock, size): + result = bytearray() + while len(result) < size: + chunk = sock.recv(size - len(result)) + if not chunk: + raise EOFError('unexpected end of TLS stream') + result += chunk + return bytes(result) + + +def receive(sock): + size, = struct.unpack('!H', recv_exact(sock, 2)) + return recv_exact(sock, size) + + +def peer(root, ca, port, *, cert='client', alpn='pure/1', server_name='localhost'): + ctx = ssl.create_default_context(cafile=str(ca)) + ctx.minimum_version = ssl.TLSVersion.TLSv1_3 + ctx.maximum_version = ssl.TLSVersion.TLSv1_3 + if cert: + ctx.load_cert_chain(str(root / (cert + '.pem')), str(root / (cert + '.key'))) + ctx.set_alpn_protocols([alpn]) + plain = socket.create_connection(('127.0.0.1', port), timeout=3) + try: + secure = ctx.wrap_socket(plain, server_hostname=server_name) + secure.settimeout(3) + return secure + except BaseException: + plain.close() + raise + + +def start(binary, root, ca, store): + command = [str(binary), 'serve', '127.0.0.1', '0', str(store), 'scene', + str(ca), str(root / 'server.pem'), str(root / 'server.key'), 'pure-client'] + proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, bufsize=1) + try: + assert select.select([proc.stdout], [], [], 5)[0], 'server did not become ready' + line = proc.stdout.readline() + assert line.startswith('READY '), (line, proc.stderr.read()) + return proc, int(line.split()[1]) + except BaseException: + proc.kill() + proc.wait(timeout=3) + raise + + +def stop(proc): + proc.terminate() + proc.communicate(timeout=5) + assert proc.returncode == 0, proc.returncode + + +def state(sock): + sock.sendall(frame(b'\x00')) + reply = receive(sock) + assert len(reply) >= 26 and reply[0] == 0 + revision, stored = struct.unpack_from('!QQ', reply, 1) + assert len(reply) == 26 + 8 * reply[25] + values = struct.unpack_from('!' + 'd' * reply[25], reply, 26) + return revision, stored, values, struct.unpack_from('!I', reply, 21)[0] + + +def rss_kib(pid): + status = Path(f'/proc/{pid}/status') + if not status.exists(): + return None + for line in status.read_text().splitlines(): + if line.startswith('VmRSS:'): + return int(line.split()[1]) + return None + + +def check(binary): + with tempfile.TemporaryDirectory(prefix='pure-peer-check-') as tmp: + root = Path(tmp) + ca = certificates(root) + server, port = start(binary, root, ca, root / 'store') + idle_rss = rss_kib(server.pid) + connected_rss = None + try: + # The peer is an ordinary Python TLS client: no Pure client library. + with peer(root, ca, port) as sock: + hello = receive(sock) + assert len(hello) == 53 and hello[:4] == b'PUR1' + revision, _, _, held = state(sock) + connected_rss = rss_kib(server.pid) + assert held == 0 + stale = struct.pack('!BQ', 2, revision + 1) + sock.sendall(frame(stale)) + conflict = receive(sock) + assert conflict[0] == 2 and struct.unpack_from('!Q', conflict, 1)[0] == revision + assert state(sock)[0] == revision + + # Routed target zero; ending the session while held must cancel it. + down = struct.pack('!BQBBBdd', 1, revision, 4, 0, 0, 90., 130.) + sock.sendall(frame(down)) + assert receive(sock)[0] == 0 + with peer(root, ca, port) as sock: + receive(sock) + revision, _, values, held = state(sock) + assert held == 0, (revision, values, held) + + # A malformed request and overlong frame close their sessions without mutation. + for bad in [frame(b'\xff'), b'\x01\x00']: + with peer(root, ca, port) as sock: + receive(sock) + sock.sendall(bad) + try: + assert sock.recv(2) == b'' + except (ssl.SSLError, ConnectionResetError): + pass + with peer(root, ca, port) as sock: + receive(sock) + assert state(sock)[0] == revision + + # An authenticated client that sends nothing cannot retain the app forever. + with peer(root, ca, port) as sock: + receive(sock) + sock.settimeout(7) + try: + assert sock.recv(1) == b'' + except ssl.SSLError: + pass + with peer(root, ca, port) as sock: + receive(sock) + assert state(sock)[0] == revision + + # Authentication is mandatory on both sides; ALPN is mandatory too. + for kwargs in [{'cert': None}, {'cert': 'other'}, {'alpn': 'http/1.1'}]: + try: + with peer(root, ca, port, **kwargs) as sock: + receive(sock) + except (ssl.SSLError, EOFError, ConnectionResetError, TimeoutError): + pass + else: + raise AssertionError('untrusted or unsupported peer accepted: ' + str(kwargs)) + try: + peer(root, ca, port, server_name='wrong-server').close() + except ssl.SSLError: + pass + else: + raise AssertionError('wrong server name accepted') + + commands = 'point drag down green_box 90 130\npoint drag move green_box 230 210\npoint drag up green_box 230 210\nsave\nquit\n' + result = subprocess.run([str(binary), 'call', '127.0.0.1', str(port), 'localhost', + str(ca), str(root / 'client.pem'), str(root / 'client.key')], + input=commands, text=True, capture_output=True, timeout=10) + assert result.returncode == 0, (result.stdout, result.stderr) + rows = [json.loads(line) for line in result.stdout.splitlines()] + assert rows[-1]['status'] == 0 and rows[-1]['stored_revision'] == '1' + assert rows[-1]['states']['green.x'] == 220 and rows[-1]['states']['green.y'] == 200 + assert rows[-1]['held'] == 0 + finally: + stop(server) + + server, port = start(binary, root, ca, root / 'store') + try: + with peer(root, ca, port) as sock: + receive(sock) + _, stored, _, held = state(sock) + assert stored == 1 and held == 0 + result = subprocess.run([str(binary), 'call', '127.0.0.1', str(port), 'localhost', + str(ca), str(root / 'client.pem'), str(root / 'client.key')], + input='quit\n', text=True, capture_output=True, timeout=10) + assert result.returncode == 0, (result.stdout, result.stderr) + restored = json.loads(result.stdout.splitlines()[0]) + assert restored['states']['green.x'] == 220 and restored['states']['green.y'] == 200 + finally: + stop(server) + return {'python_client': 'pass', 'revision_conflict': 'pass', + 'disconnect_release': 'pass', 'malformed_frames': 'pass', + 'idle_deadline': 'pass', + 'mutual_authentication': 'pass', 'alpn': 'pass', + 'server_hostname': 'pass', 'persist_restart': 'pass', + 'server_idle_rss_kib': idle_rss, 'server_connected_rss_kib': connected_rss} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('binary', type=Path) + args = parser.parse_args() + print(json.dumps(check(args.binary.resolve()), sort_keys=True)) diff --git a/prototypes/workbench/native/peer-wire-test.c b/prototypes/workbench/native/peer-wire-test.c new file mode 100644 index 0000000..7b28145 --- /dev/null +++ b/prototypes/workbench/native/peer-wire-test.c @@ -0,0 +1,85 @@ +#include "peer-wire.h" +#include +#include +#include +#include + +/* Codec test double; no VM instance or TLS process is needed to check the + * on-wire representation and malformed-frame rejection. */ +double pure_scalar_get_state(PureScalar *scalar, uint32_t id) { + (void)scalar; + return id == 0 ? 12.5 : -0.0; +} +int main(void) { + PureScalarState states[] = {{"x", 0}, {"y", 0}}; + PureScalarInput inputs[] = { + {"key", "Space", NULL, PURE_KEY}, + {"point", NULL, "shape", PURE_POINT}, + {"routed", NULL, NULL, PURE_ROUTED}, + {"value", NULL, NULL, PURE_VALUE}, + }; + PureScalarTarget targets[] = {{2, 0, 1, "shape"}}; + PureScalarPlan plan = {.state_count = 2, .input_count = 4, .target_count = 1, + .states = states, .inputs = inputs, .targets = targets}; + for (unsigned i = 0; i < 32; ++i) plan.identity[i] = (unsigned char)(i + 1); + PureNativeApp app = {.plan = &plan, .revision = 7, .stored_revision = 3}; + unsigned char hello[PURE_PEER_HELLO_BYTES]; + pure_peer_hello(&app, hello); + int64_t revision = -1, stored = -1; + assert(pure_peer_check_hello(hello, sizeof(hello), &plan, &revision, &stored)); + assert(revision == 7 && stored == 3 && !memcmp(hello, "PUR1", 4)); + hello[4] ^= 1; + assert(!pure_peer_check_hello(hello, sizeof(hello), &plan, &revision, &stored)); + hello[4] ^= 1; + assert(!pure_peer_check_hello(hello, sizeof(hello) - 1, &plan, &revision, &stored)); + + unsigned char wire[PURE_PEER_REQUEST_MAX]; + PurePeerRequest request = {.operation = PURE_PEER_STATE}; + assert(pure_peer_encode_request(&request, &plan, wire) == 1); + PurePeerRequest decoded; + assert(pure_peer_decode_request(wire, 1, &plan, &decoded) && decoded.operation == PURE_PEER_STATE); + assert(!pure_peer_decode_request(wire, 2, &plan, &decoded)); + request = (PurePeerRequest){.operation = PURE_PEER_SAVE, .expected = 0x0102030405060708LL}; + assert(pure_peer_encode_request(&request, &plan, wire) == 9); + assert(wire[1] == 1 && wire[8] == 8); + assert(pure_peer_decode_request(wire, 9, &plan, &decoded) && decoded.expected == request.expected); + wire[1] = 255; + assert(!pure_peer_decode_request(wire, 9, &plan, &decoded)); + + request = (PurePeerRequest){.operation = PURE_PEER_ACTION, .expected = 7, + .action = {.input = 0, .phase = PURE_DOWN, .target = UINT32_MAX}}; + assert(pure_peer_encode_request(&request, &plan, wire) == 12); + assert(pure_peer_decode_request(wire, 12, &plan, &decoded) && decoded.action.input == 0); + request.action = (PureAction){.input = 3, .phase = PURE_CHANGE, .target = UINT32_MAX, .x = 12.5}; + assert(pure_peer_encode_request(&request, &plan, wire) == 20); + assert(pure_peer_decode_request(wire, 20, &plan, &decoded) && decoded.action.x == 12.5); + memset(wire + 12, 255, 8); + assert(!pure_peer_decode_request(wire, 20, &plan, &decoded)); + request.action = (PureAction){.input = 1, .phase = PURE_MOVE, .target = UINT32_MAX, .x = 100, .y = -0.0}; + assert(pure_peer_encode_request(&request, &plan, wire) == 28); + assert(pure_peer_decode_request(wire, 28, &plan, &decoded) && decoded.action.x == 100 && signbit(decoded.action.y)); + request.action = (PureAction){.input = 2, .phase = PURE_UP, .target = 0, .x = 5, .y = 6}; + assert(pure_peer_encode_request(&request, &plan, wire) == 28); + assert(pure_peer_decode_request(wire, 28, &plan, &decoded) && decoded.action.target == 0); + wire[11] = 1; + assert(!pure_peer_decode_request(wire, 28, &plan, &decoded)); + request.action = (PureAction){.input = UINT32_MAX, .phase = PURE_FRAME, .target = UINT32_MAX}; + assert(pure_peer_encode_request(&request, &plan, wire) == 12); + assert(pure_peer_decode_request(wire, 12, &plan, &decoded) && decoded.action.phase == PURE_FRAME); + + unsigned char reply[PURE_PEER_REPLY_MAX]; + size_t length = pure_peer_encode_reply(&app, PURE_CONFLICT, reply); + assert(length == 42 && reply[0] == 2 && reply[25] == 2); + PurePeerReply decoded_reply; + assert(pure_peer_decode_reply(reply, length, &plan, &decoded_reply)); + assert(decoded_reply.revision == 7 && decoded_reply.stored_revision == 3); + assert(decoded_reply.states[0] == 12.5 && signbit(decoded_reply.states[1])); + reply[25] = 3; + assert(!pure_peer_decode_reply(reply, length, &plan, &decoded_reply)); + reply[25] = 2; + assert(!pure_peer_decode_reply(reply, length - 1, &plan, &decoded_reply)); + reply[0] = 255; + assert(!pure_peer_decode_reply(reply, length, &plan, &decoded_reply)); + puts("peer wire: bounded identity, revisions, actions, finite states and malformed frames pass"); + return 0; +} diff --git a/prototypes/workbench/native/peer-wire.c b/prototypes/workbench/native/peer-wire.c new file mode 100644 index 0000000..131e709 --- /dev/null +++ b/prototypes/workbench/native/peer-wire.c @@ -0,0 +1,152 @@ +#include "peer-wire.h" +#include +#include +#include +#include + +_Static_assert(sizeof(double) == 8 && FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024, + "peer v1 requires IEEE binary64"); + +static void put(unsigned char *at, uint64_t value, unsigned bytes) { + for (unsigned i = 0; i < bytes; ++i) at[bytes - 1 - i] = (unsigned char)(value >> (8 * i)); +} +static uint64_t get(const unsigned char *at, unsigned bytes) { + uint64_t value = 0; + for (unsigned i = 0; i < bytes; ++i) value = (value << 8) | at[i]; + return value; +} +static void put_double(unsigned char *at, double value) { + uint64_t bits; + memcpy(&bits, &value, 8); + put(at, bits, 8); +} +static double get_double(const unsigned char *at) { + uint64_t bits = get(at, 8); + double value; + memcpy(&value, &bits, 8); + return value; +} + +static bool valid_action(const PureScalarPlan *plan, const PureAction *a, size_t bytes) { + if (a->phase == PURE_FRAME || a->phase == PURE_RESET) + return bytes == 12 && a->input == UINT32_MAX && a->target == UINT32_MAX; + if (a->input >= plan->input_count || a->input >= 8) return false; + PureScalarInputKind kind = plan->inputs[a->input].kind; + if (kind == PURE_KEY) + return bytes == 12 && (a->phase == PURE_DOWN || a->phase == PURE_UP) && a->target == UINT32_MAX; + if (kind == PURE_VALUE) + return bytes == 20 && a->phase == PURE_CHANGE && a->target == UINT32_MAX && isfinite(a->x); + if (kind != PURE_POINT && kind != PURE_ROUTED) return false; + if (bytes != 28 || a->phase > PURE_CANCEL || !isfinite(a->x) || !isfinite(a->y)) return false; + if (kind == PURE_POINT) return a->target == UINT32_MAX; + return a->target < plan->target_count && a->target < 24 && + plan->targets[a->target].input == a->input; +} + +void pure_peer_hello(const PureNativeApp *app, unsigned char out[PURE_PEER_HELLO_BYTES]) { + memcpy(out, "PUR1", 4); + memcpy(out + 4, app->plan->identity, 32); + out[36] = (unsigned char)app->plan->state_count; + put(out + 37, (uint64_t)app->revision, 8); + put(out + 45, (uint64_t)app->stored_revision, 8); +} +bool pure_peer_check_hello(const unsigned char *wire, size_t length, const PureScalarPlan *plan, + int64_t *revision, int64_t *stored_revision) { + if (!wire || !plan || !revision || !stored_revision || length != PURE_PEER_HELLO_BYTES || + memcmp(wire, "PUR1", 4) || memcmp(wire + 4, plan->identity, 32) || + wire[36] != plan->state_count) return false; + uint64_t local = get(wire + 37, 8), stored = get(wire + 45, 8); + if (local > INT64_MAX || stored > INT64_MAX) return false; + *revision = (int64_t)local; *stored_revision = (int64_t)stored; + return true; +} + +size_t pure_peer_encode_request(const PurePeerRequest *request, const PureScalarPlan *plan, + unsigned char out[PURE_PEER_REQUEST_MAX]) { + if (!request || !plan || !out || request->operation > PURE_PEER_LOAD) return 0; + out[0] = (unsigned char)request->operation; + if (request->operation == PURE_PEER_STATE) return 1; + if (request->expected < 0) return 0; + put(out + 1, (uint64_t)request->expected, 8); + if (request->operation != PURE_PEER_ACTION) return 9; + const PureAction *a = &request->action; + size_t length = 12; + if (a->phase != PURE_FRAME && a->phase != PURE_RESET && a->input < plan->input_count) { + PureScalarInputKind kind = plan->inputs[a->input].kind; + if (kind == PURE_VALUE) length = 20; + if (kind == PURE_POINT || kind == PURE_ROUTED) length = 28; + } + if (!valid_action(plan, a, length)) return 0; + out[9] = a->input == UINT32_MAX ? 255 : (unsigned char)a->input; + out[10] = (unsigned char)a->phase; + out[11] = a->target == UINT32_MAX ? 255 : (unsigned char)a->target; + if (length >= 20) put_double(out + 12, a->x); + if (length == 28) put_double(out + 20, a->y); + return length; +} + +bool pure_peer_decode_request(const unsigned char *wire, size_t length, const PureScalarPlan *plan, + PurePeerRequest *out) { + if (!wire || !plan || !out || !length || wire[0] > PURE_PEER_LOAD) return false; + PurePeerRequest candidate = {.operation = (PurePeerOperation)wire[0]}; + if (candidate.operation == PURE_PEER_STATE) { + if (length != 1) return false; + } else { + if (length < 9 || get(wire + 1, 8) > INT64_MAX) return false; + candidate.expected = (int64_t)get(wire + 1, 8); + if (candidate.operation != PURE_PEER_ACTION) { + if (length != 9) return false; + } else { + if (length != 12 && length != 20 && length != 28) return false; + candidate.action.input = wire[9] == 255 ? UINT32_MAX : wire[9]; + candidate.action.phase = wire[10]; + candidate.action.target = wire[11] == 255 ? UINT32_MAX : wire[11]; + if (length >= 20) candidate.action.x = get_double(wire + 12); + if (length == 28) candidate.action.y = get_double(wire + 20); + if (!valid_action(plan, &candidate.action, length)) return false; + } + } + *out = candidate; + return true; +} + +/* These numbers belong to the wire version; do not serialize the C enum. */ +static unsigned char wire_status(PureResult code) { + switch (code) { + case PURE_OK: return 0; case PURE_NOT_FOUND: return 1; case PURE_CONFLICT: return 2; + case PURE_BUSY: return 3; case PURE_BUDGET: return 4; case PURE_LIMIT: return 5; + case PURE_STORE_ERROR: return 6; case PURE_UNCERTAIN: return 7; case PURE_INVALID: return 8; + case PURE_DENIED: return 9; case PURE_CLOSED: return 10; case PURE_CANCELLED: return 11; + case PURE_ALLOCATION: return 12; + } + return 8; +} +size_t pure_peer_encode_reply(const PureNativeApp *app, PureResult status, + unsigned char out[PURE_PEER_REPLY_MAX]) { + if (!app || !out || app->plan->state_count > 32 || app->revision < 0 || app->stored_revision < 0) return 0; + out[0] = wire_status(status); + put(out + 1, (uint64_t)app->revision, 8); + put(out + 9, (uint64_t)app->stored_revision, 8); + put(out + 17, app->last_scalar_error, 4); + put(out + 21, app->held, 4); + out[25] = (unsigned char)app->plan->state_count; + for (uint32_t i = 0; i < app->plan->state_count; ++i) + put_double(out + 26 + i * 8, pure_scalar_get_state(app->scalar, i)); + return 26 + 8u * app->plan->state_count; +} +bool pure_peer_decode_reply(const unsigned char *wire, size_t length, const PureScalarPlan *plan, + PurePeerReply *out) { + if (!wire || !plan || !out || length < 26 || plan->state_count > 32 || wire[0] > 12 || + wire[25] != plan->state_count || length != 26 + 8u * plan->state_count) return false; + uint64_t revision = get(wire + 1, 8), stored = get(wire + 9, 8); + if (revision > INT64_MAX || stored > INT64_MAX) return false; + PurePeerReply candidate = {.status = wire[0], .revision = (int64_t)revision, + .stored_revision = (int64_t)stored, .scalar_error = (uint32_t)get(wire + 17, 4), + .held = (uint32_t)get(wire + 21, 4), .state_count = plan->state_count}; + for (uint32_t i = 0; i < plan->state_count; ++i) { + candidate.states[i] = get_double(wire + 26 + i * 8); + if (!isfinite(candidate.states[i])) return false; + } + *out = candidate; + return true; +} diff --git a/prototypes/workbench/native/peer-wire.h b/prototypes/workbench/native/peer-wire.h new file mode 100644 index 0000000..6d78e07 --- /dev/null +++ b/prototypes/workbench/native/peer-wire.h @@ -0,0 +1,32 @@ +#ifndef PURE_PEER_WIRE_H +#define PURE_PEER_WIRE_H +#include "app.h" + +/* Version 1 is a boundary format, not a native struct or a runtime core ABI. + * Every TLS record is still a byte stream: peer.c prefixes these payloads + * with a two-byte big-endian length. One request is outstanding per session. */ +enum { PURE_PEER_HELLO_BYTES = 53, PURE_PEER_REQUEST_MAX = 28, + PURE_PEER_REPLY_MAX = 26 + 32 * 8 }; +typedef enum { PURE_PEER_STATE, PURE_PEER_ACTION, PURE_PEER_SAVE, PURE_PEER_LOAD } PurePeerOperation; +typedef struct { + PurePeerOperation operation; + int64_t expected; + PureAction action; +} PurePeerRequest; +typedef struct { + uint8_t status; + int64_t revision, stored_revision; + uint32_t scalar_error, held, state_count; + double states[32]; +} PurePeerReply; + +void pure_peer_hello(const PureNativeApp *, unsigned char out[PURE_PEER_HELLO_BYTES]); +bool pure_peer_check_hello(const unsigned char *, size_t, const PureScalarPlan *, + int64_t *revision, int64_t *stored_revision); +size_t pure_peer_encode_request(const PurePeerRequest *, const PureScalarPlan *, + unsigned char out[PURE_PEER_REQUEST_MAX]); +bool pure_peer_decode_request(const unsigned char *, size_t, const PureScalarPlan *, PurePeerRequest *); +size_t pure_peer_encode_reply(const PureNativeApp *, PureResult, + unsigned char out[PURE_PEER_REPLY_MAX]); +bool pure_peer_decode_reply(const unsigned char *, size_t, const PureScalarPlan *, PurePeerReply *); +#endif diff --git a/prototypes/workbench/native/peer.c b/prototypes/workbench/native/peer.c new file mode 100644 index 0000000..7a18b3a --- /dev/null +++ b/prototypes/workbench/native/peer.c @@ -0,0 +1,297 @@ +#define _POSIX_C_SOURCE 200809L +/* Optional authenticated Pure-peer adapter. No TLS, socket, or protocol type + * enters runtime.c, the scalar module, or the standalone native CLI. */ +#include "peer-wire.h" +#include "../../common/tls-link.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; +static const unsigned char alpn[] = {6, 'p','u','r','e','/','1'}; +static volatile sig_atomic_t stopping; +static void signal_stop(int signum) { (void)signum; stopping = 1; } +static SSL_CTX *context(bool server, const char *ca, const char *cert, const char *key) { + return pure_link_context(server, ca, cert, key, alpn); +} +static bool handshake(PureLink *channel, SSL_CTX *ctx, bool server, const char *peer_name) { + return pure_link_handshake(channel, ctx, server, peer_name, alpn); +} + +/* A connection has one physical input source. Disconnect ends all held input + * phases before another peer can use the same source slot. */ +static void release_inputs(PureNativeApp *app) { + for (uint32_t i = 0; i < app->plan->input_count && i < 8; ++i) { + PureScalarInputKind kind = app->plan->inputs[i].kind; + if (kind == PURE_KEY && (app->key_sources[i] & (1u << 1))) + (void)pure_native_app_action(app, (PureAction){.input = i, .phase = PURE_UP, .source = 1}); + if (kind == PURE_POINT && (app->held & (1u << i)) && app->pointer_owners[i] == 1) + (void)pure_native_app_action(app, (PureAction){.input = i, .phase = PURE_CANCEL, + .target = UINT32_MAX, .source = 1}); + } + for (uint32_t i = 0; i < app->plan->target_count && i < 24; ++i) + if ((app->held & (1u << (8 + i))) && app->pointer_owners[8 + i] == 1) + (void)pure_native_app_action(app, (PureAction){.input = app->plan->targets[i].input, + .phase = PURE_CANCEL, .target = i, .source = 1}); +} +static PureResult execute(PureNativeApp *app, const PurePeerRequest *request) { + if (request->operation == PURE_PEER_STATE) return PURE_OK; + if (request->expected != app->revision) return PURE_CONFLICT; + PureOutcome result; + if (request->operation == PURE_PEER_SAVE) result = pure_native_app_save(app); + else if (request->operation == PURE_PEER_LOAD) result = pure_native_app_restore(app); + else if (request->operation == PURE_PEER_ACTION) { + PureAction action = request->action; + action.source = 1; + result = pure_native_app_action(app, action); + } else return PURE_INVALID; + return result.status; +} +static bool serve_connection(PureLink *channel, PureNativeApp *app) { + unsigned char hello[PURE_PEER_HELLO_BYTES]; + pure_peer_hello(app, hello); + if (!pure_link_write_frame(channel, hello, sizeof(hello), pure_link_now_ms() + PURE_LINK_DEADLINE_MS)) return false; + for (;;) { + unsigned char buffer[PURE_PEER_REQUEST_MAX]; + size_t length = 0; + uint64_t deadline = pure_link_now_ms() + PURE_LINK_DEADLINE_MS; + int got = pure_link_read_frame(channel, buffer, sizeof(buffer), &length, deadline); + if (!got) return true; + if (got < 0) return false; + PurePeerRequest request; + if (!pure_peer_decode_request(buffer, length, app->plan, &request)) return false; + PureResult status = execute(app, &request); + unsigned char reply[PURE_PEER_REPLY_MAX]; + length = pure_peer_encode_reply(app, status, reply); + if (!length || !pure_link_write_frame(channel, reply, length, deadline)) return false; + } +} +static int serve(const char *bind_address, const char *port, const char *directory, + const char *document, const char *ca, const char *cert, const char *key, + const char *client_name, unsigned max_connections) { + if (!pure_link_port_number(port, true) || !client_name || !*client_name) return 2; + SSL_CTX *ctx = context(true, ca, cert, key); + if (!ctx) { ERR_print_errors_fp(stderr); return 2; } + size_t size = pure_scalar_bytes(); + void *memory = malloc(size); + PureNativeApp app; + if (!memory || pure_native_app_init(&app, &pure_app_plan, memory, size, directory, document, 4096) != PURE_OK) { + fprintf(stderr, "app initialization failed\n"); free(memory); SSL_CTX_free(ctx); return 2; + } + PureOutcome restore = pure_native_app_restore(&app); + if (restore.status != PURE_OK && restore.status != PURE_NOT_FOUND) { + fprintf(stderr, "existing state is invalid or unavailable: %u\n", restore.status); + free(memory); SSL_CTX_free(ctx); return 2; + } + int listener = pure_link_listen_at(bind_address, port); + if (listener < 0) { perror("listen"); free(memory); SSL_CTX_free(ctx); return 2; } + struct sockaddr_storage address; + socklen_t length = sizeof(address); + if (getsockname(listener, (struct sockaddr *)&address, &length)) abort(); + unsigned actual_port = address.ss_family == AF_INET + ? ntohs(((struct sockaddr_in *)&address)->sin_port) + : ntohs(((struct sockaddr_in6 *)&address)->sin6_port); + printf("READY %u\n", actual_port); fflush(stdout); + signal(SIGTERM, signal_stop); signal(SIGINT, signal_stop); + unsigned accepted = 0; + while (!stopping && (!max_connections || accepted < max_connections)) { + struct pollfd p = {.fd = listener, .events = POLLIN}; + int result = poll(&p, 1, 1000); + if (result < 0) { if (errno == EINTR) continue; break; } + if (!result) continue; + int fd = accept(listener, NULL, NULL); + if (fd < 0) { if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; break; } + ++accepted; + PureLink channel = {.fd = fd}; + bool healthy = pure_link_nonblocking(fd) && handshake(&channel, ctx, true, client_name); + if (healthy) healthy = serve_connection(&channel, &app); + pure_link_close(&channel, healthy); + release_inputs(&app); + } + close(listener); + pure_runtime_stop(&app.execution); + int status = app.execution.active || app.execution.retained ? 1 : 0; + free(memory); SSL_CTX_free(ctx); + return status; +} + +static void json_string(const char *value) { + putchar('"'); + for (const unsigned char *p = (const unsigned char *)value; *p; ++p) { + if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); } + else if (*p < 32) printf("\\u%04x", *p); + else putchar(*p); + } + putchar('"'); +} +static void print_reply(const PurePeerReply *reply) { + printf("{\"status\":%u,\"revision\":\"%" PRId64 "\",\"stored_revision\":\"%" PRId64 + "\",\"scalar_error\":%u,\"held\":%u,\"states\":{", + reply->status, reply->revision, reply->stored_revision, reply->scalar_error, reply->held); + for (uint32_t i = 0; i < reply->state_count; ++i) { + if (i) putchar(','); + json_string(pure_app_plan.states[i].name); + printf(":%.17g", reply->states[i]); + } + puts("}}"); fflush(stdout); +} +static bool number(const char *text, double *value) { + if (!text || !*text) return false; + char *end; + errno = 0; + *value = strtod(text, &end); + return !errno && !*end && isfinite(*value); +} +static bool parse_command(char *line, PurePeerRequest *request, bool *quit) { + char *words[8]; + size_t count = 0; + for (char *part = strtok(line, " \t\r\n"); part; part = strtok(NULL, " \t\r\n")) { + if (count == 8) return false; + words[count++] = part; + } + if (!count) return false; + *request = (PurePeerRequest){0}; + if (count == 1) { + if (!strcmp(words[0], "quit")) { *quit = true; return true; } + if (!strcmp(words[0], "state")) request->operation = PURE_PEER_STATE; + else if (!strcmp(words[0], "save")) request->operation = PURE_PEER_SAVE; + else if (!strcmp(words[0], "load")) request->operation = PURE_PEER_LOAD; + else if (!strcmp(words[0], "frame") || !strcmp(words[0], "reset")) { + request->operation = PURE_PEER_ACTION; + request->action = (PureAction){.input = UINT32_MAX, .target = UINT32_MAX, + .phase = !strcmp(words[0], "frame") ? PURE_FRAME : PURE_RESET}; + } else return false; + return true; + } + if (count < 3) return false; + uint32_t input = pure_app_plan.input_count; + for (uint32_t i = 0; i < pure_app_plan.input_count; ++i) + if (!strcmp(words[1], pure_app_plan.inputs[i].name)) { input = i; break; } + if (input >= pure_app_plan.input_count) return false; + PureScalarInputKind kind = pure_app_plan.inputs[input].kind; + request->operation = PURE_PEER_ACTION; + request->action = (PureAction){.input = input, .target = UINT32_MAX}; + if (count == 3 && !strcmp(words[0], "key") && kind == PURE_KEY) { + if (!strcmp(words[2], "down")) request->action.phase = PURE_DOWN; + else if (!strcmp(words[2], "up")) request->action.phase = PURE_UP; + else return false; + } else if (count == 3 && !strcmp(words[0], "value") && kind == PURE_VALUE) { + request->action.phase = PURE_CHANGE; + if (!number(words[2], &request->action.x)) return false; + } else if (count == 6 && !strcmp(words[0], "point") && (kind == PURE_POINT || kind == PURE_ROUTED)) { + const char *phases[] = {"down", "up", "move", "cancel"}; + bool phase = false; + for (unsigned i = 0; i < 4; ++i) if (!strcmp(words[2], phases[i])) { + request->action.phase = i; phase = true; break; + } + if (!phase || !number(words[4], &request->action.x) || !number(words[5], &request->action.y)) return false; + if (kind == PURE_POINT) { + if (strcmp(words[3], pure_app_plan.inputs[input].target)) return false; + } else { + bool found = false; + for (uint32_t i = 0; i < pure_app_plan.target_count; ++i) + if (pure_app_plan.targets[i].input == input && !strcmp(words[3], pure_app_plan.targets[i].view)) { + request->action.target = i; found = true; break; + } + if (!found) return false; + } + } else return false; + return true; +} +static bool request_reply(PureLink *channel, const PurePeerRequest *request, PurePeerReply *reply) { + unsigned char wire[PURE_PEER_REPLY_MAX]; + size_t length = pure_peer_encode_request(request, &pure_app_plan, wire); + if (!length || !pure_link_write_frame(channel, wire, length, pure_link_now_ms() + PURE_LINK_DEADLINE_MS)) return false; + int got = pure_link_read_frame(channel, wire, sizeof(wire), &length, pure_link_now_ms() + PURE_LINK_DEADLINE_MS); + return got == 1 && pure_peer_decode_reply(wire, length, &pure_app_plan, reply); +} +static int call(const char *host, const char *port, const char *server_name, + const char *ca, const char *cert, const char *key) { + if (!pure_link_port_number(port, false) || !server_name || !*server_name) return 2; + SSL_CTX *ctx = context(false, ca, cert, key); + if (!ctx) { ERR_print_errors_fp(stderr); return 2; } + PureLink channel = {.fd = pure_link_connect_to(host, port)}; + if (channel.fd < 0 || !handshake(&channel, ctx, false, server_name)) { + fprintf(stderr, "peer authentication or connection failed\n"); + pure_link_close(&channel, false); SSL_CTX_free(ctx); return 2; + } + unsigned char hello[PURE_PEER_HELLO_BYTES]; + size_t length = 0; + int64_t revision, stored; + if (pure_link_read_frame(&channel, hello, sizeof(hello), &length, pure_link_now_ms() + PURE_LINK_DEADLINE_MS) != 1 || + !pure_peer_check_hello(hello, length, &pure_app_plan, &revision, &stored)) { + fprintf(stderr, "peer application definition mismatch\n"); + pure_link_close(&channel, false); SSL_CTX_free(ctx); return 2; + } + (void)stored; + PurePeerReply reply; + PurePeerRequest state = {.operation = PURE_PEER_STATE}; + if (!request_reply(&channel, &state, &reply)) { + fprintf(stderr, "initial state failed\n"); + pure_link_close(&channel, false); SSL_CTX_free(ctx); return 2; + } + print_reply(&reply); + revision = reply.revision; + int failed = 0; + char line[512]; + while (fgets(line, sizeof(line), stdin)) { + if (!strchr(line, '\n') && !feof(stdin)) { + int c; + while ((c = getchar()) != '\n' && c != EOF) {} + fprintf(stderr, "command exceeds 510 bytes\n"); failed = 1; continue; + } + PurePeerRequest request; + bool quit = false; + if (!parse_command(line, &request, &quit)) { + fprintf(stderr, "invalid command\n"); failed = 1; continue; + } + if (quit) break; + request.expected = revision; + if (!request_reply(&channel, &request, &reply)) { + fprintf(stderr, "peer request failed or timed out; effect may have committed; query before retry\n"); + failed = 1; break; + } + print_reply(&reply); + revision = reply.revision; + if (reply.status) failed = 1; + } + if (ferror(stdin) || ferror(stdout)) failed = 1; + pure_link_close(&channel, true); SSL_CTX_free(ctx); + return failed; +} + +int main(int argc, char **argv) { + signal(SIGPIPE, SIG_IGN); + if (argc == 8 && !strcmp(argv[1], "call")) + return call(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7]); + if ((argc == 10 || argc == 11) && !strcmp(argv[1], "serve")) { + unsigned limit = 0; + if (argc == 11) { + char *end; + errno = 0; + if (!argv[10][0] || argv[10][0] < '0' || argv[10][0] > '9') goto usage; + unsigned long parsed = strtoul(argv[10], &end, 10); + if (errno || *end || !parsed || parsed > UINT_MAX) goto usage; + limit = (unsigned)parsed; + } + return serve(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], limit); + } +usage: + fprintf(stderr, "pure-peer serve BIND_IP PORT STORE_DIR DOCUMENT_KEY CA.pem SERVER_CERT.pem SERVER_KEY.pem CLIENT_DNS [MAX_CONNECTIONS]\n" + "pure-peer call HOST PORT SERVER_DNS CA.pem CLIENT_CERT.pem CLIENT_KEY.pem\n" + "Commands: state, save, load, frame, reset, key NAME down|up, value NAME NUMBER,\n" + "point NAME down|move|up|cancel TARGET X Y, quit.\n"); + return 2; +} diff --git a/prototypes/workbench/native/plan.c b/prototypes/workbench/native/plan.c new file mode 100644 index 0000000..4b82da0 --- /dev/null +++ b/prototypes/workbench/native/plan.c @@ -0,0 +1,45 @@ +#include "plan.h" +#include + +uint32_t pure_scalar_load(PureScalar *vm, const PureScalarPlan *plan, + const PureScalarPlan *previous, const double *restored) { +#ifdef PURE_FIXED_DEFINITION + if (!vm || !plan || previous) return 2; + return pure_scalar_install_fixed(vm, plan->identity, plan->state_count, plan->input_count, + plan->instruction_count, plan->handler_count, + plan->target_count, restored); +#else + uint32_t code = pure_scalar_begin_program(vm, plan->state_count, plan->input_count); + if (code) return code; + for (uint32_t i = 0; i < plan->state_count; ++i) { + int32_t old = -1; + if (!restored && previous) + for (uint32_t j = 0; j < previous->state_count; ++j) + if (!strcmp(plan->states[i].name, previous->states[j].name)) { old = (int32_t)j; break; } + code = pure_scalar_put_state(vm, i, plan->states[i].initial, old); + if (code) goto fail; + if (restored && (code = pure_scalar_put_restored_value(vm, i, restored[i]))) goto fail; + } + for (uint32_t i = 0; i < plan->instruction_count; ++i) { + const PureScalarInstruction *op = &plan->instructions[i]; + if ((code = pure_scalar_put_instruction(vm, op->op, op->arg, op->value))) goto fail; + } + for (uint32_t i = 0; i < plan->target_count; ++i) { + const PureScalarTarget *target = &plan->targets[i]; + if ((code = pure_scalar_put_pointer_target(vm, target->input, target->base, target->count))) goto fail; + } + for (uint32_t i = 0; i < plan->handler_count; ++i) { + const PureScalarHandler *handler = &plan->handlers[i]; + if ((code = pure_scalar_put_handler(vm, handler->event, handler->start, handler->length))) goto fail; + if (handler->count) { + code = handler->base == 255 ? pure_scalar_bind_target_handler(vm, i, handler->count) + : pure_scalar_bind_handler(vm, i, handler->base, handler->count); + if (code) goto fail; + } + } + return restored ? pure_scalar_commit_restored_program(vm) : pure_scalar_commit_program(vm); +fail: + pure_scalar_abort_program(vm); + return code; +#endif +} diff --git a/prototypes/workbench/native/plan.h b/prototypes/workbench/native/plan.h new file mode 100644 index 0000000..2ca886c --- /dev/null +++ b/prototypes/workbench/native/plan.h @@ -0,0 +1,39 @@ +#ifndef PURE_SCALAR_PLAN_H +#define PURE_SCALAR_PLAN_H +#include "scalar.h" +#include + +typedef struct { const char *name; double initial; } PureScalarState; +typedef struct { uint32_t op, arg; double value; } PureScalarInstruction; +typedef struct { uint32_t event, start, length, base, count; } PureScalarHandler; +typedef struct { uint32_t input, base, count; const char *view; } PureScalarTarget; +typedef enum { PURE_KEY, PURE_POINT, PURE_ROUTED, PURE_VALUE } PureScalarInputKind; +typedef struct { const char *name, *key, *target; PureScalarInputKind kind; } PureScalarInput; +typedef struct { uint32_t start, length; } PureScalarBinding; +typedef struct { + const char *id, *type, *decoration; + uint32_t count; + PureScalarBinding fields[4]; +} PureScalarView; +/* Immutable, build-prepared application data. In fixed CLI/peer builds, + * instructions and handlers are NULL: the same validated program is linked + * once into the scalar module. Other plan fields remain available to hosts. + * C layout is an internal build contract; no pointers or native structs are + * persisted or sent on the wire. */ +typedef struct { + uint32_t state_count, input_count, instruction_count, handler_count, target_count, view_count; + const PureScalarState *states; + const PureScalarInput *inputs; + const PureScalarInstruction *instructions; + const PureScalarHandler *handlers; + const PureScalarTarget *targets; + const PureScalarView *views; + unsigned char identity[32]; +} PureScalarPlan; + +/* Commit a prepared definition with initials, name-preserved previous values, + * or a complete restored state vector. A failed candidate leaves active state + * intact. The caller ensures the application is idle before replacing it. */ +uint32_t pure_scalar_load(PureScalar *, const PureScalarPlan *, + const PureScalarPlan *previous, const double *restored); +#endif diff --git a/prototypes/workbench/native/prepare.mjs b/prototypes/workbench/native/prepare.mjs new file mode 100644 index 0000000..897b75b --- /dev/null +++ b/prototypes/workbench/native/prepare.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node +// Build tool only: reuse the existing authoring compiler. Its parser, JS runtime +// and source syntax do not become dependencies of the resulting native app. +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { compile } from '../web/compiler.mjs'; + +const [input, output, profile] = process.argv.slice(2); +if (!input || !output || ![4, 5].includes(process.argv.length)) + throw new Error('prepare.mjs SOURCE.pure OUTPUT.c [FIXED_PROFILE.zig]'); +const source = await readFile(input, 'utf8'); +const p = compile(source); +// Encode strings as byte escapes, not JSON/C escape substitution. No source text +// or user-authored identifier can become executable C through this preparation. +const string = s => '"' + [...Buffer.from(s ?? '', 'utf8')].map(b => `\\${b.toString(8).padStart(3, '0')}`).join('') + '"'; +const number = n => Object.is(n, -0) ? '-0.0' : Number.isFinite(n) ? String(n) : (() => { throw new Error('nonfinite prepared constant'); })(); +const declaration = (type, name, rows) => `static const ${type} ${name}[] = {\n${rows.length ? rows.map(r => ` ${r},`).join('\n') : ' {0},'}\n};\n`; +let text = '#include "plan.h"\n\n'; +text += declaration('PureScalarState', 'states', p.states.map(s => `{${string(s.name)}, ${number(s.initial)}}`)); +text += declaration('PureScalarInput', 'inputs', p.inputs.map(i => `{${string(i.name)}, ${string(i.key)}, ${string(i.target)}, ${i.kind === 'value' ? 'PURE_VALUE' : i.routes ? 'PURE_ROUTED' : i.target ? 'PURE_POINT' : 'PURE_KEY'}}`)); +text += '#ifndef PURE_FIXED_DEFINITION\n'; +text += declaration('PureScalarInstruction', 'instructions', p.instructions.map(i => `{${i.op}, ${i.arg}, ${number(i.value)}}`)); +text += declaration('PureScalarHandler', 'handlers', p.handlers.map(h => `{${h.event}, ${h.start}, ${h.length}, ${h.parameterBase ?? 0}, ${h.parameterCount ?? 0}}`)); +text += '#endif\n'; +text += declaration('PureScalarTarget', 'targets', p.pointerTargets.map((t, index) => { + const route = p.inputs[t.input].routes.find(r => r.route === index); + if (!route) throw new Error('missing prepared pointer target'); + return `{${t.input}, ${t.base}, ${t.count}, ${string(route.target)}}`; +})); +text += declaration('PureScalarView', 'views', p.views.map(v => { + const fields = Object.values(v.bindings); + return `{${string(v.id)}, ${string(v.type)}, ${string(v.color ?? v.prefix)}, ${fields.length}, {${fields.map(b => `{${b.start}, ${b.length}}`).join(', ')}}}`; +})); +// Definition identity protects restore compatibility, not authenticity. Persisted +// values from a different definition are rejected; storage authentication is separate. +const identity = createHash('sha256').update(JSON.stringify(p)).digest(); +text += '#ifdef PURE_FIXED_DEFINITION\n#define PURE_INSTRUCTIONS 0\n#define PURE_HANDLERS 0\n#else\n#define PURE_INSTRUCTIONS instructions\n#define PURE_HANDLERS handlers\n#endif\n'; +text += `\nconst PureScalarPlan pure_app_plan = {\n ${p.states.length}, ${p.inputs.length}, ${p.instructions.length}, ${p.handlers.length}, ${p.pointerTargets.length}, ${p.views.length},\n states, inputs, PURE_INSTRUCTIONS, PURE_HANDLERS, targets, views,\n {${[...identity].join(', ')}}\n};\n`; +await writeFile(output, text); +if (profile) { + // A fixed CLI/peer has no source-replacement path. These values size the + // existing VM arrays; GTK and Wasm keep the full authoring capacities. + const limits = [ + ['states', p.states.length], ['inputs', p.inputs.length], + ['instructions', p.instructions.length], ['handlers', p.handlers.length], + ['pointer_targets', p.pointerTargets.length], + ]; + // Zig rejects an index operation on a statically empty array even when a + // preceding runtime bound check makes that operation unreachable. + const profileText = [ + ...limits.map(([name, count]) => `pub const ${name} = ${Math.max(1, count)};`), + ...limits.map(([name, count]) => `pub const ${name}_count = ${count};`), + `pub const identity = [_]u8{${[...identity].join(',')}};`, + `pub const initials = [_]f64{${p.states.map(s => number(s.initial)).join(',')}};`, + 'pub const Instruction = struct { op: u32, arg: u32, value: f64 };', + `pub const code = [_]Instruction{${p.instructions.map(i => + `.{.op=${i.op},.arg=${i.arg},.value=${number(i.value)}}`).join(',')}};`, + 'pub const Handler = struct { event: u32, start: u32, length: u32, base: u8, count: u8 };', + `pub const handlers_data = [_]Handler{${p.handlers.map(h => + `.{.event=${h.event},.start=${h.start},.length=${h.length},.base=${h.parameterBase ?? 0},.count=${h.parameterCount ?? 0}}`).join(',')}};`, + 'pub const Target = struct { input: u8, base: u8, count: u8 };', + `pub const targets_data = [_]Target{${p.pointerTargets.map(t => + `.{.input=${t.input},.base=${t.base},.count=${t.count}}`).join(',')}};`, + `pub const routed_inputs = ${p.pointerTargets.reduce((mask, t) => mask | (1 << t.input), 0)};`, + ].join('\n') + '\n'; + await writeFile(profile, profileText); +} diff --git a/prototypes/workbench/native/scalar.h b/prototypes/workbench/native/scalar.h new file mode 100644 index 0000000..15343d1 --- /dev/null +++ b/prototypes/workbench/native/scalar.h @@ -0,0 +1,51 @@ +#ifndef PURE_SCALAR_H +#define PURE_SCALAR_H +#include +#include +#ifdef __cplusplus +extern "C" { +#endif +/* This selected scalar module has no default instance or allocator. Allocate + * bytes() aligned to alignment(), initialize once, and retain it through all + * calls. Each instance has one owner at a time; independent instances share + * no mutable state. Passing invalid pointers is outside this trusted C ABI. + * This is an in-process API, not a stable wire or persistent representation. */ +typedef struct PureScalar PureScalar; +size_t pure_scalar_bytes(void); +size_t pure_scalar_alignment(void); +PureScalar *pure_scalar_initialize(void *memory, size_t size); +void pure_scalar_reset_all(PureScalar *instance); +/* Fixed-definition builds only. The caller's identity and counts must match + * the compiled program; all restored values are checked before publication. */ +uint32_t pure_scalar_install_fixed(PureScalar *instance, const uint8_t identity[32], + uint32_t state_count, uint32_t input_count, uint32_t instruction_count, + uint32_t handler_count, uint32_t target_count, const double *restored); +uint32_t pure_scalar_begin_program(PureScalar *instance, uint32_t state_count, uint32_t input_count); +void pure_scalar_abort_program(PureScalar *instance); +uint32_t pure_scalar_put_state(PureScalar *instance, uint32_t id, double initial, int32_t old_id); +uint32_t pure_scalar_put_instruction(PureScalar *instance, uint32_t op, uint32_t arg, double value); +uint32_t pure_scalar_put_handler(PureScalar *instance, uint32_t event, uint32_t start, uint32_t length); +uint32_t pure_scalar_bind_handler(PureScalar *instance, uint32_t handler, uint32_t base, uint32_t count); +uint32_t pure_scalar_put_pointer_target(PureScalar *instance, uint32_t input, uint32_t base, uint32_t count); +uint32_t pure_scalar_bind_target_handler(PureScalar *instance, uint32_t handler, uint32_t count); +uint32_t pure_scalar_put_restored_value(PureScalar *instance, uint32_t id, double value); +uint32_t pure_scalar_commit_program(PureScalar *instance); +uint32_t pure_scalar_commit_restored_program(PureScalar *instance); +void pure_scalar_reset_state(PureScalar *instance); +uint32_t pure_scalar_dispatch(PureScalar *instance, uint32_t event, double dt); +uint32_t pure_scalar_dispatch_point(PureScalar *instance, uint32_t event, double x, double y); +uint32_t pure_scalar_dispatch_target(PureScalar *instance, uint32_t event, uint32_t target, double x, double y); +uint32_t pure_scalar_dispatch_value(PureScalar *instance, uint32_t event, double value); +double pure_scalar_eval_at(PureScalar *instance, uint32_t start, uint32_t length); +uint32_t pure_scalar_configure_evaluation(PureScalar *instance, uint32_t cache, uint32_t history); +void pure_scalar_forget_evaluation(PureScalar *instance, uint32_t flags); +void pure_scalar_begin_evaluation(PureScalar *instance, uint32_t limit); +double pure_scalar_eval_bounded(PureScalar *instance, uint32_t start, uint32_t length); +uint32_t pure_scalar_evaluation_info(PureScalar *instance, uint32_t id); +double pure_scalar_get_state(PureScalar *instance, uint32_t id); +uint32_t pure_scalar_info(PureScalar *instance, uint32_t id); +uint32_t pure_scalar_layout(PureScalar *instance, uint32_t id); +#ifdef __cplusplus +} +#endif +#endif diff --git a/prototypes/workbench/native/test-fixed.c b/prototypes/workbench/native/test-fixed.c new file mode 100644 index 0000000..adf1556 --- /dev/null +++ b/prototypes/workbench/native/test-fixed.c @@ -0,0 +1,68 @@ +#include "plan.h" +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; + +int main(void) { + size_t bytes = pure_scalar_bytes(); + /* This authored three-target app is the selected repeated-instance cost. */ + assert(bytes <= 512); + void *first_memory = malloc(bytes), *second_memory = malloc(bytes); + assert(first_memory && second_memory); + PureScalar *first = pure_scalar_initialize(first_memory, bytes); + PureScalar *second = pure_scalar_initialize(second_memory, bytes); + assert(first && second); + assert(pure_scalar_info(first, 5) == 0 && pure_scalar_info(first, 6) == 0); + assert(pure_scalar_layout(first, 4) == 0 && pure_scalar_layout(first, 5) == 0); + assert(isnan(pure_scalar_get_state(first, 0)) && isnan(pure_scalar_eval_at(first, 0, 0))); + assert(pure_scalar_dispatch(first, 1, 0) == 9); + assert(pure_scalar_dispatch(first, 0, 1.0 / 60.0) == 0); + assert(pure_scalar_info(first, 0) == 1 && pure_scalar_info(first, 9) == 1); + pure_scalar_reset_state(first); + assert(pure_scalar_info(first, 0) == 0 && pure_scalar_info(first, 9) == 0); + assert(first && second && pure_scalar_load(first, &pure_app_plan, NULL, NULL) == 0); + assert(pure_scalar_load(second, &pure_app_plan, NULL, NULL) == 0); + assert(pure_scalar_layout(first, 0) == bytes && pure_scalar_layout(first, 1) > bytes); + assert(pure_scalar_get_state(first, 0) == 0 && pure_scalar_get_state(second, 0) == 0); + assert(pure_scalar_evaluation_info(first, 8) == 0); + assert(pure_scalar_configure_evaluation(first, 1, 1) == 1); + assert(pure_scalar_configure_evaluation(first, 0, 0) == 0); + + /* Two state vectors execute the same compiled program independently. */ + assert(pure_scalar_dispatch_target(first, 9, 0, 90, 130) == 0); + assert(pure_scalar_get_state(first, 0) == 1 && pure_scalar_get_state(second, 0) == 0); + uint32_t before = pure_scalar_info(first, 4); + double values[32] = {0}; + for (uint32_t i = 0; i < pure_app_plan.state_count; ++i) + values[i] = pure_scalar_get_state(first, i); + + PureScalarPlan wrong = pure_app_plan; + wrong.identity[0] ^= 1; + assert(pure_scalar_load(first, &wrong, NULL, NULL) == 2); + wrong = pure_app_plan; + wrong.instruction_count++; + assert(pure_scalar_load(first, &wrong, NULL, NULL) == 2); + assert(pure_scalar_load(first, &pure_app_plan, &pure_app_plan, NULL) == 2); + values[0] = NAN; + assert(pure_scalar_load(first, &pure_app_plan, NULL, values) == 5); + assert(pure_scalar_info(first, 4) == before && pure_scalar_get_state(first, 0) == 1); + values[0] = -0.0; + assert(pure_scalar_load(second, &pure_app_plan, NULL, values) == 0); + assert(signbit(pure_scalar_get_state(second, 0)) && pure_scalar_get_state(first, 0) == 1); + assert(pure_scalar_begin_program(first, pure_app_plan.state_count, pure_app_plan.input_count) == 1); + assert(pure_scalar_info(first, 4) == before && pure_scalar_get_state(first, 0) == 1); + pure_scalar_reset_all(first); + assert(pure_scalar_info(first, 5) == 0); + assert(pure_scalar_load(first, &pure_app_plan, NULL, NULL) == 0); + assert(pure_scalar_get_state(first, 0) == 0); + + printf("{\"native_instances\":2,\"scalar_instance_bytes\":%zu," + "\"shared_program_bytes\":%u,\"checks\":" + "\"empty-state-independent-state-identity-capacity-atomic-restore-signed-zero-reset\"}\n", + bytes, pure_scalar_layout(first, 1)); + free(first_memory); free(second_memory); + return 0; +} diff --git a/prototypes/workbench/native/test.c b/prototypes/workbench/native/test.c new file mode 100644 index 0000000..cc4c529 --- /dev/null +++ b/prototypes/workbench/native/test.c @@ -0,0 +1,152 @@ +#define _POSIX_C_SOURCE 200809L +#include "app.h" +#include "../../connections/store.h" +#include +#include +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; +static void move(PureNativeApp *app, uint32_t target, double x, double y) { + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_DOWN, .target = target, .x = x, .y = y}).status == PURE_OK); + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_MOVE, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK); + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_UP, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK); +} +static void drained(PureNativeApp *app) { + assert(app->execution.active == 0 && app->execution.retained == 0); + assert(app->execution.work == NULL && app->execution.capacity == 0); + assert(app->execution.peak_active <= 2 && app->execution.peak_retained <= app->execution.budget); +} +static void values(PureNativeApp *app, double output[32]) { + for (uint32_t i = 0; i < app->plan->state_count; ++i) output[i] = pure_scalar_get_state(app->scalar, i); +} +static void same(PureNativeApp *app, const double *expected) { + for (uint32_t i = 0; i < app->plan->state_count; ++i) assert(pure_scalar_get_state(app->scalar, i) == expected[i]); +} + +int main(void) { + size_t size = pure_scalar_bytes(); + unsigned char *a = malloc(size), *b = malloc(size), *c = malloc(size); + assert(a && b && c); + assert(!pure_scalar_initialize(NULL, size)); + assert(!pure_scalar_initialize(a, size - 1)); + assert(pure_scalar_alignment() > 1 && !pure_scalar_initialize(a + 1, size)); + char path[] = "/tmp/pure-native-instance-XXXXXX"; + assert(mkdtemp(path)); + PureNativeApp first, second, limited; + assert(pure_native_app_init(&first, &pure_app_plan, a, size, path, "scene", 4096) == PURE_OK); + assert(pure_native_app_init(&second, &pure_app_plan, b, size, path, "scene", 4096) == PURE_OK); + assert(pure_native_app_init(&limited, &pure_app_plan, c, size, path, "limited", sizeof(PureAction) - 1) == PURE_OK); + assert(pure_native_app_action(&limited, (PureAction){.phase = PURE_FRAME}).status == PURE_BUDGET); + assert(pure_scalar_info(limited.scalar, 9) == 0); + + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .x = 90, .y = 130}).status == PURE_OK); + assert(pure_native_app_save(&first).status == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 1, .x = 230, .y = 210}).status == PURE_INVALID); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .x = 230, .y = 210}).status == PURE_OK); + move(&second, 1, 290, 170); + assert(pure_scalar_get_state(second.scalar, 1) == 80 && pure_scalar_get_state(second.scalar, 9) == 350); + assert(pure_scalar_get_state(first.scalar, 1) == 220 && pure_scalar_get_state(first.scalar, 9) == 280); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 0, .x = 230, .y = 210}).status == PURE_OK); + assert(first.held == 0 && second.held == 0); + assert(pure_native_app_save(&first).status == PURE_OK && first.stored_revision == 1); + double second_values[32] = {0}; + values(&second, second_values); + PureOutcome conflict = pure_native_app_save(&second); + assert(conflict.status == PURE_CONFLICT && conflict.revision == 1 && second.stored_revision == 0); + same(&second, second_values); + assert(pure_native_app_restore(&second).status == PURE_OK && second.stored_revision == 1); + assert(pure_scalar_get_state(second.scalar, 1) == 220 && pure_scalar_get_state(second.scalar, 9) == 280); + + /* Staging/error/evaluation state also belongs to the selected instance. */ + assert(pure_scalar_begin_program(first.scalar, 1, 0) == 0); + assert(pure_scalar_put_state(first.scalar, 0, 999, -1) == 0); + assert(pure_scalar_configure_evaluation(first.scalar, 2, 2) == 0); + move(&second, 2, 460, 230); + assert(pure_scalar_put_instruction(first.scalar, 999, 0, 0) != 0); + assert(pure_scalar_commit_program(first.scalar) != 0); + assert(pure_scalar_get_state(first.scalar, 1) == 220); + assert(pure_scalar_get_state(first.scalar, 17) == 450 && pure_scalar_get_state(second.scalar, 17) == 520); + assert(pure_native_app_save(&second).status == PURE_OK && second.stored_revision == 2); + assert(pure_native_app_save(&first).status == PURE_CONFLICT && first.stored_revision == 1); + + /* Mismatched definition and nonfinite restored values never replace state. */ + PureStoredView saved; + assert(pure_store_get(path, "scene", 300, &saved) == PURE_OK); + unsigned char invalid[300]; + assert(saved.body.size <= sizeof(invalid)); + size_t length = saved.body.size; + memcpy(invalid, saved.body.data, length); + pure_store_release(&saved); + invalid[8] ^= 1; + int64_t revision; + assert(pure_store_put(path, "scene", 2, (PureSpan){invalid, length}, &revision) == PURE_OK); + double first_values[32] = {0}; + values(&first, first_values); + assert(pure_native_app_restore(&first).status == PURE_INVALID && first.stored_revision == 1); + same(&first, first_values); + invalid[8] ^= 1; + const unsigned char nan[8] = {0,0,0,0,0,0,248,127}; + memcpy(invalid + 44, nan, 8); + assert(pure_store_put(path, "scene", 3, (PureSpan){invalid, length}, &revision) == PURE_OK); + assert(pure_native_app_restore(&first).status == PURE_INVALID && first.last_scalar_error == 5); + same(&first, first_values); + assert(first.stored_revision == 1); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .source = 11, .x = 230, .y = 210}).status == PURE_OK); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 1, .source = 22, .x = 290, .y = 170}).status == PURE_OK); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .source = 22}).status == PURE_INVALID); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_CANCEL, .target = 0, .source = 11}).status == PURE_OK); + assert(first.held && pure_native_app_save(&first).status == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 1, .source = 22}).status == PURE_OK); + assert(first.held == 0); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 1}).status == PURE_OK); + double after_key = pure_scalar_get_state(first.scalar, 9); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 2}).status == PURE_OK); + assert(pure_scalar_get_state(first.scalar, 9) == after_key); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 1}).status == PURE_OK && first.held); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 2}).status == PURE_OK && !first.held); + + /* The native editor swaps only a fully accepted definition. Matching state + * names survive; a new name starts at its declared initial value. */ + PureScalarState changed_states[32]; + memcpy(changed_states, pure_app_plan.states, pure_app_plan.state_count * sizeof(changed_states[0])); + changed_states[1] = (PureScalarState){"new.green.x", 123}; + PureScalarPlan changed = pure_app_plan; + changed.states = changed_states; + changed.identity[0] ^= 1; + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN}).status == PURE_OK); + assert(pure_native_app_replace_plan(&first, &changed) == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP}).status == PURE_OK); + double old_y = pure_scalar_get_state(first.scalar, 2); + int64_t before_replace = first.revision; + uint64_t before_accept = first.execution.accepted; + assert(pure_native_app_replace_plan(&first, &changed) == PURE_OK); + assert(first.execution.accepted == before_accept + 1); + assert(first.plan == &changed && first.revision == before_replace + 1); + assert(pure_scalar_get_state(first.scalar, 1) == 123 && pure_scalar_get_state(first.scalar, 2) == old_y); + PureScalarInstruction invalid_program[512]; + assert(changed.instruction_count <= 512); + memcpy(invalid_program, changed.instructions, changed.instruction_count * sizeof(invalid_program[0])); + invalid_program[0].op = 999; + PureScalarPlan rejected = changed; + rejected.instructions = invalid_program; + before_replace = first.revision; + before_accept = first.execution.accepted; + assert(pure_native_app_replace_plan(&first, &rejected) == PURE_INVALID && first.last_scalar_error != 0); + assert(first.execution.accepted == before_accept + 1); + assert(first.plan == &changed && first.revision == before_replace); + assert(pure_scalar_get_state(first.scalar, 1) == 123 && pure_scalar_get_state(first.scalar, 2) == old_y); + drained(&first); drained(&second); drained(&limited); + + char file[256]; + snprintf(file, sizeof(file), "%s/obj-scene", path); assert(unlink(file) == 0); + snprintf(file, sizeof(file), "%s/.writer.lock", path); assert(unlink(file) == 0); + assert(rmdir(path) == 0); + free(a); free(b); free(c); + printf("{\"native_instances\":3,\"checks\":\"independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload\"," + "\"scalar_instance_bytes\":%zu,\"scalar_alignment\":%zu,\"app_controller_bytes\":%zu,\"action_bytes\":%zu," + "\"snapshot_bytes\":%zu,\"all_work_drained\":true}\n", size, pure_scalar_alignment(), sizeof(PureNativeApp), sizeof(PureAction), length); + return 0; +} diff --git a/prototypes/workbench/pointer.test.mjs b/prototypes/workbench/pointer.test.mjs new file mode 100644 index 0000000..8bf530d --- /dev/null +++ b/prototypes/workbench/pointer.test.mjs @@ -0,0 +1,118 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = await readFile(new URL('./examples/direct.pure', import.meta.url), 'utf8'); +const fresh = async () => { const r = await createRuntime(wasm, {cacheEntries:32}); r.apply(source); return r; }; + +test('pointer declarations resolve forward targets and reject ambiguous or contextless inputs', () => { + const p = compile(source); + assert.equal(p.inputs[0].target, 'tile'); + assert(p.handlers.some(h => h.event === 17)); assert(p.handlers.some(h => h.event === 25)); + assert(p.instructions.some(i => i.op === 22)); assert(p.instructions.some(i => i.op === 23)); + for (const [code, message] of [ + ['input p = pointer(missing)', /Unknown pointer target/], + ['input p=pointer(tile)\ninput q=pointer(tile)\ntext tile(0,0,0)', /Duplicate pointer/], + ['input p=Space\non p.move {}', /require a pointer/], + ['input p=Space\non p.cancel {}', /require a pointer/], + ['state x=0\non frame { x=event_x }', /only available/], + ['text v(event_y,0,0)', /only available/], + ['state event_x=0', /Reserved name/], + ]) assert.throws(() => compile(code), message); + const full = compile(Array.from({length:8}, (_, i) => `input p${i}=pointer(v${i})\ntext v${i}(0,0,0)\non p${i}.move {}\non p${i}.cancel {}`).join('\n')); + assert.deepEqual(full.handlers.map(h => h.event), Array.from({length:8}, (_, i) => [17+i,25+i]).flat()); +}); + +test('authored drag keeps grab offset and commits the displayed position even if release coordinates differ', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'pen'); + assert.equal(r.snapshot().states.dragging, 1); + assert.throws(() => r.apply(''), /놓은 뒤/); + r.point('drag', 'move', 360, 220, 'pen'); + let s = r.snapshot(); + assert.equal(s.states.x, 350); assert.equal(s.states.y, 210); + assert.equal(s.views.find(v => v.id === 'region').value, 1); + r.point('drag', 'up', 410, 240, 'pen'); + s = r.snapshot(); + assert.equal(s.states.x, 350); assert.equal(s.states.y, 210); + assert.equal(s.states.dragging, 0); assert.deepEqual(s.held, []); + assert.equal(s.successfulEvents, 3); + assert.equal(s.lastEvent.x, 410); + r.down('left'); r.up('left'); + assert.equal(r.snapshot().states.x, 340); + r.point('drag', 'down', 350, 220, 'pen'); + r.point('drag', 'move', 450, 250, 'pen'); + r.point('drag', 'up', 350, 220, 'pen'); // A host may repeat the press coordinates. + assert.equal(r.snapshot().states.x, 440); + assert.equal(r.snapshot().states.y, 240); + r.apply('state x=0\nstate y=0\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.up { x=event_x\ny=event_y }'); + r.point('p','down',1,2); r.point('p','up',30,40); + assert.deepEqual(r.snapshot().states, {x:30,y:40}); // Payload remains available to authors. +}); + +test('ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'a'); + r.point('drag', 'down', 500, 500, 'a'); + assert.throws(() => r.point('drag', 'down', 90, 170, 'b'), /다른 포인터/); + r.point('drag', 'move', 400, 200, 'b'); + assert.equal(r.snapshot().successfulEvents, 1); + r.point('drag', 'move', 400, 200, 'a'); + r.releaseAll(); + const canceled = r.snapshot(); + assert.equal(canceled.states.x, 80); assert.equal(canceled.states.y, 160); + assert.equal(canceled.states.dragging, 0); assert.equal(canceled.lastEvent.phase, 'cancel'); + assert.deepEqual(canceled.held, []); + r.point('drag', 'cancel', 1, 1, 'a'); r.point('drag', 'move', 500, 500, 'a'); r.releaseAll(); + assert.deepEqual(r.snapshot(), canceled); + r.point('drag', 'down', 90, 170, 'new'); r.reset(); + r.point('drag', 'up', 600, 500, 'new'); + assert.equal(r.snapshot().states.x, 80); assert.deepEqual(r.snapshot().held, []); +}); + +test('invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event', async () => { + const r = await fresh(); r.point('drag', 'down', 90, 170); + const before = r.snapshot(); + for (const invoke of [() => r.point('drag','move',NaN,1), () => r.point('drag','up',1,Infinity), + () => r.point('drag','unknown',1,2), () => r.point('left','down',1,2), + () => r.down('drag'), () => r.up('drag'), () => r.point('drag','move',1,2,'')]) assert.throws(invoke); + assert.deepEqual(r.snapshot(), before); + r.releaseAll(); + r.apply(source.replace('x = clamp(event_x - grab_x, 20, 564)', 'x = event_x\n y = 1 / 0')); + r.point('drag', 'down', 90, 170); + assert.equal(r.point('drag', 'move', 500, 220), 4); + let s = r.snapshot(); assert.equal(s.states.x, 80); assert.equal(s.states.y, 160); + assert.equal(s.lastFailure.phase, 'move'); + r.releaseAll(); s = r.snapshot(); assert.deepEqual(s.held, []); assert.equal(s.lastError, 4); + assert.equal(s.states.dragging, 0); +}); + +test('independent targets share the event pipeline and publication remains non-reentrant', async () => { + const r = await createRuntime(wasm); + r.apply('state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\ntext one(0,0,x)\ntext two(0,0,y)\non a.move { x=event_x }\non b.move { y=event_y }'); + r.point('a','down',1,2,'first'); r.point('b','down',3,4,'second'); + r.point('a','move',10,20,'first'); r.point('b','move',30,40,'second'); + assert.deepEqual(r.snapshot().states,{x:10,y:40}); + r.present({begin(){assert.throws(() => r.point('a','move',0,0,'first'), /출력 전달 중/);},state(){},view(){},value(){},end(){}}); + r.releaseAll(); assert.deepEqual(r.snapshot().held, []); +}); + +test('failing down, up and cancel never prevent terminal ownership cleanup', async () => { + for (const phase of ['down', 'up', 'cancel']) { + const r = await createRuntime(wasm); + r.apply(`state x=7\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.${phase} { x=event_x\nx=1/0 }`); + const code = r.point('p','down',10,20,'owner'); + assert.equal(code, phase === 'down' ? 4 : 0); + assert.deepEqual(r.snapshot().held, ['p']); + if (phase === 'up') assert.equal(r.point('p','up',30,40,'owner'), 4); + else assert.equal(r.releaseAll(), phase === 'cancel' ? 4 : 0); + const s = r.snapshot(); + assert.equal(s.states.x, 7); assert.equal(s.lastError, 4); + assert.equal(s.lastFailure.phase, phase); assert.deepEqual(s.held, []); + r.apply('state x=100'); // Cleanup also unblocks definition changes. + assert.equal(r.snapshot().states.x, 7); + } +}); diff --git a/prototypes/workbench/presentation.test.mjs b/prototypes/workbench/presentation.test.mjs new file mode 100644 index 0000000..98bef6e --- /dev/null +++ b/prototypes/workbench/presentation.test.mjs @@ -0,0 +1,132 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; +import { createRuntime as before } from './results/2026-09-23-presentation/before/web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = `state x = 3 +state radius = 5 +input move = KeyM +input shrink = KeyS +box tile(x, 10, x * 2, 20, "#123456") +circle dot(50, 50, radius, "#abcdef") +text label(0, 0, 10 / x, "Value: ") +on move.down { x = x + 0.25 } +on shrink.down { radius = radius - 10 } +on frame { x = x + dt } +`; + +// The archived adapter predates route diagnostics. These legacy fixtures have +// no routes; keep all original fields in the equality comparison. +function archivedSnapshot(runtime) { + const value = runtime.snapshot(); + value.memory.pointerTargetBytes = 3; + value.memory.pointerTargets = 0; + return value; +} + +// Reconstruct only in tests, for comparison with the archived snapshot path. +function collect(runtime, hooks = {}) { + let states, views, declared; + const result = runtime.present({ + begin(program) { + declared = program; states = {}; views = []; + hooks.begin?.(); + }, + state(i, value) { states[declared.states[i].name] = value; }, + view(i, valid) { + if (!valid) return; + const {id, type, color, prefix} = declared.views[i]; + views.push({id, type, color, prefix}); + }, + value(i, name, value) { views.at(-1)[name] = value; hooks.value?.(); }, + end(count) { assert.equal(views.length, count); }, + }); + return {states, views, ...result}; +} + +test('direct publication matches archived snapshots and work counts through authored changes', async () => { + for (const cacheEntries of [0, 1, 32]) { + const left = await createRuntime(wasm, {cacheEntries}), right = await before(wasm, {cacheEntries}); + const compare = () => { + assert.deepEqual(collect(left), archivedSnapshot(right)); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + }; + compare(); + for (const r of [left, right]) r.apply(source); + compare(); + for (let i = 0; i < 120; i++) { + for (const r of [left, right]) { + r.down('move', 'keyboard'); r.down('move', 'pointer'); + r.up('move', 'keyboard'); r.up('move', 'pointer'); + if (i % 11 === 0) r.frame(2); + if (i % 17 === 0) r.forgetEvaluation('history'); + if (i % 23 === 0) r.reset(); + } + compare(); + } + for (const r of [left, right]) { r.down('shrink'); r.up('shrink'); } + compare(); // invalid circle only; other views still publish + for (const r of [left, right]) assert.throws(() => r.apply('state broken =')); + compare(); + for (const r of [left, right]) r.apply('state radius = 1\nstate x = 0\ntext label(1, 2, x)'); + compare(); + for (const r of [left, right]) r.apply(''); + compare(); + } +}); + +test('whole-scene budget failure never publishes partial bindings and recovers exactly', async () => { + const left = await createRuntime(wasm, {cacheEntries:32}), right = await before(wasm, {cacheEntries:32}); + for (const r of [left, right]) r.apply(source); + for (let workLimit = 0; workLimit <= 80; workLimit++) { + for (const r of [left, right]) r.configureEvaluation({workLimit}); + let calls = 0; + const result = collect(left, {value: () => calls++}); + assert.deepEqual(result, archivedSnapshot(right)); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + if (result.displayError?.code === 101) assert.equal(calls, 0); + else assert.equal(calls, 10); + } +}); + +test('latched event rollback, nonfinite bindings, reset and signed zero keep their semantics', async () => { + const left = await createRuntime(wasm), right = await before(wasm); + const definition = 'state x = 0\ninput fail = Space\ntext value(0, 0, 1 / x)\non fail.down { x = 2\nx = 1 / 0 }'; + for (const r of [left, right]) { r.apply(definition); r.down('fail'); r.up('fail'); } + assert.deepEqual(collect(left), archivedSnapshot(right)); + for (const r of [left, right]) r.configureEvaluation({workLimit:0}); + assert.deepEqual(collect(left), archivedSnapshot(right)); + for (const r of [left, right]) { + r.apply('state x = -0\ntext value(x, 0, x)'); r.reset(); r.configureEvaluation({workLimit:2048}); + } + const result = collect(left); + assert.deepEqual(result, archivedSnapshot(right)); + assert(Object.is(result.states.x, -0)); +}); + +test('publication rejects reentrant runtime changes and reads before they affect the scene', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const calls = [() => runtime.apply(''), () => runtime.reset(), () => runtime.frame(), + () => runtime.down('move'), () => runtime.up('move'), () => runtime.releaseAll(), + () => runtime.configureEvaluation({workLimit:0}), () => runtime.forgetEvaluation(), + () => runtime.snapshot(), () => runtime.present({})]; + const expected = runtime.snapshot(); + assert.deepEqual(collect(runtime, {begin() { + for (const call of calls) assert.throws(call, /출력 전달 중/); + }}), expected); + assert.deepEqual(runtime.snapshot(), expected); +}); + +test('invalid or throwing output adapters do not strand the runtime or alter authored state', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const expected = runtime.snapshot(); + assert.throws(() => runtime.present({}), TypeError); + assert.throws(() => collect(runtime, {value() { throw new Error('destination failed'); }}), /destination failed/); + assert.deepEqual(collect(runtime), expected); + runtime.down('move'); runtime.up('move'); + assert.equal(collect(runtime).states.x, 3.25); +}); diff --git a/prototypes/workbench/profile-refresh.js b/prototypes/workbench/profile-refresh.js new file mode 100644 index 0000000..75c086e --- /dev/null +++ b/prototypes/workbench/profile-refresh.js @@ -0,0 +1,66 @@ +// Playwright CLI function: one diagnostic run, no implementation changes. +async (page) => { + const onDialog = dialog => dialog.accept(); + page.on('dialog', onDialog); + const routeApp = async route => { + const response = await route.fetch(); + let body = await response.text(); + const measured = (key, expression) => `measure('${key}', () => ${expression})`; + body = body.replace("const output = createOutput(document);", `const output = createOutput(document); + function measure(name, action) { + const start = performance.now(); + try { return action(); } finally { + const all = globalThis.__phase ??= {}; + const item = all[name] ??= {calls:0, ms:0}; + item.calls++; item.ms += performance.now() - start; + } + }`); + body = body.replace('function refresh() {', "function refresh() { return measure('refresh', refreshBody); }\nfunction refreshBody() {"); + body = body.replace('function draftChanged() {', "function draftChanged() { return measure('draft', draftBody); }\nfunction draftBody() {"); + body = body.replace('runtime.present(output)', measured('present', 'runtime.present(output)')); + body = body.replace('output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks)', + measured('costs', 'output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks)')); + await route.fulfill({response, body}); + }; + const client = await page.context().newCDPSession(page); + const metrics = async () => Object.fromEntries((await client.send('Performance.getMetrics')).metrics.map(m => [m.name, m.value])); + const source = ['state x = 40', 'input move = KeyM', + 'box moving(x, 100, 30, 30, "#7ed9ae")', 'text label(20, 40, x)', + ...Array.from({length:22}, (_, i) => `box static${i}(${20 + (i % 7) * 70}, ${200 + Math.floor(i / 7) * 30}, 20, 20, "#8da5bb")`), + 'on move.down { x = x + 0.25 }'].join('\n'); + try { + await page.route('**/app.js', routeApp); + await page.reload(); await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await page.locator('#source').fill(source); await page.locator('#apply').click(); + await page.locator('#scene').focus(); + await page.evaluate(() => { + window.__tap = () => { + const scene = document.getElementById('scene'); + scene.dispatchEvent(new KeyboardEvent('keydown', {code:'KeyM', bubbles:true})); + scene.dispatchEvent(new KeyboardEvent('keyup', {code:'KeyM', bubbles:true})); + }; + for (let i = 0; i < 20; i++) window.__tap(); + }); + await client.send('Performance.enable'); + const before = await metrics(); + const run = await page.evaluate(() => { + window.__phase = {}; + const start = performance.now(); + for (let i = 0; i < 100; i++) window.__tap(); + return {elapsedMs:performance.now()-start, phase:window.__phase}; + }); + const after = await metrics(); + const delta = Object.fromEntries(['LayoutCount','LayoutDuration','RecalcStyleCount','RecalcStyleDuration','ScriptDuration'] + .map(key => [key, after[key]-before[key]])); + const evidence = {utc:new Date().toISOString(), taps:100, ...run, metrics:delta, source, + scope:'Phase timings overlap: draft/present/costs are inside refresh. CDP counters bracket the synchronous batch and may include surrounding browser work.'}; + await page.evaluate(value => {window.__pureRefreshProfile = value;}, evidence); + return evidence; + } catch (error) { + const evidence = {failed:true, message:error.stack}; + await page.evaluate(value => {window.__pureRefreshProfile = value;}, evidence); + return evidence; + } finally { + await page.unroute('**/app.js', routeApp); page.off('dialog', onDialog); await client.detach(); + } +} diff --git a/prototypes/workbench/project.test.mjs b/prototypes/workbench/project.test.mjs new file mode 100644 index 0000000..ad8a48d --- /dev/null +++ b/prototypes/workbench/project.test.mjs @@ -0,0 +1,79 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {createRuntime} from './web/runtime.mjs'; +import {parseProject, MAX_PROJECT_BYTES} from './web/project.mjs'; +const wasm = await readFile(new URL('./web/core.wasm',import.meta.url)); +const layout = await readFile(new URL('./examples/layout.pure',import.meta.url),'utf8'); +const fresh = () => createRuntime(wasm,{cacheEntries:32}); +const roundTrip = value => parseProject(JSON.stringify(value)); + +test('multiple authored placements and a separate invalid draft survive a fresh runtime restore',async()=>{ + const a=await fresh(); a.apply(layout); + for(const [name,x,y,nx,ny] of [['drag1',90,130,180,200],['drag2',290,170,370,210],['drag3',460,230,520,280]]) { + a.point(name,'down',x,y);a.point(name,'move',nx,ny);a.point(name,'up',x,y); + } + a.down('left');a.up('left'); + const before=a.snapshot(), document=roundTrip(a.exportDocument('state unfinished =')); + assert.equal(document.version,2);assert.equal(document.source,layout); + const b=await fresh(); b.apply('state old=99'); b.frame(5); + const restored=b.restoreDocument(document), after=b.snapshot(); + assert.equal(restored.draft,'state unfinished =');assert.equal(restored.source,layout); + assert.deepEqual(after.states,before.states);assert.deepEqual(after.views,before.views); + assert.equal(after.tick,0);assert.equal(after.successfulEvents,0);assert.equal(after.lastEvent,null);assert.deepEqual(after.held,[]); + b.down('right');b.up('right');assert.equal(b.snapshot().states.x3,before.states.x3+10); + b.reset();assert.equal(b.snapshot().states.x1,80);assert.equal(b.snapshot().states.x2,280);assert.equal(b.snapshot().states.x3,450); +}); + +test('malformed files reject before changing active code, values, diagnostics or warm display records',async()=>{ + const r=await fresh();r.apply('state x=5\ntext t(0,0,x)');r.snapshot(); + const good=r.exportDocument(), before=r.snapshot(), active=r.currentProgram; + const bad=[null,{}, {...good,version:99},{...good,draft:null},{...good,source:'state x='},{...good,states:[]}, + {...good,states:[{name:'other',value:2}]},{...good,states:[{name:'x',value:null}]}, + {...good,states:[{name:'x',value:'2'}]},{...good,states:[{name:'x',value:Infinity}]}, + {...good,states:[{name:'x',value:NaN}]},{...good,draft:'x'.repeat(32769)}, + {...good,source:'state x=0\nstate y=0',states:[{name:'x',value:1},{name:'x',value:2}]}]; + for(const doc of bad){assert.throws(()=>r.restoreDocument(doc));assert.equal(r.currentProgram,active);assert.deepEqual(r.snapshot(),before);} + assert(r.evaluationStats().cacheHits>0); + assert.throws(()=>parseProject('{'));assert.throws(()=>parseProject(' '.repeat(MAX_PROJECT_BYTES+1))); +}); + +test('restoration matches names rather than ordering and never reuses stale cached values',async()=>{ + const r=await fresh();r.apply('state x=1\nstate y=2\ntext t(0,0,x+y)');r.snapshot();r.snapshot(); + const doc=r.exportDocument();doc.states=[{name:'y',value:20},{name:'x',value:10}]; + r.restoreDocument(roundTrip(doc));assert.deepEqual(r.snapshot().states,{x:10,y:20});assert.equal(r.snapshot().views[0].value,30); + r.apply('state y=200\nstate x=100');assert.deepEqual(r.snapshot().states,{y:20,x:10}); + r.reset();assert.deepEqual(r.snapshot().states,{y:200,x:100}); +}); + +test('finite float values including negative zero round trip without changing authored reset values',async()=>{ + const r=await fresh();r.apply('state z=-0\nstate tiny=5e-324\nstate large=1.7976931348623157e308\nstate constructor=1'); + const doc=roundTrip(r.exportDocument());assert.equal(doc.states[0].value,'-0'); + r.restoreDocument(doc);const s=r.snapshot().states; + assert(Object.is(s.z,-0));assert.equal(s.tiny,Number.MIN_VALUE);assert.equal(s.large,Number.MAX_VALUE);assert.equal(s.constructor,1); +}); + +test('held and failed interactions cannot be saved and restoration cannot steal an active input',async()=>{ + const r=await fresh();r.apply(layout);const saved=r.exportDocument(); + r.point('drag1','down',90,130);const before=r.snapshot(); + assert.throws(()=>r.exportDocument(),/조작/);assert.throws(()=>r.restoreDocument(saved),/놓은 뒤/);assert.deepEqual(r.snapshot(),before); + r.releaseAll();r.apply('state x=0\ninput fail=Space\non fail.down { x=1/0 }'); + r.down('fail');r.up('fail');assert.throws(()=>r.exportDocument(),/오류/); + r.restoreDocument(saved);assert.equal(r.snapshot().lastError,0); +}); + +test('empty and full documents restore without imposing a new source size or state count',async()=>{ + const r=await fresh();r.apply('');r.restoreDocument(roundTrip(r.exportDocument()));assert.deepEqual(r.snapshot().states,{}); + r.apply(Array.from({length:32},(_,i)=>`state s${i}=${i}`).join('\n'));const doc=r.exportDocument(); + for(const s of doc.states) s.value+=100; + r.restoreDocument(roundTrip(doc));assert.equal(r.snapshot().states.s31,131); + assert.equal(parseProject(JSON.stringify({format:'pure-workbench',version:1,source:'state unfinished ='})).version,1); + assert.throws(()=>r.restoreDocument({format:'pure-workbench',version:1,source:''})); +}); + +test('document APIs respect the output callback boundary and export independent values',async()=>{ + const r=await fresh();r.apply('state x=1');const doc=r.exportDocument();doc.states[0].value=500; + assert.equal(r.snapshot().states.x,1); + r.present({begin(){assert.throws(()=>r.restoreDocument(doc),/출력 전달/);assert.throws(()=>r.exportDocument(),/출력 전달/);},state(){},view(){},value(){},end(){}}); + assert.equal(r.snapshot().states.x,1); +}); diff --git a/prototypes/workbench/qa-browser.js b/prototypes/workbench/qa-browser.js new file mode 100644 index 0000000..3590fff --- /dev/null +++ b/prototypes/workbench/qa-browser.js @@ -0,0 +1,228 @@ +// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-workbench-blank.png', 'output/playwright/pure-workbench-authored.png', 'output/playwright/pure-workbench-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-workbench-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} diff --git a/prototypes/workbench/qa-document-guards.js b/prototypes/workbench/qa-document-guards.js new file mode 100644 index 0000000..c938ec3 --- /dev/null +++ b/prototypes/workbench/qa-document-guards.js @@ -0,0 +1,30 @@ +async page => { + const checks=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const onDialog=d=>d.accept();page.on('dialog',onDialog); + try { + await page.goto('http://127.0.0.1:8766/document.html');await page.waitForFunction(()=>window.pureDocument?.snapshot().ready); + await page.setViewportSize({width:1440,height:1000});await page.locator('#document-text').fill('보존할 문서'); + await page.locator('.policy summary').click();const source=await page.locator('#policy-source').inputValue(); + const bad=source.replace('clamp(event_value, 20, 80)','999');check(bad!==source,'invalid-output fixture changes the numeric rule'); + await page.locator('#policy-source').fill(bad);await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('End'); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='50'&&await page.locator('#split-range').inputValue()==='50'&&(await page.locator('#policy-status').textContent()).includes('20~80'),'out-of-range authored output preserves both the layout and its native control projection'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('Home'); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='20','a corrected policy and valid request recover from invalid host output'); + await page.locator('#policy-source').fill(source.replace('clamp(event_value, 20, 80)','1 / 0'));await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('End'); + const s=await page.evaluate(()=>window.pureDocument.snapshot());check(s.runtime.lastError===4&&s.runtime.states.ratio===20&&await page.locator('#split-range').inputValue()==='20','arithmetic rollback also restores the native range projection'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click(); + await page.locator('#divider').scrollIntoViewIfNeeded();const box=await page.locator('#divider').boundingBox();await page.mouse.move(box.x+box.width/2,box.y+box.height/2);await page.mouse.down(); + const revision=await page.evaluate(()=>window.pureDocument.snapshot().runtime.revision); + await page.evaluate(()=>document.querySelector('#apply-policy').click()); + check(await page.evaluate(r=>window.pureDocument.snapshot().dragging&&window.pureDocument.snapshot().runtime.revision===r,revision)&&(await page.locator('#policy-status').textContent()).includes('경계를 놓거나'),'policy replacement is refused during an active native gesture');await page.mouse.up(); + await page.locator('#file-input').evaluate(input=>{const d=new DataTransfer();d.items.add(new File([new Uint8Array(2*1024*1024+1)],'oversize.txt'));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('2 MiB')); + check(await page.locator('#document-text').inputValue()==='보존할 문서','oversize files cannot replace the editable text'); + await page.locator('.policy summary').click();await page.locator('#sample').click();await page.locator('#reset-split').click(); + await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); + await page.screenshot({path:'output/playwright/pure-document-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844});await page.screenshot({path:'output/playwright/pure-document-mobile.png',fullPage:true}); + await page.evaluate(e=>window.__pureDocumentGuards=e,{checks});return{checks}; + }catch(error){await page.evaluate(e=>window.__pureDocumentGuards=e,{failed:String(error),checks});throw error;} + finally{page.off('dialog',onDialog);} +} diff --git a/prototypes/workbench/qa-document.js b/prototypes/workbench/qa-document.js new file mode 100644 index 0000000..46b9b8c --- /dev/null +++ b/prototypes/workbench/qa-document.js @@ -0,0 +1,103 @@ +// Playwright CLI run-code. Functional and ownership checks, not a timing benchmark. +async page => { + const checks=[],errors=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const onDialog=d=>d.accept(),onError=e=>errors.push(e.message);page.on('dialog',onDialog);page.on('pageerror',onError); + const state=()=>page.evaluate(()=>window.pureDocument.snapshot()); + const settled=()=>page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); + const ratio=async expected=>{await page.waitForFunction(n=>Math.abs(window.pureDocument.snapshot().runtime.states.ratio-n)<0.2,expected);}; + const geometry=()=>page.evaluate(()=>{const p=document.querySelector('#panes').getBoundingClientRect(),h=document.querySelector('#divider').getBoundingClientRect();return{left:p.left,width:p.width,handle:h.width,x:h.left+h.width/2,y:h.top+h.height/2};}); + const target=(g,r)=>g.left+g.handle/2+(g.width-g.handle)*r/100; + const dragStart=async()=>{await page.locator('#divider').scrollIntoViewIfNeeded();const g=await geometry();await page.mouse.move(g.x,g.y);await page.mouse.down();return g;}; + const upload=async(content,name='notes.txt')=>page.locator('#file-input').evaluate((input,{content,name})=>{const d=new DataTransfer();d.items.add(new File([content],name,{type:'text/plain'}));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));},{content,name}); + try { + await page.setViewportSize({width:1440,height:1000});await page.reload(); + await page.waitForFunction(()=>window.pureDocument?.snapshot().ready); + check((await state()).runtime.memory.activeStateBytes===8,'only the authored ratio occupies VM state; no document or geometry mirror'); + await page.locator('#sample').click();await settled(); + const original=await page.locator('#document-text').inputValue(); + check(await page.locator('#reading-text').textContent()===original,'native editor and reading pane show the same Unicode document'); + check(await page.locator('#reading-text div').count()===0,'document markup is displayed as text'); + const beforeResize=await state(); + await page.locator('#document-text').evaluate(t=>{t.focus();t.setSelectionRange(20,25);t.scrollTop=80;}); + await page.setViewportSize({width:1040,height:900});await settled(); + const afterResize=await state(); + check(afterResize.runtime.successfulEvents===beforeResize.runtime.successfulEvents&&afterResize.stats.previewWrites===beforeResize.stats.previewWrites&&afterResize.stats.geometryReads===beforeResize.stats.geometryReads,'resize reflows through CSS without VM events, document publication or application geometry reads'); + check(await page.locator('#document-text').evaluate(t=>t.selectionStart===20&&t.selectionEnd===25),'reflow preserves the native editing selection'); + const scrolling=await page.evaluate(()=>{const t=document.querySelector('#document-text'),r=document.querySelector('#reading-scroll');r.scrollTop=120;return{editor:t.scrollTop,reading:r.scrollTop,scrollable:r.scrollHeight>r.clientHeight};}); + check(scrolling.scrollable&&scrolling.reading>0,'long wrapped content uses the native reading scroll area'); + await page.locator('#reading-scroll').evaluate(r=>r.scrollTop+=80);await settled(); + check(await page.locator('#document-text').evaluate((t,y)=>t.scrollTop===y,scrolling.editor),'reading scroll does not move the editing pane'); + check((await state()).runtime.successfulEvents===afterResize.runtime.successfulEvents,'native scrolling does not dispatch layout data to the VM'); + await page.locator('#split-range').focus();await page.keyboard.press('End');await ratio(80); + check((await state()).runtime.states.ratio===80,'a native range input uses the authored numeric-input policy'); + await page.locator('#divider').focus();await page.keyboard.press('Home');await ratio(20);await page.keyboard.press('Shift+ArrowRight');await ratio(30); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='30','keyboard separator control publishes the same value and accessibility state'); + await page.locator('#reset-split').click();await ratio(50); + const textWrites=(await state()).stats.previewWrites; + let g=await dragStart();await page.mouse.move(target(g,65),g.y);await ratio(65);await page.mouse.up(); + check(Math.abs((await state()).runtime.states.ratio-65)<0.2&&!(await state()).dragging,'mouse release commits the last delivered split position'); + check((await state()).stats.previewWrites===textWrites,'resizing panes never republishes unchanged document text'); + g=await dragStart();await page.mouse.move(target(g,35),g.y);await ratio(35);await page.keyboard.press('Escape');await page.mouse.up();await ratio(65); + check(!(await state()).dragging,'Escape restores the ratio captured at gesture start'); + g=await dragStart();await page.setViewportSize({width:1240,height:1000});await settled();g=await geometry(); + await page.mouse.move(target(g,45),g.y);await ratio(45);await page.mouse.up(); + check(Math.abs((await state()).runtime.states.ratio-45)<0.2,'a resize during capture invalidates geometry before the next move'); + // Capture a real touch; mouse compatibility termination must not release it. + const cdp=await page.context().newCDPSession(page); + try { + await cdp.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2});g=await geometry(); + await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{id:1,x:g.x,y:g.y}]}); + await cdp.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,x:target(g,70),y:g.y}]});await ratio(70); + await page.evaluate(()=>window.dispatchEvent(new MouseEvent('mouseup',{button:0,bubbles:true}))); + check((await state()).dragging,'a compatibility mouseup cannot release a touch owner'); + await cdp.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]});await ratio(45); + check(!(await state()).dragging,'touch cancellation restores the original panel ratio'); + } finally {await cdp.send('Emulation.setTouchEmulationEnabled',{enabled:false});await cdp.detach();} + g=await dragStart();await page.mouse.move(target(g,60),g.y);await ratio(60); + await page.setViewportSize({width:390,height:844});await settled();await page.mouse.up();await ratio(45); + check(await page.locator('#divider').isHidden()&&!(await state()).dragging,'narrow layout cancels the active split gesture and stacks the panes'); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'the narrow document workspace has no page-level horizontal overflow'); + const oldStats=await state(); + await page.locator('#document-text').fill('한글 문서 📝\n새 줄과 <태그>도 그대로.');await settled(); + check(await page.locator('#reading-text').textContent()==='한글 문서 📝\n새 줄과 <태그>도 그대로.','editing Unicode updates the text projection'); + check((await state()).runtime.successfulEvents===oldStats.runtime.successfulEvents&& (await state()).stats.geometryReads===oldStats.stats.geometryReads,'text editing neither sends document copies to the VM nor reads geometry'); + await page.locator('#document-text').focus();await page.keyboard.press('Control+End');await page.keyboard.insertText(' 추가');await settled();await page.keyboard.press('Control+z');await settled(); + check(await page.locator('#document-text').inputValue()==='한글 문서 📝\n새 줄과 <태그>도 그대로.','native editing undo survives the Pure integration'); + const compositionBefore=(await state()).stats.previewWrites; + await page.locator('#document-text').evaluate(t=>{t.dispatchEvent(new CompositionEvent('compositionstart'));t.value+=' 조합';t.dispatchEvent(new InputEvent('input',{bubbles:true,isComposing:true}));});await settled(); + check((await state()).stats.previewWrites===compositionBefore,'in-progress composition does not replace the reading projection'); + await page.locator('#document-text').evaluate(t=>t.dispatchEvent(new CompositionEvent('compositionend')));await settled(); + check((await page.locator('#reading-text').textContent()).endsWith(' 조합'),'composition completion publishes the final text (synthetic event sequence)'); + const fileText='표준 UTF-8 파일\n외부 편집기에서 만든 내용\n😀
문자
\n'; + await upload(fileText,'external.txt');await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('파일을 열었습니다'));await settled(); + check(await page.locator('#document-text').inputValue()===fileText,'a standard UTF-8 text file opens without a custom container'); + const download=page.waitForEvent('download');await page.locator('#save').click();const saved=await download;const savedPath='output/playwright/pure-document-saved.txt';await saved.saveAs(savedPath); + check(saved.suggestedFilename()==='external.txt','export keeps the standard text filename'); + const fresh=await page.context().newPage();await fresh.goto(page.url());await fresh.waitForFunction(()=>window.pureDocument?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(savedPath);await fresh.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('파일을 열었습니다')); + check(await fresh.locator('#document-text').inputValue()===fileText,'the exported file reopens in a fresh page with exact text');await fresh.close();await page.bringToFront(); + await page.locator('#file-input').evaluate(input=>{const f=new File(['late'],'late.txt');f.arrayBuffer=()=>new Promise(resolve=>window.finishDocumentRead=()=>resolve(new TextEncoder().encode('late').buffer));const d=new DataTransfer();d.items.add(f);Object.defineProperty(input,'files',{configurable:true,value:d.files});input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.locator('#document-text').fill('읽는 동안 새로 편집한 문서');await page.evaluate(()=>window.finishDocumentRead());await settled(); + check(await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서'&&(await page.locator('#document-status').textContent()).includes('읽는 동안'),'a delayed file read cannot overwrite newer editing'); + await page.locator('#file-input').evaluate(input=>{delete input.files;const d=new DataTransfer();d.items.add(new File([new Uint8Array([255])],'invalid.txt'));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('UTF-8 텍스트로 읽을 수'));check(await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서','invalid UTF-8 leaves the current document intact'); + await page.setViewportSize({width:1440,height:1000});await settled(); + await page.locator('.policy summary').click();const source=await page.locator('#policy-source').inputValue(); + await page.locator('#policy-source').fill(source.replace('20, 80','30, 70'));await page.locator('#apply-policy').click(); + await page.locator('#split-range').focus();await page.keyboard.press('End');await ratio(70); + check((await state()).runtime.states.ratio===70,'changing the authored rule changes native control behavior without touching the HTML adapter'); + const revision=(await state()).runtime.revision;await page.locator('#policy-source').fill('state broken =');await page.locator('#apply-policy').click(); + check((await state()).runtime.revision===revision&&await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서','invalid policy edits preserve the executable policy and document'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click();await page.locator('.policy summary').click(); + await page.locator('#sample').click();await settled();await page.locator('#reset-split').click(); + await page.screenshot({path:'output/playwright/pure-document-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844});await page.screenshot({path:'output/playwright/pure-document-mobile.png',fullPage:true}); + const fallback=await page.context().newPage();await fallback.route('**/core.wasm',r=>r.abort());await fallback.goto(page.url());await fallback.waitForFunction(()=>window.pureDocument); + await fallback.locator('#document-text').fill('실행기 없이도 보존되는 텍스트');await fallback.waitForFunction(()=>document.querySelector('#reading-text').textContent.includes('실행기 없이도')); + check(!await fallback.evaluate(()=>window.pureDocument.snapshot().ready)&&await fallback.locator('#split-range').isDisabled()&&await fallback.locator('#save').isEnabled(),'missing Wasm disables split controls while standard text editing and saving remain available');await fallback.close(); + check(errors.length===0,'no uncaught page errors'); + const evidence={checks,errors,fileText,savedPath,final:await state(),browser:await page.evaluate(()=>navigator.userAgent)}; + await page.evaluate(e=>window.__pureDocumentEvidence=e,evidence);return{checks:checks.length,errors}; + } catch(error){await page.evaluate(e=>window.__pureDocumentEvidence=e,{failed:String(error),checks,errors,state:await state()});throw error;} + finally{page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/qa-evaluation.js b/prototypes/workbench/qa-evaluation.js new file mode 100644 index 0000000..248436a --- /dev/null +++ b/prototypes/workbench/qa-evaluation.js @@ -0,0 +1,89 @@ +// Run with Playwright CLI run-code on the local workbench. The final UI case +// overrides only createRuntime's default options in the served module so that +// a deliberately tiny budget can exercise the normally unreachable limit UI. +async (page) => { + const checks = [], errors = []; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); + page.on('dialog', onDialog); + const check = (condition, name) => { + if (!condition) throw new Error(name); + checks.push(name); + }; + const source = 'state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n'; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async source => { + await page.locator('#source').fill(source); + await page.locator('#apply').click(); + }; + try { + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); + await page.locator('[data-input="next"]').click(); + check((await state()).states.x === 3.25, 'authored browser input commits fractional state'); + check((await state()).views[0].value === 11.15625, 'bounded evaluation renders the exact authored expression'); + await apply(source.replace('(x + 1)', '(x + 10)')); + check((await state()).states.x === 3.25 && (await state()).views[0].value === 34.78125, + 'code replacement preserves state and changes binding result'); + + const api = await page.evaluate(async source => { + const { createRuntime } = await import('./runtime.mjs'); + const runtime = await createRuntime(undefined, {cacheEntries:32}); + runtime.apply(source); runtime.snapshot(); + const before = runtime.snapshot(), warm = runtime.evaluationStats(); + runtime.forgetEvaluation('all'); + const forgotten = runtime.snapshot(); + runtime.configureEvaluation({workLimit:1}); + const limited = runtime.snapshot(), limitStats = runtime.evaluationStats(); + runtime.configureEvaluation({workLimit:2048}); + return {before,warm,forgotten,limited,limitStats,recovered:runtime.snapshot()}; + }, source); + check(api.warm.cacheHits > 0, 'browser Wasm reuses completed bindings'); + check(api.warm.dedicatedBytes === 48 && api.warm.sharedBytes === 1152 && + api.before.memory.reservedEngineBytes === 19904, + 'browser runs the shared workspace including document restoration and shared-rule bindings'); + check(JSON.stringify(api.before) === JSON.stringify(api.forgotten), 'discarding records preserves the browser snapshot'); + check(api.limited.lastError === 101 && api.limited.views.length === 0 && api.limitStats.work <= 1, + 'browser API publishes no partial scene after budget exhaustion'); + check(JSON.stringify(api.before) === JSON.stringify(api.recovered), 'raising the budget recovers the browser snapshot'); + + await page.route('**/runtime.mjs', async route => { + const response = await route.fetch(); + const body = await response.text(); + if (!body.includes('evaluationOptions = {}')) throw new Error('configuration setup no longer matches'); + await route.fulfill({response, body:body.replace('evaluationOptions = {}', 'evaluationOptions = { workLimit: 1 }')}); + }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); + await page.locator('[data-input="next"]').click(); + const limitedUi = await state(); + const message = await page.locator('#runtime-error').textContent(); + check(limitedUi.states.x === 3.25 && limitedUi.views.length === 0, + 'configured UI keeps committed state while withholding incomplete display'); + check(await page.locator('#runtime-error').isVisible() && message.includes('표시 오류') && + message.includes('상태 변경은 유지'), 'UI distinguishes display limit from event rollback'); + await page.screenshot({path:'output/playwright/pure-workspace-limit.png',fullPage:true}); + await page.unroute('**/runtime.mjs'); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).views.length === 0 && (await state()).lastError === 0, 'normal configuration reloads to a clean blank work'); + check(errors.length === 0, 'no uncaught browser errors'); + const evidence = {utc:new Date().toISOString(),checks,api,limitedUi,message,errors, + uiLimitSetup:'Test-only module response overrides default workLimit to 1; production files are unchanged.', + screenshot:'output/playwright/pure-workspace-limit.png', + browser:await page.evaluate(()=>navigator.userAgent)}; + await page.evaluate(value=>{window.__pureEvaluationEvidence=value;},evidence); + return evidence; + } catch (error) { + const evidence = {failed:true,message:error.message,checks,errors}; + await page.evaluate(value=>{window.__pureEvaluationEvidence=value;},evidence); + return evidence; + } finally { + await page.unroute('**/runtime.mjs'); + page.off('pageerror',onError); + page.off('dialog',onDialog); + } +} diff --git a/prototypes/workbench/qa-layout.js b/prototypes/workbench/qa-layout.js new file mode 100644 index 0000000..95500ce --- /dev/null +++ b/prototypes/workbench/qa-layout.js @@ -0,0 +1,103 @@ +// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states.x1-150)<0.1&&Math.abs(s.states.x2-350)<0.1&&Math.abs(s.states.x3-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-layout-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states.x1===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states.x3-(expected.states.x3+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-layout-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-layout-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureLayoutEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureLayoutEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/qa-pointer.js b/prototypes/workbench/qa-pointer.js new file mode 100644 index 0000000..0b0c591 --- /dev/null +++ b/prototypes/workbench/qa-pointer.js @@ -0,0 +1,122 @@ +// Playwright CLI run-code: real mouse / CDP touch input; no timing benchmark. +async (page) => { + const checks = [], errors = []; + const check = (condition, label) => { if (!condition) throw new Error(label); checks.push(label); }; + const close = (a, b) => Math.abs(a - b) < 0.1; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const client = (x, y) => page.evaluate(({x,y}) => { + const p = new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM()); + return {x:p.x,y:p.y}; + }, {x,y}); + const move = async (x,y) => { const p = await client(x,y); await page.mouse.move(p.x,p.y); }; + const start = async (x,y) => { await move(x,y); await page.mouse.down(); }; + const reset = async () => { await page.locator('#reset').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const apply = async source => { await page.locator('#source').fill(source); await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); page.on('dialog', onDialog); + let session; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('direct'); + await page.locator('#load-example').click(); + const source = await page.locator('#source').inputValue(); + check((await state()).views.length === 0, 'loading the pointer example leaves the running program unchanged until Apply'); + await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); + check(await page.locator('[data-point-input="drag"]').count() === 1, 'authored pointer declaration binds the visible target'); + check(await page.locator('#input-controls button').count() === 2, 'keyboard alternatives remain available without a fake pointer button'); + await page.evaluate(() => { + window.__pointerTile = document.querySelector('[data-point-input="drag"]'); + document.querySelector('#scene').addEventListener('pointerdown', e => { window.__pointerID = e.pointerId; }); + }); + await start(90,170); + let s = await state(); + check(s.states.dragging === 1 && s.held.includes('drag'), 'real mouse down reaches the authored rule'); + await move(370,230); s = await state(); + check(close(s.states.x,360) && close(s.states.y,220), 'drag retains grab offset in scene coordinates'); + check(s.views.find(v => v.id === 'region').value === 1 && await page.locator('[data-view-id="region"]').textContent() === '오른쪽 영역: 1', 'linked output updates when the authored boundary is crossed'); + check(await page.evaluate(() => window.__pointerTile === document.querySelector('[data-point-input="drag"]')), 'drag keeps its bound DOM target'); + // Physical mouse outside the SVG still reaches the stable captured scene. + await move(750,400); s = await state(); + check(close(s.states.x,564) && close(s.states.y,276), 'capture delivers movement outside the scene and authored clamp applies'); + await page.mouse.up(); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'release outside the scene ends the gesture once'); + await reset(); + await start(90,170); await move(380,240); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + s = await state(); + check(close(s.states.x,80) && close(s.states.y,160) && s.states.dragging === 0 && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'focus loss invokes authored rollback and clears ownership'); + const ended = s.successfulEvents; + await page.mouse.up(); + check((await state()).successfulEvents === ended, 'late mouse release after cancel does not dispatch a second terminal event'); + await start(90,170); await move(350,220); + await page.evaluate(() => document.querySelector('#scene').releasePointerCapture(window.__pointerID)); + await move(360,230); s = await state(); + check(close(s.states.x,80) && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'explicit capture loss cancels instead of committing a drop'); + await page.mouse.up(); + await start(90,170); await move(300,220); + await page.evaluate(() => document.querySelector('#scene').dispatchEvent(new PointerEvent('pointercancel', {pointerId:window.__pointerID,bubbles:true}))); + check((await state()).held.length === 0 && close((await state()).states.x,80), 'browser cancellation routes to the authored cancel handler'); + await page.mouse.up(); + // A source edit, not a renderer option, changes the movement constraint. + await apply(source.replaceAll('20, 564','20, 200')); + await start(90,170); await move(500,220); await page.mouse.up(); s = await state(); + check(close(s.states.x,200), 'editing the authored rule changes the drag limit'); + await apply(source + '\non drag.move { x = 1 / 0 }\n'); await reset(); + await start(90,170); await move(300,220); s = await state(); + check(s.lastError === 4 && close(s.states.x,80) && close(s.states.y,160), 'later handler failure rolls back all earlier movement writes'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); await page.mouse.up(); + s = await state(); + check(s.lastError === 4 && s.held.length === 0 && s.states.dragging === 0, 'cleanup releases a failed gesture while preserving its error'); + await apply(source); await reset(); + await page.screenshot({path:'output/playwright/pure-pointer-desktop.png',fullPage:true}); + + await page.setViewportSize({width:390,height:844}); + await page.locator('#scene').scrollIntoViewIfNeeded(); + session = await page.context().newCDPSession(page); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const touch = async (type, points) => session.send('Input.dispatchTouchEvent',{type,touchPoints:points}); + const a = await client(90,170), b = await client(270,210); + const scroll = await page.evaluate(() => window.scrollY); + await touch('touchStart',[{id:1,...a}]); + check((await state()).states.dragging === 1, 'touch contact starts the same authored interaction'); + await page.evaluate(() => window.dispatchEvent(new MouseEvent('mouseup', {button:0,buttons:0,bubbles:true}))); + check((await state()).held.includes('drag'), 'mouse release fallback cannot terminate an active touch contact'); + await touch('touchMove',[{id:1,...b}]); s = await state(); + check(close(s.states.x,260) && close(s.states.y,200), 'mobile scaled SVG maps touch coordinates back to world coordinates'); + check(await page.evaluate(() => window.scrollY) === scroll, 'touch drag manipulates the target without scrolling the page'); + await touch('touchEnd',[]); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'touch release clears capture ownership'); + await page.screenshot({path:'output/playwright/pure-pointer-mobile.png',fullPage:true}); + // Independent contacts are not collapsed into one global drag state. + const two = 'state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\nbox one(20,100,100,80,"#c9f08b")\nbox two(300,100,100,80,"#c9f08b")\non a.move { x=event_x }\non b.move { y=event_y }'; + await apply(two); + const p1 = {id:1,...await client(50,140)}, p2 = {id:2,...await client(330,140)}; + await touch('touchStart',[p1]); await touch('touchStart',[p1,p2]); + check((await state()).held.length === 2, 'two contacts on different authored targets retain separate owners'); + await touch('touchMove',[{id:1,...await client(180,160)},{id:2,...await client(430,200)}]); s = await state(); + check(close(s.states.x,180) && close(s.states.y,200), 'independent touch payloads reach their own rules'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'touch cancellation releases both targets'); + // A second contact on the same target cannot steal the first one. + await touch('touchStart',[p1]); + await touch('touchStart',[p1,{id:2,...await client(70,150)}]); + check((await state()).held.length === 1, 'second contact on an already owned target does not steal it'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'ignored second contact cannot strand a logical input'); + check(errors.length === 0,'no browser page errors'); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); + await session.detach(); session = null; + await page.setViewportSize({width:1440,height:1000}); await apply(source); await reset(); + await page.evaluate(result => { window.__purePointerEvidence = result; }, {checks,errors,kind:'functional input/output checks; no performance benchmark'}); + return {checks:checks.length,errors}; + } catch (error) { + await page.evaluate(result => { window.__purePointerEvidence = result; }, {failed:String(error),checks,errors}); + throw error; + } finally { + if (session) { await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); await session.detach(); } + page.off('pageerror',onError); page.off('dialog',onDialog); + } +} diff --git a/prototypes/workbench/qa-presentation.js b/prototypes/workbench/qa-presentation.js new file mode 100644 index 0000000..90595e3 --- /dev/null +++ b/prototypes/workbench/qa-presentation.js @@ -0,0 +1,141 @@ +// Playwright CLI run-code function. The Python runner injects the archived +// app/runtime source at BASELINE_SOURCES; production files are never replaced. +async (page) => { + const baseline = /* BASELINE_SOURCES */ null; + const checks = [], errors = [], samples = []; + const check = (ok, name) => { if (!ok) throw new Error(name); checks.push(name); }; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); page.on('dialog', onDialog); + let variant = 'after'; + const source = [ + 'state x = 40', 'state radius = 12', 'input move = KeyM', 'input shrink = KeyS', + 'box moving(x, 100, 30, 30, "#7ed9ae")', 'text label(20, 40, x, "x: ")', + 'circle bubble(500, 150, radius, "#c9f08b")', + ...Array.from({length:21}, (_, i) => `box static${i}(${20 + (i % 7) * 70}, ${200 + Math.floor(i / 7) * 40}, 20, 20, "#8da5bb")`), + 'on move.down { x = x + 0.25 }', 'on shrink.down { radius = radius - 24 }', + ].join('\n'); + const apply = async value => { await page.locator('#source').fill(value); await page.locator('#apply').click(); }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const reload = async () => { + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); + }; + const routeApp = async route => { + const response = await route.fetch(); + await route.fulfill({response, body: variant === 'before' ? baseline.app : await response.text()}); + }; + const routeRuntime = async route => { + const response = await route.fetch(); + const body = variant === 'before' ? baseline.runtime : await response.text(); + if (!body.includes('snapshot() {')) throw new Error('snapshot instrumentation does not match'); + await route.fulfill({response, body:body.replace('snapshot() {', 'snapshot() { globalThis.__snapshotCalls = (globalThis.__snapshotCalls || 0) + 1;')}); + }; + try { + if (!baseline) throw new Error('Run through qa-presentation.py to supply the archived baseline.'); + await page.route('**/app.js', routeApp); + await page.route('**/runtime.mjs', routeRuntime); + await reload(); + await page.locator('#scene').focus(); + await page.keyboard.press('KeyM'); + check((await state()).states.x === 40.25, 'real keyboard event reaches the authored output'); + await page.evaluate(() => { window.__retained = document.querySelector('[data-view-id="moving"]'); }); + await page.locator('[data-input="move"]').click(); + check((await state()).states.x === 40.5 && await page.locator('[data-view-id="moving"]').getAttribute('x') === '40.5', + 'real pointer input and keyboard input use the same authored behavior'); + check(await page.evaluate(() => window.__retained === document.querySelector('[data-view-id="moving"]')), + 'the displayed element survives an input update'); + await apply(source + '\nstate broken ='); + check((await state()).states.x === 40.5 && await page.evaluate(() => window.__retained === document.querySelector('[data-view-id="moving"]')), + 'failed editing preserves active values and output destinations'); + await page.locator('[data-input="shrink"]').click(); + check((await state()).lastError === 100 && await page.locator('[data-view-id="bubble"]').getAttribute('display') === 'none' && + await page.locator('[data-view-id="moving"]').getAttribute('display') === null, + 'invalid view is hidden while valid views remain published'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('[data-view-id="bubble"]').getAttribute('display') === null, + 'reset recovers a hidden destination without recreating the scene'); + await apply(source.replace('box moving(x, 100, 30, 30, "#7ed9ae")', 'circle moving(x, 100, 15, "#7ed9ae")')); + check(await page.locator('[data-view-id="moving"]').evaluate(node => node.localName) === 'circle', + 'successful editing replaces changed output structure'); + await apply(''); + check(await page.locator('#scene-content > *').count() === 0 && (await state()).views.length === 0, + 'applying empty work removes all old destinations'); + + // Alternating whole-page runs avoid sharing a runtime or retained DOM across + // variants. Timings cover synchronous dispatch through DOM submission, not + // OS event arrival, layout, paint, or actual display scanout. + for (const mode of ['before', 'after', 'after', 'before', 'before', 'after']) { + variant = mode; + await reload(); + await page.locator('#scene').focus(); + const result = await page.evaluate(() => { + const scene = document.getElementById('scene'); + const tap = () => { + scene.dispatchEvent(new KeyboardEvent('keydown', {code:'KeyM', bubbles:true})); + scene.dispatchEvent(new KeyboardEvent('keyup', {code:'KeyM', bubbles:true})); + }; + for (let i = 0; i < 20; i++) tap(); + const retained = [...document.querySelectorAll('#scene-content > *')]; + const scopes = ['scene-content', 'state-values', 'cost-values']; + const observers = scopes.map(id => { + const observer = new MutationObserver(() => {}); + observer.observe(document.getElementById(id), {subtree:true, childList:true, attributes:true, characterData:true}); + return observer; + }); + const createElement = document.createElement, createElementNS = document.createElementNS; + let createdElements = 0; + document.createElement = function(...args) { createdElements++; return createElement.apply(this, args); }; + document.createElementNS = function(...args) { createdElements++; return createElementNS.apply(this, args); }; + window.__snapshotCalls = 0; + let elapsedMs; + try { + const start = performance.now(); + for (let i = 0; i < 100; i++) tap(); + elapsedMs = performance.now() - start; + } finally { + document.createElement = createElement; document.createElementNS = createElementNS; + } + const snapshotCalls = window.__snapshotCalls; + const mutations = Object.fromEntries(observers.map((observer, i) => { + const records = observer.takeRecords(); observer.disconnect(); + return [scopes[i], {records:records.length, + childList:records.filter(record => record.type === 'childList').length, + attributes:records.filter(record => record.type === 'attributes').length}]; + })); + const nodes = [...document.querySelectorAll('#scene-content > *')]; + const identityPreserved = nodes.every((node, i) => node === retained[i]); + const final = window.pureWorkbench.snapshot(); // outside timed/count interval + return {elapsedMs, createdElements, snapshotCalls, mutations, identityPreserved, + final: {states:final.states, views:final.views, successfulEvents:final.successfulEvents}, + scene:document.getElementById('scene-content').outerHTML}; + }); + samples.push({variant:mode, taps:100, events:200, ...result}); + } + check(samples.every(sample => JSON.stringify(sample.final) === JSON.stringify(samples[0].final)), + 'all six archived/current runs have identical final authored state and views'); + check(samples.filter(s => s.variant === 'after').every(s => s.createdElements === 0 && s.snapshotCalls === 0 && s.identityPreserved), + 'current hot path creates no DOM elements and no full snapshots'); + check(samples.filter(s => s.variant === 'before').every(s => s.createdElements > 0 && s.snapshotCalls === 200 && !s.identityPreserved), + 'archived hot path reproduces full snapshots and element replacement'); + check(samples.filter(s => s.variant === 'after').every(s => s.mutations['scene-content'].attributes === 100 && s.mutations['scene-content'].childList === 100), + 'each tap changes only the moving x attribute and label text in the scene'); + variant = 'after'; await reload(); + await page.screenshot({path:'output/playwright/pure-presentation.png', fullPage:true}); + check(errors.length === 0, 'no uncaught browser errors'); + const evidence = {utc:new Date().toISOString(), checks, errors, samples, source, + timingScope:'100 synchronous synthetic keyboard taps after 20 warm-up taps; JS dispatch to DOM submission only, including instrumentation; not paint/input latency.', + setup:'Archived app/runtime are supplied by test-only routing; snapshot calls and element creation are instrumented. Same Wasm/compiler and production configuration.', + browser:await page.evaluate(() => navigator.userAgent), screenshot:'output/playwright/pure-presentation.png'}; + await page.evaluate(value => { window.__purePresentationEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = {failed:true, message:error.stack || error.message, checks, errors, samples}; + await page.evaluate(value => { window.__purePresentationEvidence = value; }, evidence); + return evidence; + } finally { + await page.unroute('**/app.js', routeApp); await page.unroute('**/runtime.mjs', routeRuntime); + page.off('pageerror', onError); page.off('dialog', onDialog); + } +} diff --git a/prototypes/workbench/qa-presentation.py b/prototypes/workbench/qa-presentation.py new file mode 100644 index 0000000..b3adb02 --- /dev/null +++ b/prototypes/workbench/qa-presentation.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Run the local browser comparison with the archived implementation injected.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import time + +ROOT = Path(__file__).resolve().parent +parser = argparse.ArgumentParser() +parser.add_argument('--cli', required=True, help='Path to Playwright CLI 0.1.21 JS entry point') +parser.add_argument('--session', default='pure-presentation') +parser.add_argument('--output', required=True, type=Path) +args = parser.parse_args() +if args.output.exists(): + raise SystemExit('Choose a new output file; evidence is never overwritten.') +archive = ROOT / 'results/2026-09-23-presentation/before' +sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest() +manifest = json.loads((archive / 'manifest.json').read_text()) +for name, digest in manifest.items(): + if sha(archive / name) != digest: + raise SystemExit(f'Archived source mismatch: {name}') +baseline = {name: (archive / f'web/{file}').read_text() + for name, file in [('app', 'app.js'), ('runtime', 'runtime.mjs')]} +script = (ROOT / 'qa-presentation.js').read_text().replace('/* BASELINE_SOURCES */ null', json.dumps(baseline)) +command = ['node', args.cli, f'--session={args.session}'] +run = subprocess.run([*command, 'run-code', script], text=True, capture_output=True, check=True) +# Navigation can return early while the run-code function is accepting a +# beforeunload dialog. Collect the published result instead of rerunning it. +for attempt in range(30): + result = subprocess.run([*command, 'eval', 'window.__purePresentationEvidence'], + text=True, capture_output=True, check=True) + if '### Result\n' not in result.stdout: + raise SystemExit(result.stdout + result.stderr) + payload = result.stdout.split('### Result\n', 1)[1].split('\n### ', 1)[0].strip() + if payload != 'undefined': + value = json.loads(payload) + if value.get('failed'): + raise SystemExit(json.dumps(value, ensure_ascii=False, indent=2)) + value['sourceSha256'] = {name: sha(ROOT / name) for name in + ['web/runtime.mjs', 'web/app.js', 'web/output.mjs', 'web/core.wasm', + 'web/compiler.mjs', 'qa-presentation.js', 'qa-presentation.py']} + value['baselineManifestSha256'] = sha(archive / 'manifest.json') + with args.output.open('x') as output: + json.dump(value, output, ensure_ascii=False, indent=2) + output.write('\n') + print(json.dumps({'checks':len(value['checks']), 'samples':[ + {k:s[k] for k in ['variant', 'elapsedMs', 'createdElements', 'snapshotCalls']} + for s in value['samples']]}, indent=2)) + break + time.sleep(0.2) +else: + raise SystemExit('No finished evidence; inspect the browser session before retrying.') diff --git a/prototypes/workbench/qa-refresh.js b/prototypes/workbench/qa-refresh.js new file mode 100644 index 0000000..c74ef4c --- /dev/null +++ b/prototypes/workbench/qa-refresh.js @@ -0,0 +1,141 @@ +// Run through qa-refresh.py. Only the archived app module is replaced for the +// baseline; runtime, compiler, output adapter and Wasm stay identical. +async (page) => { + const baselineApp = /* BASELINE_APP */ null; + const checks = [], samples = [], errors = []; + const check = (ok, name) => { if (!ok) throw new Error(name); checks.push(name); }; + const onDialog = dialog => dialog.accept(); + const onError = error => errors.push(error.message); + page.on('dialog', onDialog); page.on('pageerror', onError); + let variant = 'after'; + const routeApp = async route => { + const response = await route.fetch(); + await route.fulfill({response, body:variant === 'before' ? baselineApp : await response.text()}); + }; + const client = await page.context().newCDPSession(page); + const metrics = async () => Object.fromEntries((await client.send('Performance.getMetrics')).metrics.map(m => [m.name, m.value])); + const source = ['state x = 40', 'input move = KeyM', + 'box moving(x, 100, 30, 30, "#7ed9ae")', 'text label(20, 40, x)', + ...Array.from({length:22}, (_, i) => `box static${i}(${20 + (i % 7) * 70}, ${200 + Math.floor(i / 7) * 30}, 20, 20, "#8da5bb")`), + 'on move.down { x = x + 0.25 }'].join('\n'); + const apply = async value => { await page.locator('#source').fill(value); await page.locator('#apply').click(); }; + const snapshot = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const reload = async () => { + await page.reload(); await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); await page.locator('#scene').focus(); + }; + try { + if (!baselineApp) throw new Error('Archived app source is required.'); + await page.route('**/app.js', routeApp); + for (const mode of ['before', 'after', 'after', 'before', 'before', 'after']) { + variant = mode; await reload(); + await page.evaluate(() => { + window.__tap = () => { + const scene = document.getElementById('scene'); + scene.dispatchEvent(new KeyboardEvent('keydown', {code:'KeyM', bubbles:true})); + scene.dispatchEvent(new KeyboardEvent('keyup', {code:'KeyM', bubbles:true})); + }; + for (let i = 0; i < 20; i++) window.__tap(); + }); + await client.send('Performance.enable'); + const before = await metrics(); + const sample = await page.evaluate(() => { + const observer = new MutationObserver(() => {}); + for (const id of ['line-numbers','source-size','cursor-position','draft-status']) + observer.observe(document.getElementById(id), {subtree:true, childList:true, characterData:true, attributes:true}); + const originalEncode = TextEncoder.prototype.encode; + let encodingCalls = 0, encodedBytes = 0; + TextEncoder.prototype.encode = function(...args) { + const value = originalEncode.apply(this, args); encodingCalls++; encodedBytes += value.byteLength; return value; + }; + let elapsedMs; + try { + const start = performance.now(); + for (let i = 0; i < 100; i++) window.__tap(); + elapsedMs = performance.now() - start; + } finally { TextEncoder.prototype.encode = originalEncode; } + const editorMutations = observer.takeRecords().length; + observer.disconnect(); + return {elapsedMs, encodingCalls, encodedBytes, editorMutations}; + }); + const after = await metrics(); + const s = await snapshot(); + samples.push({variant:mode, taps:100, events:200, ...sample, + metrics:Object.fromEntries(['LayoutCount','LayoutDuration','RecalcStyleCount','RecalcStyleDuration','ScriptDuration'] + .map(key => [key, after[key]-before[key]])), + final:{states:s.states, views:s.views, successfulEvents:s.successfulEvents, tick:s.tick, lastError:s.lastError}, + renderedX:await page.locator('[data-view-id="moving"]').getAttribute('x')}); + } + check(samples.every(s => JSON.stringify(s.final) === JSON.stringify(samples[0].final) && s.renderedX === '70'), + 'six runs preserve exact authored states, outputs, event counts and rendered position'); + check(samples.filter(s => s.variant === 'before').every(s => s.encodingCalls === 400 && s.editorMutations > 0), + 'archived path reproduces unrelated editor work on every event'); + check(samples.filter(s => s.variant === 'after').every(s => s.encodingCalls === 0 && s.editorMutations === 0), + 'runtime input performs no source encoding or editor metadata mutations'); + + variant = 'after'; await reload(); + const draft = source + '\n# 변경🙂\n'; + await page.locator('#source').fill(draft); + const editor = () => page.evaluate(() => ({ + source:document.getElementById('source').value, + size:document.getElementById('source-size').textContent, + lines:document.getElementById('line-numbers').textContent, + status:document.getElementById('draft-status').textContent, + cursor:document.getElementById('cursor-position').textContent, + expectedBytes:new TextEncoder().encode(document.getElementById('source').value).byteLength, + })); + let e = await editor(); + check(e.status === '미적용 변경' && e.size.startsWith(e.expectedBytes.toLocaleString('en-US')) && + e.lines.split('\n').length === draft.split('\n').length, + 'Unicode source editing updates byte count, line numbers and dirty status'); + await page.locator('#scene').focus(); + const prior = await editor(); + await page.keyboard.press('KeyM'); + await page.locator('#step').click(); + check(JSON.stringify(await editor()) === JSON.stringify(prior) && (await snapshot()).states.x === 40.25 && (await snapshot()).tick === 1, + 'keyboard input and frame stepping preserve unapplied editor metadata'); + await page.locator('#reset').click(); + check(JSON.stringify(await editor()) === JSON.stringify(prior) && (await snapshot()).states.x === 40, + 'reset preserves draft and its metadata while restoring active state'); + await page.locator('#apply').click(); + check((await editor()).status === '적용됨' && !(await snapshot()).dirty, + 'successful apply updates draft status without requiring a source edit'); + await apply(draft + 'state broken ='); + check((await editor()).status === '미적용 변경' && (await snapshot()).appliedSource === draft && + await page.locator('#error-location').isVisible(), + 'failed apply preserves dirty status and active work with a source diagnostic'); + await page.locator('#error-location').click(); + check((await editor()).cursor.startsWith(`${draft.split('\n').length}행`), + 'diagnostic navigation still updates the cursor position'); + await page.locator('#source').fill('state count = 1\n'); + await page.locator('#source').press('End'); + await page.locator('#source').press('Tab'); + e = await editor(); + check(e.source.endsWith(' ') && e.size.startsWith(e.expectedBytes.toLocaleString('en-US')), + 'programmatic Tab insertion updates source metadata'); + await apply(''); + check((await editor()).status === '적용됨' && (await snapshot()).views.length === 0, + 'applying an empty program has a correct applied status'); + await page.locator('[data-snippet="state"]').click(); + e = await editor(); + check(e.status === '미적용 변경' && e.source.includes('state count') && + e.size.startsWith(e.expectedBytes.toLocaleString('en-US')), + 'snippet insertion updates source metadata'); + check(errors.length === 0, 'no uncaught browser errors'); + await page.unroute('**/app.js', routeApp); + await page.reload(); await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + const evidence = {utc:new Date().toISOString(), checks, errors, samples, source, + browser:await page.evaluate(() => navigator.userAgent), + scope:'100 synchronous synthetic keyboard taps after 20 warm-up taps. Timings include encoding/mutation instrumentation, exclude actual input arrival and presentation latency. CDP metrics may include browser work around the batch.', + setup:'Only app.js is substituted from the verified archive for before runs. Other production modules are identical. No phase-timing wrappers in this comparison.'}; + await page.evaluate(value => {window.__pureRefreshEvidence = value;}, evidence); + return evidence; + } catch (error) { + const evidence = {failed:true, message:error.stack || error.message, checks, errors, samples}; + await page.evaluate(value => {window.__pureRefreshEvidence = value;}, evidence); + return evidence; + } finally { + await page.unroute('**/app.js', routeApp); + page.off('dialog', onDialog); page.off('pageerror', onError); await client.detach(); + } +} diff --git a/prototypes/workbench/qa-refresh.py b/prototypes/workbench/qa-refresh.py new file mode 100644 index 0000000..e209503 --- /dev/null +++ b/prototypes/workbench/qa-refresh.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Compare editor-independent runtime refresh against the archived app.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import time + +ROOT = Path(__file__).resolve().parent +parser = argparse.ArgumentParser() +parser.add_argument('--cli', required=True, help='Path to Playwright CLI 0.1.21 JS entry point') +parser.add_argument('--session', default='pure-path') +parser.add_argument('--output', required=True, type=Path) +args = parser.parse_args() +if args.output.exists(): + raise SystemExit('Choose a new output file; evidence is never overwritten.') +archive = ROOT / 'results/2026-09-23-refresh/before' +sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest() +manifest = json.loads((archive / 'manifest.json').read_text()) +for name, digest in manifest.items(): + if sha(archive / name) != digest: + raise SystemExit(f'Archived source mismatch: {name}') +baseline = (archive / 'web/app.js').read_text() +script = (ROOT / 'qa-refresh.js').read_text().replace('/* BASELINE_APP */ null', json.dumps(baseline)) +command = ['node', args.cli, f'--session={args.session}'] +run = subprocess.run([*command, 'run-code', script], text=True, capture_output=True, check=True) +# Navigation can return early while the run-code function is accepting a +# beforeunload dialog. Collect the published result instead of rerunning it. +for attempt in range(30): + result = subprocess.run([*command, 'eval', 'window.__pureRefreshEvidence'], + text=True, capture_output=True, check=True) + if '### Result\n' not in result.stdout: + raise SystemExit(result.stdout + result.stderr) + payload = result.stdout.split('### Result\n', 1)[1].split('\n### ', 1)[0].strip() + if payload != 'undefined': + value = json.loads(payload) + if value.get('failed'): + raise SystemExit(json.dumps(value, ensure_ascii=False, indent=2)) + value['sourceSha256'] = {name: sha(ROOT / name) for name in + ['web/runtime.mjs', 'web/app.js', 'web/output.mjs', 'web/core.wasm', + 'web/compiler.mjs', 'qa-refresh.js', 'qa-refresh.py']} + value['baselineManifestSha256'] = sha(archive / 'manifest.json') + with args.output.open('x') as output: + json.dump(value, output, ensure_ascii=False, indent=2) + output.write('\n') + print(json.dumps({'checks':len(value['checks']), 'samples':[ + {k:s[k] for k in ['variant', 'elapsedMs', 'encodingCalls', 'editorMutations']} + for s in value['samples']]}, indent=2)) + break + time.sleep(0.2) +else: + raise SystemExit('No finished evidence; inspect the browser session before retrying.') diff --git a/prototypes/workbench/qa-release.js b/prototypes/workbench/qa-release.js new file mode 100644 index 0000000..1674c5b --- /dev/null +++ b/prototypes/workbench/qa-release.js @@ -0,0 +1,59 @@ +// Playwright CLI run-code. Covers release routing and stale release coordinates. +async page => { + const checks = []; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const check = (ok, label) => checks.push({label, passed:Boolean(ok)}); + const near = (a,b) => Math.abs(a-b)<0.1; + const client = (x,y) => page.evaluate(({x,y}) => { + const p = new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM()); + return {x:p.x,y:p.y}; + },{x,y}); + const move = async(x,y) => { const p=await client(x,y); await page.mouse.move(p.x,p.y); }; + const start = async() => { + await page.locator('#reset').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); + await move(90,170); await page.mouse.down(); await move(370,230); + }; + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('direct'); + await page.locator('#load-example').click(); await page.locator('#apply').click(); + await page.evaluate(() => document.querySelector('#scene').addEventListener('pointerdown',e=>{ window.__releaseID=e.pointerId; })); + await start(); await page.mouse.up(); let s=await state(); + check(near(s.states.x,360)&&near(s.states.y,220)&&!s.held.length&&s.lastEvent.phase==='up','ordinary in-scene release retains the displayed position'); + await start(); await move(750,400); await page.mouse.up(); s=await state(); + check(near(s.states.x,564)&&near(s.states.y,276)&&!s.held.length&&s.lastEvent.phase==='up','ordinary release outside the scene retains the clamped position'); + for (const scenario of ['document-pointerup','child-nonbubbling-up','mouse-only-up','stale-scene-up']) { + await start(); + const position=await client(90,170); // Deliberately stale press coordinates. + await page.evaluate(({scenario,position})=>{ + const scene=document.querySelector('#scene'), tile=document.querySelector('[data-point-input="drag"]'); + const fields={pointerId:window.__releaseID,pointerType:'mouse',button:0,buttons:0,clientX:position.x,clientY:position.y,bubbles:scenario!=='child-nonbubbling-up'}; + if(scenario==='mouse-only-up') window.dispatchEvent(new MouseEvent('mouseup',fields)); + else (scenario==='document-pointerup'?document:scenario==='child-nonbubbling-up'?tile:scene).dispatchEvent(new PointerEvent('pointerup',fields)); + if(scene.hasPointerCapture(window.__releaseID)) scene.releasePointerCapture(window.__releaseID); + },{scenario,position}); + await page.mouse.up(); s=await state(); + check(near(s.states.x,360)&&near(s.states.y,220)&&!s.held.length&&s.lastEvent.phase==='up',`${scenario}: release commits once without a coordinate jump or later cancel`); + check(s.successfulEvents===3,`${scenario}: duplicate release/capture loss produces no extra authored event`); + } + await start(); + await page.evaluate(()=>document.querySelector('[data-point-input="drag"]').dispatchEvent(new PointerEvent('lostpointercapture',{pointerId:window.__releaseID,bubbles:true}))); + s=await state(); check(s.held.includes('drag')&&near(s.states.x,360),'descendant capture-loss notification cannot cancel the root owner'); + await page.mouse.up(); + await start(); + await page.evaluate(()=>document.querySelector('#scene').releasePointerCapture(window.__releaseID)); + await move(380,240); s=await state(); + check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','actual root capture loss while held still cancels'); + await page.mouse.up(); + await start(); + await page.evaluate(()=>document.dispatchEvent(new PointerEvent('pointercancel',{pointerId:window.__releaseID,bubbles:true}))); + await page.mouse.up(); s=await state(); + check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','cancellation outside the scene still rolls back and ignores the late up'); + await start(); + await page.evaluate(()=>window.dispatchEvent(new Event('blur'))); + await page.mouse.up(); s=await state(); + check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','focus loss still cancels rather than committing'); + const result={checks,passed:checks.filter(c=>c.passed).length,failed:checks.filter(c=>!c.passed)}; + await page.evaluate(result=>{window.__pureReleaseEvidence=result;},result); + return result; +} diff --git a/prototypes/workbench/qa-routed-input.js b/prototypes/workbench/qa-routed-input.js new file mode 100644 index 0000000..b935499 --- /dev/null +++ b/prototypes/workbench/qa-routed-input.js @@ -0,0 +1,164 @@ +// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('routed-layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + check(await page.locator('[data-point-input="drag"]').count()===3,'one authored pointer input connects all three view targets'); + check(await page.locator('[data-point-target]').count()===3,'DOM destinations retain distinct target identities'); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + check((await page.locator('#last-event').textContent()).includes('drag.up → amber_box'),'last input identifies the released target within its group'); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-routed-layout-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states['green.x']===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + // One source edit affects every bound target, without editing input adapters. + const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,'); + await page.locator('#source').fill(edited);await page.locator('#apply').click(); + const beforeEdit=await state(); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target'); + await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10); + const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target'); + const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse(); + const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n'); + const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click(); + const afterReorder=await state(); + check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder'); + // Two contacts execute the same body with different bound fields. + await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + const session=await page.context().newCDPSession(page); + try { + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]}); + await page.waitForFunction(()=>{const s=window.pureWorkbench.snapshot().states;return Math.abs(s['green.x']-150)<0.1&&Math.abs(s['blue.x']-350)<0.1;}); + const touch=await state(); + check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + const canceled=await state(); + check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position'); + + // A real second contact on the same view must not steal its owner. + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,{id:2,...await client(100,140)}]}); + check((await state()).held.length===1,'a second touch on the same target cannot steal ownership'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + // End one actual contact and continue the other in the same logical input. + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + const m1={id:1,...await client(160,180)},m2={id:2,...await client(360,220)}; + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[m1,m2]}); + // In this Chromium, touchEnd's supplied point identifies the contact ending. + // touch-reproduction.js records the resulting pointerup and retained blue owner. + await session.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[m1]}); + let partial=await state(); + check(partial.held.length===1&&partial.states['green.held']===0&&partial.states['blue.held']===1,'releasing green keeps blue captured within the same input'); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:2,...await client(400,250)}]}); + await page.waitForFunction(()=>Math.abs(window.pureWorkbench.snapshot().states['blue.x']-390)<0.1); + partial=await state(); + check(Math.abs(partial.states['green.x']-150)<0.1&&Math.abs(partial.states['blue.x']-390)<0.1,'the remaining contact keeps moving only its own target'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + partial=await state(); + check(Math.abs(partial.states['green.x']-150)<0.1&&partial.states['blue.x']===280&&!partial.held.length,'canceling the remaining target preserves the already released position'); + } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();} + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-routed-layout-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-routed-layout-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/qa-shared-rules.js b/prototypes/workbench/qa-shared-rules.js new file mode 100644 index 0000000..6dad3dc --- /dev/null +++ b/prototypes/workbench/qa-shared-rules.js @@ -0,0 +1,137 @@ +// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('shared-layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-shared-layout-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states['green.x']===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + // One source edit affects every bound target, without editing input adapters. + const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,'); + await page.locator('#source').fill(edited);await page.locator('#apply').click(); + const beforeEdit=await state(); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target'); + await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10); + const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target'); + const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse(); + const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n'); + const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click(); + const afterReorder=await state(); + check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder'); + // Two contacts execute the same body with different bound fields. + await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + const session=await page.context().newCDPSession(page); + try { + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]}); + const touch=await state(); + check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + const canceled=await state(); + check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position'); + } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();} + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-shared-layout-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-shared-layout-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/remote.mjs b/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/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/workbench/results/2026-09-22-evaluation/browser.json b/prototypes/workbench/results/2026-09-22-evaluation/browser.json new file mode 100644 index 0000000..c07fdbe --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/browser.json @@ -0,0 +1,231 @@ +{ + "utc": "2026-09-22T14:04:57.852Z", + "checks": [ + "authored browser input commits fractional state", + "bounded evaluation renders the exact authored expression", + "code replacement preserves state and changes binding result", + "browser Wasm reuses completed bindings", + "discarding records preserves the browser snapshot", + "browser API publishes no partial scene after budget exhaustion", + "raising the budget recovers the browser snapshot", + "configured UI keeps committed state while withholding incomplete display", + "UI distinguishes display limit from event rollback", + "normal configuration reloads to a clean blank work", + "no uncaught browser errors" + ], + "api": { + "before": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "warm": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 6, + "instructions": 0, + "cacheHits": 3, + "reservedBytes": 1200 + }, + "forgotten": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limited": { + "states": { + "x": 3 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limitStats": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 1, + "work": 1, + "instructions": 0, + "cacheHits": 0, + "reservedBytes": 1200 + }, + "recovered": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + } + }, + "limitedUi": { + "states": { + "x": 3.25 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 2, + "held": [], + "lastEvent": { + "name": "next", + "phase": "up", + "source": "pointer:1", + "status": 0, + "line": null, + "ops": 0 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "appliedSource": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "message": "표시 오류 · 3행 · 표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요. — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.", + "errors": [], + "uiLimitSetup": "Test-only module response overrides default workLimit to 1; production files are unchanged.", + "screenshot": "output/playwright/pure-evaluation-limit.png", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "qaSourceSha256": "262d97f0aa409eb68bdfe8aa3e82ad037e6b10681f8b5025e59d148ee49564e1" +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/comparison.json b/prototypes/workbench/results/2026-09-22-evaluation/comparison.json new file mode 100644 index 0000000..bf4a5cf --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/comparison.json @@ -0,0 +1,309 @@ +{ + "purpose": "Choose whether to enable binding reuse by default", + "utc": "2026-09-22T13:56:13.104Z", + "host": { + "platform": "linux", + "release": "6.18.39+rpt-rpi-2712", + "arch": "arm64", + "cpu": "Cortex-A76", + "node": "v24.20.0", + "affinityControlled": false + }, + "sourceSha256": { + "web/core.wasm": "b79eecefba5e7da0f5dd945fa1df196ee627803243a430517f5c49c3a5e54ce1", + "core.zig": "dfb9fd7f3862806c2bd88ce1c668b795d9df196e1f2e50cceff1b5c4809273df", + "evaluation.zig": "2192e563385105f130d373b2e8724ea234c51e2909c7dbfc38e72acc3b70b2ed", + "web/runtime.mjs": "d9981282612cb06b50d24bf8919e829dce202f72b59dab607c8684bc0f297939", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "evaluation-check.mjs": "11492759b5eff878fb8e43cad69422f9c34da6a85b5d8808478cc1845d737d4f", + "evaluation.test.mjs": "d3026ab5c6bffce56c0bde3155d7dddd6fa442a5688656ee710626c762258ff1", + "examples/counter.pure": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "examples/toggle.pure": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "examples/charge.pure": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56" + }, + "cases": [ + { + "name": "counter", + "checkedSnapshots": 720, + "traceSha256": "5b6a375413a890f53aad8542121341bd4d460d8f7614bde17b359692b7723f04", + "totals": { + "plain": { + "work": 14400, + "instructions": 9360, + "cacheHits": 0 + }, + "cached": { + "work": 10251, + "instructions": 93, + "cacheHits": 5013 + } + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 284, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "evaluation": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 14, + "instructions": 0, + "cacheHits": 7, + "reservedBytes": 1200 + }, + "samples": [ + { + "trial": 0, + "key": "plain", + "elapsedMs": 4.274374999999964 + }, + { + "trial": 0, + "key": "cached", + "elapsedMs": 3.667929999999842 + }, + { + "trial": 1, + "key": "cached", + "elapsedMs": 2.7680219999999736 + }, + { + "trial": 1, + "key": "plain", + "elapsedMs": 2.9006329999999707 + }, + { + "trial": 2, + "key": "plain", + "elapsedMs": 3.10952199999997 + }, + { + "trial": 2, + "key": "cached", + "elapsedMs": 2.6932429999999385 + }, + { + "trial": 3, + "key": "cached", + "elapsedMs": 2.495447000000013 + }, + { + "trial": 3, + "key": "plain", + "elapsedMs": 3.31631799999991 + }, + { + "trial": 4, + "key": "plain", + "elapsedMs": 3.110725000000002 + }, + { + "trial": 4, + "key": "cached", + "elapsedMs": 2.476169999999911 + } + ], + "medianMs": { + "plain": 3.110725000000002, + "cached": 2.6932429999999385 + }, + "cachedOverPlain": 0.8657927010584145 + }, + { + "name": "toggle", + "checkedSnapshots": 720, + "traceSha256": "5b6a375413a890f53aad8542121341bd4d460d8f7614bde17b359692b7723f04", + "totals": { + "plain": { + "work": 17280, + "instructions": 10080, + "cacheHits": 0 + }, + "cached": { + "work": 14692, + "instructions": 54, + "cacheHits": 7170 + } + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 456, + "activeStateBytes": 16, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "evaluation": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 20, + "instructions": 0, + "cacheHits": 10, + "reservedBytes": 1200 + }, + "samples": [ + { + "trial": 0, + "key": "plain", + "elapsedMs": 3.189095999999836 + }, + { + "trial": 0, + "key": "cached", + "elapsedMs": 2.4810959999999795 + }, + { + "trial": 1, + "key": "cached", + "elapsedMs": 2.673020999999835 + }, + { + "trial": 1, + "key": "plain", + "elapsedMs": 3.064632999999958 + }, + { + "trial": 2, + "key": "plain", + "elapsedMs": 3.1030960000000505 + }, + { + "trial": 2, + "key": "cached", + "elapsedMs": 2.6690209999999297 + }, + { + "trial": 3, + "key": "cached", + "elapsedMs": 2.628114000000096 + }, + { + "trial": 3, + "key": "plain", + "elapsedMs": 2.907020999999986 + }, + { + "trial": 4, + "key": "plain", + "elapsedMs": 3.06628099999989 + }, + { + "trial": 4, + "key": "cached", + "elapsedMs": 2.6631320000001324 + } + ], + "medianMs": { + "plain": 3.06628099999989, + "cached": 2.6631320000001324 + }, + "cachedOverPlain": 0.8685218347569019 + }, + { + "name": "charge", + "checkedSnapshots": 720, + "traceSha256": "5b6a375413a890f53aad8542121341bd4d460d8f7614bde17b359692b7723f04", + "totals": { + "plain": { + "work": 21600, + "instructions": 11520, + "cacheHits": 0 + }, + "cached": { + "work": 26909, + "instructions": 566, + "cacheHits": 9676 + } + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 1092, + "activeStateBytes": 32, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "evaluation": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 28, + "instructions": 0, + "cacheHits": 14, + "reservedBytes": 1200 + }, + "samples": [ + { + "trial": 0, + "key": "plain", + "elapsedMs": 4.6710420000001704 + }, + { + "trial": 0, + "key": "cached", + "elapsedMs": 4.135338000000047 + }, + { + "trial": 1, + "key": "cached", + "elapsedMs": 4.094503999999915 + }, + { + "trial": 1, + "key": "plain", + "elapsedMs": 4.58693100000005 + }, + { + "trial": 2, + "key": "plain", + "elapsedMs": 4.541652999999997 + }, + { + "trial": 2, + "key": "cached", + "elapsedMs": 4.269986000000017 + }, + { + "trial": 3, + "key": "cached", + "elapsedMs": 4.093262999999979 + }, + { + "trial": 3, + "key": "plain", + "elapsedMs": 4.703355999999985 + }, + { + "trial": 4, + "key": "plain", + "elapsedMs": 4.554652999999917 + }, + { + "trial": 4, + "key": "cached", + "elapsedMs": 4.1000779999999395 + } + ], + "medianMs": { + "plain": 4.58693100000005, + "cached": 4.1000779999999395 + }, + "cachedOverPlain": 0.8938608407233279 + } + ], + "timingScope": "Event dispatch plus snapshot construction. Excludes loading, compilation, DOM, equality checks and statistics reads. Five batch samples are not latency percentiles." +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/baseline.test.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/baseline.test.mjs new file mode 100644 index 0000000..c9ef2de --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/baseline.test.mjs @@ -0,0 +1,78 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChargeModel } from './web/baseline.mjs'; + +test('reference begins with the authored states and matching scene', () => { + const model = createChargeModel(); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); + assert.deepEqual(model.snapshot().held, []); + assert.equal(model.snapshot().tick, 0); + assert.equal(model.snapshot().views.find(view => view.id === 'floor').y, 261); + assert.equal(model.snapshot().views.find(view => view.id === 'actor').radius, 20); +}); + +test('press, hold, release, and frame order produce exact expected values', () => { + const model = createChargeModel(); + model.down(); model.frame(15); + let expectedCharge = 0; + for (let frame = 0; frame < 15; frame++) expectedCharge = Math.min(Math.max(expectedCharge + 1 / 60, 0), 1); + assert.deepEqual(model.snapshot().states, { holding: 1, charge: expectedCharge, y: 240, velocity: 0 }); + model.up(); + const initialVelocity = -(160 + expectedCharge * 300); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: initialVelocity }); + model.frame(); + const nextVelocity = initialVelocity + 600 * (1 / 60); + assert.deepEqual(model.snapshot().states, { + holding: 0, charge: 0, y: 240 + nextVelocity * (1 / 60), velocity: nextVelocity, + }); +}); + +test('charge clamps at one and explicit floor restores rest', () => { + const model = createChargeModel(); + model.down(); model.frame(120); + assert.equal(model.snapshot().states.charge, 1); + model.up(); assert.equal(model.snapshot().states.velocity, -460); + model.frame(); assert.equal(model.snapshot().states.y, 232.5); + model.frame(120); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); +}); + +test('repeat and concurrent physical sources produce only logical edges', () => { + const model = createChargeModel(); + model.down('jump', 'keyboard'); model.frame(6); + const charge = model.snapshot().states.charge; + model.down('jump', 'keyboard'); model.down('jump', 'pointer'); + assert.equal(model.snapshot().states.charge, charge); + model.up('jump', 'keyboard'); + assert.equal(model.snapshot().states.holding, 1); + model.up('jump', 'unknown'); assert.equal(model.snapshot().states.holding, 1); + model.up('jump', 'pointer'); assert.equal(model.snapshot().states.velocity, -(160 + charge * 300)); + const released = model.snapshot(); model.up('jump', 'pointer'); + assert.deepEqual(model.snapshot(), released); +}); + +test('airborne presses cannot charge or cause another jump', () => { + const model = createChargeModel(); model.down(); model.up(); model.frame(); + const airborne = model.snapshot().states; + model.down(); model.up(); + assert.deepEqual(model.snapshot().states, airborne); +}); + +test('releaseAll emits release and reset clears latches without a jump', () => { + const model = createChargeModel(); + model.down('jump', 'keyboard'); model.down('jump', 'pointer'); model.frame(10); + model.releaseAll(); assert.equal(model.snapshot().states.holding, 0); + assert.ok(model.snapshot().states.velocity < 0); assert.deepEqual(model.snapshot().held, []); + model.reset(); assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); + assert.equal(model.snapshot().tick, 0); + model.down(); model.reset(); model.up(); assert.equal(model.snapshot().states.velocity, 0); +}); + +test('unknown inputs and invalid frame counts reject without mutation', () => { + const model = createChargeModel(), initial = model.snapshot(); + for (const method of ['down', 'up']) assert.throws(() => model[method]('other'), /Unknown input/); + for (const count of [-1, .5, 601, Infinity, NaN, '1']) assert.throws(() => model.frame(count), /integer/); + model.frame(0); assert.deepEqual(model.snapshot(), initial); + const snapshot = model.snapshot(); snapshot.states.y = -999; + assert.equal(model.snapshot().states.y, 240); +}); diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/cli.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/cli.mjs new file mode 100644 index 0000000..aafcd51 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/cli.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; +import { createInterface } from 'node:readline'; +import { createRuntime } from './web/runtime.mjs'; + +const help = `Pure authoring workbench · 같은 정의와 Zig 실행기 + load PATH .pure 파일을 적용 (같은 이름의 상태 보존) + down NAME 정의한 입력 누르기 + up NAME 정의한 입력 놓기 + 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 적용한 정의를 저장 + 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))); + runtime.apply(args[0] ? await readFile(args[0], 'utf8') : ''); + 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) runtime.apply(await readFile(arg, 'utf8')); + 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 === '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/workbench/results/2026-09-22-evaluation/sources/cli.test.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/cli.test.mjs new file mode 100644 index 0000000..ef5c535 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/cli.test.mjs @@ -0,0 +1,83 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/compiler.test.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/compiler.test.mjs new file mode 100644 index 0000000..89b8c6c --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/compiler.test.mjs @@ -0,0 +1,240 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile, LIMITS } from './web/compiler.mjs'; + +function diagnostic(source, pattern, line, column) { + assert.throws(() => compile(source), error => { + assert.match(error.message, pattern); + assert.ok(Number.isInteger(error.line) && error.line > 0); + assert.ok(Number.isInteger(error.column) && error.column > 0); + if (line !== undefined) assert.equal(error.line, line); + if (column !== undefined) assert.equal(error.column, column); + return true; + }); +} + +// A small independent evaluator checks compiler lowering. The product executes +// these instructions in Zig; the VM has its own native and integration checks. +function evaluate(program, start, length, values, dt = 0) { + const stack = []; + for (let pc = start; pc < start + length; pc++) { + const instruction = program.instructions[pc]; + const { op, arg, value } = instruction; + if (op === 0) stack.push(value); + else if (op === 1) stack.push(values[arg]); + else if (op === 2) stack.push(dt); + else if (op === 7) stack.push(-stack.pop()); + else if (op === 16) stack.push(Number(!stack.pop())); + else if (op === 19) { + const high = stack.pop(), low = stack.pop(), x = stack.pop(); + assert.ok(low <= high); stack.push(Math.min(Math.max(x, low), high)); + } else if (op === 20) values[arg] = stack.pop(); + else if (op === 21) { if (!stack.pop()) pc = arg - 1; } + else { + const right = stack.pop(), left = stack.pop(); + const operations = { + 3: () => left + right, 4: () => left - right, 5: () => left * right, + 6: () => left / right, 8: () => Number(left < right), + 9: () => Number(left <= right), 10: () => Number(left > right), + 11: () => Number(left >= right), 12: () => Number(left === right), + 13: () => Number(left !== right), 14: () => Number(Boolean(left) && Boolean(right)), + 15: () => Number(Boolean(left) || Boolean(right)), + 17: () => Math.min(left, right), 18: () => Math.max(left, right), + }; + assert.ok(operations[op], `Unknown opcode ${op}`); + stack.push(operations[op]()); + } + assert.ok(stack.every(Number.isFinite)); + } + return stack; +} + +function dispatch(program, event, values, dt = 0) { + for (const handler of program.handlers.filter(handler => handler.event === event)) { + assert.deepEqual(evaluate(program, handler.start, handler.length, values, dt), []); + } +} + +test('a blank document is an empty program with no implied behavior', () => { + const result = compile(''); + for (const field of ['states', 'inputs', 'views', 'instructions', 'handlers']) assert.deepEqual(result[field], []); + assert.equal(result.metrics.sourceBytes, 0); + assert.equal(result.metrics.maxStack, 0); + assert.deepEqual(compile('# comment only\n;').instructions, []); +}); + +test('forward references and separate state/input namespaces preserve declaration IDs', () => { + const program = compile('on tap.down { second = tap + first }\ntext label(0,0,second)\nstate first = -2\nstate tap = +3\ninput tap = Space\nstate second = .5e2'); + assert.deepEqual(program.states.map(state => state.name), ['first', 'tap', 'second']); + assert.equal(program.handlers[0].event, 1); + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + assert.deepEqual(values, [-2, 3, 1]); + const binding = program.views[0].bindings.value; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [1]); +}); + +test('ordered assignments, repeated handlers, and condition evaluation once', () => { + const program = compile(`state x = 1 +input tap = Enter +on tap.down { + if x == 1 { + x = 0 + x = x + 5 + } +} +on tap.down { x = x * 2 } +on tap.up { x = x - 1 }`); + const values = [1]; + dispatch(program, 1, values); + assert.equal(values[0], 10); + dispatch(program, 2, values); + assert.equal(values[0], 9); + const branch = program.instructions.find(instruction => instruction.op === 21); + assert.equal(branch.arg, program.handlers[0].start + program.handlers[0].length); +}); + +test('nested branches use absolute forward targets and skip correct bodies', () => { + const program = compile(`state x = 0 +text label(0, 0, x) +on frame { + if x > 0 { + x = 8 + if x == 8 { x = 9 } + } + x = x + 1 +}`); + const values = [0]; + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 1); + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 10); + const handler = program.handlers[0]; + program.instructions.forEach((instruction, pc) => { + if (instruction.op === 21) assert.ok(instruction.arg > pc && instruction.arg <= handler.start + handler.length); + }); +}); + +test('operator precedence, associativity, unary and builtin function lowering', () => { + const expressions = new Map([ + ['2 + 3 * 4', 14], ['8 - 3 - 2', 3], ['12 / 3 / 2', 2], + ['-2 * -3', 6], ['!0 + !4', 1], ['1 || 0 && 0', 1], + ['1 < 2 && 2 <= 2 && 4 > 3 && 4 >= 4', 1], + ['3 == 3 && 3 != 4', 1], ['clamp(max(3, 9) - min(4, 7), 0, 4)', 4], + ]); + for (const [source, expected] of expressions) { + const program = compile(`state x = 0\non frame { x = ${source} }`); + const values = [0]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], expected, source); + } + const eager = compile('state x = 0\non frame { x = 1 || x / 0 }'); + assert.ok(eager.instructions.some(instruction => instruction.op === 6)); + assert.ok(!eager.instructions.some(instruction => instruction.op === 21)); +}); + +test('multiline parentheses, scientific literals, comments, and string escapes', () => { + const program = compile(`state x = 1e-2 # comment +box tile( + (x + + 2), 0, max( + 1, 2), 4, + "#AABBCC") +text label(0, 0, x, "# count: \\\"value\\\" \\n") +on frame { x = x + dt }`); + assert.equal(program.views[0].color, '#AABBCC'); + assert.equal(program.views[1].prefix, '# count: "value" \n'); + const values = [.01]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], .01 + 1 / 60); + const binding = program.views[0].bindings.x; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [values[0] + 2]); +}); + +test('all instruction fields are numeric and carry original source line', () => { + const program = compile('state x = 0\non frame {\n x = x + dt\n}'); + for (const instruction of program.instructions) { + assert.deepEqual(Object.keys(instruction), ['op', 'arg', 'value', 'line']); + assert.ok(Object.values(instruction).every(Number.isFinite)); + assert.equal(instruction.line, 3); + } +}); + +test('contextual declaration words remain valid names outside reserved builtins', () => { + const program = compile('state if = 1\nstate state = 2\non frame { if = if + state }'); + const values = [1, 2]; dispatch(program, 0, values, 1 / 60); + assert.deepEqual(values, [3, 2]); +}); + +test('unknown and reserved symbols have useful one-based locations', () => { + diagnostic('state x = 0\non frame {\n x = missing\n}', /Unknown state missing/, 3, 7); + diagnostic('on missing.down {}', /Unknown input missing/, 1, 4); + diagnostic('on frame { absent = 1 }', /Unknown state absent/, 1, 12); + for (const name of ['frame', 'dt', 'min', 'max', 'clamp']) { + diagnostic(`state ${name} = 0`, /Reserved name/, 1, 7); + diagnostic(`input ${name} = Space`, /Reserved name/); + } + diagnostic('text clock(0, 0, dt)', /dt is only available in handlers/); +}); + +test('duplicates, unsupported keys, initializers, and invalid syntax are rejected', () => { + const invalid = [ + ['state x=0\nstate x=1', /Duplicate state/], + ['input tap=Space\ninput tap=Enter', /Duplicate input/], + ['input tap=Space\ninput other=Space', /Duplicate keyboard/], + ['box a(0,0,1,1,"#000000")\ncircle a(0,0,1,"#000000")', /Duplicate view/], + ['input tap=Escape', /Unsupported keyboard/], + ['state x=1+2', /Expected newline/], + ['state x=1e999', /finite/], + ['state x=NaN', /numeric state initializer/], + ['on tap.click {}', /Expected down or up/], + ['on frame.down {}', /Expected {/], + ['state x=0\non frame { x = min(1) }', /min expects 2/], + ['state x=0\non frame { x = clamp(1,2) }', /clamp expects 3/], + ['state x=0\non frame { x = 1 +\n2 }', /Expected a number/], + ['text a(0,0,1,"broken)', /Unterminated string/], + ['text a(0,0,1,"bad\\q")', /Unsupported string escape/], + ['box a(0,0,1,1,"blue")', /Color must use/], + ['state x=0 @', /Unexpected character/], + ['on frame {', /assignment, if, or closing brace/], + ['state x = 0 state y = 1', /Expected newline/], + ]; + for (const [source, pattern] of invalid) diagnostic(source, pattern); +}); + +test('document, declaration, instruction, and nesting limits reject overflow', () => { + diagnostic('#' + '한'.repeat(11000), /32768 UTF-8 bytes/); + diagnostic(Array.from({ length: 33 }, (_, i) => `state s${i}=0`).join('\n'), /32 states/); + diagnostic(Array.from({ length: 9 }, (_, i) => `input i${i}=Digit${i}`).join('\n'), /8 inputs/); + diagnostic(Array.from({ length: 25 }, (_, i) => `text t${i}(0,0,0)`).join('\n'), /24 views/); + diagnostic('on frame {}\n'.repeat(65), /64 handlers/); + diagnostic(`state x=0\non frame {\n${'x=1\n'.repeat(257)}}`, /512 instructions/); + assert.equal(compile(`state x=0\non frame {\n${'x=1\n'.repeat(256)}}`).instructions.length, LIMITS.instructions); + diagnostic(`state x=0\non frame { x=${'('.repeat(65)}1${')'.repeat(65)} }`, /nesting exceeds 64/); + diagnostic(`on frame { ${'if 1 { '.repeat(65)}${'}\n'.repeat(66)}`, /nesting exceeds 64/); + // Right-nested clamp's two waiting operands exceed the stack before its depth limit. + let deep = '0'; + for (let i = 0; i < 33; i++) deep = `clamp(0,0,${deep})`; + diagnostic(`text stack(0,0,${deep})`, /stack exceeds 64/); +}); + +test('every supported input maps to a bounded ordered event ID', () => { + const keys = ['Space', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyZ', 'Digit9']; + const program = compile(keys.map((key, i) => `input i${i}=${key}\non i${i}.down {}\non i${i}.up {}`).join('\n')); + assert.deepEqual(program.handlers.map(handler => handler.event), Array.from({ length: 16 }, (_, i) => i + 1)); +}); + +test('optional examples compile and charge behavior is entirely authored', async () => { + for (const name of ['counter', 'charge', 'toggle']) { + const program = compile(await readFile(new URL(`./examples/${name}.pure`, import.meta.url), 'utf8')); + assert.ok(program.instructions.length <= LIMITS.instructions); + assert.ok(program.views.length > 0); + if (name === 'charge') { + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + for (let frame = 0; frame < 90; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], 1); assert.equal(values[1], 1); assert.equal(values[2], 240); + dispatch(program, 2, values); assert.deepEqual(values, [0, 0, 240, -460]); + dispatch(program, 0, values, 1 / 60); assert.ok(values[2] < 240); + for (let frame = 0; frame < 120; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[2], 240); assert.equal(values[3], 0); + } + } +}); diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/core.zig b/prototypes/workbench/results/2026-09-22-evaluation/sources/core.zig new file mode 100644 index 0000000..e2c3028 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/core.zig @@ -0,0 +1,813 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program and all +//! observable state/diagnostics. Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { event: u32 = 0, start: u32 = 0, length: u32 = 0 }; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +const Engine = struct { + active: Program = .{}, + staging: Program = .{}, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_open = false; + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) return stagingFail(.capacity); + engine.staging = .{ .state_count = state_count, .input_count = input_count }; + engine.staging_open = true; + return 0; +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.staging.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.staging.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.staging.initials[id] = initial; + engine.staging.old_ids[id] = old_id; + engine.staging.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.staging; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 21) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.staging; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (event > p.input_count * 2) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (h.event > 2 * p.input_count) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) return .invalid; + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 21) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op == 20 or inst.op == 21) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + break; + } + } + const h = owner orelse return .invalid; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + } + } + return .ok; +} + +export fn commit_program() u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const code = validate(&engine.staging); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.staging.state_count) |i| { + const old_id = engine.staging.old_ids[i]; + next[i] = if (old_id < 0) engine.staging.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.staging; + engine.values = next; + engine.evaluation.forget(3); + engine.staging_open = false; + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +fn execute(values: *[max_states]f64, start: u32, length: u32, dt: f64, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op == 20 or inst.op == 21)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2 => 0, + 7, 16, 20, 21 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 2 => dt, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20) { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[inst.arg] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (event > engine.active.input_count * 2) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + const result = execute(&pending, h.start, h.length, dt, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, 0, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, 0, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Program), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.staging.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + try std.testing.expectEqualDeep(before, engine); +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 12), layout(3)); + try std.testing.expectEqual(@as(u32, 76), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(layout(1), layout(6)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation-check.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation-check.mjs new file mode 100644 index 0000000..bb3d616 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation-check.mjs @@ -0,0 +1,76 @@ +// A bounded adoption check on existing authored files, not a platform benchmark. +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { platform, release, arch, cpus } from 'node:os'; +import { performance } from 'node:perf_hooks'; +import { createRuntime } from './web/runtime.mjs'; + +const args = process.argv.slice(2); +if (args.length !== 2 || args[0] !== '--output') throw new Error('Use --output NEW.json'); +const root = new URL('.', import.meta.url); +const wasm = await readFile(new URL('web/core.wasm', root)); +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +const evidence = { purpose: 'Choose whether to enable binding reuse by default', + utc: new Date().toISOString(), host: { platform: platform(), release: release(), + arch: arch(), cpu: cpus()[0]?.model, node: process.version, affinityControlled: false }, + sourceSha256: {}, cases: [], + timingScope: 'Event dispatch plus snapshot construction. Excludes loading, compilation, DOM, equality checks and statistics reads. Five batch samples are not latency percentiles.', +}; +for (const path of ['web/core.wasm', 'core.zig', 'evaluation.zig', 'web/runtime.mjs', + 'web/compiler.mjs', 'evaluation-check.mjs', 'evaluation.test.mjs']) + evidence.sourceSha256[path] = hash(await readFile(new URL(path, root))); + +for (const name of ['counter', 'toggle', 'charge']) { + const path = `examples/${name}.pure`; + const source = await readFile(new URL(path, root), 'utf8'); + evidence.sourceSha256[path] = hash(source); + const plain = await createRuntime(wasm), cached = await createRuntime(wasm, {cacheEntries:32}); + const program = plain.apply(source); cached.apply(source); + const input = program.inputs[0]?.name; + const trace = Array.from({length:100}, () => 'show'); + for (let i = 0; i < 600; i++) { + if (input && i % 60 === 0) trace.push('down'); + if (input && i % 60 === 15) trace.push('up'); + trace.push('frame'); + } + function action(runtime, kind) { + if (kind === 'frame') assert.equal(runtime.frame(), 0); + else if (kind !== 'show') assert.equal(runtime[kind](input, 'replay'), 0); + return runtime.snapshot(); + } + const totals = {plain:{work:0,instructions:0,cacheHits:0},cached:{work:0,instructions:0,cacheHits:0}}; + for (const kind of trace) { + const left = action(plain, kind), right = action(cached, kind); + assert.equal(left.lastError, 0); assert.deepEqual(right, left); + for (const [key, runtime] of Object.entries({plain,cached})) { + const stats = runtime.evaluationStats(); + for (const field of Object.keys(totals[key])) totals[key][field] += stats[field]; + } + } + function replay(runtime) { + runtime.reset(); + for (const kind of trace) { + if (kind === 'frame') runtime.frame(); + else if (kind !== 'show') runtime[kind](input, 'replay'); + runtime.snapshot(); + } + } + for (let warm = 0; warm < 3; warm++) { replay(plain); replay(cached); } + const samples = []; + for (let trial = 0; trial < 5; trial++) { + for (const key of trial % 2 ? ['cached','plain'] : ['plain','cached']) { + const runtime = key === 'plain' ? plain : cached; + const start = performance.now(); replay(runtime); + samples.push({trial,key,elapsedMs:performance.now()-start}); + } + assert.deepEqual(cached.snapshot(), plain.snapshot()); + } + const median = key => samples.filter(s=>s.key===key).map(s=>s.elapsedMs).sort((a,b)=>a-b)[2]; + evidence.cases.push({name,checkedSnapshots:trace.length, traceSha256:hash(JSON.stringify(trace)), + totals, memory:cached.snapshot().memory, evaluation:cached.evaluationStats(), samples, + medianMs:{plain:median('plain'),cached:median('cached')}, cachedOverPlain:median('cached')/median('plain')}); +} +await writeFile(args[1], JSON.stringify(evidence,null,2)+'\n', {flag:'wx'}); +for (const c of evidence.cases) console.log(JSON.stringify({name:c.name,checkedSnapshots:c.checkedSnapshots, + totals:c.totals,medianMs:c.medianMs,cachedOverPlain:c.cachedOverPlain})); diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.test.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.test.mjs new file mode 100644 index 0000000..21a9590 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.test.mjs @@ -0,0 +1,133 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const make = (options = {}) => createRuntime(wasm, options); +const source = `state x = 3 +state spare = 0 +input next = KeyN +input other = KeyO +input fail = KeyF +text value(0, 0, (x + 1) * (x + 2) / 2) +on next.down { x = x + 0.25 } +on other.down { spare = spare + 1 } +on fail.down { x = 900; spare = 1 / 0 } +`; + +test('cache reuse, unrelated changes and discarded records preserve authored values', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + cached.apply(source); plain.apply(source); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(cached.evaluationStats().cacheHits > 0); + assert(cached.evaluationStats().instructions < plain.evaluationStats().instructions); + for (let i = 0; i < 100; i++) { + const name = i % 3 ? 'other' : 'next'; + for (const runtime of [cached, plain]) { runtime.down(name); runtime.up(name); } + if (i % 7 === 0) cached.forgetEvaluation('history'); + if (i % 11 === 0) cached.forgetEvaluation('values'); + if (i % 13 === 0) cached.forgetEvaluation('all'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + } + // Omit observations long enough to overwrite every change record. + for (let i = 0; i < 20; i++) for (const runtime of [cached, plain]) { + runtime.down('next'); runtime.up('next'); + } + assert.deepEqual(cached.snapshot(), plain.snapshot()); +}); + +test('committed changes only: failed events and failed edits preserve reusable output', async () => { + const cached = await make({ cacheEntries: 32 }); cached.apply(source); + const before = cached.snapshot(); + cached.down('fail'); cached.up('fail'); + const failed = cached.snapshot(); + assert.deepEqual(failed.states, before.states); + assert.deepEqual(failed.views, before.views); + assert.equal(failed.lastError, 4); + assert.equal(cached.evaluationStats().instructions, 0); + assert.throws(() => cached.apply('state x = missing')); + assert.deepEqual(cached.snapshot(), failed); + assert.equal(cached.evaluationStats().instructions, 0); +}); + +test('program edits, state reordering and reset cannot reuse old definitions', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + for (const r of [cached, plain]) { r.apply(source); r.down('next'); r.up('next'); r.snapshot(); } + const changed = source.replace('state x = 3\nstate spare = 0', 'state spare = 9\nstate x = 300') + .replace('(x + 1) * (x + 2) / 2', '(x + 10) * (x + 20) / 2'); + for (const r of [cached, plain]) r.apply(changed); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.reset(); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().states.x, 300); +}); + +test('finite float semantics include signed zero, high state bit and arithmetic errors', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + const program = `${Array.from({ length: 32 }, (_, i) => `state s${i} = 0`).join('\n')} +input flip = KeyF +text sign(0, 0, s31) +text error(0, 0, 1 / s31) +on flip.down { s31 = -s31 } +on flip.up { s31 = 2 } +`; + for (const r of [cached, plain]) r.apply(program); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.down('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(Object.is(cached.snapshot().views[0].value, -0)); + assert.equal(cached.snapshot().displayError.code, 100); + for (const r of [cached, plain]) r.up('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().displayError, null); +}); + +test('one budget covers the whole scene; failures publish no partial scene or state rollback', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + r.down('next'); r.up('next'); + const good = r.snapshot(); + r.configureEvaluation({ workLimit: 5 }); + const limited = r.snapshot(); + assert.equal(limited.lastError, 101); + assert.deepEqual(limited.views, []); + assert.deepEqual(limited.states, good.states); + assert.equal(limited.successfulEvents, good.successfulEvents); + assert(r.evaluationStats().work <= 5); + r.down('fail'); r.up('fail'); + assert.equal(r.snapshot().lastError, 4); + assert.equal(r.snapshot().displayError.code, 101); + r.configureEvaluation({ workLimit: 2048 }); + assert.deepEqual(r.snapshot().views, good.views); + assert.equal(r.snapshot().lastError, 4); +}); + +test('small caches, no history and every small work limit give exact results or explicit failure', async () => { + const plain = await make(); plain.apply(source); + for (const cacheEntries of [0, 1, 32]) for (const historyEntries of [0, 1, 8]) { + const r = await make({ cacheEntries, historyEntries }); r.apply(source); + for (let workLimit = 0; workLimit < 100; workLimit++) { + r.configureEvaluation({ workLimit }); + const actual = r.snapshot(); + assert(r.evaluationStats().work <= workLimit); + if (actual.lastError) { + assert.equal(actual.lastError, 101); + assert.deepEqual(actual.views, []); + } else assert.deepEqual(actual, plain.snapshot()); + } + } +}); + +test('invalid settings and forget names preserve policy and state', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + const before = r.snapshot(), policy = r.evaluationStats(); + for (const option of [{cacheEntries:33}, {historyEntries:9}, {workLimit:-1}, + {workLimit:0x100000000}, {workLimit:NaN}, {cacheEntries:0.5}]) { + assert.throws(() => r.configureEvaluation(option)); + assert.deepEqual(r.evaluationStats(), policy); + } + for (const name of ['unknown', '__proto__', 'constructor']) assert.throws(() => r.forgetEvaluation(name)); + assert.deepEqual(r.snapshot(), before); + assert.equal(r.snapshot().memory.linearBytes, 65536); +}); diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.zig b/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.zig new file mode 100644 index 0000000..5473d59 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/evaluation.zig @@ -0,0 +1,156 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + memo: [max_cache]Memo = @splat(.{}), + history: [max_history]Change = @splat(.{}), + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(3); + return true; + } + + pub fn forget(self: *Evaluation, flags: u32) void { + if (flags & 1 != 0) self.memo = @splat(.{}); + if (flags & 2 != 0) { + self.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, mask: u32) void { + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &self.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, start: u32, length: u32) ?f64 { + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &self.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (self.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, start: u32, length: u32, value: f64, dependencies: u32) void { + if (self.cache_count == 0) return; + self.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var e: Evaluation = .{}; + try std.testing.expect(e.configure(1, 1)); + e.begin(100); + e.remember(0, 3, 12, 1); + e.changed(2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(0, 3)); + e.changed(2); + e.changed(2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(0, 3)); + e.remember(0, 3, 12, 1); + e.changed(1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(0, 3)); + e.remember(0, 3, 20, 1); + e.changed(2); + e.forget(2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(0, 3)); + e.remember(0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var e: Evaluation = .{}; + try std.testing.expect(e.configure(1, 0)); + e.begin(100); + e.remember(0, 3, 12, 1); + e.remember(1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(1, 3)); + e.changed(0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(1, 3)); + try std.testing.expect(!e.configure(33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(2, 1, 99, 0); + e.changed(1); + e.forget(2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/charge.pure b/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/charge.pure new file mode 100644 index 0000000..eac55c4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/charge.pure @@ -0,0 +1,34 @@ +# All holding, charging, gravity, jumping, and floor behavior is authored here. +state holding = 0 +state charge = 0 +state y = 240 +state velocity = 0 +input jump = Space +box floor(40, 261, 560, 4, "#356548") +circle actor(180, y, 20, "#c78638") +box meter(80, 80, charge * 200, 12, "#c78638") +text amount(80, 60, charge, "Charge: ") +on jump.down { + if y >= 240 { + holding = 1 + charge = 0 + } +} +on jump.up { + if holding { + velocity = -(160 + charge * 300) + holding = 0 + charge = 0 + } +} +on frame { + if holding { + charge = clamp(charge + dt, 0, 1) + } + velocity = velocity + 600 * dt + y = y + velocity * dt + if y >= 240 { + y = 240 + velocity = 0 + } +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/counter.pure b/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/counter.pure new file mode 100644 index 0000000..ca5f18e --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/counter.pure @@ -0,0 +1,8 @@ +# Space or the tap control increments a state that is displayed and drawn. +state count = 0 +input tap = Space +text label(80, 90, count, "Count: ") +box tile(80, 150, 40 + min(count, 20) * 8, 60, "#356548") +on tap.down { + count = count + 1 +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/toggle.pure b/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/toggle.pure new file mode 100644 index 0000000..a29f38f --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/examples/toggle.pure @@ -0,0 +1,17 @@ +# Space toggles the circle. Right arrow moves the tile. +state visible = 1 +state position = 120 +input toggle = Space +input move = ArrowRight +circle marker(position, 140, visible * 24, "#c78638") +box tile(position - 24, 190, 48, 48, "#356548") +text value(80, 80, visible, "Visible: ") +on toggle.down { + visible = !visible +} +on move.down { + position = position + 40 + if position > 560 { + position = 80 + } +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/manage.py b/prototypes/workbench/results/2026-09-22-evaluation/sources/manage.py new file mode 100644 index 0000000..c643997 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/manage.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Build the authoring VM or serve its local browser adapter.""" +import argparse +from datetime import datetime, timezone +from functools import partial +import hashlib +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import shutil +import subprocess + +ROOT = Path(__file__).resolve().parent + + +def sync_examples(): + target = ROOT / 'web/examples' + target.mkdir(exist_ok=True) + for source in (ROOT / 'examples').glob('*.pure'): + shutil.copyfile(source, target / source.name) + + +def build(zig): + compiler = shutil.which(zig) + if not compiler: + raise SystemExit('Zig 0.15.2가 필요합니다. --zig /path/to/zig로 지정할 수 있습니다.') + version = subprocess.check_output([compiler, 'version'], text=True).strip() + if version != '0.15.2': + raise SystemExit(f'이 실험은 Zig 0.15.2로 고정했습니다. 현재 {version}') + folder = ROOT / 'build' + folder.mkdir(exist_ok=True) + cache = ['--cache-dir', str(folder / 'cache'), '--global-cache-dir', str(folder / 'global-cache')] + source = ROOT / 'core.zig' + result = subprocess.run([compiler, 'test', str(source), *cache], capture_output=True, text=True) + print(result.stdout + result.stderr, end='', flush=True) + result.check_returncode() + flags = ['-target', 'wasm32-freestanding', '-O', 'ReleaseSafe', '-fstrip', '-fno-entry', + '-rdynamic', '--export-memory', '--stack', '16384', + '--initial-memory=65536', '--max-memory=65536'] + artifact = ROOT / 'web/core.wasm' + subprocess.run([compiler, 'build-exe', str(source), *flags, f'-femit-bin={artifact}', *cache], check=True) + sync_examples() + sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() + (folder / 'build.json').write_text(json.dumps({ + 'builtUtc': datetime.now(timezone.utc).isoformat(), 'zigVersion': version, + 'sourceSha256': sha(source), 'wasmSha256': sha(artifact), + 'evaluationSourceSha256': sha(ROOT / 'evaluation.zig'), + 'wasmFileBytes': artifact.stat().st_size, 'flags': flags, + 'nativeTests': result.stdout + result.stderr, + }, indent=2) + '\n') + print(f'Built {artifact} ({artifact.stat().st_size:,} bytes)') + + +class Handler(SimpleHTTPRequestHandler): + extensions_map = {**SimpleHTTPRequestHandler.extensions_map, '.wasm': 'application/wasm', '.mjs': 'text/javascript', '.pure': 'text/plain'} + + def end_headers(self): + self.send_header('Cache-Control', 'no-store') + super().end_headers() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='action', required=True) + b = sub.add_parser('build') + b.add_argument('--zig', default='zig') + s = sub.add_parser('serve') + s.add_argument('--port', type=int, default=8766) + args = parser.parse_args() + if args.action == 'build': + build(args.zig) + return + if not 1 <= args.port <= 65535: + parser.error('port must be 1..65535') + if not (ROOT / 'web/core.wasm').is_file(): + parser.error('먼저 manage.py build를 실행하세요.') + sync_examples() + with ThreadingHTTPServer(('127.0.0.1', args.port), partial(Handler, directory=str(ROOT / 'web'))) as server: + print(f'Pure workbench: http://127.0.0.1:{args.port}', flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + main() diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/manifest.json b/prototypes/workbench/results/2026-09-22-evaluation/sources/manifest.json new file mode 100644 index 0000000..7c5e3dc --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/manifest.json @@ -0,0 +1,21 @@ +{ + "baseline.test.mjs": "3ea469a29d301b95701bc6f6ffafa2471ac0f334c5bd462c0cbc7c320bdc0764", + "cli.mjs": "2e0627ef84a45c8a9e91fb28c0010673212d459d0b0d09a5342856a049060030", + "cli.test.mjs": "655c6d80b51af818fa7cdeb27cb5462962a0e1aef70f609ef5e2ddc85f4a6bc7", + "compiler.test.mjs": "6aa442d9d1eeb69453082f1780ff4597e46b1fc09d8ba3cf955545bc94d81431", + "core.zig": "dfb9fd7f3862806c2bd88ce1c668b795d9df196e1f2e50cceff1b5c4809273df", + "evaluation-check.mjs": "11492759b5eff878fb8e43cad69422f9c34da6a85b5d8808478cc1845d737d4f", + "evaluation.test.mjs": "d3026ab5c6bffce56c0bde3155d7dddd6fa442a5688656ee710626c762258ff1", + "evaluation.zig": "2192e563385105f130d373b2e8724ea234c51e2909c7dbfc38e72acc3b70b2ed", + "examples/charge.pure": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "examples/counter.pure": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "examples/toggle.pure": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "manage.py": "e04199a0b5addc73bea7748420eee21ccf0aaa1a5b8c83082e5588187d6ed669", + "qa-evaluation.js": "262d97f0aa409eb68bdfe8aa3e82ad037e6b10681f8b5025e59d148ee49564e1", + "runtime.test.mjs": "5fe66115aab0c178a283dcb592376a2e7209308368004865e6c0a1cc58fa7f62", + "web/app.js": "6d63f3bfdfe212146894dbb1772509eb4ecc2f3be3b106675f0fc10cdeb5bfd3", + "web/baseline.mjs": "dcd567e5568d3c798d6b48e555e6d54773963c70d23594c175706b061f8b043f", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "web/core.wasm": "b79eecefba5e7da0f5dd945fa1df196ee627803243a430517f5c49c3a5e54ce1", + "web/runtime.mjs": "d9981282612cb06b50d24bf8919e829dce202f72b59dab607c8684bc0f297939" +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/qa-evaluation.js b/prototypes/workbench/results/2026-09-22-evaluation/sources/qa-evaluation.js new file mode 100644 index 0000000..eb19e15 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/qa-evaluation.js @@ -0,0 +1,86 @@ +// Run with Playwright CLI run-code on the local workbench. The final UI case +// overrides only createRuntime's default options in the served module so that +// a deliberately tiny budget can exercise the normally unreachable limit UI. +async (page) => { + const checks = [], errors = []; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); + page.on('dialog', onDialog); + const check = (condition, name) => { + if (!condition) throw new Error(name); + checks.push(name); + }; + const source = 'state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n'; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async source => { + await page.locator('#source').fill(source); + await page.locator('#apply').click(); + }; + try { + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); + await page.locator('[data-input="next"]').click(); + check((await state()).states.x === 3.25, 'authored browser input commits fractional state'); + check((await state()).views[0].value === 11.15625, 'bounded evaluation renders the exact authored expression'); + await apply(source.replace('(x + 1)', '(x + 10)')); + check((await state()).states.x === 3.25 && (await state()).views[0].value === 34.78125, + 'code replacement preserves state and changes binding result'); + + const api = await page.evaluate(async source => { + const { createRuntime } = await import('./runtime.mjs'); + const runtime = await createRuntime(undefined, {cacheEntries:32}); + runtime.apply(source); runtime.snapshot(); + const before = runtime.snapshot(), warm = runtime.evaluationStats(); + runtime.forgetEvaluation('all'); + const forgotten = runtime.snapshot(); + runtime.configureEvaluation({workLimit:1}); + const limited = runtime.snapshot(), limitStats = runtime.evaluationStats(); + runtime.configureEvaluation({workLimit:2048}); + return {before,warm,forgotten,limited,limitStats,recovered:runtime.snapshot()}; + }, source); + check(api.warm.cacheHits > 0, 'browser Wasm reuses completed bindings'); + check(JSON.stringify(api.before) === JSON.stringify(api.forgotten), 'discarding records preserves the browser snapshot'); + check(api.limited.lastError === 101 && api.limited.views.length === 0 && api.limitStats.work <= 1, + 'browser API publishes no partial scene after budget exhaustion'); + check(JSON.stringify(api.before) === JSON.stringify(api.recovered), 'raising the budget recovers the browser snapshot'); + + await page.route('**/runtime.mjs', async route => { + const response = await route.fetch(); + const body = await response.text(); + if (!body.includes('evaluationOptions = {}')) throw new Error('configuration setup no longer matches'); + await route.fulfill({response, body:body.replace('evaluationOptions = {}', 'evaluationOptions = { workLimit: 1 }')}); + }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); + await page.locator('[data-input="next"]').click(); + const limitedUi = await state(); + const message = await page.locator('#runtime-error').textContent(); + check(limitedUi.states.x === 3.25 && limitedUi.views.length === 0, + 'configured UI keeps committed state while withholding incomplete display'); + check(await page.locator('#runtime-error').isVisible() && message.includes('표시 오류') && + message.includes('상태 변경은 유지'), 'UI distinguishes display limit from event rollback'); + await page.screenshot({path:'output/playwright/pure-evaluation-limit.png',fullPage:true}); + await page.unroute('**/runtime.mjs'); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).views.length === 0 && (await state()).lastError === 0, 'normal configuration reloads to a clean blank work'); + check(errors.length === 0, 'no uncaught browser errors'); + const evidence = {utc:new Date().toISOString(),checks,api,limitedUi,message,errors, + uiLimitSetup:'Test-only module response overrides default workLimit to 1; production files are unchanged.', + screenshot:'output/playwright/pure-evaluation-limit.png', + browser:await page.evaluate(()=>navigator.userAgent)}; + await page.evaluate(value=>{window.__pureEvaluationEvidence=value;},evidence); + return evidence; + } catch (error) { + const evidence = {failed:true,message:error.message,checks,errors}; + await page.evaluate(value=>{window.__pureEvaluationEvidence=value;},evidence); + return evidence; + } finally { + await page.unroute('**/runtime.mjs'); + page.off('pageerror',onError); + page.off('dialog',onDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/runtime.test.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/runtime.test.mjs new file mode 100644 index 0000000..f6e71c6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/runtime.test.mjs @@ -0,0 +1,197 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const fresh = () => createRuntime(wasm); +const countSource = `state total = 3 +input add = KeyA +box bar(20, 80, total * 10, 30, "#abcdef") +text label(20, 60, total, "Total: ") +on add.down { total = total + 2 } +on add.up { total = total + 1 } +`; + +test('blank startup and applied empty source contain no application objects or behavior', async () => { + const runtime = await fresh(); + assert.equal(runtime.currentProgram, null); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + runtime.apply(''); + runtime.frame(60); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + assert.equal(runtime.snapshot().tick, 60); + assert.throws(() => runtime.down('jump'), /jump/); +}); + +test('author can add unrelated states inputs views and behavior entirely through source', async () => { + const runtime = await fresh(); + runtime.apply(countSource); + runtime.down('add'); runtime.up('add'); + assert.equal(runtime.snapshot().states.total, 6); + const added = `${countSource}\nstate radius = 12 +input inflate = KeyI +circle bubble(240, 160, radius, "#123456") +on inflate.down { radius = radius + total } +on frame { radius = radius + 60 * dt } +`; + runtime.apply(added); + runtime.down('inflate'); runtime.up('inflate'); runtime.frame(2); + assert.deepEqual(runtime.snapshot().states, { total: 6, radius: 20 }); + assert.equal(runtime.snapshot().views.find(view => view.id === 'bubble').radius, 20); + assert.equal(runtime.snapshot().views.length, 3); +}); + +test('keyboard and pointer sources aggregate first down and last up; repeats are ignored', async () => { + const runtime = await fresh(); runtime.apply(countSource); + runtime.down('add', 'keyboard'); runtime.down('add', 'keyboard'); runtime.down('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 5); + runtime.up('add', 'unknown'); runtime.up('add', 'keyboard'); + assert.equal(runtime.snapshot().states.total, 5); + assert.deepEqual(runtime.snapshot().held, ['add']); + runtime.releaseAll(); runtime.up('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 6); + assert.deepEqual(runtime.snapshot().held, []); + assert.equal(runtime.snapshot().successfulEvents, 2); +}); + +test('held input rejects apply without changing applied source, metadata, or values', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); + const before = runtime.snapshot(); + assert.throws(() => runtime.apply('state changed = 9'), /놓은 뒤/); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + runtime.up('add'); runtime.apply('state changed = 9'); + assert.deepEqual(runtime.snapshot().states, { changed: 9 }); +}); + +test('invalid syntax, unknown references and capacity overflow preserve the previous program', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (const source of ['state broken =', 'state n = 0\non frame { n = missing }', + Array.from({ length: 33 }, (_, i) => `state v${i} = ${i}`).join('\n'), '#'.repeat(32769)]) { + assert.throws(() => runtime.apply(source)); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + } +}); + +test('state preservation follows names across reorder/add/remove; reset uses new initializers', async () => { + const runtime = await fresh(); + runtime.apply('state x = 11\nstate y = 22'); + runtime.apply('state z = 30\nstate y = 2\nstate x = 1'); + assert.deepEqual(runtime.snapshot().states, { z: 30, y: 22, x: 11 }); + runtime.apply('state y = 200\nstate renamed = 9'); + assert.deepEqual(runtime.snapshot().states, { y: 22, renamed: 9 }); + runtime.reset(); + assert.deepEqual(runtime.snapshot().states, { y: 200, renamed: 9 }); + assert.equal(runtime.snapshot().revision, 3); +}); + +test('callers cannot mutate active metadata and corrupt name-based state preservation', async () => { + const runtime = await fresh(); const program = runtime.apply('state x = 11\nstate y = 22'); + assert.throws(() => program.states.reverse(), TypeError); + assert.throws(() => { runtime.currentProgram.states[0].name = 'y'; }, TypeError); + runtime.apply('state x = 0\nstate y = 0'); + assert.deepEqual(runtime.snapshot().states, { x: 11, y: 22 }); +}); + +test('conditions execute once, and assignments and multiple handlers follow source order', async () => { + const runtime = await fresh(); + runtime.apply(`state x = 0 +state seen = 0 +on frame { + if x == 0 { + x = 1 + seen = x + 1 + } + x = x + 3 +} +on frame { seen = seen + x } +`); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 4, seen: 6 }); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 7, seen: 13 }); +}); + +test('a failed event rolls back all handlers and keeps its source diagnostic through release', async () => { + const runtime = await fresh(); + const source = `state x = 7 +state y = 9 +input breakit = Space +on breakit.down { x = 99 } +on breakit.down { + y = 2 + x = 1 / 0 +} +`; + runtime.apply(source); + assert.equal(runtime.down('breakit'), 4); + assert.deepEqual(runtime.snapshot().states, { x: 7, y: 9 }); + assert.equal(runtime.snapshot().successfulEvents, 0); + assert.equal(runtime.snapshot().errorLine, 7); + runtime.up('breakit'); runtime.frame(); + assert.equal(runtime.snapshot().lastEvent.status, 0); + assert.equal(runtime.snapshot().lastFailure.phase, 'down'); + assert.equal(runtime.snapshot().lastError, 4); + assert.equal(runtime.snapshot().errorLine, 7); + assert.throws(() => runtime.apply('bad')); + assert.equal(runtime.snapshot().lastError, 4); + runtime.apply(source.replace('1 / 0', '10')); + assert.equal(runtime.snapshot().lastError, 0); + runtime.down('breakit'); runtime.up('breakit'); + assert.deepEqual(runtime.snapshot().states, { x: 10, y: 2 }); +}); + +test('reset clears held inputs and latched errors; invalid frame counts have no effects', async () => { + const runtime = await fresh(); + runtime.apply('state x = 5\ninput tap = Space\non tap.down { x = 1 / 0 }'); + runtime.down('tap'); runtime.reset(); + const before = runtime.snapshot(); + assert.equal(before.lastError, 0); assert.equal(before.lastFailure, null); + assert.deepEqual(before.held, []); + for (const count of [-1, 601, 0.5, NaN, Infinity]) assert.throws(() => runtime.frame(count)); + assert.deepEqual(runtime.snapshot(), before); +}); + +test('render evaluation is pure and invalid binding is distinct from an event rollback', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (let i = 0; i < 100; i++) assert.deepEqual(runtime.snapshot(), before); + runtime.apply('state n = 2\ncircle bad(20, 20, -n, "#123456")\ntext fine(0, 0, n)'); + const result = runtime.snapshot(); + assert.equal(result.lastError, 100); assert.equal(result.errorLine, 2); + assert.deepEqual(result.states, { n: 2 }); assert.equal(result.views.length, 1); + assert.equal(result.lastFailure, null); + assert.deepEqual(runtime.snapshot(), result); +}); + +test('independent adapters replay the same author program without sharing live state', async () => { + const left = await fresh(), right = await fresh(); + left.apply(countSource); right.apply(countSource); + for (const runtime of [left, right]) { + runtime.down('add', 'keyboard'); runtime.frame(3); runtime.up('add', 'keyboard'); + } + assert.deepEqual(left.snapshot(), right.snapshot()); + left.down('add'); + assert.equal(right.snapshot().states.total, 6); + assert.equal(left.snapshot().states.total, 8); +}); + +test('Wasm has no imports and reports fixed memory and exact active IR/state storage', async () => { + const module = await WebAssembly.compile(wasm); + assert.deepEqual(WebAssembly.Module.imports(module), []); + const runtime = await fresh(); const program = runtime.apply(countSource); + const before = runtime.snapshot().memory; + assert.equal(before.linearBytes, 65536); + assert.equal(before.instructionBytes, 16); assert.equal(before.handlerBytes, 12); + assert.equal(before.activeIRBytes, program.instructions.length * 16 + program.handlers.length * 12); + assert.equal(before.activeStateBytes, program.states.length * 8); + assert(before.stagingReservedBytes < before.reservedEngineBytes); + assert(before.reservedEngineBytes < before.linearBytes); + for (let i = 0; i < 20; i++) runtime.frame(600); + assert.deepEqual(runtime.snapshot().memory, before); +}); diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/web/app.js b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/app.js new file mode 100644 index 0000000..c931b1a --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/app.js @@ -0,0 +1,427 @@ +import { createRuntime } from './runtime.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const NS = 'http://www.w3.org/2000/svg'; +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString(); +const formatBytes = value => `${Number(value).toLocaleString('en-US')} B`; + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + $('cursor-position').textContent = `${lines.length}행 · ${lines.at(-1).length + 1}열`; +} + +function draftChanged() { + const count = source.value.split('\n').length; + $('line-numbers').textContent = Array.from({ length: count }, (_, i) => i + 1).join('\n'); + $('line-numbers').scrollTop = source.scrollTop; + $('source-size').textContent = `${bytes(source.value).toLocaleString('en-US')} / 32,768 bytes`; + $('source-size').style.color = bytes(source.value) > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + $('draft-status').textContent = dirty() ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'; + $('draft-status').classList.toggle('dirty', dirty()); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + $('play').textContent = '▶ 실행'; + $('play').setAttribute('aria-pressed', 'false'); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function svgElement(type, attrs, text) { + const node = document.createElementNS(NS, type); + for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); + if (text !== undefined) node.textContent = text; + return node; +} + +function renderScene(views) { + const elements = []; + for (const view of views) { + let element; + const attrs = { 'data-view-id': view.id }; + if (view.type === 'box') element = svgElement('rect', { ...attrs, x: view.x, y: view.y, width: Math.max(0, view.width), height: Math.max(0, view.height), fill: view.color }); + else if (view.type === 'circle') element = svgElement('circle', { ...attrs, cx: view.x, cy: view.y, r: Math.max(0, view.radius), fill: view.color }); + else if (view.type === 'text') element = svgElement('text', { ...attrs, x: view.x, y: view.y, fill: '#e7ece6', 'font-family': 'ui-monospace, monospace', 'font-size': 20 }, `${view.prefix || ''}${number(view.value)}`); + if (element) elements.push(element); + } + $('scene-content').replaceChildren(...elements); + $('empty-scene').hidden = elements.length !== 0; + $('scene-description').textContent = elements.length ? `사용자가 만든 장면. 도형과 글자 ${elements.length}개.` : '사용자가 코드로 만든 장면. 현재 비어 있습니다.'; +} + +function renderStates(states) { + const children = []; + for (const [name, value] of Object.entries(states)) { + const holder = document.createElement('dl'); + holder.className = 'state-value'; + const label = document.createElement('dt'); + label.textContent = name; + const amount = document.createElement('dd'); + amount.textContent = number(value); + amount.dataset.state = name; + holder.append(label, amount); + children.push(holder); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '선언한 상태의 값이 표시됩니다.'; + children.push(message); + } + $('state-values').replaceChildren(...children); +} + +function renderCosts(snapshot) { + const memory = snapshot.memory || {}; + const metrics = runtime.currentProgram?.metrics || {}; + const rows = [ + ['Wasm 선형 메모리', memory.linearBytes], + ['코어 예약 공간', memory.reservedEngineBytes], + ['사용 중인 명령·핸들러', memory.activeIRBytes], + ['현재 상태 값', memory.activeStateBytes], + ['새 코드 준비 공간 (예약에 포함)', memory.stagingReservedBytes], + ['평가 스택 용량', memory.evalStackBytes], + ]; + const children = []; + for (const [label, amount] of rows) { + if (!Number.isFinite(amount)) continue; + const dt = document.createElement('dt'); dt.textContent = label; + const dd = document.createElement('dd'); dd.textContent = formatBytes(amount); + children.push(dt, dd); + } + const dt = document.createElement('dt'); dt.textContent = '마지막 이벤트 실행 명령'; + const dd = document.createElement('dd'); dd.textContent = `${snapshot.lastOps || 0}개`; + children.push(dt, dd); + const sourceBytes = metrics.sourceBytes; + if (Number.isFinite(sourceBytes)) { + const a = document.createElement('dt'); a.textContent = '적용된 소스 크기'; + const b = document.createElement('dd'); b.textContent = formatBytes(sourceBytes); + children.push(a, b); + } + $('cost-values').replaceChildren(...children); + $('dropped-ticks').textContent = `시간 지연으로 건너뛴 프레임: ${droppedTicks}`; +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.snapshot(); + if (snapshot.lastError) pause(); + renderScene(snapshot.views || []); + renderStates(snapshot.states || {}); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + button.setAttribute('aria-pressed', String(held.has(name))); + } + $('scene-status').textContent = runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'; + $('clock-status').textContent = `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`; + $('revision').textContent = `revision ${snapshot.revision || 0}`; + for (const id of ['play', 'step', 'reset']) $(id).disabled = !runtime.currentProgram; + const event = snapshot.lastEvent; + $('last-event').textContent = event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'; + $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + $('runtime-error').textContent = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + $('runtime-error').textContent += ` · ${snapshot.displayError.message}`; + renderCosts(snapshot); + draftChanged(); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime) { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic('편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.'); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_SOURCE_BYTES * 7 + 1024) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = source.value; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + let project; + try { project = JSON.parse(content); } catch { throw new Error('올바른 JSON 파일이 아닙니다.'); } + if (!project || typeof project !== 'object' || Array.isArray(project) || project.format !== 'pure-workbench' || project.version !== 1 || typeof project.source !== 'string') throw new Error('Pure 작업물 형식이 아닙니다. format, version, source를 확인하세요.'); + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/web/baseline.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/baseline.mjs new file mode 100644 index 0000000..3873e31 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/baseline.mjs @@ -0,0 +1,68 @@ +// Handwritten JavaScript reference for examples/charge.pure. +// This fixed app has no source compiler, live apply, or state-name migration. +// Its scene and behavior must be changed by editing this JavaScript file. +export function createChargeModel() { + const held = new Set(); + let holding = 0, charge = 0, y = 240, velocity = 0, tick = 0; + const checkInput = name => { + if (name !== 'jump') throw new Error(`Unknown input: ${name}`); + }; + + const model = { + down(name = 'jump', source = 'external') { + checkInput(name); + const wasDown = held.size > 0; + held.add(source); + if (!wasDown && y >= 240) { + holding = 1; + charge = 0; + } + return 0; + }, + up(name = 'jump', source = 'external') { + checkInput(name); + if (!held.delete(source) || held.size) return 0; + if (holding) { + velocity = -(160 + charge * 300); + holding = 0; + charge = 0; + } + return 0; + }, + frame(count = 1) { + if (!Number.isInteger(count) || count < 0 || count > 600) { + throw new Error('Frame count must be an integer from 0 through 600.'); + } + const dt = 1 / 60; + for (let frame = 0; frame < count; frame++) { + if (holding) charge = Math.min(Math.max(charge + dt, 0), 1); + velocity = velocity + 600 * dt; + y = y + velocity * dt; + if (y >= 240) { y = 240; velocity = 0; } + tick++; + } + return 0; + }, + releaseAll() { + for (const source of [...held]) model.up('jump', source); + return 0; + }, + reset() { + held.clear(); + holding = 0; charge = 0; y = 240; velocity = 0; tick = 0; + }, + snapshot() { + return { + states: { holding, charge, y, velocity }, tick, + held: held.size ? ['jump'] : [], + views: [ + { id: 'floor', type: 'box', x: 40, y: 261, width: 560, height: 4, color: '#356548' }, + { id: 'actor', type: 'circle', x: 180, y, radius: 20, color: '#c78638' }, + { id: 'meter', type: 'box', x: 80, y: 80, width: charge * 200, height: 12, color: '#c78638' }, + { id: 'amount', type: 'text', x: 80, y: 60, value: charge, prefix: 'Charge: ' }, + ], + }; + }, + }; + return Object.freeze(model); +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/web/compiler.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/compiler.mjs new file mode 100644 index 0000000..016ea9e --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/compiler.mjs @@ -0,0 +1,303 @@ +// 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', '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(); + } + 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: token.value }); + } 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 { + this.expect('='); + statements.push({ kind: 'assign', token, name: token.value, 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('='); + let sign = 1; + if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1; + const value = this.expect('number', 'numeric state initializer'); + declarations.push({ kind, token, name, initial: sign * value.value }); + } else if (kind === 'input') { + const name = this.expect('name', 'input name'); this.expect('='); + const key = this.expect('name', 'keyboard code'); + 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 or up'); + if (edge.value !== 'down' && edge.value !== 'up') fail(edge, 'Expected down or up'); + } + 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: [], metrics: {} }; + const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(); + 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 === '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 (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'); + } + 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 === 'dt') { + if (binding) fail(token, 'dt is only available in handlers'); + emit(2, token, 0, 0, 1); + } else { + 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 (!states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`); + expression(statement.expression); + emit(20, statement.token, states.get(statement.name), 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; + } + } + }; + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + if (declaration.event.value !== 'frame') { + if (!inputs.has(declaration.event.value)) fail(declaration.event, `Unknown input ${declaration.event.value}`); + event = 1 + inputs.get(declaration.event.value) * 2 + (declaration.edge.value === 'up' ? 1 : 0); + } + 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); + } + } + 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/workbench/results/2026-09-22-evaluation/sources/web/core.wasm b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/core.wasm new file mode 100755 index 0000000..69c75ec Binary files /dev/null and b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-22-evaluation/sources/web/runtime.mjs b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/runtime.mjs new file mode 100644 index 0000000..47560c6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/sources/web/runtime.mjs @@ -0,0 +1,197 @@ +import { compile } from './compiler.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') 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(); + 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) { + const status = 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + const api = { + get currentProgram() { return program; }, + configureEvaluation({ cacheEntries = evaluationPolicy.cacheEntries, + historyEntries = evaluationPolicy.historyEntries, workLimit = evaluationPolicy.workLimit } = {}) { + 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') { + 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) }; + }, + apply(source) { + 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)); + const oldIds = new Map(program?.states.map((state, index) => [state.name, index]) ?? []); + checked(core.begin_program(next.states.length, next.inputs.length)); + 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); + } + for (const op of next.instructions) checked(core.put_instruction(op.op, op.arg, op.value), op.line); + for (const handler of next.handlers) checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + checked(core.commit_program()); + program = next; + appliedSource = source; + lastEvent = null; + lastFailure = null; + return next; + }, + reset() { + held.clear(); + core.reset_state(); + lastEvent = null; + lastFailure = null; + }, + frame(count = 1) { + 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') { + const index = inputIndex(name); + 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') { + const index = inputIndex(name); + 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); + }, + releaseAll() { + let firstError = 0; + 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 }; + }, + snapshot() { + core.begin_evaluation(evaluationPolicy.workLimit); + const states = Object.fromEntries((program?.states ?? []).map((state, index) => [state.name, core.get_state(index)])); + const views = []; + let viewError = null; + viewLoop: for (const view of program?.views ?? []) { + const rendered = { id: view.id, type: view.type, color: view.color, prefix: view.prefix }; + let valid = true; + for (const [name, binding] of Object.entries(view.bindings)) { + const value = core.eval_bounded(binding.start, binding.length); + if (core.evaluation_info(0) === 10) { + // A snapshot must not mix complete views with budget-truncated ones. + views.length = 0; + viewError = { code: 101, line: view.line, + message: '표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.' }; + break viewLoop; + } + if (!Number.isFinite(value) || (['width', 'height', 'radius'].includes(name) && value < 0)) { + viewError ??= { line: view.line, message: `${view.id}.${name}: 표시할 수 없는 값입니다. 0 나눗셈이나 음수 크기를 확인하세요.` }; + valid = false; + } + rendered[name] = value; + } + if (valid) views.push(rendered); + } + const executionError = lastFailure?.status || 0; + return { + states, views, 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), + }, + }; + }, + }; + api.configureEvaluation(evaluationOptions); + return Object.freeze(api); +} diff --git a/prototypes/workbench/results/2026-09-22-evaluation/verification.json b/prototypes/workbench/results/2026-09-22-evaluation/verification.json new file mode 100644 index 0000000..070da3d --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-evaluation/verification.json @@ -0,0 +1,68 @@ +{ + "utc": "2026-09-22T13:59:18.937195+00:00", + "build": { + "builtUtc": "2026-09-22T13:55:49.821902+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "dfb9fd7f3862806c2bd88ce1c668b795d9df196e1f2e50cceff1b5c4809273df", + "wasmSha256": "b79eecefba5e7da0f5dd945fa1df196ee627803243a430517f5c49c3a5e54ce1", + "evaluationSourceSha256": "2192e563385105f130d373b2e8724ea234c51e2909c7dbfc38e72acc3b70b2ed", + "wasmFileBytes": 53125, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/20 core.test.blank program, no hidden application behavior, and reset...OK\n2/20 core.test.arbitrary input counter and unused events...OK\n3/20 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/20 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/20 core.test.branch to program end is valid...OK\n6/20 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/20 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/20 core.test.stack underflow overflow and leftover values reject the event...OK\n9/20 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/20 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/20 core.test.capacity boundaries and sticky load errors...OK\n12/20 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/20 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/20 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/20 core.test.binding evaluation is pure on both success and failure...OK\n16/20 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/20 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/20 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/20 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n20/20 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 20 tests passed.\n" + }, + "javascript": [ + { + "file": "compiler.test.mjs", + "tests": 13, + "output": "✔ a blank document is an empty program with no implied behavior (4.767599ms)\n✔ forward references and separate state/input namespaces preserve declaration IDs (2.310706ms)\n✔ ordered assignments, repeated handlers, and condition evaluation once (1.271946ms)\n✔ nested branches use absolute forward targets and skip correct bodies (0.732297ms)\n✔ operator precedence, associativity, unary and builtin function lowering (2.140688ms)\n✔ multiline parentheses, scientific literals, comments, and string escapes (0.846335ms)\n✔ all instruction fields are numeric and carry original source line (1.755169ms)\n✔ contextual declaration words remain valid names outside reserved builtins (0.726316ms)\n✔ unknown and reserved symbols have useful one-based locations (1.823799ms)\n✔ duplicates, unsupported keys, initializers, and invalid syntax are rejected (1.598724ms)\n✔ document, declaration, instruction, and nesting limits reject overflow (7.090898ms)\n✔ every supported input maps to a bounded ordered event ID (0.835279ms)\n✔ optional examples compile and charge behavior is entirely authored (12.957276ms)\nℹ tests 13\nℹ suites 0\nℹ pass 13\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 48.386655\n", + "sha256": "6aa442d9d1eeb69453082f1780ff4597e46b1fc09d8ba3cf955545bc94d81431" + }, + { + "file": "runtime.test.mjs", + "tests": 13, + "output": "✔ blank startup and applied empty source contain no application objects or behavior (11.310311ms)\n✔ author can add unrelated states inputs views and behavior entirely through source (6.741194ms)\n✔ keyboard and pointer sources aggregate first down and last up; repeats are ignored (3.252523ms)\n✔ held input rejects apply without changing applied source, metadata, or values (2.028058ms)\n✔ invalid syntax, unknown references and capacity overflow preserve the previous program (2.550577ms)\n✔ state preservation follows names across reorder/add/remove; reset uses new initializers (1.55202ms)\n✔ callers cannot mutate active metadata and corrupt name-based state preservation (1.051539ms)\n✔ conditions execute once, and assignments and multiple handlers follow source order (1.328224ms)\n✔ a failed event rolls back all handlers and keeps its source diagnostic through release (2.714633ms)\n✔ reset clears held inputs and latched errors; invalid frame counts have no effects (1.79928ms)\n✔ render evaluation is pure and invalid binding is distinct from an event rollback (4.471172ms)\n✔ independent adapters replay the same author program without sharing live state (1.705539ms)\n✔ Wasm has no imports and reports fixed memory and exact active IR/state storage (7.750899ms)\nℹ tests 13\nℹ suites 0\nℹ pass 13\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 64.820621\n", + "sha256": "5fe66115aab0c178a283dcb592376a2e7209308368004865e6c0a1cc58fa7f62" + }, + { + "file": "baseline.test.mjs", + "tests": 7, + "output": "✔ reference begins with the authored states and matching scene (2.672337ms)\n✔ press, hold, release, and frame order produce exact expected values (0.589537ms)\n✔ charge clamps at one and explicit floor restores rest (1.429872ms)\n✔ repeat and concurrent physical sources produce only logical edges (0.524167ms)\n✔ airborne presses cannot charge or cause another jump (0.285575ms)\n✔ releaseAll emits release and reset clears latches without a jump (0.576501ms)\n✔ unknown inputs and invalid frame counts reject without mutation (0.846019ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 17.303448\n", + "sha256": "3ea469a29d301b95701bc6f6ffafa2471ac0f334c5bd462c0cbc7c320bdc0764" + }, + { + "file": "cli.test.mjs", + "tests": 5, + "output": "✔ terminal starts empty and produces the same charge trace as the shared runtime (145.440706ms)\n✔ invalid terminal commands do not alter state and fail piped execution (55.457108ms)\n✔ file load preserves by name, rejected load preserves active work, and save never overwrites (61.176894ms)\n✔ terminal exposes arithmetic rollback and retains failure through matching release (55.231312ms)\n✔ terminal configures and discards evaluation records and recovers from a display limit (53.719162ms)\nℹ tests 5\nℹ suites 0\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 380.392621\n", + "sha256": "655c6d80b51af818fa7cdeb27cb5462962a0e1aef70f609ef5e2ddc85f4a6bc7" + }, + { + "file": "evaluation.test.mjs", + "tests": 7, + "output": "✔ cache reuse, unrelated changes and discarded records preserve authored values (18.441968ms)\n✔ committed changes only: failed events and failed edits preserve reusable output (2.9613ms)\n✔ program edits, state reordering and reset cannot reuse old definitions (3.086097ms)\n✔ finite float semantics include signed zero, high state bit and arithmetic errors (2.875652ms)\n✔ one budget covers the whole scene; failures publish no partial scene or state rollback (1.415632ms)\n✔ small caches, no history and every small work limit give exact results or explicit failure (28.082684ms)\n✔ invalid settings and forget names preserve policy and state (1.779484ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 69.418978\n", + "sha256": "d3026ab5c6bffce56c0bde3155d7dddd6fa442a5688656ee710626c762258ff1" + } + ], + "javascriptTestCount": 45, + "sourceSha256": { + "core.zig": "dfb9fd7f3862806c2bd88ce1c668b795d9df196e1f2e50cceff1b5c4809273df", + "evaluation.zig": "2192e563385105f130d373b2e8724ea234c51e2909c7dbfc38e72acc3b70b2ed", + "web/core.wasm": "b79eecefba5e7da0f5dd945fa1df196ee627803243a430517f5c49c3a5e54ce1", + "web/runtime.mjs": "d9981282612cb06b50d24bf8919e829dce202f72b59dab607c8684bc0f297939", + "web/app.js": "6d63f3bfdfe212146894dbb1772509eb4ecc2f3be3b106675f0fc10cdeb5bfd3", + "cli.mjs": "2e0627ef84a45c8a9e91fb28c0010673212d459d0b0d09a5342856a049060030", + "manage.py": "e04199a0b5addc73bea7748420eee21ccf0aaa1a5b8c83082e5588187d6ed669" + } +} diff --git a/prototypes/workbench/results/2026-09-22-linux-aarch64/authored.png b/prototypes/workbench/results/2026-09-22-linux-aarch64/authored.png new file mode 100644 index 0000000..9acff70 Binary files /dev/null and b/prototypes/workbench/results/2026-09-22-linux-aarch64/authored.png differ diff --git a/prototypes/workbench/results/2026-09-22-linux-aarch64/blank.png b/prototypes/workbench/results/2026-09-22-linux-aarch64/blank.png new file mode 100644 index 0000000..8b06a0a Binary files /dev/null and b/prototypes/workbench/results/2026-09-22-linux-aarch64/blank.png differ diff --git a/prototypes/workbench/results/2026-09-22-linux-aarch64/browser-checks.json b/prototypes/workbench/results/2026-09-22-linux-aarch64/browser-checks.json new file mode 100644 index 0000000..04d7b88 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-linux-aarch64/browser-checks.json @@ -0,0 +1,159 @@ +{ + "recordedUtc": "2026-09-22T10:57:50.563Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19072, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 464, + "activeStateBytes": 24, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "exported-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "blank.png", + "authored.png", + "mobile.png" + ], + "checkCount": 44, + "artifactSha256": { + "authored.png": "bb599b72b8e061bc28101086d664f4e288a014e737db37e958ce4902d0efa23d", + "exported-draft.json": "60f35c25dc371bce5667a779d49860d056b7e9ce5884c9755379ed6e526e1710", + "blank.png": "967727bf28b71d34f32cb6913fd31f84275eee5909a01bb188b32a041e9a8ca4", + "mobile.png": "b6d352f6e8260006a5220aebf9b3c9bc93dcb2a68a7db7124cf3a6f08adb2a4d" + }, + "sourceSha256": { + "qa-browser.js": "0d4c155b3575481c60f175b71acdc332f1c0d8489297794deca6f55999ead1b8", + "web/index.html": "4fff6eb67d8cff098302465449251d17f0efb39d9144e87d15fefcc152e5d97e", + "web/app.js": "d364d777d704e871d90ed03cd2cd74a7e80d895480668b15d7f64c8946ac1e2a", + "web/style.css": "43301298028c5bc431eecd5ff30d7362fae83cce1fd0fdd7e8bea858100c22b9", + "web/runtime.mjs": "7819737fa37f058e9134f2cc64c5dc2497cece9b364d5409f98e59ac979fa698", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "web/core.wasm": "3e2184d515407fcbd422becb56e5dca93fd40feb07475165a0cdbd187e33a0c8", + "web/baseline.html": "325e21c004882d83c626db4ca9a305b7aa72518794514d7205f6120e4d736173" + }, + "visualReview": { + "reviewed": [ + "blank.png", + "authored.png", + "mobile.png" + ], + "findings": "All three screenshots inspected. Editor and scene are empty on start; user-defined source and generated controls are visible; mobile page has no horizontal overflow. Placeholder directional copy corrected from right to below; final blank screenshot recaptured and checked after this text-only correction." + }, + "limits": [ + "Automated authoring behavior checks, not a human developer-experience evaluation.", + "Generated custom scenario is not a bundled example; it was authored by the test through the editor.", + "Expected network failure was intentionally induced for the missing-Wasm test. No uncaught page errors occurred.", + "Native dialog events can make Playwright CLI yield before the scenario finishes; final report was retrieved from window.__pureWorkbenchQAEvidence.", + "Browser user-agent architecture is recorded as reported by Chromium; it is not a claim about CPU or macOS/M3 performance." + ] +} diff --git a/prototypes/workbench/results/2026-09-22-linux-aarch64/comparison.json b/prototypes/workbench/results/2026-09-22-linux-aarch64/comparison.json new file mode 100644 index 0000000..0427838 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-linux-aarch64/comparison.json @@ -0,0 +1,298 @@ +{ + "experiment": "Pure Workbench charge / handwritten JavaScript reference", + "environment": { + "startedAt": "2026-09-22T10:54:56.773Z", + "completedAt": "2026-09-22T10:54:57.201Z", + "node": "v24.20.0", + "v8": "13.6.233.17-node.53", + "platform": "linux", + "osRelease": "6.18.39+rpt-rpi-2712", + "architecture": "arm64", + "cpuModel": "Cortex-A76", + "logicalCPUs": 4, + "availableParallelism": 4, + "affinityControlled": false, + "powerControlled": false, + "command": "node /home/eunhhu/work/pure/prototypes/workbench/compare.mjs --output prototypes/workbench/results/2026-09-22-linux-aarch64/comparison.json" + }, + "artifacts": { + "charge": { + "bytes": 674, + "sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56" + }, + "baseline": { + "bytes": 2251, + "sha256": "dcd567e5568d3c798d6b48e555e6d54773963c70d23594c175706b061f8b043f" + }, + "runtime": { + "bytes": 7219, + "sha256": "7819737fa37f058e9134f2cc64c5dc2497cece9b364d5409f98e59ac979fa698" + }, + "compiler": { + "bytes": 14327, + "sha256": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02" + }, + "wasm": { + "bytes": 48182, + "sha256": "3e2184d515407fcbd422becb56e5dca93fd40feb07475165a0cdbd187e33a0c8" + }, + "harness": { + "bytes": 11869, + "sha256": "f304cd9191fdf737777a8286e223ee32a483a64ba6cfd80fcf9d4c4a5b1bb6d8" + } + }, + "correctness": { + "passed": true, + "checkedSnapshots": 6664, + "checkedNumbers": 126616, + "maxAbsoluteDifference": 0, + "absoluteTolerance": 1e-11, + "relativeTolerance": 1e-12, + "checkedFields": [ + "states", + "all view fields", + "tick", + "held input names" + ], + "traceSha256": "db8110b5aa105921dea390950fed8bcb5ff9a3dc8929f8d368bfd24453b5001f", + "counts": { + "hostActions": 6663, + "frames": 6457, + "logicalDown": 52, + "logicalUp": 51, + "resets": 9, + "releaseAllCalls": 10, + "downCalls": 118, + "upCalls": 69, + "dispatchedEvents": 6560 + } + }, + "sources": { + "chargePure": { + "bytes": 674, + "physicalLines": 34, + "nonblankLines": 34, + "sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56" + }, + "fixedJavaScriptModel": { + "bytes": 2251, + "physicalLines": 68, + "nonblankLines": 67, + "sha256": "dcd567e5568d3c798d6b48e555e6d54773963c70d23594c175706b061f8b043f" + }, + "note": "Raw file size and line counts only; excludes HTML/host/compiler and does not establish authoring effort or DX quality." + }, + "compilerMetrics": { + "sourceBytes": 674, + "sourceLines": 35, + "states": 4, + "inputs": 1, + "views": 4, + "handlers": 3, + "instructions": 66, + "maxStack": 3 + }, + "memory": { + "pure": { + "linearBytes": 65536, + "reservedEngineBytes": 19072, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 1092, + "activeStateBytes": 32, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "note": "Wasm linear capacity and core layout counters; not process RSS or browser memory. No equivalent JS heap measurement collected. Do not add overlapping counters." + }, + "timing": { + "seed": 20260922, + "warmupTracesPerImplementation": 5, + "trialsPerImplementation": 7, + "targetTrialMs": 12, + "calibration": { + "pureHostWasm": { + "elapsedMs": 10.597304000000008, + "traces": 3, + "selectedRepetitions": 4 + }, + "handwrittenJavaScript": { + "elapsedMs": 1.182612000000006, + "traces": 3, + "selectedRepetitions": 31 + } + }, + "traceCounts": { + "hostActions": 6663, + "frames": 6457, + "logicalDown": 52, + "logicalUp": 51, + "resets": 9, + "releaseAllCalls": 10, + "downCalls": 118, + "upCalls": 69, + "dispatchedEvents": 6560 + }, + "order": "Alternated each trial; Pure first on odd trials, JavaScript first on even trials.", + "included": "Host method dispatch, source-latch aggregation, state logic, and resets in the fixed trace. Pure additionally maintains generic VM/event diagnostics.", + "excluded": "Compilation, loading, snapshots, binding evaluation, DOM rendering, scene editing, and live apply.", + "normalization": "nsPerHostAction divides total time by calls including repeats, releaseAll and reset; nsPerDispatchedEvent also amortizes that host overhead.", + "limitations": "Single-machine batch throughput; no CPU pinning or energy/thermal control. Seven trial averages are not event latency percentiles. Fixed JS has no live program replacement, general compiler, or state migration; not feature-equivalent whole-product timing.", + "samples": [ + { + "trial": 1, + "position": 1, + "implementation": "pureHostWasm", + "repetitions": 4, + "elapsedMs": 13.928176000000008, + "nsPerTrace": 3482044.000000002, + "nsPerHostAction": 522.5940267146933, + "nsPerDispatchedEvent": 530.7993902439027 + }, + { + "trial": 1, + "position": 2, + "implementation": "handwrittenJavaScript", + "repetitions": 31, + "elapsedMs": 11.025396, + "nsPerTrace": 355657.93548387097, + "nsPerHostAction": 53.37804824911766, + "nsPerDispatchedEvent": 54.2161487018096 + }, + { + "trial": 2, + "position": 1, + "implementation": "handwrittenJavaScript", + "repetitions": 31, + "elapsedMs": 13.416638000000034, + "nsPerTrace": 432794.7741935495, + "nsPerHostAction": 64.95494134677315, + "nsPerDispatchedEvent": 65.9748131392606 + }, + { + "trial": 2, + "position": 2, + "implementation": "pureHostWasm", + "repetitions": 4, + "elapsedMs": 19.406993999999997, + "nsPerTrace": 4851748.499999999, + "nsPerHostAction": 728.1627645204861, + "nsPerDispatchedEvent": 739.5958079268291 + }, + { + "trial": 3, + "position": 1, + "implementation": "pureHostWasm", + "repetitions": 4, + "elapsedMs": 19.585902000000033, + "nsPerTrace": 4896475.500000008, + "nsPerHostAction": 734.8755065285919, + "nsPerDispatchedEvent": 746.413948170733 + }, + { + "trial": 3, + "position": 2, + "implementation": "handwrittenJavaScript", + "repetitions": 31, + "elapsedMs": 15.407935000000009, + "nsPerTrace": 497030.1612903229, + "nsPerHostAction": 74.5955517470093, + "nsPerDispatchedEvent": 75.76679287962239 + }, + { + "trial": 4, + "position": 1, + "implementation": "handwrittenJavaScript", + "repetitions": 31, + "elapsedMs": 15.146732000000043, + "nsPerTrace": 488604.2580645175, + "nsPerHostAction": 73.3309707435866, + "nsPerDispatchedEvent": 74.48235641227402 + }, + { + "trial": 4, + "position": 2, + "implementation": "pureHostWasm", + "repetitions": 4, + "elapsedMs": 19.476624000000015, + "nsPerTrace": 4869156.000000004, + "nsPerHostAction": 730.7753264295368, + "nsPerDispatchedEvent": 742.249390243903 + }, + { + "trial": 5, + "position": 1, + "implementation": "pureHostWasm", + "repetitions": 4, + "elapsedMs": 15.168768999999998, + "nsPerTrace": 3792192.2499999995, + "nsPerHostAction": 569.1418655260393, + "nsPerDispatchedEvent": 578.0780868902439 + }, + { + "trial": 5, + "position": 2, + "implementation": "handwrittenJavaScript", + "repetitions": 31, + "elapsedMs": 10.88178499999998, + "nsPerTrace": 351025.3225806445, + "nsPerHostAction": 52.68277391274869, + "nsPerDispatchedEvent": 53.5099577104641 + }, + { + "trial": 6, + "position": 1, + "implementation": "handwrittenJavaScript", + "repetitions": 31, + "elapsedMs": 10.942656, + "nsPerTrace": 352988.9032258064, + "nsPerHostAction": 52.97747309407271, + "nsPerDispatchedEvent": 53.80928402832415 + }, + { + "trial": 6, + "position": 2, + "implementation": "pureHostWasm", + "repetitions": 4, + "elapsedMs": 14.048269000000005, + "nsPerTrace": 3512067.2500000014, + "nsPerHostAction": 527.0999924958729, + "nsPerDispatchedEvent": 535.3761051829271 + }, + { + "trial": 7, + "position": 1, + "implementation": "pureHostWasm", + "repetitions": 4, + "elapsedMs": 13.74212, + "nsPerTrace": 3435530, + "nsPerHostAction": 515.6130871979589, + "nsPerDispatchedEvent": 523.7088414634146 + }, + { + "trial": 7, + "position": 2, + "implementation": "handwrittenJavaScript", + "repetitions": 31, + "elapsedMs": 10.938469999999995, + "nsPerTrace": 352853.87096774176, + "nsPerHostAction": 52.957207109071256, + "nsPerDispatchedEvent": 53.78869984264356 + } + ], + "summary": { + "pureHostWasm": { + "medianNsPerHostAction": 569.1418655260393, + "minNsPerHostAction": 515.6130871979589, + "maxNsPerHostAction": 734.8755065285919 + }, + "handwrittenJavaScript": { + "medianNsPerHostAction": 53.37804824911766, + "minNsPerHostAction": 52.68277391274869, + "maxNsPerHostAction": 74.5955517470093 + }, + "pureOverJavaScriptMedianRatio": 10.66247051353076 + } + } +} diff --git a/prototypes/workbench/results/2026-09-22-linux-aarch64/exported-draft.json b/prototypes/workbench/results/2026-09-22-linux-aarch64/exported-draft.json new file mode 100644 index 0000000..c5acd9b --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-linux-aarch64/exported-draft.json @@ -0,0 +1,5 @@ +{ + "format": "pure-workbench", + "version": 1, + "source": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" +} diff --git a/prototypes/workbench/results/2026-09-22-linux-aarch64/mobile.png b/prototypes/workbench/results/2026-09-22-linux-aarch64/mobile.png new file mode 100644 index 0000000..3bb653d Binary files /dev/null and b/prototypes/workbench/results/2026-09-22-linux-aarch64/mobile.png differ diff --git a/prototypes/workbench/results/2026-09-22-linux-aarch64/verification.json b/prototypes/workbench/results/2026-09-22-linux-aarch64/verification.json new file mode 100644 index 0000000..6fffd98 --- /dev/null +++ b/prototypes/workbench/results/2026-09-22-linux-aarch64/verification.json @@ -0,0 +1,239 @@ +{ + "recordedUtc": "2026-09-22T11:02:59.248Z", + "scope": "Build record, test outcomes observed in this task, and direct runtime operation-count samples. Browser and timing evidence are separate files.", + "build": { + "builtUtc": "2026-09-22T10:49:43.509286+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "147f0fb73edfb021be48961541ca4a2e110efb0fbc2ff6e777528b43ea06fbdd", + "wasmSha256": "3e2184d515407fcbd422becb56e5dca93fd40feb07475165a0cdbd187e33a0c8", + "wasmFileBytes": 48182, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/18 core.test.blank program, no hidden application behavior, and reset...OK\n2/18 core.test.arbitrary input counter and unused events...OK\n3/18 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/18 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/18 core.test.branch to program end is valid...OK\n6/18 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/18 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/18 core.test.stack underflow overflow and leftover values reject the event...OK\n9/18 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/18 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/18 core.test.capacity boundaries and sticky load errors...OK\n12/18 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/18 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/18 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/18 core.test.binding evaluation is pure on both success and failure...OK\n16/18 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/18 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/18 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\nAll 18 tests passed.\n" + }, + "tests": [ + { + "command": "node prototypes/workbench/compiler.test.mjs", + "passed": 13, + "failed": 0 + }, + { + "command": "node prototypes/workbench/runtime.test.mjs", + "passed": 13, + "failed": 0 + }, + { + "command": "node prototypes/workbench/baseline.test.mjs", + "passed": 7, + "failed": 0 + }, + { + "command": "node prototypes/workbench/cli.test.mjs", + "passed": 4, + "failed": 0 + } + ], + "artifacts": { + "core.zig": { + "bytes": 28150, + "sha256": "147f0fb73edfb021be48961541ca4a2e110efb0fbc2ff6e777528b43ea06fbdd" + }, + "web/core.wasm": { + "bytes": 48182, + "sha256": "3e2184d515407fcbd422becb56e5dca93fd40feb07475165a0cdbd187e33a0c8" + }, + "web/compiler.mjs": { + "bytes": 14327, + "sha256": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02" + }, + "web/runtime.mjs": { + "bytes": 7219, + "sha256": "7819737fa37f058e9134f2cc64c5dc2497cece9b364d5409f98e59ac979fa698" + }, + "web/baseline.mjs": { + "bytes": 2251, + "sha256": "dcd567e5568d3c798d6b48e555e6d54773963c70d23594c175706b061f8b043f" + }, + "cli.mjs": { + "bytes": 2995, + "sha256": "9785d754c07991a004a1ec02e2ceeebf972d80fd142c933ecf82654a7dd01dbd" + }, + "manage.py": { + "bytes": 3442, + "sha256": "bb18c531e00595555b7864f6fe0a24c1049854997b6dfe4966365d41d89ce8a8" + }, + "compare.mjs": { + "bytes": 11869, + "sha256": "f304cd9191fdf737777a8286e223ee32a483a64ba6cfd80fcf9d4c4a5b1bb6d8" + }, + "compiler.test.mjs": { + "bytes": 11198, + "sha256": "6aa442d9d1eeb69453082f1780ff4597e46b1fc09d8ba3cf955545bc94d81431" + }, + "runtime.test.mjs": { + "bytes": 8833, + "sha256": "5fe66115aab0c178a283dcb592376a2e7209308368004865e6c0a1cc58fa7f62" + }, + "baseline.test.mjs": { + "bytes": 3983, + "sha256": "3ea469a29d301b95701bc6f6ffafa2471ac0f334c5bd462c0cbc7c320bdc0764" + }, + "cli.test.mjs": { + "bytes": 3746, + "sha256": "b3a1730850aa041ec97a67831e85563e07266f37ca76e029d42969915c60fae3" + } + }, + "program": { + "metrics": { + "sourceBytes": 674, + "sourceLines": 35, + "states": 4, + "inputs": 1, + "views": 4, + "handlers": 3, + "instructions": 66, + "maxStack": 3 + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19072, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 1092, + "activeStateBytes": 32, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "operationSamples": { + "note": "lastOps counts interpreted instructions in the last dispatched event only; excludes compiler, host bookkeeping, projection evaluation, Wasm-engine instructions and CPU instructions.", + "trace": [ + { + "action": "apply", + "states": { + "holding": 0, + "charge": 0, + "y": 240, + "velocity": 0 + }, + "tick": 0, + "lastOps": 0, + "lastEvent": null, + "lastError": 0 + }, + { + "action": "one grounded frame", + "states": { + "holding": 0, + "charge": 0, + "y": 240, + "velocity": 0 + }, + "tick": 1, + "lastOps": 22, + "lastEvent": { + "name": "frame", + "phase": "frame", + "source": "clock", + "status": 0, + "line": null, + "ops": 22 + }, + "lastError": 0 + }, + { + "action": "down grounded", + "states": { + "holding": 1, + "charge": 0, + "y": 240, + "velocity": 0 + }, + "tick": 1, + "lastOps": 8, + "lastEvent": { + "name": "jump", + "phase": "down", + "source": "external", + "status": 0, + "line": null, + "ops": 8 + }, + "lastError": 0 + }, + { + "action": "one charging frame", + "states": { + "holding": 1, + "charge": 0.016666666666666666, + "y": 240, + "velocity": 0 + }, + "tick": 2, + "lastOps": 29, + "lastEvent": { + "name": "frame", + "phase": "frame", + "source": "clock", + "status": 0, + "line": null, + "ops": 29 + }, + "lastError": 0 + }, + { + "action": "release after one charging frame", + "states": { + "holding": 0, + "charge": 0, + "y": 240, + "velocity": -165 + }, + "tick": 2, + "lastOps": 13, + "lastEvent": { + "name": "jump", + "phase": "up", + "source": "external", + "status": 0, + "line": null, + "ops": 13 + }, + "lastError": 0 + }, + { + "action": "one airborne frame", + "states": { + "holding": 0, + "charge": 0, + "y": 237.41666666666666, + "velocity": -155 + }, + "tick": 3, + "lastOps": 18, + "lastEvent": { + "name": "frame", + "phase": "frame", + "source": "clock", + "status": 0, + "line": null, + "ops": 18 + }, + "lastError": 0 + } + ] + } +} diff --git a/prototypes/workbench/results/2026-09-23-layout/README.md b/prototypes/workbench/results/2026-09-23-layout/README.md new file mode 100644 index 0000000..5eac784 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/README.md @@ -0,0 +1,21 @@ +# Layout persistence — 2026-09-23 + +Functional validation, not throughput/latency benchmarking. + +- build.json: matching core/Wasm hashes, 29 passing native tests. +- verification.json: 68 passing Node tests. +- layout-browser.json: 17 browser checks, actual download and fresh-page restore, + independent editing, malformed documents and asynchronous file-read races. +- authoring-browser.json: 44 passing legacy authoring checks. +- pure-layout-saved.json: actual downloaded document, including unfinished draft. +- pure-layout-desktop.png / pure-layout-mobile.png: browser captures. +- sources/ and manifest.json: exact implementation, contracts and source hashes. + +Browser: Linux Chromium via Playwright CLI. qa-layout.js uses real mouse input, +file download/upload and explicit delayed File.text injection for race cases. +The legacy qa-browser.js changes only output prefixes to preserve prior artifacts. +No claim of embedded Codex browser or physical touchscreen verification is made. + +Reproduce: build with Zig 0.15.2 using manage.py build; run Node *.test.mjs with +child-process support. Serve manage.py serve --port 8766 and execute qa-layout.js +with Playwright CLI run-code against a fresh blank workbench page. diff --git a/prototypes/workbench/results/2026-09-23-layout/authoring-browser.json b/prototypes/workbench/results/2026-09-23-layout/authoring-browser.json new file mode 100644 index 0000000..1d7f1b0 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/authoring-browser.json @@ -0,0 +1,128 @@ +{ + "recordedUtc": "2026-09-23T06:47:19.951Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19392, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 464, + "activeStateBytes": 24, + "stagingReservedBytes": 9656, + "evalStackBytes": 512 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "output/playwright/pure-layout-workbench-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "output/playwright/pure-layout-workbench-blank.png", + "output/playwright/pure-layout-workbench-authored.png", + "output/playwright/pure-layout-workbench-mobile.png" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-layout/build.json b/prototypes/workbench/results/2026-09-23-layout/build.json new file mode 100644 index 0000000..5b5302d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/build.json @@ -0,0 +1,23 @@ +{ + "builtUtc": "2026-09-23T06:47:08.570632+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "0e6d98258d2e92547dc6cf726a0cfb309ce61881647d98b20c784e2ac2025465", + "wasmSha256": "eb81bdd6c97a3cc34634d23fba58819345a71208adf05a70b563b98f11f57745", + "evaluationSourceSha256": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "wasmFileBytes": 56203, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/29 core.test.blank program, no hidden application behavior, and reset...OK\n2/29 core.test.arbitrary input counter and unused events...OK\n3/29 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/29 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/29 core.test.branch to program end is valid...OK\n6/29 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/29 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/29 core.test.stack underflow overflow and leftover values reject the event...OK\n9/29 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/29 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/29 core.test.capacity boundaries and sticky load errors...OK\n12/29 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/29 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/29 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/29 core.test.binding evaluation is pure on both success and failure...OK\n16/29 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/29 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/29 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/29 core.test.point coordinates are ephemeral event operands and unavailable to pure evaluation...OK\n20/29 core.test.point event ranges reject holes and invalid payloads without committing...OK\n21/29 core.test.point event failure rolls back writes from earlier matching handlers...OK\n22/29 core.test.loading shares storage with records while old events and bindings remain executable...OK\n23/29 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK\n24/29 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK\n25/29 core.test.restored definition and values commit together without changing reset initializers...OK\n26/29 core.test.missing duplicate nonfinite or invalid restored candidates leave active work untouched...OK\n27/29 core.test.empty and full restored programs validate all values and discard old display records...OK\n28/29 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n29/29 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 29 tests passed.\n" +} diff --git a/prototypes/workbench/results/2026-09-23-layout/layout-browser.json b/prototypes/workbench/results/2026-09-23-layout/layout-browser.json new file mode 100644 index 0000000..a0452e8 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/layout-browser.json @@ -0,0 +1,97 @@ +{ + "checks": [ + "placement save requires an applied definition", + "three independently authored targets move and selection directs keyboard adjustment", + "placement download is a separate document file", + "saving neither applies nor discards the unfinished draft", + "reset returns to authored initial positions", + "downloaded file restores all states and visible positions", + "the draft remains separate from the restored executable definition", + "restored work starts stopped with no replayed inputs or clock", + "restored selection and bindings remain editable", + "a fresh browser page reconstructs the saved work without old runtime state", + "invalid saved state cannot partially replace code, draft or scene", + "a delayed file read cannot overwrite a newer draft", + "a delayed restore cannot overwrite placement changes made while reading", + "saving while time advances requests a stopped document", + "saving with a held input is refused", + "extra persistence control fits a mobile viewport", + "no uncaught page errors" + ], + "errors": [], + "saved": "output/playwright/pure-layout-saved.json", + "expected": { + "states": { + "selected": 3, + "x1": 150.00000659728084, + "y1": 169.9999983506798, + "ox1": 9.999986805438311, + "oy1": 10.00000164932024, + "sx1": 80, + "sy1": 120, + "held1": 0, + "x2": 350.00000659728073, + "y2": 209.9999983506798, + "ox2": 9.999980208157467, + "oy2": 10.00000329864045, + "sx2": 280, + "sy2": 160, + "held2": 0, + "x3": 509.999947221753, + "y3": 260.0000016493202, + "ox3": 10.000013194561802, + "oy3": 9.999998350679817, + "sx3": 450, + "sy3": 220, + "held3": 0 + }, + "views": [ + { + "id": "board", + "type": "box", + "color": "#233a31", + "x": 20, + "y": 90, + "width": 600, + "height": 240 + }, + { + "id": "help", + "type": "text", + "prefix": "선택한 도형: ", + "x": 25, + "y": 40, + "value": 3 + }, + { + "id": "tile1", + "type": "box", + "color": "#c9f08b", + "x": 150.00000659728084, + "y": 169.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "tile2", + "type": "box", + "color": "#74c4d4", + "x": 350.00000659728073, + "y": 209.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "tile3", + "type": "box", + "color": "#dfb184", + "x": 509.999947221753, + "y": 260.0000016493202, + "width": 56, + "height": 44 + } + ] + }, + "source": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n", + "draft": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n\nstate unfinished =" +} diff --git a/prototypes/workbench/results/2026-09-23-layout/manifest.json b/prototypes/workbench/results/2026-09-23-layout/manifest.json new file mode 100644 index 0000000..f90f5f6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/manifest.json @@ -0,0 +1,21 @@ +{ + "recordedUtc": "2026-09-23T06:49:39.960675+00:00", + "purpose": "Functional persistence validation; no performance benchmark", + "sha256": { + "core.zig": "0e6d98258d2e92547dc6cf726a0cfb309ce61881647d98b20c784e2ac2025465", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "web/runtime.mjs": "265cc84b4d2f1f8be05c95d5775a58b64de27cf8c285b7533321c1b93fed0013", + "web/project.mjs": "fcd40c0010ba4ad4d47ef96e0aada7b959e0c1d67fab8ee142f24191a9d64466", + "web/app.js": "d5e062f9eab74f7e2bd6ab9f44e1217b12f2c17537054f5f7e4ec55b96801a09", + "web/index.html": "c433f12a58e669e5ea80681abcf987ff9b5dc5ab58910c39a1ccfdb743d98254", + "web/scene-input.mjs": "a7de0d0c26328fa6885b1d1633783f1550acfa4a2233db604b0ce4646e633a25", + "web/core.wasm": "eb81bdd6c97a3cc34634d23fba58819345a71208adf05a70b563b98f11f57745", + "examples/layout.pure": "cebd9c3485a2be4ec16ae665c1ed4ee200d51fc56ad0151306c8e9c31138f346", + "cli.mjs": "3114b6aafcd9af661b9fbfefaac549c191503964f0306a3072389baad40486b0", + "project.test.mjs": "7efd09c75b89af384e4020e8fc59df42932897c26c0a0cb6cb7f960684393568", + "cli.test.mjs": "7e4688824e26dc7e56396f9d4d62726de0ccb1490e1a388bb64690ae2aae52f4", + "workspace.test.mjs": "9ebe7675967ad9d070902fd7f182abf1a76fd66bc9aa07f513e45d0299115543", + "qa-layout.js": "d54272e1ef45fd077b1e81dbcfe54f87151f75eb499c94358811fcec79d1672e", + "CONTRACT.md": "f996e56575bdd137a34173976c64cf0e9f548d72ea8643a8f1c8e7db73392c61" + } +} diff --git a/prototypes/workbench/results/2026-09-23-layout/pure-layout-desktop.png b/prototypes/workbench/results/2026-09-23-layout/pure-layout-desktop.png new file mode 100644 index 0000000..ad70562 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-layout/pure-layout-desktop.png differ diff --git a/prototypes/workbench/results/2026-09-23-layout/pure-layout-mobile.png b/prototypes/workbench/results/2026-09-23-layout/pure-layout-mobile.png new file mode 100644 index 0000000..8bde7c4 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-layout/pure-layout-mobile.png differ diff --git a/prototypes/workbench/results/2026-09-23-layout/pure-layout-saved.json b/prototypes/workbench/results/2026-09-23-layout/pure-layout-saved.json new file mode 100644 index 0000000..ab358a7 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/pure-layout-saved.json @@ -0,0 +1,96 @@ +{ + "format": "pure-workbench", + "version": 2, + "source": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n", + "draft": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n\nstate unfinished =", + "states": [ + { + "name": "selected", + "value": 3 + }, + { + "name": "x1", + "value": 150.00000659728084 + }, + { + "name": "y1", + "value": 169.9999983506798 + }, + { + "name": "ox1", + "value": 9.999986805438311 + }, + { + "name": "oy1", + "value": 10.00000164932024 + }, + { + "name": "sx1", + "value": 80 + }, + { + "name": "sy1", + "value": 120 + }, + { + "name": "held1", + "value": 0 + }, + { + "name": "x2", + "value": 350.00000659728073 + }, + { + "name": "y2", + "value": 209.9999983506798 + }, + { + "name": "ox2", + "value": 9.999980208157467 + }, + { + "name": "oy2", + "value": 10.00000329864045 + }, + { + "name": "sx2", + "value": 280 + }, + { + "name": "sy2", + "value": 160 + }, + { + "name": "held2", + "value": 0 + }, + { + "name": "x3", + "value": 509.999947221753 + }, + { + "name": "y3", + "value": 260.0000016493202 + }, + { + "name": "ox3", + "value": 10.000013194561802 + }, + { + "name": "oy3", + "value": 9.999998350679817 + }, + { + "name": "sx3", + "value": 450 + }, + { + "name": "sy3", + "value": 220 + }, + { + "name": "held3", + "value": 0 + } + ] +} diff --git a/prototypes/workbench/results/2026-09-23-layout/qa-browser.js b/prototypes/workbench/results/2026-09-23-layout/qa-browser.js new file mode 100644 index 0000000..34f9ca2 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/qa-browser.js @@ -0,0 +1,228 @@ +// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-layout-workbench-blank.png', 'output/playwright/pure-layout-workbench-authored.png', 'output/playwright/pure-layout-workbench-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-layout-workbench-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/CONTRACT.md b/prototypes/workbench/results/2026-09-23-layout/sources/CONTRACT.md new file mode 100644 index 0000000..5c2bfae --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/CONTRACT.md @@ -0,0 +1,371 @@ +# Workbench experiment contract + +An author starts with empty source. There is no built-in player, physics, jump, +score, or timer behavior. All behavior is declared by the author. This is a +bounded scalar interaction language experiment, not a general language claim. + +## Source language + +```text +state count = 0 +input tap = Space +box tile(80, 150, 60, 60, "#356548") +circle dot(200, 180, 20, "#c78638") +text label(80, 100, count, "Count: ") +on tap.down { + count = count + 1 +} +on frame { + if count > 20 { + count = 0 + } +} +``` + +Declarations and statements end at a newline or semicolon; `#` comments outside +strings. ASCII identifiers, finite numeric literals, state initializers must be +numeric constants (optional unary sign). Input key codes accepted: +Space, Enter, ArrowLeft/Right/Up/Down, KeyA..KeyZ, Digit0..Digit9. Input names +must be unique, key codes unique, names cannot shadow `frame`, `dt`, `min`, `max`, +`clamp`, `event_x`, `event_y`. State and input names use separate namespaces, entity IDs unique. + +Expressions: parentheses, state references, dt (seconds on frame, zero on input), +unary - and !, + - * /, < <= > >= == !=, && ||, min(a,b), max(a,b), clamp(x,lo,hi). +Logical operators evaluate both sides. Division by zero/non-finite results fail +the whole event without committing state changes. No loops, recursion or eval. +An if without else executes its condition once; assignment statements are ordered. +Multiple handlers for the same event execute in source order within one atomic event. +`dt` is allowed in handlers, not view bindings. A frame always uses 1/60 seconds. + +box: x,y,width,height expressions + literal hex color; circle: x,y,radius + color; +text: x,y,value expression + optional literal prefix. Values are world units in a +640x360 view. No implicit clipping/collision/physics. Render expressions are pure. +Unknown identifiers and invalid syntax include 1-based line+column diagnostics. + +Initial scene is empty. Optional examples are explicit insertions, never auto-run. +Successful apply preserves scalar states by exact name; new names use initializers, +removed names disappear. Apply/reset are separate. Failed parse/load changes nothing. +Apply is refused while any input is held; release first. This avoids inventing +release events across changed mappings. Focus loss sends .up to ordinary buttons +and .cancel to pointer inputs. + +## Compiler interface (web/compiler.mjs, browser and Node) + +`compile(source)` returns: +``` +{ + states:[{name,initial,line}], inputs:[{name,key,line} | {name,target,line}], + instructions:[{op,arg,value,line}], handlers:[{event,start,length,line}], + views:[{id,type,line,color?,prefix?,bindings:{x:{start,length},...}}], + metrics:{...} +} +``` +Bindings: box x/y/width/height; circle x/y/radius; text x/y/value. +Compiler throws an Error with .line and .column. Empty source is a valid empty program. +Handlers and binding expressions all occupy instructions (maximum 512 total), but +only handlers execute on events; bindings are evaluated on demand by VM eval_at. +Limits: 32 states, 8 inputs, 24 views, 64 handlers, 512 instructions, stack 64, +source 32768 UTF-8 bytes. State IDs follow declaration order. Input event IDs: +frame=0; input i down=1+2*i, up=2+2*i, move=17+i, cancel=25+i. +Move/cancel handlers require a pointer input in source. States may be referenced before declaration; +symbols must be resolved after gathering top-level declarations. `metrics` can +record source bytes/lines and instruction/handler/view/state counts. + +## VM opcodes + +| op | operation | +|---:|---| +|0| push constant `value` | +|1| push state `arg` | +|2| push dt | +|3..6| add, subtract, multiply, divide | +|7| negate | +|8..13| less, less-equal, greater, greater-equal, equal, not-equal | +|14..16| AND, OR, NOT (nonzero true) | +|17..19| min, max, clamp (push x,lo,hi; invalid lo>hi fails) | +|20| store state `arg` (pop) | +|21| jump-if-zero to absolute instruction index `arg` (pop), forward only | +|22,23| push current pointer event x,y; fail without point context | + +Rules start with an empty stack and must end with empty stack. Bindings must finish +with exactly one value and cannot STORE, branch, or read dt/event coordinates. Branch targets may be handler end. + +## Zig exported ABI + +`reset_all()` clears programs and state. `begin_program(state_count,input_count)` +starts staging, returns0 success; `put_state(id,initial,old_id)` old_id=-1 means init; +`put_instruction(op,arg,value)` append; `put_handler(event,start,length)` append; +`commit_program()` all0 success otherwise error code, atomic; candidate is separate +from the active program, but shares its workspace with optional evaluation records. +Preserved IDs read active values only at successful commit. Validate limits, +indices, finite values and forward branch ranges; runtime stack errors are checked. +`reset_state()` restores active initializers, resets tick/event diagnostics. +`dispatch(event,dt)` accepts0..16 and finite dt; event0 dt must equal1/60, input dt0; +returns0 success, other code failure with no state mutation. Tick increments for +successful frame; one event runs all matching handlers in source order. +`dispatch_point(event,x,y)` accepts existing input down/up IDs plus move/cancel IDs +within input_count, never frame0. Coordinates must be finite; dt is implicitly0. +The core only knows input count, not host device kinds. The compiler and host enforce +pointer ownership and device mapping. Calling legacy dispatch on a coordinate-reading +handler fails with bad_event9. Point operands are ephemeral execution context, not +extra persistent state slots. Both dispatch paths use the same atomic commit/history. +The host requires this new export; older Wasm must be rebuilt. +`eval_at(start,length)` returns f64 or NaN on invalid/pure-expression failure; +must not mutate state or event diagnostics. `get_state(id)` returns f64 orNaN. +`info(id)` u32: 0 tick,1 last_error,2 error_pc (0xffffffff none),3 last_executed_ops, +4 revision,5 state_count,6 instruction_count,7 handler_count,8 input_count, +9 successful_event_count. `layout(id)` u32: 0 Engine reserved @sizeOf, +1 Program reserved @sizeOf,2 Instruction @sizeOf,3 Handler @sizeOf, +4 active used IR bytes (instructions+handlers),5 active state-value bytes, +6 staging reserved bytes (subset of Engine),7 evaluation stack capacity bytes. +`error_message` belongs to host map; core error codes documented in core.zig. +No allocation or imported runtime in Wasm. Fixed64KiB linear memory target with +16KiB stack; increase only if actual linker evidence requires and document it. + +## Host interface (web/runtime.mjs) + +`await createRuntime(wasmBytes?, evaluationOptions?)` fetches core.wasm in browser if omitted. +Returns object with apply(source), reset(), frame(count=1), down(name,source), +up(name,source), point(name,phase,x,y,source), releaseAll(), snapshot(), exportProject(), currentProgram. +Multiple sources per input share one logical held state; down emits only first +source, up only last. Source strings identify keyboard vs onscreen pointer. +`apply` returns compiled program or throws with diagnostic. It must fully validate +in host and stage+commit in core. It preserves by name. `reset` keeps active program. +`snapshot` returns {states:{name:value}, views:[{id,type,x,y,width?,height?,radius?, +value?,prefix?,color?}], tick,revision,lastError,errorLine,lastOps,memory:{...},held:[]} +`currentProgram` is the latest successfully applied compiled program or null. +The program and all nested metadata are frozen so readers cannot corrupt state +name mappings or binding ranges. `lastFailure` records the last failed event; +its `lastError`/line remain visible through successful release or focus cleanup +until successful apply or explicit reset. `lastEvent` still describes the latest +event. A view evaluation failure uses host error100 and does not roll back events. +UI draft and applied code are distinct. Browser/CLI share compiler/runtime/wasm; +instances are separate, not a live collaborative session. No arbitrary JS evaluation. + +## Bounded display evaluation (2026-09-22) + +The existing event and float semantics above are unchanged. The forgetful C +prototype's contract is adapted into `evaluation.zig`; its uint64 DAG is not +used as a substitute for this VM's float instructions. + +Each snapshot starts one display budget, shared by all bindings. Default options +are `{cacheEntries:0, historyEntries:8, workLimit:2048}`. A unit is a binding +request, a candidate-cache probe, a change-record probe, or an executed VM +instruction. State extraction, host object creation, event dispatch and DOM work +are outside this budget. It is not a time limit or a bound on whole-app work. +For the current compiler limits, 96 bindings * 10 units plus 512 instructions +is at most 1,472 units; the default admits even an uncached valid scene with +cache lookup/validation enabled. Malformed bindings may fail sooner. + +`configureEvaluation({cacheEntries?,historyEntries?,workLimit?})` accepts integers +0..32, 0..8, and 0..4294967295 respectively. Missing fields retain prior settings. +Valid configuration clears reuse records; invalid configuration changes nothing. +`forgetEvaluation('values'|'history'|'all')` drops optional records without +changing authored state, event diagnostics or program identity. +`evaluationStats()` returns the options plus work, instructions, cacheHits for +the last snapshot, and reservedBytes for the complete fixed evaluation structure. +Statistics are separate from snapshots so repeated observations remain equivalent. + +Memo entries contain the value, instruction range, input dependency bit mask +and validated revision. Direct mapping uses one candidate probe, with collisions +causing recomputation. Only successful computations are cached. Constant results +need no input-change history. All other reuse requires a complete retained +history since validation and no intersection with changed states. Dependencies +are collected from actual VM LOADs, not trusted compiler-supplied annotations. +History loss causes recomputation. All history and memo storage is fixed size; +setting capacities to zero does not return reserved linear memory. + +Only bitwise-different committed states advance change history (including signed +zero). Failed events and net-zero updates do not. Successful apply and reset +discard all reuse records. Parse/host validation failure before staging preserves +records. Beginning core staging discards them, including if that load is later +rejected or aborted. Epoch wrap also clears records. No inputs or effectful event +handlers are replayed to rebuild a binding. + +Budget exhaustion returns an empty views array for the entire snapshot, with +displayError `{code:101,line,message}`. Successfully committed event state remains. +There is no automatic retry or silent return of an old scene. Raising the budget +allows a fresh attempt. Arithmetic/size errors retain the earlier per-view error100 +behavior. `displayError` is null on display success; a latched execution error +still takes precedence in lastError/errorMessage, while displayError independently +reports display failure. Partial successful binding memos may survive a failed +snapshot but no partial scene is published. + +New core exports: `configure_evaluation(cache,history)` returns 0 or capacity1; +`forget_evaluation(flags)` bits 1=values, 2=history; `begin_evaluation(limit)`; +`eval_bounded(start,length)` returns f64 or NaN and consumes that shared budget. +`evaluation_info(id)` returns 0 status (10=work limit), 1 work, 2 instructions, +3 cache hits, 4 cache capacity, 5 history capacity, 6 reserved bytes. These +diagnostics do not replace event info. The unbounded pure `eval_at` remains for +reference checks and is not the host snapshot path. + +## Shared load/reuse workspace (2026-09-23) + +The pending definition/restore values and evaluation Tables occupy one untagged union. The active +Program, current state and evaluation control fields remain separate. All core +calls are single-threaded and synchronous; no callback can reenter a phase change. +No pointer to the reused tables is retained between calls. + +`begin_program` switches the workspace to staging and drops optional records. +Until commit/abort, old events and bindings still run against active. Binding +evaluation bypasses reuse and recomputes within its budget; cache configuration, +forget and reset must not write the staging payload. Old-id state preservation +reads the current values at successful commit, including events/reset during load. + +Successful commit publishes the new Program and then initializes empty Tables in +the shared workspace. `abort_program()` discards a pending definition and restores +empty Tables without changing active program/state/event diagnostics. Inactive +abort is a no-op. A failed commit leaves staging open for explicit abort or the +next begin. An invalid begin closes any previous pending load. Host apply aborts +after a core loading failure. Lost reuse may make a previously warm tiny budget +insufficient; neither a preserved cache nor completion at that budget is promised. + +`evaluation_info` additionally reports 7 dedicated control bytes, 8 shared Tables +bytes, 9 reuse available (false during load), and 10 union safety-tag overhead. +The host exposes these as dedicatedBytes, sharedBytes, reuseAvailable and +workspaceTagBytes. reservedBytes is control + Tables capacity, not incremental +Engine storage. The Tables capacity overlaps stagingReservedBytes and must not +be added to it. Before document restoration was added, the Wasm control was 48 B, shared Tables +1,152 B, union overhead 8 B. That Engine was 19,128 B, only 56 B larger than the +19,072 B pre-evaluation core; linear memory remains 65,536 B. No OS pages are +returned and this is not a measurement of browser/process memory. + +## Direct output publication (2026-09-23) + +`present(target)` evaluates the same bindings, in the same order and shared +budget as `snapshot()`, then calls a synchronous trusted output adapter: + +- `begin(program)`: the frozen current definition, or null at startup. +- `state(index, value)`: each current authored state, without a host state map. +- `view(index, valid)`: every declared view's publication status. +- `value(viewIndex, bindingName, value)`: each binding of a valid view only. +- `end(visibleCount)`: number of valid views. + +It returns the snapshot's diagnostics, held inputs, event information and memory +report, without `states` or `views`. Evaluation finishes before the first adapter +callback. On budget error101 every view is invalid and no binding values are +published; committed state values are still published. Error100 invalidates only +the affected views, as before. No stale value is silently substituted. + +Runtime mutations, `snapshot()` and nested `present()` are rejected during +callbacks. Read-only program/export/statistics access remains available. Adapters +must be synchronous and must not throw. If one does throw, the exception +propagates and the runtime unlocks in a finally block; arbitrary external adapter +effects cannot be rolled back. A later call republishes every value. This is not +a transaction across arbitrary output devices or an untrusted plugin boundary. + +Evaluation uses a private reusable Float64Array(96) and Uint8Array(24), allocated +on first snapshot/presentation. Their payload totals 792 host bytes, outside the +64 KiB Wasm memory. Typed-array object overhead, binding-entry arrays built at +apply, and DOM destination references are additional host memory. The numeric +buffer is a publication barrier, not another authoritative state or public +mutable scene. `snapshot()` remains an independent object representation for +CLI/export/debugging, built only when explicitly requested. + +The browser calls `present()` for normal refreshes. Its output adapter binds DOM +destinations once per successful definition change and compares values with the +existing DOM before writing. Input refreshes reuse scene, state and cost elements. +Invalid views remain allocated but are hidden using SVG `display="none"`; recovery +reveals them. A definition change can replace all scene/state destinations. This +does not skip unchanged binding evaluation, eliminate all host allocations, or +provide measured process-memory/paint-latency savings. + +## Authored direct manipulation (2026-09-23) + +`input drag = pointer(tile)` binds a logical input to a declared view, including a +forward declaration. One input per target; it shares the existing 8-input limit +with keyboard inputs. `on drag.down/move/up/cancel` handlers may read `event_x` +and `event_y` in the same 640x360 world coordinates as output. They are unavailable +in keyboard/frame handlers and display bindings; copy to authored state to retain +them. There is no built-in drag constraint, snap, grab offset, or gesture rollback. +The direct.pure example authors these using ordinary assignments and expressions. + +Host `point` requires a pointer input, valid phase, finite coordinates and nonempty +source string. Each logical input has one owning source; different targets may +have independent active contacts. Same-owner repeated down is a no-op; another +owner's down throws. Move/up/cancel without that owner are no-ops. Down establishes +ownership before dispatch; up/cancel release it before dispatch, even if authored +arithmetic fails. An event failure rolls back that event's state writes, not the +whole gesture. `releaseAll` cancels owned pointers at their last delivered position, +then releases ordinary buttons. `reset` directly clears ownership and resets state. +Last point events include x,y. A failed cancel can leave authored state unchanged, +but cannot strand host ownership; the failure remains visible until reset/apply. + +Browser hit testing selects the declared target on primary-button pointerdown. +The stable SVG root captures that pointer, including movement/release outside the +scene. Client coordinates are transformed through the inverse SVG screen matrix. +Pointerup exposes its own final coordinates to authored handlers. The direct.pure +example updates position only on move and ends dragging on up, preserving the +last displayed position even if a host repeats stale coordinates on release. Termination is observed in the window capture phase, including releases outside +the SVG. A primary mouseup can finish an owned mouse gesture if pointerup was +not delivered; it cannot finish a touch gesture. Duplicate termination is ignored. +Only capture loss of the SVG root ends its owner, not a bubbled descendant event. +Pointercancel, lostpointercapture and +focus loss use the last delivered position and dispatch cancel, never fake up. +Missing/non-invertible coordinate transforms cancel an active gesture. A second +contact on the same target is ignored; contacts on different targets are independent. +Touch scenes disable native panning on the SVG. Mouse/touch share this adapter; +pen-specific pressure, tilt and button meanings are not implemented or validated. + +Every delivered pointermove executes synchronously; this layer adds no event queue, +coalescing, frame gating or newest-only drop policy. This is not a guarantee that +browsers expose every hardware sample. Keyboard commands and pointer coordinates +enter the same authored state/commit/output path. CLI `point NAME PHASE X Y` uses +that host API and core with source `terminal`; it does not replay browser hit testing. + + +## Placement documents (2026-09-23) + +Code-only version1 JSON and .pure files keep their earlier behavior. Version2 +stores a restorable authored document, not a suspended execution image: + +```json +{"format":"pure-workbench","version":2,"source":"state x=80","draft":"state x=80", + "states":[{"name":"x","value":350}]} +``` + +`source` is the applied definition; optional `draft` is separate unapplied editor +text (default source). Each is at most 32768 UTF-8 bytes. Draft syntax may be +unfinished. State records must contain exactly one finite value for every declared +state name, with no extra, missing or duplicate names. Record order is irrelevant. +JSON numbers preserve finite f64 values, except negative zero which is encoded as +the exact string `"-0"`. No other numeric string is accepted. Input latches, running +clock, diagnostics, caches, DOM, compiled IR and external resources are not saved. +The serialized input file limit is 401408 bytes, including escaping/metadata. + +`exportDocument(draft?)` returns an independent version2 object. It refuses an +unapplied runtime, held input or latched execution failure. It reads core state, +without constructing an output snapshot. `restoreDocument(document)` validates +and compiles before staging, refuses held input, then commits the definition and +all saved values together. It returns `{source,draft}`. Failed validation leaves +active program/values/diagnostics intact. A failed core load is aborted; optional +reuse records may be lost as with ordinary apply. Successful restore discards old +reuse, clears diagnostics, resets tick/event count and starts with no held inputs. +The original source initializers remain intact: explicit reset restores those, +not saved values. Existing `apply` continues to preserve current values by name. +Both new document APIs reject reentry during output publication. + +Core exports `put_restored_value(id,value)` and `commit_restored_program()` extend +the existing begin/put/commit loader. Every declared state needs exactly one finite +restored value; duplicate/out-of-range/nonfinite input poisons staging. Restored +commit checks completeness and program validity before touching active state. +Ordinary commit rejects staged restored values. Pending values live alongside the +pending Program in the same union as optional evaluation Tables; active Program +layout does not grow. `layout(6)` now includes Pending, rather than Program only. +The current Wasm Pending is 9656 B (264 B added for 32 values, mask and alignment), +Engine is 19392 B; linear memory remains fixed at 65536 B. These are build-layout +facts, not a browser RAM or performance improvement claim. `evaluation_info(10)` +continues to report only union tag overhead, excluding restoration payload. + +Browser **배치 저장** stores applied source, all current values and editor draft. +It requires a stopped clock. **파일 열기** restores version2 after replacement +confirmation when existing work is present; it starts stopped. Version1/plain +source still only replaces the editor draft. Async reading checks draft generation, +program identity and successful event count so intervening edits/inputs cannot be +overwritten by a stale read. Invalid files do not alter the working document. +**.pure 저장 / 코드 JSON 저장** remain draft-only exports. + +CLI `save-project PATH` writes version2 with exclusive creation; `restore PATH` +restores it. Loading a .json path (including at startup) distinguishes versions. +A browser draft is retained through CLI save-project, but never applied implicitly. +Existing `save PATH` remains applied-source-only. No filesystem overwrite is added. diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/cli.mjs b/prototypes/workbench/results/2026-09-23-layout/sources/cli.mjs new file mode 100644 index 0000000..78e0b49 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/cli.mjs @@ -0,0 +1,93 @@ +#!/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'; + +const help = `Pure authoring workbench · 같은 정의와 Zig 실행기 + load PATH .pure 파일을 적용 (같은 이름의 상태 보존) + down NAME 정의한 입력 누르기 + up NAME 정의한 입력 놓기 + point NAME PHASE X Y 좌표 입력 (down / move / up / cancel) + 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 저장한 배치를 복원 (초안이 있으면 파일에 보존) + 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; + 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 === '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 === '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/workbench/results/2026-09-23-layout/sources/cli.test.mjs b/prototypes/workbench/results/2026-09-23-layout/sources/cli.test.mjs new file mode 100644 index 0000000..f355d2d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/cli.test.mjs @@ -0,0 +1,121 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => { + const file = join(root, 'examples/direct.pure'); + const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]]) + runtime.point('drag', phase, x, y, 'terminal'); + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert.equal(result.snapshots.at(-1).states.x, 350); + assert.equal(result.snapshots.at(-1).states.y, 210); + assert.deepEqual(result.snapshots.at(-1).held, []); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); + +test('terminal saves and restores placement independently of reset and preserves a browser draft', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-layout-cli-')); + t.after(() => rm(directory, {recursive:true,force:true})); + const saved=join(directory,'layout.json'), copied=join(directory,'copy.json'); + const first=run(`point drag down 90 170\npoint drag move 360 220\npoint drag up 90 170\nsave-project ${saved}\nreset\nrestore ${saved}\nshow\nquit\n`,join(root,'examples/direct.pure')); + assert.equal(first.status,0,first.stderr);assert.equal(first.snapshots.at(-1).states.x,350); + const document=JSON.parse(await readFile(saved,'utf8'));document.draft='state incomplete ='; + await writeFile(saved,JSON.stringify(document)); + const second=run(`save-project ${copied}\nreset\nshow\nquit\n`,saved); + assert.equal(second.status,0,second.stderr);assert.equal(second.snapshots.at(-1).states.x,80); + const copy=JSON.parse(await readFile(copied,'utf8'));assert.equal(copy.draft,document.draft);assert.deepEqual(copy.states,document.states); + const rejected=run(`save-project ${saved}\nquit\n`,saved); + assert.equal(rejected.status,1);assert.match(rejected.stderr,/EEXIST/); +}); + +test('invalid restoration leaves terminal code and state unchanged', async t => { + const directory=await mkdtemp(join(tmpdir(),'pure-invalid-layout-')); + t.after(()=>rm(directory,{recursive:true,force:true})); + const file=join(directory,'bad.json'); + await writeFile(file,JSON.stringify({format:'pure-workbench',version:2,source:'state x=1',states:[]})); + const result=run(`show\nrestore ${file}\nshow\nquit\n`,join(root,'examples/counter.pure')); + assert.equal(result.status,1);assert.deepEqual(result.snapshots[0],result.snapshots[1]); +}); diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/core.zig b/prototypes/workbench/results/2026-09-23-layout/sources/core.zig new file mode 100644 index 0000000..1154297 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/core.zig @@ -0,0 +1,1136 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { event: u32 = 0, start: u32 = 0, length: u32 = 0 }; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +const Workspace = union { staging: Pending, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.program.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.program.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.program.initials[id] = initial; + engine.workspace.staging.program.old_ids[id] = old_id; + engine.workspace.staging.program.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 23) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) return .invalid; + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 23) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + break; + } + } + const h = owner orelse return .invalid; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + } + } + return .ok; +} + +export fn put_restored_value(id: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const pending = &engine.workspace.staging; + if (id >= pending.program.state_count) return stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return stagingFail(.bad_state); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; +} + +export fn commit_program() u32 { + return commit(false); +} + +export fn commit_restored_program() u32 { + return commit(true); +} + +fn commit(restoring: bool) u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const count = engine.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (engine.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return stagingFail(.bad_state); + const code = validate(&engine.workspace.staging.program); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.program.state_count) |i| { + const old_id = engine.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) engine.workspace.staging.values[i] else if (old_id < 0) engine.workspace.staging.program.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging.program; + engine.values = next; + finishStaging(); + if (restoring) { + engine.tick = 0; + engine.successful_events = 0; + } + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, point: bool = false }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23 => 0, + 7, 16, 20, 21 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20) { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[inst.arg] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + (context.point and event == 0) or (!context.point and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + const result = execute(&pending, h.start, h.length, context, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 12), layout(3)); + try std.testing.expectEqual(@as(u32, 76), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/evaluation.zig b/prototypes/workbench/results/2026-09-23-layout/sources/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/examples/layout.pure b/prototypes/workbench/results/2026-09-23-layout/sources/examples/layout.pure new file mode 100644 index 0000000..41cf149 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/examples/layout.pure @@ -0,0 +1,92 @@ +# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요. +# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다. +state selected = 0 +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +state x1 = 80 +state y1 = 120 +state ox1 = 0 +state oy1 = 0 +state sx1 = 80 +state sy1 = 120 +state held1 = 0 +input drag1 = pointer(tile1) +state x2 = 280 +state y2 = 160 +state ox2 = 0 +state oy2 = 0 +state sx2 = 280 +state sy2 = 160 +state held2 = 0 +input drag2 = pointer(tile2) +state x3 = 450 +state y3 = 220 +state ox3 = 0 +state oy3 = 0 +state sx3 = 450 +state sy3 = 220 +state held3 = 0 +input drag3 = pointer(tile3) +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box tile1(x1,y1,56,44,"#c9f08b") +box tile2(x2,y2,56,44,"#74c4d4") +box tile3(x3,y3,56,44,"#dfb184") +on drag1.down { + selected = 1 + sx1 = x1; sy1 = y1 + ox1 = event_x - x1; oy1 = event_y - y1 + held1 = 1 +} +on drag1.move { + x1 = clamp(event_x - ox1, 20, 564) + y1 = clamp(event_y - oy1, 90, 286) +} +on drag1.up { held1 = 0 } +on drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 } +on drag2.down { + selected = 2 + sx2 = x2; sy2 = y2 + ox2 = event_x - x2; oy2 = event_y - y2 + held2 = 1 +} +on drag2.move { + x2 = clamp(event_x - ox2, 20, 564) + y2 = clamp(event_y - oy2, 90, 286) +} +on drag2.up { held2 = 0 } +on drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 } +on drag3.down { + selected = 3 + sx3 = x3; sy3 = y3 + ox3 = event_x - x3; oy3 = event_y - y3 + held3 = 1 +} +on drag3.move { + x3 = clamp(event_x - ox3, 20, 564) + y3 = clamp(event_y - oy3, 90, 286) +} +on drag3.up { held3 = 0 } +on drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 } +on left.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) } +} +on right.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) } +} +on up.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) } +} +on down.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) } +} diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/project.test.mjs b/prototypes/workbench/results/2026-09-23-layout/sources/project.test.mjs new file mode 100644 index 0000000..ad8a48d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/project.test.mjs @@ -0,0 +1,79 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {createRuntime} from './web/runtime.mjs'; +import {parseProject, MAX_PROJECT_BYTES} from './web/project.mjs'; +const wasm = await readFile(new URL('./web/core.wasm',import.meta.url)); +const layout = await readFile(new URL('./examples/layout.pure',import.meta.url),'utf8'); +const fresh = () => createRuntime(wasm,{cacheEntries:32}); +const roundTrip = value => parseProject(JSON.stringify(value)); + +test('multiple authored placements and a separate invalid draft survive a fresh runtime restore',async()=>{ + const a=await fresh(); a.apply(layout); + for(const [name,x,y,nx,ny] of [['drag1',90,130,180,200],['drag2',290,170,370,210],['drag3',460,230,520,280]]) { + a.point(name,'down',x,y);a.point(name,'move',nx,ny);a.point(name,'up',x,y); + } + a.down('left');a.up('left'); + const before=a.snapshot(), document=roundTrip(a.exportDocument('state unfinished =')); + assert.equal(document.version,2);assert.equal(document.source,layout); + const b=await fresh(); b.apply('state old=99'); b.frame(5); + const restored=b.restoreDocument(document), after=b.snapshot(); + assert.equal(restored.draft,'state unfinished =');assert.equal(restored.source,layout); + assert.deepEqual(after.states,before.states);assert.deepEqual(after.views,before.views); + assert.equal(after.tick,0);assert.equal(after.successfulEvents,0);assert.equal(after.lastEvent,null);assert.deepEqual(after.held,[]); + b.down('right');b.up('right');assert.equal(b.snapshot().states.x3,before.states.x3+10); + b.reset();assert.equal(b.snapshot().states.x1,80);assert.equal(b.snapshot().states.x2,280);assert.equal(b.snapshot().states.x3,450); +}); + +test('malformed files reject before changing active code, values, diagnostics or warm display records',async()=>{ + const r=await fresh();r.apply('state x=5\ntext t(0,0,x)');r.snapshot(); + const good=r.exportDocument(), before=r.snapshot(), active=r.currentProgram; + const bad=[null,{}, {...good,version:99},{...good,draft:null},{...good,source:'state x='},{...good,states:[]}, + {...good,states:[{name:'other',value:2}]},{...good,states:[{name:'x',value:null}]}, + {...good,states:[{name:'x',value:'2'}]},{...good,states:[{name:'x',value:Infinity}]}, + {...good,states:[{name:'x',value:NaN}]},{...good,draft:'x'.repeat(32769)}, + {...good,source:'state x=0\nstate y=0',states:[{name:'x',value:1},{name:'x',value:2}]}]; + for(const doc of bad){assert.throws(()=>r.restoreDocument(doc));assert.equal(r.currentProgram,active);assert.deepEqual(r.snapshot(),before);} + assert(r.evaluationStats().cacheHits>0); + assert.throws(()=>parseProject('{'));assert.throws(()=>parseProject(' '.repeat(MAX_PROJECT_BYTES+1))); +}); + +test('restoration matches names rather than ordering and never reuses stale cached values',async()=>{ + const r=await fresh();r.apply('state x=1\nstate y=2\ntext t(0,0,x+y)');r.snapshot();r.snapshot(); + const doc=r.exportDocument();doc.states=[{name:'y',value:20},{name:'x',value:10}]; + r.restoreDocument(roundTrip(doc));assert.deepEqual(r.snapshot().states,{x:10,y:20});assert.equal(r.snapshot().views[0].value,30); + r.apply('state y=200\nstate x=100');assert.deepEqual(r.snapshot().states,{y:20,x:10}); + r.reset();assert.deepEqual(r.snapshot().states,{y:200,x:100}); +}); + +test('finite float values including negative zero round trip without changing authored reset values',async()=>{ + const r=await fresh();r.apply('state z=-0\nstate tiny=5e-324\nstate large=1.7976931348623157e308\nstate constructor=1'); + const doc=roundTrip(r.exportDocument());assert.equal(doc.states[0].value,'-0'); + r.restoreDocument(doc);const s=r.snapshot().states; + assert(Object.is(s.z,-0));assert.equal(s.tiny,Number.MIN_VALUE);assert.equal(s.large,Number.MAX_VALUE);assert.equal(s.constructor,1); +}); + +test('held and failed interactions cannot be saved and restoration cannot steal an active input',async()=>{ + const r=await fresh();r.apply(layout);const saved=r.exportDocument(); + r.point('drag1','down',90,130);const before=r.snapshot(); + assert.throws(()=>r.exportDocument(),/조작/);assert.throws(()=>r.restoreDocument(saved),/놓은 뒤/);assert.deepEqual(r.snapshot(),before); + r.releaseAll();r.apply('state x=0\ninput fail=Space\non fail.down { x=1/0 }'); + r.down('fail');r.up('fail');assert.throws(()=>r.exportDocument(),/오류/); + r.restoreDocument(saved);assert.equal(r.snapshot().lastError,0); +}); + +test('empty and full documents restore without imposing a new source size or state count',async()=>{ + const r=await fresh();r.apply('');r.restoreDocument(roundTrip(r.exportDocument()));assert.deepEqual(r.snapshot().states,{}); + r.apply(Array.from({length:32},(_,i)=>`state s${i}=${i}`).join('\n'));const doc=r.exportDocument(); + for(const s of doc.states) s.value+=100; + r.restoreDocument(roundTrip(doc));assert.equal(r.snapshot().states.s31,131); + assert.equal(parseProject(JSON.stringify({format:'pure-workbench',version:1,source:'state unfinished ='})).version,1); + assert.throws(()=>r.restoreDocument({format:'pure-workbench',version:1,source:''})); +}); + +test('document APIs respect the output callback boundary and export independent values',async()=>{ + const r=await fresh();r.apply('state x=1');const doc=r.exportDocument();doc.states[0].value=500; + assert.equal(r.snapshot().states.x,1); + r.present({begin(){assert.throws(()=>r.restoreDocument(doc),/출력 전달/);assert.throws(()=>r.exportDocument(),/출력 전달/);},state(){},view(){},value(){},end(){}}); + assert.equal(r.snapshot().states.x,1); +}); diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/qa-layout.js b/prototypes/workbench/results/2026-09-23-layout/sources/qa-layout.js new file mode 100644 index 0000000..95500ce --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/qa-layout.js @@ -0,0 +1,103 @@ +// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states.x1-150)<0.1&&Math.abs(s.states.x2-350)<0.1&&Math.abs(s.states.x3-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-layout-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states.x1===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states.x3-(expected.states.x3+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-layout-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-layout-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureLayoutEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureLayoutEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/web/app.js b/prototypes/workbench/results/2026-09-23-layout/sources/web/app.js new file mode 100644 index 0000000..e163ebc --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/web/app.js @@ -0,0 +1,403 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; +import { bindSceneInput } from './scene-input.mjs'; +import { parseProject, MAX_PROJECT_BYTES } from './project.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +let draftGeneration = 0; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); +const sceneInput = bindSceneInput($('scene'), () => runtime, safeAction); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +// DOM is the published value. Avoid rewriting unchanged diagnostics/controls. +function text(id, value) { + const node = $(id); + if (node.textContent !== value) node.textContent = value; +} +function pressed(node, value) { + const next = String(value); + if (node.getAttribute('aria-pressed') !== next) node.setAttribute('aria-pressed', next); +} +function draftStatus() { + const changed = dirty(); + text('draft-status', changed ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'); + $('draft-status').classList.toggle('dirty', changed); +} + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + text('cursor-position', `${lines.length}행 · ${lines.at(-1).length + 1}열`); +} + +function draftChanged() { + draftGeneration++; + // Read scroll position before writes; this path runs only for source edits. + const scrollTop = source.scrollTop; + const count = source.value.split('\n').length; + const size = bytes(source.value); + text('line-numbers', Array.from({ length: count }, (_, i) => i + 1).join('\n')); + $('line-numbers').scrollTop = scrollTop; + text('source-size', `${size.toLocaleString('en-US')} / 32,768 bytes`); + $('source-size').style.color = size > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + draftStatus(); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + text('play', '▶ 실행'); + pressed($('play'), false); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + sceneInput.releaseCaptures(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + pressed(button, held.has(name)); + } + text('scene-status', runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'); + text('clock-status', `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`); + text('revision', `revision ${snapshot.revision || 0}`); + for (const id of ['play', 'step', 'reset', 'download-layout']) { + if ($(id).disabled !== !runtime.currentProgram) $(id).disabled = !runtime.currentProgram; + } + const event = snapshot.lastEvent; + text('last-event', event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'); + if ($('runtime-error').hidden !== !snapshot.lastError) $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + let message = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + message += ` · ${snapshot.displayError.message}`; + text('runtime-error', message); + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + if (input.target) { + const note = document.createElement('p'); + note.className = 'empty-note'; + note.textContent = `${input.name} · 장면의 ${input.target}을 잡아 조작하세요.`; + children.push(note); + continue; + } + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + draftStatus(); + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime, message = '편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.') { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic(message); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('download-layout').addEventListener('click', () => { + try { + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 저장하세요.'); + const project = runtime.exportDocument(source.value); + download('my-layout.pure.json', JSON.stringify(project, null, 2) + '\n', 'application/json', + '적용된 코드·현재 배치·미적용 초안을 함께 저장했습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_PROJECT_BYTES) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = draftGeneration; + const active = runtime?.currentProgram; + const events = runtime?.snapshot().successfulEvents; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + const project = parseProject(content); + if (project.version === 2) { + if (!runtime) throw new Error('실행기가 준비되지 않았습니다.'); + if (draftGeneration !== before || runtime.currentProgram !== active || runtime.snapshot().successfulEvents !== events) + throw new Error('파일을 읽는 동안 작업이 바뀌었습니다. 정지한 뒤 다시 불러오세요.'); + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 복원하세요.'); + if ((source.value || appliedSource) && !window.confirm('현재 코드와 배치를 파일의 내용으로 복원할까요? 저장하지 않은 작업은 사라집니다.')) return; + const restored = runtime.restoreDocument(project); + pause(); droppedTicks = 0; + appliedSource = restored.source; + rebuildInputControls(); + replaceDraft(restored.draft, '배치를 복원했습니다. 미적용 초안도 보존했고, 시간은 정지 상태입니다.'); + refresh(); + return; + } + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (draftGeneration !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/web/core.wasm b/prototypes/workbench/results/2026-09-23-layout/sources/web/core.wasm new file mode 100644 index 0000000..f657ed7 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-layout/sources/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/web/index.html b/prototypes/workbench/results/2026-09-23-layout/sources/web/index.html new file mode 100644 index 0000000..bb42768 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/web/index.html @@ -0,0 +1,94 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

직접 조작
input drag = pointer(tile)로 도형에 입력을 연결합니다. on drag.down / .move / .up / .cancel에서 event_x, event_y는 장면 좌표입니다. 이동 방식과 취소 시 동작도 코드로 정합니다. 초점이나 포인터 캡처를 잃으면 .cancel이 전달됩니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/web/project.mjs b/prototypes/workbench/results/2026-09-23-layout/sources/web/project.mjs new file mode 100644 index 0000000..0df2711 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/web/project.mjs @@ -0,0 +1,45 @@ +import { compile } from './compiler.mjs'; + +export const MAX_PROJECT_BYTES = 2 * 32768 * 6 + 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/workbench/results/2026-09-23-layout/sources/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-layout/sources/web/runtime.mjs new file mode 100644 index 0000000..8a9fdce --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/web/runtime.mjs @@ -0,0 +1,324 @@ +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') + 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 = []; + // 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) { + const status = point ? core.dispatch_point(event, 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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), + }, + }; + } + + 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 handler of next.handlers) checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + checked(restored ? core.commit_restored_program() : core.commit_program()); + } catch (error) { + core.abort_program(); + throw error; + } + 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].target) 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].target) 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); + }, + point(name, phase, x, y, source = 'external') { + available(); + const index = inputIndex(name); + if (!program.inputs[index].target) throw new Error('포인터에 연결된 입력이 아닙니다.'); + 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(name); + if (phase === 'down') { + if (point?.source === source) return 0; + if (point) throw new Error('이 입력은 다른 포인터가 사용 중입니다.'); + point = {source, x, y}; + points.set(name, point); + held.set(name, new Set([source])); + } else { + if (!point || point.source !== source) return 0; + point.x = x; point.y = y; + if (phase === 'up' || phase === 'cancel') { points.delete(name); held.delete(name); } + } + 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 [name, point] of [...points]) { + const status = api.point(name, 'cancel', point.x, point.y, point.source); + 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/workbench/results/2026-09-23-layout/sources/web/scene-input.mjs b/prototypes/workbench/results/2026-09-23-layout/sources/web/scene-input.mjs new file mode 100644 index 0000000..0f7cbef --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/web/scene-input.mjs @@ -0,0 +1,83 @@ +// This adapter owns browser capture only. Authored state changes stay in the VM. +export function bindSceneInput(scene, runtime, act) { + const captures = new Map(); + const events = scene.ownerDocument.defaultView; + const coordinates = event => { + const matrix = scene.getScreenCTM(); + if (!matrix) return null; + try { + const p = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse()); + return Number.isFinite(p.x) && Number.isFinite(p.y) ? p : null; + } catch { return null; } + }; + const release = id => { + if (scene.hasPointerCapture(id)) scene.releasePointerCapture(id); + }; + function finish(event, phase, pointerId = event.pointerId) { + const entry = captures.get(pointerId); + if (!entry) return; + event.preventDefault(); + // Capture loss/cancel may not carry a meaningful position. Use the last + // delivered coordinates. Up must use its own final position. + const p = phase === 'up' ? coordinates(event) : null; + if (p) { entry.x = p.x; entry.y = p.y; } + if (phase === 'up' && !p) phase = 'cancel'; + captures.delete(pointerId); + release(pointerId); + act(() => runtime().point(entry.name, phase, entry.x, entry.y, entry.source)); + } + scene.addEventListener('pointerdown', event => { + if (event.button !== 0 || !runtime() || captures.has(event.pointerId)) return; + const target = event.target.closest?.('[data-point-input]'); + if (!target || !scene.contains(target)) return; + const name = target.getAttribute('data-point-input'); + // A logical input has one coordinate owner. A second contact on it must not + // steal the gesture; other authored targets can be manipulated independently. + if ([...captures.values()].some(entry => entry.name === name)) return; + const p = coordinates(event); + if (!p) return; + event.preventDefault(); scene.focus(); + act(() => { + scene.setPointerCapture(event.pointerId); + const entry = {name, source:`scene:${event.pointerId}`, pointerType:event.pointerType, x:p.x, y:p.y}; + captures.set(event.pointerId, entry); + try { runtime().point(name, 'down', p.x, p.y, entry.source); } + catch (error) { captures.delete(event.pointerId); release(event.pointerId); throw error; } + }); + }); + scene.addEventListener('pointermove', event => { + const entry = captures.get(event.pointerId); + if (!entry) return; + const p = coordinates(event); + if (!p) { finish(event, 'cancel'); return; } + event.preventDefault(); + entry.x = p.x; entry.y = p.y; + act(() => runtime().point(entry.name, 'move', p.x, p.y, entry.source)); + }); + // A host may deliver release outside the SVG even while it owns capture. + // Observe termination before target handlers or capture cleanup can cancel it. + events.addEventListener('pointerup', event => finish(event, 'up'), true); + events.addEventListener('pointercancel', event => finish(event, 'cancel'), true); + events.addEventListener('mouseup', event => { + if (event.button !== 0) return; + // Mouse-only fallback when the host delivers mouseup without pointerup. + // A normal pointerup already removed the owner; touch compatibility events + // must never finish a different contact. + for (const [id, entry] of captures) { + if (entry.pointerType === 'mouse') finish(event, 'up', id); + } + }, true); + scene.addEventListener('lostpointercapture', event => { + // Descendant capture transitions bubble here but do not release our owner. + if (event.target === scene) finish(event, 'cancel'); + }); + return Object.freeze({ + releaseCaptures() { + const ids = [...captures.keys()]; + captures.clear(); + for (const id of ids) release(id); + // The caller subsequently invokes runtime.releaseAll(), which dispatches + // each pending cancel once. Clearing first makes lostcapture harmless. + }, + }); +} diff --git a/prototypes/workbench/results/2026-09-23-layout/sources/workspace.test.mjs b/prototypes/workbench/results/2026-09-23-layout/sources/workspace.test.mjs new file mode 100644 index 0000000..285080b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/sources/workspace.test.mjs @@ -0,0 +1,89 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = factor => `state x = 10 +input next = KeyN +text label(0, 0, x * ${factor}) +on next.down { x = x + 0.25 } +`; + +function begin(core, program, preserve) { + assert.equal(core.begin_program(program.states.length, program.inputs.length), 0); + program.states.forEach((state, i) => assert.equal(core.put_state(i, state.initial, preserve ? i : -1), 0)); +} + +function finish(core, program, during = () => {}) { + for (const op of program.instructions) { + assert.equal(core.put_instruction(op.op, op.arg, op.value), 0); + during(); + } + for (const handler of program.handlers) + assert.equal(core.put_handler(handler.event, handler.start, handler.length), 0); + assert.equal(core.commit_program(), 0); +} + +function observe(core, binding) { + core.begin_evaluation(100); + const value = core.eval_bounded(binding.start, binding.length); + assert.equal(core.evaluation_info(0), 0); + return value; +} + +test('Wasm executes old events and output during staged replacement without corrupting either phase', async () => { + const {instance:{exports:core}} = await WebAssembly.instantiate(wasm); + core.reset_all(); + const old = compile(source(2)), next = compile(source(3)); + begin(core, old, false); finish(core, old); + assert.equal(core.configure_evaluation(32, 8), 0); + const binding = old.views[0].bindings.value; + assert.equal(observe(core, binding), 20); + assert.equal(observe(core, binding), 20); + assert.equal(core.evaluation_info(3), 1); + begin(core, next, true); + let value = 10; + finish(core, next, () => { + assert.equal(core.evaluation_info(9), 0); + assert.equal(core.dispatch(1, 0), 0); value += 0.25; + assert.equal(core.configure_evaluation(32, 8), 0); + core.forget_evaluation(3); + assert.equal(observe(core, binding), value * 2); + assert.equal(core.evaluation_info(3), 0); + }); + assert.equal(core.evaluation_info(9), 1); + assert.equal(core.get_state(0), value); + const replaced = next.views[0].bindings.value; + assert.equal(observe(core, replaced), value * 3); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.evaluation_info(3), 1); + + const revision = core.info(4); + begin(core, old, true); + assert.equal(core.put_instruction(0, 0, Infinity), 5); + assert.equal(core.commit_program(), 5); + assert.equal(observe(core, replaced), value * 3); + core.abort_program(); + assert.equal(core.info(4), revision); + assert.equal(core.get_state(0), value); + assert.equal(core.evaluation_info(9), 1); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.memory.buffer.byteLength, 65536); +}); + +test('workspace accounting reports shared payload and safety tag without double-counting', async () => { + const runtime = await createRuntime(wasm, {cacheEntries:32}); + runtime.apply(source(2)); runtime.snapshot(); + const stats = runtime.evaluationStats(), memory = runtime.snapshot().memory; + assert.equal(stats.dedicatedBytes, 48); + assert.equal(stats.sharedBytes, 1152); + assert.equal(stats.workspaceTagBytes, 8); + assert.equal(stats.reservedBytes, stats.dedicatedBytes + stats.sharedBytes); + const restoreStorage = memory.stagingReservedBytes - memory.programReservedBytes; + assert.equal(restoreStorage, 264); // 32 values + mask/alignment, only in pending storage. + assert.equal(memory.reservedEngineBytes, 19072 + stats.dedicatedBytes + stats.workspaceTagBytes + restoreStorage); + assert(stats.sharedBytes < memory.stagingReservedBytes); + assert.equal(stats.reuseAvailable, true); +}); diff --git a/prototypes/workbench/results/2026-09-23-layout/verification.json b/prototypes/workbench/results/2026-09-23-layout/verification.json new file mode 100644 index 0000000..a021b9a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-layout/verification.json @@ -0,0 +1,56 @@ +[ + { + "file": "prototypes/workbench/baseline.test.mjs", + "exitCode": 0, + "stdout": "\u2714 reference begins with the authored states and matching scene (2.390107ms)\n\u2714 press, hold, release, and frame order produce exact expected values (0.529425ms)\n\u2714 charge clamps at one and explicit floor restores rest (1.33572ms)\n\u2714 repeat and concurrent physical sources produce only logical edges (0.417351ms)\n\u2714 airborne presses cannot charge or cause another jump (0.245111ms)\n\u2714 releaseAll emits release and reset clears latches without a jump (0.408129ms)\n\u2714 unknown inputs and invalid frame counts reject without mutation (0.728128ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 87.76119\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/cli.test.mjs", + "exitCode": 0, + "stdout": "\u2714 terminal starts empty and produces the same charge trace as the shared runtime (147.899406ms)\n\u2714 invalid terminal commands do not alter state and fail piped execution (51.409953ms)\n\u2714 terminal replays authored pointer movement and cancellation through the shared runtime (58.411904ms)\n\u2714 file load preserves by name, rejected load preserves active work, and save never overwrites (54.468837ms)\n\u2714 terminal exposes arithmetic rollback and retains failure through matching release (53.599468ms)\n\u2714 terminal configures and discards evaluation records and recovers from a display limit (50.104196ms)\n\u2714 terminal saves and restores placement independently of reset and preserves a browser draft (166.566375ms)\n\u2714 invalid restoration leaves terminal code and state unchanged (54.509559ms)\n\u2139 tests 8\n\u2139 suites 0\n\u2139 pass 8\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 711.531406\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/compiler.test.mjs", + "exitCode": 0, + "stdout": "\u2714 a blank document is an empty program with no implied behavior (4.889714ms)\n\u2714 forward references and separate state/input namespaces preserve declaration IDs (1.963422ms)\n\u2714 ordered assignments, repeated handlers, and condition evaluation once (0.880387ms)\n\u2714 nested branches use absolute forward targets and skip correct bodies (0.584591ms)\n\u2714 operator precedence, associativity, unary and builtin function lowering (1.469961ms)\n\u2714 multiline parentheses, scientific literals, comments, and string escapes (0.657092ms)\n\u2714 all instruction fields are numeric and carry original source line (1.405072ms)\n\u2714 contextual declaration words remain valid names outside reserved builtins (0.478295ms)\n\u2714 unknown and reserved symbols have useful one-based locations (1.539498ms)\n\u2714 duplicates, unsupported keys, initializers, and invalid syntax are rejected (1.273683ms)\n\u2714 document, declaration, instruction, and nesting limits reject overflow (6.691063ms)\n\u2714 every supported input maps to a bounded ordered event ID (0.683203ms)\n\u2714 optional examples compile and charge behavior is entirely authored (19.247691ms)\n\u2139 tests 13\n\u2139 suites 0\n\u2139 pass 13\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 112.656204\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/evaluation.test.mjs", + "exitCode": 0, + "stdout": "\u2714 cache reuse, unrelated changes and discarded records preserve authored values (19.031877ms)\n\u2714 committed changes only: failed events and failed edits preserve reusable output (2.886995ms)\n\u2714 program edits, state reordering and reset cannot reuse old definitions (3.726605ms)\n\u2714 finite float semantics include signed zero, high state bit and arithmetic errors (4.550215ms)\n\u2714 one budget covers the whole scene; failures publish no partial scene or state rollback (1.971312ms)\n\u2714 small caches, no history and every small work limit give exact results or explicit failure (26.454123ms)\n\u2714 invalid settings and forget names preserve policy and state (2.972995ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 136.239388\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/pointer.test.mjs", + "exitCode": 0, + "stdout": "\u2714 pointer declarations resolve forward targets and reject ambiguous or contextless inputs (9.252003ms)\n\u2714 authored drag keeps grab offset and commits the displayed position even if release coordinates differ (10.117335ms)\n\u2714 ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input (4.090548ms)\n\u2714 invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event (3.814938ms)\n\u2714 independent targets share the event pipeline and publication remains non-reentrant (2.022608ms)\n\u2714 failing down, up and cancel never prevent terminal ownership cleanup (2.742218ms)\n\u2139 tests 6\n\u2139 suites 0\n\u2139 pass 6\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 105.219661\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/presentation.test.mjs", + "exitCode": 0, + "stdout": "\u2714 direct publication matches archived snapshots and work counts through authored changes (50.493751ms)\n\u2714 whole-scene budget failure never publishes partial bindings and recovers exactly (8.002728ms)\n\u2714 latched event rollback, nonfinite bindings, reset and signed zero keep their semantics (3.308476ms)\n\u2714 publication rejects reentrant runtime changes and reads before they affect the scene (3.977975ms)\n\u2714 invalid or throwing output adapters do not strand the runtime or alter authored state (1.996738ms)\n\u2139 tests 5\n\u2139 suites 0\n\u2139 pass 5\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 147.24287\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/project.test.mjs", + "exitCode": 0, + "stdout": "\u2714 multiple authored placements and a separate invalid draft survive a fresh runtime restore (31.329222ms)\n\u2714 malformed files reject before changing active code, values, diagnostics or warm display records (4.769531ms)\n\u2714 restoration matches names rather than ordering and never reuses stale cached values (2.511923ms)\n\u2714 finite float values including negative zero round trip without changing authored reset values (2.978108ms)\n\u2714 held and failed interactions cannot be saved and restoration cannot steal an active input (5.746993ms)\n\u2714 empty and full documents restore without imposing a new source size or state count (5.467475ms)\n\u2714 document APIs respect the output callback boundary and export independent values (4.112736ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 150.397748\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/runtime.test.mjs", + "exitCode": 0, + "stdout": "\u2714 blank startup and applied empty source contain no application objects or behavior (12.431739ms)\n\u2714 author can add unrelated states inputs views and behavior entirely through source (5.39964ms)\n\u2714 keyboard and pointer sources aggregate first down and last up; repeats are ignored (2.080515ms)\n\u2714 held input rejects apply without changing applied source, metadata, or values (2.656921ms)\n\u2714 invalid syntax, unknown references and capacity overflow preserve the previous program (2.90668ms)\n\u2714 state preservation follows names across reorder/add/remove; reset uses new initializers (1.696997ms)\n\u2714 callers cannot mutate active metadata and corrupt name-based state preservation (1.356442ms)\n\u2714 conditions execute once, and assignments and multiple handlers follow source order (1.563127ms)\n\u2714 a failed event rolls back all handlers and keeps its source diagnostic through release (1.71833ms)\n\u2714 reset clears held inputs and latched errors; invalid frame counts have no effects (1.910294ms)\n\u2714 render evaluation is pure and invalid binding is distinct from an event rollback (5.831157ms)\n\u2714 independent adapters replay the same author program without sharing live state (2.742643ms)\n\u2714 Wasm has no imports and reports fixed memory and exact active IR/state storage (7.504414ms)\n\u2139 tests 13\n\u2139 suites 0\n\u2139 pass 13\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 126.79257\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/workspace.test.mjs", + "exitCode": 0, + "stdout": "\u2714 Wasm executes old events and output during staged replacement without corrupting either phase (7.93719ms)\n\u2714 workspace accounting reports shared payload and safety tag without double-counting (3.192402ms)\n\u2139 tests 2\n\u2139 suites 0\n\u2139 pass 2\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 86.075656\n", + "stderr": "" + } +] diff --git a/prototypes/workbench/results/2026-09-23-native-instances/README.md b/prototypes/workbench/results/2026-09-23-native-instances/README.md new file mode 100644 index 0000000..f829fce --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/README.md @@ -0,0 +1,36 @@ +# 네이티브 인스턴스와 기존 작성 동작의 연결 + +2026-09-23, Linux AArch64 / Zig 0.15.2 / GCC 14.2.0. + +- `native.json`, `sanitized.json`: 작성물 9개, 입력 156단계의 native/Wasm 상태·표시 비교, + 새 프로세스 복원, 동일 프로세스 인스턴스 3개의 독립성·충돌·실패 검사와 소스 해시. +- `browser-tests.log`: 기존 브라우저/Wasm 및 Node CLI 검사 87개. +- `browser-build.json`: Zig 의미 검사 37개와 Wasm 빌드 기록. +- `application-build.json`: 사용자가 실행할 수 있는 네이티브 라우팅 예제 빌드 기록. +- `verification.json`: 결과의 소스·실행물 확인, 선택 모듈과 전체 실행물 비용. +- `default-linkage.json`: 초기 기본 panic/compiler-rt 설정의 링크 전 아카이브 기록. 최종 앱 크기와 비교하는 수치가 아니다. +- `sources/`: 결과에 기록한 구현과 재현에 필요한 작성물·검사 소스. +- `auxiliary-sources.json`: 재현용 작성물·기존 검사 소스의 별도 해시 목록. + +저장소 루트에서 실행한 빌드: + +```sh +python3 prototypes/workbench/manage.py build --zig /path/to/zig-0.15.2 +python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure --zig /path/to/zig-0.15.2 +``` + +그 뒤 `prototypes/workbench`에서: + +```sh +node --test --test-reporter=spec *.test.mjs +node native/check.mjs --output results/2026-09-23-native-instances/native.json +node native/check.mjs --sanitize --output results/2026-09-23-native-instances/sanitized.json +``` + +기존 결과는 덮어쓰지 않는다. 재현할 때 `--output`을 생략하거나 새 경로를 사용한다. +동결 소스의 `sources`를 저장소 루트로 삼아 같은 명령을 실행할 수 있다. Node·C17 컴파일러· +Zig 0.15.2 및 로컬 자식 프로세스 실행이 필요하다. 비용 확인에는 GNU ar/nm/size/strip/readelf를 사용했다. + +sanitizer는 C 호스트에 적용됐다. Zig 모듈은 두 빌드 모두 ReleaseSafe다. 네이티브 창·macOS· +스레드 간 동시 실행·처리량·RSS 검증 자료는 아니다. +[구현과 남은 범위](../../../../docs/research/024-native-app-instances.md). diff --git a/prototypes/workbench/results/2026-09-23-native-instances/application-build.json b/prototypes/workbench/results/2026-09-23-native-instances/application-build.json new file mode 100644 index 0000000..ff35c7a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/application-build.json @@ -0,0 +1,40 @@ +{ + "source": "/home/eunhhu/work/pure/prototypes/workbench/examples/routed-layout.pure", + "sourceSha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "zigVersion": "0.15.2", + "zigFlags": [ + "-O", + "ReleaseSafe", + "-fno-compiler-rt" + ], + "cFlags": [ + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror" + ], + "binary": "/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-native", + "binarySha256": "6d587d072b6cfc93777fece74b99cde1ea5ff581b3276a6de545568bbcd5c69c", + "librarySha256": "b45932025312c12eea6cd1083c43ed57f3cf3dbbb752c47adba634af8e21c5c2", + "preparedSha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "implementationSha256": { + "prototypes/workbench/engine.zig": "555b9ff3143888921d90a475bcb7957c18dbf7101ee9e3d2853c01a562cbf3f5", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/native/prepare.mjs": "3989d4e545255fccf9d846afb79246e874f4cefb03eecef9ee1e2b1ce8fd257e", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/app.h": "0ea9c9dc18e3d9c34763198c6efbaa3219c6f91764b24f00591d958ca0678ce2", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/app.c": "a8c0547ca789f498e453f8b8b982d1e06a874f34e72d905d86923d0e573eff66", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f" + } +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/auxiliary-sources.json b/prototypes/workbench/results/2026-09-23-native-instances/auxiliary-sources.json new file mode 100644 index 0000000..911a77d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/auxiliary-sources.json @@ -0,0 +1,28 @@ +{ + "prototypes/workbench/baseline.test.mjs": "3ea469a29d301b95701bc6f6ffafa2471ac0f334c5bd462c0cbc7c320bdc0764", + "prototypes/workbench/cli.mjs": "55a5ead56fc8ad4c3f2391505fc3dbfa477ccff5b68fc208f700da24d5e194da", + "prototypes/workbench/cli.test.mjs": "7e4688824e26dc7e56396f9d4d62726de0ccb1490e1a388bb64690ae2aae52f4", + "prototypes/workbench/compare.mjs": "f304cd9191fdf737777a8286e223ee32a483a64ba6cfd80fcf9d4c4a5b1bb6d8", + "prototypes/workbench/compiler.test.mjs": "6aa442d9d1eeb69453082f1780ff4597e46b1fc09d8ba3cf955545bc94d81431", + "prototypes/workbench/evaluation-check.mjs": "11492759b5eff878fb8e43cad69422f9c34da6a85b5d8808478cc1845d737d4f", + "prototypes/workbench/evaluation.test.mjs": "d3026ab5c6bffce56c0bde3155d7dddd6fa442a5688656ee710626c762258ff1", + "prototypes/workbench/examples/charge.pure": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prototypes/workbench/examples/counter.pure": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prototypes/workbench/examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prototypes/workbench/examples/document.pure": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prototypes/workbench/examples/layout.pure": "cebd9c3485a2be4ec16ae665c1ed4ee200d51fc56ad0151306c8e9c31138f346", + "prototypes/workbench/examples/routed-layout.pure": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prototypes/workbench/examples/shared-layout.pure": "be8296244b768f9adda99e7afc89249f34853fa27124a277aecb0841eb8176eb", + "prototypes/workbench/examples/toggle.pure": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prototypes/workbench/pointer.test.mjs": "5c5c94933b9b3d33ee29c0861e302209bef4dbb71d353424d29091f97f9d1c5a", + "prototypes/workbench/presentation.test.mjs": "1596733634b31125d81a324ca633c51b90ce848486f7909ecc0619f949ead4d1", + "prototypes/workbench/project.test.mjs": "7efd09c75b89af384e4020e8fc59df42932897c26c0a0cb6cb7f960684393568", + "prototypes/workbench/remote.mjs": "6f5aa2bc5a3c88749b2b688c5631eed9e4a5ab41d72eca168bfb67941a8cff2f", + "prototypes/workbench/routing.test.mjs": "37f6705a496599778eed306e554147b3d9e408447043f4e75abb7225e548999a", + "prototypes/workbench/rules.test.mjs": "caab0cb2618a39c7dcd547b402561d8f9edb11acc7cf389ae16761b70464db7b", + "prototypes/workbench/runtime.test.mjs": "1b9bdbae7379fcdd106cebf1acc6454839fd0e989d7ce31bfd4924fb8501cb8a", + "prototypes/workbench/value-input.test.mjs": "8b08813ed64949cb0d26b9b4e02a5fbf28e84dcccf8641c0ec7c8b6e686c1267", + "prototypes/workbench/web/baseline.mjs": "dcd567e5568d3c798d6b48e555e6d54773963c70d23594c175706b061f8b043f", + "prototypes/workbench/workspace-check.mjs": "a841167a2be597a56c1adc1cf31e4b1e8841579a42d853e260059531009015f0", + "prototypes/workbench/workspace.test.mjs": "053c3883d69a528c244d5298bd67b95642a5d6e8ceceea3d189dce6c9747f6ac" +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/browser-build.json b/prototypes/workbench/results/2026-09-23-native-instances/browser-build.json new file mode 100644 index 0000000..c5243c7 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/browser-build.json @@ -0,0 +1,24 @@ +{ + "builtUtc": "2026-09-23T15:43:54.625372+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "wasmSha256": "8fe2f501cb9639fea0817aa57e0b7292ca6779cda4ac156b7c6b3df31d7921e9", + "evaluationSourceSha256": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "engineSourceSha256": "555b9ff3143888921d90a475bcb7957c18dbf7101ee9e3d2853c01a562cbf3f5", + "wasmFileBytes": 61065, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/37 core.test.blank program, no hidden application behavior, and reset...OK\n2/37 core.test.arbitrary input counter and unused events...OK\n3/37 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/37 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/37 core.test.branch to program end is valid...OK\n6/37 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/37 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/37 core.test.stack underflow overflow and leftover values reject the event...OK\n9/37 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/37 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/37 core.test.capacity boundaries and sticky load errors...OK\n12/37 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/37 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/37 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/37 core.test.binding evaluation is pure on both success and failure...OK\n16/37 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/37 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/37 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/37 core.test.point coordinates are ephemeral event operands and unavailable to pure evaluation...OK\n20/37 core.test.point event ranges reject holes and invalid payloads without committing...OK\n21/37 core.test.point event failure rolls back writes from earlier matching handlers...OK\n22/37 core.test.loading shares storage with records while old events and bindings remain executable...OK\n23/37 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK\n24/37 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK\n25/37 core.test.restored definition and values commit together without changing reset initializers...OK\n26/37 core.test.missing duplicate nonfinite or invalid restored candidates leave active work untouched...OK\n27/37 core.test.empty and full restored programs validate all values and discard old display records...OK\n28/37 core.test.shared handler bodies access their own bound state slots in source order...OK\n29/37 core.test.failure in a later shared binding rolls back all earlier bindings...OK\n30/37 core.test.shared bindings reject missing arguments partial overlaps and invalid slot mappings...OK\n31/37 core.test.one routed handler changes only its event target and rejects missing or foreign routes...OK\n32/37 core.test.routes reject invalid mappings and bindings without replacing active work...OK\n33/37 core.test.routed event rollback includes globals and earlier handlers and survives staged route replacement...OK\n34/37 core.test.scalar inputs carry a finite ephemeral value without adding state or pointer ownership...OK\n35/37 core.test.scalar input failure rolls back earlier writes and old scalar events survive staging...OK\n36/37 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n37/37 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 37 tests passed.\n" +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/browser-tests.log b/prototypes/workbench/results/2026-09-23-native-instances/browser-tests.log new file mode 100644 index 0000000..cdfd51b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/browser-tests.log @@ -0,0 +1,95 @@ +✔ reference begins with the authored states and matching scene (2.666509ms) +✔ press, hold, release, and frame order produce exact expected values (0.626349ms) +✔ charge clamps at one and explicit floor restores rest (2.167844ms) +✔ repeat and concurrent physical sources produce only logical edges (0.54998ms) +✔ airborne presses cannot charge or cause another jump (0.305665ms) +✔ releaseAll emits release and reset clears latches without a jump (0.460091ms) +✔ unknown inputs and invalid frame counts reject without mutation (0.806034ms) +✔ terminal starts empty and produces the same charge trace as the shared runtime (302.941322ms) +✔ invalid terminal commands do not alter state and fail piped execution (115.357513ms) +✔ terminal replays authored pointer movement and cancellation through the shared runtime (133.075486ms) +✔ file load preserves by name, rejected load preserves active work, and save never overwrites (142.619156ms) +✔ terminal exposes arithmetic rollback and retains failure through matching release (128.85302ms) +✔ terminal configures and discards evaluation records and recovers from a display limit (114.771589ms) +✔ terminal saves and restores placement independently of reset and preserves a browser draft (316.213387ms) +✔ invalid restoration leaves terminal code and state unchanged (85.826729ms) +✔ a blank document is an empty program with no implied behavior (10.648277ms) +✔ forward references and separate state/input namespaces preserve declaration IDs (3.800505ms) +✔ ordered assignments, repeated handlers, and condition evaluation once (1.596902ms) +✔ nested branches use absolute forward targets and skip correct bodies (0.981996ms) +✔ operator precedence, associativity, unary and builtin function lowering (2.365177ms) +✔ multiline parentheses, scientific literals, comments, and string escapes (3.012359ms) +✔ all instruction fields are numeric and carry original source line (0.752497ms) +✔ contextual declaration words remain valid names outside reserved builtins (1.527846ms) +✔ unknown and reserved symbols have useful one-based locations (3.273044ms) +✔ duplicates, unsupported keys, initializers, and invalid syntax are rejected (2.754824ms) +✔ document, declaration, instruction, and nesting limits reject overflow (13.724877ms) +✔ every supported input maps to a bounded ordered event ID (0.875534ms) +✔ optional examples compile and charge behavior is entirely authored (40.07306ms) +✔ cache reuse, unrelated changes and discarded records preserve authored values (42.423015ms) +✔ committed changes only: failed events and failed edits preserve reusable output (6.538365ms) +✔ program edits, state reordering and reset cannot reuse old definitions (10.174723ms) +✔ finite float semantics include signed zero, high state bit and arithmetic errors (7.63651ms) +✔ one budget covers the whole scene; failures publish no partial scene or state rollback (3.713079ms) +✔ small caches, no history and every small work limit give exact results or explicit failure (44.796673ms) +✔ invalid settings and forget names preserve policy and state (8.188563ms) +✔ pointer declarations resolve forward targets and reject ambiguous or contextless inputs (24.927503ms) +✔ authored drag keeps grab offset and commits the displayed position even if release coordinates differ (34.138248ms) +✔ ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input (5.890461ms) +✔ invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event (4.376762ms) +✔ independent targets share the event pipeline and publication remains non-reentrant (3.597617ms) +✔ failing down, up and cancel never prevent terminal ownership cleanup (4.033986ms) +✔ direct publication matches archived snapshots and work counts through authored changes (100.465381ms) +✔ whole-scene budget failure never publishes partial bindings and recovers exactly (15.101038ms) +✔ latched event rollback, nonfinite bindings, reset and signed zero keep their semantics (7.581584ms) +✔ publication rejects reentrant runtime changes and reads before they affect the scene (3.836042ms) +✔ invalid or throwing output adapters do not strand the runtime or alter authored state (3.621838ms) +✔ multiple authored placements and a separate invalid draft survive a fresh runtime restore (46.870351ms) +✔ malformed files reject before changing active code, values, diagnostics or warm display records (12.714658ms) +✔ restoration matches names rather than ordering and never reuses stale cached values (6.399829ms) +✔ finite float values including negative zero round trip without changing authored reset values (3.312525ms) +✔ held and failed interactions cannot be saved and restoration cannot steal an active input (13.389711ms) +✔ empty and full documents restore without imposing a new source size or state count (11.491329ms) +✔ document APIs respect the output callback boundary and export independent values (3.581821ms) +✔ one input and one handler serve up to 24 static targets without instruction or handler expansion (37.094442ms) +✔ routes validate forward references, exact types, unique views/items and current input target (6.652994ms) +✔ grouped layout matches explicit bindings through concurrent capture, partial cancel, keyboard and restore (26.433091ms) +✔ capture remains bound to its target and releaseAll cancels each target exactly once (3.905616ms) +✔ reorder and remove/readd rebuild routes by names without persisting physical indices (8.318582ms) +✔ routed writes invalidate cached displays and failed handlers rollback without losing another target owner (4.411855ms) +✔ CLI sends grouped pointer events and rejects an unknown target (229.852547ms) +✔ a route from a different input group cannot be delivered through the named group (3.462247ms) +✔ binding more objects shares one VM body instead of expanding its instructions (9.239485ms) +✔ records and rules resolve forward names and reject invalid types fields and bindings (6.422163ms) +✔ shared layout matches the existing authored layout across independent and simultaneous gestures (27.66003ms) +✔ field names retain identity across schema and instance reorder, removal and restoration (4.160744ms) +✔ bindings share one event transaction including global effects and multiple uses of one item (5.169389ms) +✔ bound writes invalidate affected display caches and failed edits preserve active bindings (8.081342ms) +✔ generated qualified names cannot make a valid exported document unreadable by its own loader (11.070108ms) +✔ blank startup and applied empty source contain no application objects or behavior (14.430152ms) +✔ author can add unrelated states inputs views and behavior entirely through source (13.048972ms) +✔ keyboard and pointer sources aggregate first down and last up; repeats are ignored (3.87206ms) +✔ held input rejects apply without changing applied source, metadata, or values (3.873542ms) +✔ invalid syntax, unknown references and capacity overflow preserve the previous program (6.140793ms) +✔ state preservation follows names across reorder/add/remove; reset uses new initializers (3.107655ms) +✔ callers cannot mutate active metadata and corrupt name-based state preservation (4.544114ms) +✔ conditions execute once, and assignments and multiple handlers follow source order (3.42334ms) +✔ a failed event rolls back all handlers and keeps its source diagnostic through release (3.54745ms) +✔ reset clears held inputs and latched errors; invalid frame counts have no effects (4.804816ms) +✔ render evaluation is pure and invalid binding is distinct from an event rollback (12.577752ms) +✔ independent adapters replay the same author program without sharing live state (4.865927ms) +✔ Wasm has no imports and reports fixed memory and exact active IR/state storage (17.508086ms) +✔ numeric controls use explicit change context and cannot read pointer or frame payloads (7.839268ms) +✔ numeric input handles scalar rules, rollback, cache invalidation and signed zero (21.075072ms) +✔ the document policy receives range, keyboard and pointer requests without layouts or text in VM state (6.701791ms) +✔ CLI supplies the same numeric control value and rejects nonfinite input (199.975377ms) +✔ Wasm executes old events and output during staged replacement without corrupting either phase (11.735569ms) +✔ workspace accounting reports shared payload and safety tag without double-counting (5.410517ms) +ℹ tests 87 +ℹ suites 0 +ℹ pass 87 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 1433.326385 diff --git a/prototypes/workbench/results/2026-09-23-native-instances/default-linkage.json b/prototypes/workbench/results/2026-09-23-native-instances/default-linkage.json new file mode 100644 index 0000000..da899ef --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/default-linkage.json @@ -0,0 +1,12 @@ +{ + "flags": [ + "-O", + "ReleaseSafe", + "-fcompiler-rt" + ], + "source": "//! Native C ABI. The caller supplies aligned instance storage; no global VM,\n//! allocator, OS call or browser dependency is part of this selected module.\nconst Engine = @import(\"engine.zig\").Engine;\n\nexport fn pure_scalar_bytes() usize {\n return @sizeOf(Engine);\n}\nexport fn pure_scalar_alignment() usize {\n return @alignOf(Engine);\n}\n\nexport fn pure_scalar_initialize(memory: ?*anyopaque, size: usize) ?*Engine {\n const pointer = memory orelse return null;\n if (size < @sizeOf(Engine) or @intFromPtr(pointer) % @alignOf(Engine) != 0) return null;\n const instance: *Engine = @ptrCast(@alignCast(pointer));\n instance.* = .{};\n return instance;\n}\n\nexport fn pure_scalar_reset_all(instance: *Engine) void {\n instance.reset_all();\n}\n\nexport fn pure_scalar_begin_program(instance: *Engine, state_count: u32, input_count: u32) u32 {\n return instance.begin_program(state_count, input_count);\n}\n\nexport fn pure_scalar_abort_program(instance: *Engine) void {\n instance.abort_program();\n}\n\nexport fn pure_scalar_put_state(instance: *Engine, id: u32, initial: f64, old_id: i32) u32 {\n return instance.put_state(id, initial, old_id);\n}\n\nexport fn pure_scalar_put_instruction(instance: *Engine, op: u32, arg: u32, value: f64) u32 {\n return instance.put_instruction(op, arg, value);\n}\n\nexport fn pure_scalar_put_handler(instance: *Engine, event: u32, start: u32, length: u32) u32 {\n return instance.put_handler(event, start, length);\n}\n\nexport fn pure_scalar_bind_handler(instance: *Engine, handler: u32, base: u32, count: u32) u32 {\n return instance.bind_handler(handler, base, count);\n}\n\nexport fn pure_scalar_put_pointer_target(instance: *Engine, input: u32, base: u32, count: u32) u32 {\n return instance.put_pointer_target(input, base, count);\n}\n\nexport fn pure_scalar_bind_target_handler(instance: *Engine, handler: u32, count: u32) u32 {\n return instance.bind_target_handler(handler, count);\n}\n\nexport fn pure_scalar_put_restored_value(instance: *Engine, id: u32, value: f64) u32 {\n return instance.put_restored_value(id, value);\n}\n\nexport fn pure_scalar_commit_program(instance: *Engine) u32 {\n return instance.commit_program();\n}\n\nexport fn pure_scalar_commit_restored_program(instance: *Engine) u32 {\n return instance.commit_restored_program();\n}\n\nexport fn pure_scalar_reset_state(instance: *Engine) void {\n instance.reset_state();\n}\n\nexport fn pure_scalar_dispatch(instance: *Engine, event: u32, dt: f64) u32 {\n return instance.dispatch(event, dt);\n}\n\nexport fn pure_scalar_dispatch_point(instance: *Engine, event: u32, x: f64, y: f64) u32 {\n return instance.dispatch_point(event, x, y);\n}\n\nexport fn pure_scalar_dispatch_target(instance: *Engine, event: u32, target: u32, x: f64, y: f64) u32 {\n return instance.dispatch_target(event, target, x, y);\n}\n\nexport fn pure_scalar_dispatch_value(instance: *Engine, event: u32, value: f64) u32 {\n return instance.dispatch_value(event, value);\n}\n\nexport fn pure_scalar_eval_at(instance: *Engine, start: u32, length: u32) f64 {\n return instance.eval_at(start, length);\n}\n\nexport fn pure_scalar_configure_evaluation(instance: *Engine, cache: u32, history: u32) u32 {\n return instance.configure_evaluation(cache, history);\n}\n\nexport fn pure_scalar_forget_evaluation(instance: *Engine, flags: u32) void {\n instance.forget_evaluation(flags);\n}\n\nexport fn pure_scalar_begin_evaluation(instance: *Engine, limit: u32) void {\n instance.begin_evaluation(limit);\n}\n\nexport fn pure_scalar_eval_bounded(instance: *Engine, start: u32, length: u32) f64 {\n return instance.eval_bounded(start, length);\n}\n\nexport fn pure_scalar_evaluation_info(instance: *Engine, id: u32) u32 {\n return instance.evaluation_info(id);\n}\n\nexport fn pure_scalar_get_state(instance: *Engine, id: u32) f64 {\n return instance.get_state(id);\n}\n\nexport fn pure_scalar_info(instance: *Engine, id: u32) u32 {\n return instance.info(id);\n}\n\nexport fn pure_scalar_layout(instance: *Engine, id: u32) u32 {\n return instance.layout(id);\n}\n", + "archive_sha256": "544095c23edc9d9e2fd6418bcd4bc02d5ad81921fa943a31e1635eb4e323d2a7", + "size": " text\t data\t bss\t dec\t hex\tfilename\n 240820\t 80\t 149\t 241049\t 3ad99\tprototypes/workbench/build/cache/tmp/b77f55ad39a6ffce/libpure-scalar_zcu.o (ex prototypes/workbench/build/libpure-scalar.a)\n 154614\t 8\t 4096\t 158718\t 26bfe\tprototypes/workbench/build/global-cache/o/0fe48ea8b47e9365f14b0a3f1025eb57/compiler_rt.o (ex prototypes/workbench/build/libpure-scalar.a)\n", + "undefined": "\nprototypes/workbench/build/cache/tmp/b77f55ad39a6ffce/libpure-scalar_zcu.o:\n w _DYNAMIC\n U getauxval\n U memcpy\n U memmove\n U memset\n\nprototypes/workbench/build/global-cache/o/0fe48ea8b47e9365f14b0a3f1025eb57/compiler_rt.o:\n", + "scope": "Archive section totals before linking; not the deployed file size or runtime memory." +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/native.json b/prototypes/workbench/results/2026-09-23-native-instances/native.json new file mode 100644 index 0000000..2a79084 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/native.json @@ -0,0 +1,159 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": false, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "eada952dee9afd7071370215364b2a6717a28f03e0513cdd0cae4695939ec52b", + "native_binary_sha256": "df9f3e007d2813c94bbbd4d08cfae06571e7d8b190b6212735b32884c07d24a6", + "native_binary_bytes": 213768 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "fc96ee38b256117a7bfe2859b7126e22262c19e96047a8d01fbe30d4d1a28764", + "native_binary_sha256": "d446eed2ec2e8939da30c3cb0b6c968e811f725081f2b08beeb555b98ee83652", + "native_binary_bytes": 213768 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "7ba5fa094b0bc772202bb44a62b3c8377c4f3924db246ec429c2faee4948b09f", + "native_binary_sha256": "15505a1f178a40d010eb97d30c236fdcde3909282de2436aa4015b1b1f6b108d", + "native_binary_bytes": 213768 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "27f0aa76eb3e12e289bcd6edf6c2ae72892a2dfe1ff094b619671f0583e2c8d3", + "native_binary_sha256": "3cd4df84f16a734758041c6dbfb46faffcf5836a3415e80bfbec5091a09e18d1", + "native_binary_bytes": 213768 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "native_binary_sha256": "a7331278432f8c2f827ddac036dc8dab4299bffdd2db902f14420b1cf7c31435", + "native_binary_bytes": 213768 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "c74315c1b0067e167a4678b30505d1770b0753b601e8acee27f6d598ac7b9ffb", + "native_binary_sha256": "555d4f31801ac70ef93fa84c73da7a2409a60bc8743251abd7969e0a7c5c7730", + "native_binary_bytes": 213768 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "6a415d33780227158ebeae2a75c7520bf13bad3c0a6fef7ee82f5c323be226f0", + "native_binary_sha256": "c115e7e8ddbe0d622374203599042255560dcbfb5f2e31c378b5d441c23c0ce6", + "native_binary_bytes": 213776 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "1c8f5ade56ee8ec931a62fd1f68ef3635b17297d3a54b20c954860c4a43b6155", + "native_binary_sha256": "023396eb5190c11ed724712ff5bcafc34ad09e4c43509e7e8445f998cfb57ce1", + "native_binary_bytes": 213744 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e9a8b1542ecf6b008ad30188cb9e95d29638fab1c38d331c00bf706c40a06d3c", + "native_binary_sha256": "c972b112a6ea9b22921253f2ecc07df2b57f423022171f891ab4a0654e82d76e", + "native_binary_bytes": 213784 + } + ], + "units": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore", + "scalar_instance_bytes": 20064, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "555b9ff3143888921d90a475bcb7957c18dbf7101ee9e3d2853c01a562cbf3f5", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/manage.py": "02a6566306248cd4da952673d891bf73e6e8aea583b5dee8a20b96d28c61e025", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/workbench/native/app.h": "0ea9c9dc18e3d9c34763198c6efbaa3219c6f91764b24f00591d958ca0678ce2", + "prototypes/workbench/native/app.c": "a8c0547ca789f498e453f8b8b982d1e06a874f34e72d905d86923d0e573eff66", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "3989d4e545255fccf9d846afb79246e874f4cefb03eecef9ee1e2b1ce8fd257e", + "prototypes/workbench/native/test.c": "485b97a31f7b0b2fd16000a4c09e3eebfc67045724520d7ba48ffdcb105a8536", + "prototypes/workbench/native/check.mjs": "79a1131cedfae67b30122818a29e596c5f5f0eda79c18e771566dc122621006e", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "8fe2f501cb9639fea0817aa57e0b7292ca6779cda4ac156b7c6b3df31d7921e9", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "b45932025312c12eea6cd1083c43ed57f3cf3dbbb752c47adba634af8e21c5c2", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33020\t 0\t 0\t 33020\t 80fc\tprototypes/workbench/build/cache/tmp/3cadbbd6974d5d26/libpure-scalar_zcu.o (ex /home/eunhhu/work/pure/prototypes/workbench/build/native/libpure-scalar.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/3cadbbd6974d5d26/libpure-scalar_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sanitized.json b/prototypes/workbench/results/2026-09-23-native-instances/sanitized.json new file mode 100644 index 0000000..b8acea5 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sanitized.json @@ -0,0 +1,159 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": true, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "eada952dee9afd7071370215364b2a6717a28f03e0513cdd0cae4695939ec52b", + "native_binary_sha256": "7c46b5e4d087aa82625b1f55ce934ae8fcb146b1d01e58926c31ff395d826827", + "native_binary_bytes": 591912 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "fc96ee38b256117a7bfe2859b7126e22262c19e96047a8d01fbe30d4d1a28764", + "native_binary_sha256": "e104a514b32150dccc3acd5754efee82220042a55429a5b66dc413e0c24b24b5", + "native_binary_bytes": 592296 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "7ba5fa094b0bc772202bb44a62b3c8377c4f3924db246ec429c2faee4948b09f", + "native_binary_sha256": "298493b0178531dda9f6b3eed9174cae5a5f1cef44bbe90edb6b232e4b8b92df", + "native_binary_bytes": 592360 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "27f0aa76eb3e12e289bcd6edf6c2ae72892a2dfe1ff094b619671f0583e2c8d3", + "native_binary_sha256": "cda176e405e42290f3d8e4e220c42d382271235ba4ded1c814e9d9aac6b24bff", + "native_binary_bytes": 593128 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "native_binary_sha256": "e589c682ddb0bbaa7847ffa8964895dd5dca28bcb2e4356d206052d4d975fdd4", + "native_binary_bytes": 594280 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "c74315c1b0067e167a4678b30505d1770b0753b601e8acee27f6d598ac7b9ffb", + "native_binary_sha256": "5aee2ce71e864103c989ec32e14fc5dc6f6db1e43c9d55f8ee55592f2aa784b1", + "native_binary_bytes": 591592 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "6a415d33780227158ebeae2a75c7520bf13bad3c0a6fef7ee82f5c323be226f0", + "native_binary_sha256": "4e6a1f92924add1fb99b5f1db8ca9f49917dac36498a198ebcb73075df92e7bf", + "native_binary_bytes": 591728 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "1c8f5ade56ee8ec931a62fd1f68ef3635b17297d3a54b20c954860c4a43b6155", + "native_binary_sha256": "e0a01e7ba494e4840216bc0801a0db8f4bed7d25503f5117f1a4c134ed6c5251", + "native_binary_bytes": 591264 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e9a8b1542ecf6b008ad30188cb9e95d29638fab1c38d331c00bf706c40a06d3c", + "native_binary_sha256": "ff39ffa2950c2135f3054aa9592c4ce46242c92e3f76f39cadc8d22134278cf7", + "native_binary_bytes": 591672 + } + ], + "units": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore", + "scalar_instance_bytes": 20064, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "555b9ff3143888921d90a475bcb7957c18dbf7101ee9e3d2853c01a562cbf3f5", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/manage.py": "02a6566306248cd4da952673d891bf73e6e8aea583b5dee8a20b96d28c61e025", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/workbench/native/app.h": "0ea9c9dc18e3d9c34763198c6efbaa3219c6f91764b24f00591d958ca0678ce2", + "prototypes/workbench/native/app.c": "a8c0547ca789f498e453f8b8b982d1e06a874f34e72d905d86923d0e573eff66", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "3989d4e545255fccf9d846afb79246e874f4cefb03eecef9ee1e2b1ce8fd257e", + "prototypes/workbench/native/test.c": "485b97a31f7b0b2fd16000a4c09e3eebfc67045724520d7ba48ffdcb105a8536", + "prototypes/workbench/native/check.mjs": "79a1131cedfae67b30122818a29e596c5f5f0eda79c18e771566dc122621006e", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "8fe2f501cb9639fea0817aa57e0b7292ca6779cda4ac156b7c6b3df31d7921e9", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "b45932025312c12eea6cd1083c43ed57f3cf3dbbb752c47adba634af8e21c5c2", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33020\t 0\t 0\t 33020\t 80fc\tprototypes/workbench/build/cache/tmp/3cadbbd6974d5d26/libpure-scalar_zcu.o (ex /home/eunhhu/work/pure/prototypes/workbench/build/native/libpure-scalar.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/3cadbbd6974d5d26/libpure-scalar_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/evaluate.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/evaluate.c new file mode 100644 index 0000000..88ffae7 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/evaluate.c @@ -0,0 +1,53 @@ +#include "program.h" +#include + +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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/flow.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include +#include + +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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/program.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/program.h new file mode 100644 index 0000000..c102eb1 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/result.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/result.h new file mode 100644 index 0000000..8abb0f2 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.c new file mode 100644 index 0000000..aa4ed05 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.c @@ -0,0 +1,273 @@ +#include "runtime.h" +#include + +#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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.h new file mode 100644 index 0000000..1bff082 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/runtime.h @@ -0,0 +1,88 @@ +#ifndef PURE_RUNTIME_H +#define PURE_RUNTIME_H +#include "program.h" +#include "result.h" +#include + +/* 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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store-port.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store-port.c new file mode 100644 index 0000000..9860308 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store-port.c @@ -0,0 +1,44 @@ +#include "store-port.h" +#include "store.h" +#include +#include + +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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store-port.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store-port.h new file mode 100644 index 0000000..4c0d49d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.c new file mode 100644 index 0000000..4c8bdc4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.c @@ -0,0 +1,258 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.h new file mode 100644 index 0000000..99a3ef5 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/connections/store.h @@ -0,0 +1,44 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include "result.h" +#include + +#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/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/baseline.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/baseline.test.mjs new file mode 100644 index 0000000..c9ef2de --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/baseline.test.mjs @@ -0,0 +1,78 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createChargeModel } from './web/baseline.mjs'; + +test('reference begins with the authored states and matching scene', () => { + const model = createChargeModel(); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); + assert.deepEqual(model.snapshot().held, []); + assert.equal(model.snapshot().tick, 0); + assert.equal(model.snapshot().views.find(view => view.id === 'floor').y, 261); + assert.equal(model.snapshot().views.find(view => view.id === 'actor').radius, 20); +}); + +test('press, hold, release, and frame order produce exact expected values', () => { + const model = createChargeModel(); + model.down(); model.frame(15); + let expectedCharge = 0; + for (let frame = 0; frame < 15; frame++) expectedCharge = Math.min(Math.max(expectedCharge + 1 / 60, 0), 1); + assert.deepEqual(model.snapshot().states, { holding: 1, charge: expectedCharge, y: 240, velocity: 0 }); + model.up(); + const initialVelocity = -(160 + expectedCharge * 300); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: initialVelocity }); + model.frame(); + const nextVelocity = initialVelocity + 600 * (1 / 60); + assert.deepEqual(model.snapshot().states, { + holding: 0, charge: 0, y: 240 + nextVelocity * (1 / 60), velocity: nextVelocity, + }); +}); + +test('charge clamps at one and explicit floor restores rest', () => { + const model = createChargeModel(); + model.down(); model.frame(120); + assert.equal(model.snapshot().states.charge, 1); + model.up(); assert.equal(model.snapshot().states.velocity, -460); + model.frame(); assert.equal(model.snapshot().states.y, 232.5); + model.frame(120); + assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); +}); + +test('repeat and concurrent physical sources produce only logical edges', () => { + const model = createChargeModel(); + model.down('jump', 'keyboard'); model.frame(6); + const charge = model.snapshot().states.charge; + model.down('jump', 'keyboard'); model.down('jump', 'pointer'); + assert.equal(model.snapshot().states.charge, charge); + model.up('jump', 'keyboard'); + assert.equal(model.snapshot().states.holding, 1); + model.up('jump', 'unknown'); assert.equal(model.snapshot().states.holding, 1); + model.up('jump', 'pointer'); assert.equal(model.snapshot().states.velocity, -(160 + charge * 300)); + const released = model.snapshot(); model.up('jump', 'pointer'); + assert.deepEqual(model.snapshot(), released); +}); + +test('airborne presses cannot charge or cause another jump', () => { + const model = createChargeModel(); model.down(); model.up(); model.frame(); + const airborne = model.snapshot().states; + model.down(); model.up(); + assert.deepEqual(model.snapshot().states, airborne); +}); + +test('releaseAll emits release and reset clears latches without a jump', () => { + const model = createChargeModel(); + model.down('jump', 'keyboard'); model.down('jump', 'pointer'); model.frame(10); + model.releaseAll(); assert.equal(model.snapshot().states.holding, 0); + assert.ok(model.snapshot().states.velocity < 0); assert.deepEqual(model.snapshot().held, []); + model.reset(); assert.deepEqual(model.snapshot().states, { holding: 0, charge: 0, y: 240, velocity: 0 }); + assert.equal(model.snapshot().tick, 0); + model.down(); model.reset(); model.up(); assert.equal(model.snapshot().states.velocity, 0); +}); + +test('unknown inputs and invalid frame counts reject without mutation', () => { + const model = createChargeModel(), initial = model.snapshot(); + for (const method of ['down', 'up']) assert.throws(() => model[method]('other'), /Unknown input/); + for (const count of [-1, .5, 601, Infinity, NaN, '1']) assert.throws(() => model.frame(count), /integer/); + model.frame(0); assert.deepEqual(model.snapshot(), initial); + const snapshot = model.snapshot(); snapshot.states.y = -999; + assert.equal(model.snapshot().states.y, 240); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/cli.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/cli.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/cli.test.mjs new file mode 100644 index 0000000..f355d2d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/cli.test.mjs @@ -0,0 +1,121 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => { + const file = join(root, 'examples/direct.pure'); + const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]]) + runtime.point('drag', phase, x, y, 'terminal'); + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert.equal(result.snapshots.at(-1).states.x, 350); + assert.equal(result.snapshots.at(-1).states.y, 210); + assert.deepEqual(result.snapshots.at(-1).held, []); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); + +test('terminal saves and restores placement independently of reset and preserves a browser draft', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-layout-cli-')); + t.after(() => rm(directory, {recursive:true,force:true})); + const saved=join(directory,'layout.json'), copied=join(directory,'copy.json'); + const first=run(`point drag down 90 170\npoint drag move 360 220\npoint drag up 90 170\nsave-project ${saved}\nreset\nrestore ${saved}\nshow\nquit\n`,join(root,'examples/direct.pure')); + assert.equal(first.status,0,first.stderr);assert.equal(first.snapshots.at(-1).states.x,350); + const document=JSON.parse(await readFile(saved,'utf8'));document.draft='state incomplete ='; + await writeFile(saved,JSON.stringify(document)); + const second=run(`save-project ${copied}\nreset\nshow\nquit\n`,saved); + assert.equal(second.status,0,second.stderr);assert.equal(second.snapshots.at(-1).states.x,80); + const copy=JSON.parse(await readFile(copied,'utf8'));assert.equal(copy.draft,document.draft);assert.deepEqual(copy.states,document.states); + const rejected=run(`save-project ${saved}\nquit\n`,saved); + assert.equal(rejected.status,1);assert.match(rejected.stderr,/EEXIST/); +}); + +test('invalid restoration leaves terminal code and state unchanged', async t => { + const directory=await mkdtemp(join(tmpdir(),'pure-invalid-layout-')); + t.after(()=>rm(directory,{recursive:true,force:true})); + const file=join(directory,'bad.json'); + await writeFile(file,JSON.stringify({format:'pure-workbench',version:2,source:'state x=1',states:[]})); + const result=run(`show\nrestore ${file}\nshow\nquit\n`,join(root,'examples/counter.pure')); + assert.equal(result.status,1);assert.deepEqual(result.snapshots[0],result.snapshots[1]); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compare.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compare.mjs new file mode 100644 index 0000000..ceadb0c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compare.mjs @@ -0,0 +1,231 @@ +// Reproduce the fixed charge workload in the Wasm-host runtime and handwritten JS. +// This compares full host calls, not VM instructions or feature-equivalent products. +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { cpus, platform, release, arch, availableParallelism } from 'node:os'; +import { performance } from 'node:perf_hooks'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; +import { createChargeModel } from './web/baseline.mjs'; + +const args = process.argv.slice(2); +if (args.length && !(args.length === 2 && args[0] === '--output')) { + throw new Error('Usage: node compare.mjs [--output report.json]'); +} +const startedAt = new Date().toISOString(); +const seed = 20260922; +const trials = 7; +const warmupTraces = 5; +const targetTrialMs = 12; +const absoluteTolerance = 1e-11; +const relativeTolerance = 1e-12; +const files = { + charge: new URL('./examples/charge.pure', import.meta.url), + baseline: new URL('./web/baseline.mjs', import.meta.url), + runtime: new URL('./web/runtime.mjs', import.meta.url), + compiler: new URL('./web/compiler.mjs', import.meta.url), + wasm: new URL('./web/core.wasm', import.meta.url), + harness: new URL('./compare.mjs', import.meta.url), +}; +const contents = Object.fromEntries(await Promise.all( + Object.entries(files).map(async ([name, path]) => [name, await readFile(path)]), +)); +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +const sourceInfo = name => { + const text = contents[name].toString('utf8'); + return { + bytes: contents[name].length, + physicalLines: text === '' ? 0 : text.split('\n').length - Number(text.endsWith('\n')), + nonblankLines: text.split('\n').filter(line => line.trim().length).length, + sha256: hash(contents[name]), + }; +}; + +function makeTrace() { + const trace = []; + const action = (method, source = undefined) => trace.push({ method, ...(source === undefined ? {} : { source }) }); + const frames = count => { + // Individual calls let correctness checks inspect every frame, not just a batch end. + for (let i = 0; i < count; i++) action('frame'); + }; + action('reset'); action('up', 'unknown'); + action('down', 'keyboard'); action('down', 'keyboard'); frames(1); + action('down', 'pointer:1'); frames(59); action('up', 'keyboard'); frames(10); + action('up', 'pointer:1'); frames(6); + action('down', 'keyboard'); frames(2); action('up', 'keyboard'); frames(120); + action('down', 'pointer:1'); frames(600); action('releaseAll'); frames(120); + action('down', 'keyboard'); action('reset'); action('up', 'keyboard'); frames(1); + let random = seed >>> 0; + const next = limit => { + random = (Math.imul(random, 1664525) + 1013904223) >>> 0; + return random % limit; + }; + for (let cycle = 0; cycle < 48; cycle++) { + const source = next(2) ? 'keyboard' : 'pointer:1'; + action('down', source); frames(next(91)); action('down', source); + if (cycle % 3 === 0) action('down', 'pointer:2'); + frames(next(11)); action('up', source); + if (cycle % 3 === 0) { frames(next(17)); action('up', 'pointer:2'); } + if (cycle % 7 === 0) action('reset'); + else if (cycle % 5 === 0) action('releaseAll'); + frames(next(121)); + } + action('releaseAll'); frames(180); + return trace; +} + +const trace = makeTrace(); +function traceCounts() { + const counts = { hostActions: trace.length, frames: 0, logicalDown: 0, + logicalUp: 0, resets: 0, releaseAllCalls: 0, downCalls: 0, upCalls: 0 }; + const held = new Set(); + for (const action of trace) { + if (action.method === 'frame') counts.frames++; + else if (action.method === 'reset') { counts.resets++; held.clear(); } + else if (action.method === 'releaseAll') { + counts.releaseAllCalls++; + if (held.size) counts.logicalUp++; + held.clear(); + } else if (action.method === 'down') { + counts.downCalls++; + if (!held.size) counts.logicalDown++; + held.add(action.source); + } else { + counts.upCalls++; + if (held.delete(action.source) && !held.size) counts.logicalUp++; + } + } + counts.dispatchedEvents = counts.frames + counts.logicalDown + counts.logicalUp; + return counts; +} +const counts = traceCounts(); +const pure = await createRuntime(contents.wasm); +const program = pure.apply(contents.charge.toString('utf8')); +const baseline = createChargeModel(); +function perform(model, action) { + switch (action.method) { + case 'frame': return model.frame(1); + case 'down': return model.down('jump', action.source); + case 'up': return model.up('jump', action.source); + case 'reset': model.reset(); return 0; + case 'releaseAll': return model.releaseAll(); + default: throw new Error(`Unknown trace action ${action.method}`); + } +} + +let checkedNumbers = 0, maxAbsoluteDifference = 0; +function compare(actual, expected, path) { + if (typeof expected === 'number') { + if (!Number.isFinite(actual) || !Number.isFinite(expected)) throw new Error(`${path}: non-finite value`); + const difference = Math.abs(actual - expected); + maxAbsoluteDifference = Math.max(maxAbsoluteDifference, difference); + checkedNumbers++; + if (difference > absoluteTolerance + relativeTolerance * Math.max(Math.abs(actual), Math.abs(expected))) { + throw new Error(`${path}: ${actual} != ${expected}`); + } + } else if (Array.isArray(expected)) { + if (!Array.isArray(actual) || actual.length !== expected.length) throw new Error(`${path}: array shape mismatch`); + expected.forEach((item, index) => compare(actual[index], item, `${path}[${index}]`)); + } else if (expected && typeof expected === 'object') { + if (!actual || typeof actual !== 'object') throw new Error(`${path}: object missing`); + const actualKeys = Object.keys(actual).filter(key => actual[key] !== undefined).sort(); + const expectedKeys = Object.keys(expected).filter(key => expected[key] !== undefined).sort(); + if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) throw new Error(`${path}: object keys mismatch`); + for (const key of expectedKeys) compare(actual[key], expected[key], `${path}.${key}`); + } else if (actual !== expected) throw new Error(`${path}: ${JSON.stringify(actual)} != ${JSON.stringify(expected)}`); +} +function checkSnapshots(index) { + const actual = pure.snapshot(), expected = baseline.snapshot(); + if (actual.lastError) throw new Error(`Action ${index}: ${actual.errorMessage}`); + compare(actual.states, expected.states, `action[${index}].states`); + compare(actual.views, expected.views, `action[${index}].views`); + compare(actual.tick, expected.tick, `action[${index}].tick`); + compare(actual.held, expected.held, `action[${index}].held`); +} +checkSnapshots(-1); +for (let index = 0; index < trace.length; index++) { + for (const model of [pure, baseline]) { + const code = perform(model, trace[index]); + if (code !== 0) throw new Error(`Action ${index}: execution status ${code}`); + } + checkSnapshots(index); +} +const correctness = { passed: true, checkedSnapshots: trace.length + 1, + checkedNumbers, maxAbsoluteDifference, absoluteTolerance, relativeTolerance, + checkedFields: ['states', 'all view fields', 'tick', 'held input names'], + traceSha256: hash(JSON.stringify(trace)), counts }; +const memory = pure.snapshot().memory; + +// Snapshot construction, binding evaluation, compilation, Wasm loading, and DOM +// rendering are deliberately outside these timed loops. Host dispatch is inside. +function run(model, repetitions) { + for (let repeat = 0; repeat < repetitions; repeat++) { + for (const action of trace) { + const code = perform(model, action); + if (code !== 0) throw new Error(`Timing run failed with execution status ${code}`); + } + } +} +const models = { pureHostWasm: pure, handwrittenJavaScript: baseline }; +for (const model of Object.values(models)) run(model, warmupTraces); +const repetitions = {}; +const calibration = {}; +for (const [name, model] of Object.entries(models)) { + const start = performance.now(); run(model, 3); + const elapsedMs = performance.now() - start; + repetitions[name] = Math.max(1, Math.min(2048, Math.ceil(targetTrialMs / Math.max(elapsedMs / 3, .001)))); + calibration[name] = { elapsedMs, traces: 3, selectedRepetitions: repetitions[name] }; +} +const samples = []; +for (let trial = 0; trial < trials; trial++) { + const order = trial % 2 === 0 ? Object.keys(models) : Object.keys(models).reverse(); + for (const [position, name] of order.entries()) { + const start = performance.now(); run(models[name], repetitions[name]); + const elapsedMs = performance.now() - start; + const nsPerTrace = elapsedMs * 1e6 / repetitions[name]; + samples.push({ trial: trial + 1, position: position + 1, implementation: name, + repetitions: repetitions[name], elapsedMs, nsPerTrace, + nsPerHostAction: nsPerTrace / counts.hostActions, + nsPerDispatchedEvent: nsPerTrace / counts.dispatchedEvents }); + } +} +// Each trace begins with reset. Both timing implementations must still agree. +checkSnapshots('after timing'); +const summary = {}; +for (const name of Object.keys(models)) { + const values = samples.filter(sample => sample.implementation === name) + .map(sample => sample.nsPerHostAction).sort((a, b) => a - b); + summary[name] = { medianNsPerHostAction: values[Math.floor(values.length / 2)], + minNsPerHostAction: values[0], maxNsPerHostAction: values.at(-1) }; +} +summary.pureOverJavaScriptMedianRatio = summary.pureHostWasm.medianNsPerHostAction + / summary.handwrittenJavaScript.medianNsPerHostAction; + +const report = { + experiment: 'Pure Workbench charge / handwritten JavaScript reference', + environment: { startedAt, completedAt: new Date().toISOString(), + node: process.version, v8: process.versions.v8, platform: platform(), osRelease: release(), + architecture: arch(), cpuModel: cpus()[0]?.model ?? 'unknown', + logicalCPUs: cpus().length, availableParallelism: availableParallelism(), + affinityControlled: false, powerControlled: false, + command: `node ${fileURLToPath(import.meta.url)}${args.length ? ` --output ${args[1]}` : ''}` }, + artifacts: Object.fromEntries(Object.keys(files).map(name => [name, + { bytes: contents[name].length, sha256: hash(contents[name]) }])), + correctness, + sources: { chargePure: sourceInfo('charge'), fixedJavaScriptModel: sourceInfo('baseline'), + note: 'Raw file size and line counts only; excludes HTML/host/compiler and does not establish authoring effort or DX quality.' }, + compilerMetrics: program.metrics, + memory: { pure: memory, + note: 'Wasm linear capacity and core layout counters; not process RSS or browser memory. No equivalent JS heap measurement collected. Do not add overlapping counters.' }, + timing: { seed, warmupTracesPerImplementation: warmupTraces, + trialsPerImplementation: trials, targetTrialMs, calibration, traceCounts: counts, + order: 'Alternated each trial; Pure first on odd trials, JavaScript first on even trials.', + included: 'Host method dispatch, source-latch aggregation, state logic, and resets in the fixed trace. Pure additionally maintains generic VM/event diagnostics.', + excluded: 'Compilation, loading, snapshots, binding evaluation, DOM rendering, scene editing, and live apply.', + normalization: 'nsPerHostAction divides total time by calls including repeats, releaseAll and reset; nsPerDispatchedEvent also amortizes that host overhead.', + limitations: 'Single-machine batch throughput; no CPU pinning or energy/thermal control. Seven trial averages are not event latency percentiles. Fixed JS has no live program replacement, general compiler, or state migration; not feature-equivalent whole-product timing.', + samples, summary }, +}; +const json = JSON.stringify(report, null, 2) + '\n'; +if (args.length) await writeFile(args[1], json, { flag: 'wx' }); +process.stdout.write(json); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compiler.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compiler.test.mjs new file mode 100644 index 0000000..89b8c6c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/compiler.test.mjs @@ -0,0 +1,240 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile, LIMITS } from './web/compiler.mjs'; + +function diagnostic(source, pattern, line, column) { + assert.throws(() => compile(source), error => { + assert.match(error.message, pattern); + assert.ok(Number.isInteger(error.line) && error.line > 0); + assert.ok(Number.isInteger(error.column) && error.column > 0); + if (line !== undefined) assert.equal(error.line, line); + if (column !== undefined) assert.equal(error.column, column); + return true; + }); +} + +// A small independent evaluator checks compiler lowering. The product executes +// these instructions in Zig; the VM has its own native and integration checks. +function evaluate(program, start, length, values, dt = 0) { + const stack = []; + for (let pc = start; pc < start + length; pc++) { + const instruction = program.instructions[pc]; + const { op, arg, value } = instruction; + if (op === 0) stack.push(value); + else if (op === 1) stack.push(values[arg]); + else if (op === 2) stack.push(dt); + else if (op === 7) stack.push(-stack.pop()); + else if (op === 16) stack.push(Number(!stack.pop())); + else if (op === 19) { + const high = stack.pop(), low = stack.pop(), x = stack.pop(); + assert.ok(low <= high); stack.push(Math.min(Math.max(x, low), high)); + } else if (op === 20) values[arg] = stack.pop(); + else if (op === 21) { if (!stack.pop()) pc = arg - 1; } + else { + const right = stack.pop(), left = stack.pop(); + const operations = { + 3: () => left + right, 4: () => left - right, 5: () => left * right, + 6: () => left / right, 8: () => Number(left < right), + 9: () => Number(left <= right), 10: () => Number(left > right), + 11: () => Number(left >= right), 12: () => Number(left === right), + 13: () => Number(left !== right), 14: () => Number(Boolean(left) && Boolean(right)), + 15: () => Number(Boolean(left) || Boolean(right)), + 17: () => Math.min(left, right), 18: () => Math.max(left, right), + }; + assert.ok(operations[op], `Unknown opcode ${op}`); + stack.push(operations[op]()); + } + assert.ok(stack.every(Number.isFinite)); + } + return stack; +} + +function dispatch(program, event, values, dt = 0) { + for (const handler of program.handlers.filter(handler => handler.event === event)) { + assert.deepEqual(evaluate(program, handler.start, handler.length, values, dt), []); + } +} + +test('a blank document is an empty program with no implied behavior', () => { + const result = compile(''); + for (const field of ['states', 'inputs', 'views', 'instructions', 'handlers']) assert.deepEqual(result[field], []); + assert.equal(result.metrics.sourceBytes, 0); + assert.equal(result.metrics.maxStack, 0); + assert.deepEqual(compile('# comment only\n;').instructions, []); +}); + +test('forward references and separate state/input namespaces preserve declaration IDs', () => { + const program = compile('on tap.down { second = tap + first }\ntext label(0,0,second)\nstate first = -2\nstate tap = +3\ninput tap = Space\nstate second = .5e2'); + assert.deepEqual(program.states.map(state => state.name), ['first', 'tap', 'second']); + assert.equal(program.handlers[0].event, 1); + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + assert.deepEqual(values, [-2, 3, 1]); + const binding = program.views[0].bindings.value; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [1]); +}); + +test('ordered assignments, repeated handlers, and condition evaluation once', () => { + const program = compile(`state x = 1 +input tap = Enter +on tap.down { + if x == 1 { + x = 0 + x = x + 5 + } +} +on tap.down { x = x * 2 } +on tap.up { x = x - 1 }`); + const values = [1]; + dispatch(program, 1, values); + assert.equal(values[0], 10); + dispatch(program, 2, values); + assert.equal(values[0], 9); + const branch = program.instructions.find(instruction => instruction.op === 21); + assert.equal(branch.arg, program.handlers[0].start + program.handlers[0].length); +}); + +test('nested branches use absolute forward targets and skip correct bodies', () => { + const program = compile(`state x = 0 +text label(0, 0, x) +on frame { + if x > 0 { + x = 8 + if x == 8 { x = 9 } + } + x = x + 1 +}`); + const values = [0]; + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 1); + dispatch(program, 0, values, 1 / 60); assert.equal(values[0], 10); + const handler = program.handlers[0]; + program.instructions.forEach((instruction, pc) => { + if (instruction.op === 21) assert.ok(instruction.arg > pc && instruction.arg <= handler.start + handler.length); + }); +}); + +test('operator precedence, associativity, unary and builtin function lowering', () => { + const expressions = new Map([ + ['2 + 3 * 4', 14], ['8 - 3 - 2', 3], ['12 / 3 / 2', 2], + ['-2 * -3', 6], ['!0 + !4', 1], ['1 || 0 && 0', 1], + ['1 < 2 && 2 <= 2 && 4 > 3 && 4 >= 4', 1], + ['3 == 3 && 3 != 4', 1], ['clamp(max(3, 9) - min(4, 7), 0, 4)', 4], + ]); + for (const [source, expected] of expressions) { + const program = compile(`state x = 0\non frame { x = ${source} }`); + const values = [0]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], expected, source); + } + const eager = compile('state x = 0\non frame { x = 1 || x / 0 }'); + assert.ok(eager.instructions.some(instruction => instruction.op === 6)); + assert.ok(!eager.instructions.some(instruction => instruction.op === 21)); +}); + +test('multiline parentheses, scientific literals, comments, and string escapes', () => { + const program = compile(`state x = 1e-2 # comment +box tile( + (x + + 2), 0, max( + 1, 2), 4, + "#AABBCC") +text label(0, 0, x, "# count: \\\"value\\\" \\n") +on frame { x = x + dt }`); + assert.equal(program.views[0].color, '#AABBCC'); + assert.equal(program.views[1].prefix, '# count: "value" \n'); + const values = [.01]; dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], .01 + 1 / 60); + const binding = program.views[0].bindings.x; + assert.deepEqual(evaluate(program, binding.start, binding.length, values), [values[0] + 2]); +}); + +test('all instruction fields are numeric and carry original source line', () => { + const program = compile('state x = 0\non frame {\n x = x + dt\n}'); + for (const instruction of program.instructions) { + assert.deepEqual(Object.keys(instruction), ['op', 'arg', 'value', 'line']); + assert.ok(Object.values(instruction).every(Number.isFinite)); + assert.equal(instruction.line, 3); + } +}); + +test('contextual declaration words remain valid names outside reserved builtins', () => { + const program = compile('state if = 1\nstate state = 2\non frame { if = if + state }'); + const values = [1, 2]; dispatch(program, 0, values, 1 / 60); + assert.deepEqual(values, [3, 2]); +}); + +test('unknown and reserved symbols have useful one-based locations', () => { + diagnostic('state x = 0\non frame {\n x = missing\n}', /Unknown state missing/, 3, 7); + diagnostic('on missing.down {}', /Unknown input missing/, 1, 4); + diagnostic('on frame { absent = 1 }', /Unknown state absent/, 1, 12); + for (const name of ['frame', 'dt', 'min', 'max', 'clamp']) { + diagnostic(`state ${name} = 0`, /Reserved name/, 1, 7); + diagnostic(`input ${name} = Space`, /Reserved name/); + } + diagnostic('text clock(0, 0, dt)', /dt is only available in handlers/); +}); + +test('duplicates, unsupported keys, initializers, and invalid syntax are rejected', () => { + const invalid = [ + ['state x=0\nstate x=1', /Duplicate state/], + ['input tap=Space\ninput tap=Enter', /Duplicate input/], + ['input tap=Space\ninput other=Space', /Duplicate keyboard/], + ['box a(0,0,1,1,"#000000")\ncircle a(0,0,1,"#000000")', /Duplicate view/], + ['input tap=Escape', /Unsupported keyboard/], + ['state x=1+2', /Expected newline/], + ['state x=1e999', /finite/], + ['state x=NaN', /numeric state initializer/], + ['on tap.click {}', /Expected down or up/], + ['on frame.down {}', /Expected {/], + ['state x=0\non frame { x = min(1) }', /min expects 2/], + ['state x=0\non frame { x = clamp(1,2) }', /clamp expects 3/], + ['state x=0\non frame { x = 1 +\n2 }', /Expected a number/], + ['text a(0,0,1,"broken)', /Unterminated string/], + ['text a(0,0,1,"bad\\q")', /Unsupported string escape/], + ['box a(0,0,1,1,"blue")', /Color must use/], + ['state x=0 @', /Unexpected character/], + ['on frame {', /assignment, if, or closing brace/], + ['state x = 0 state y = 1', /Expected newline/], + ]; + for (const [source, pattern] of invalid) diagnostic(source, pattern); +}); + +test('document, declaration, instruction, and nesting limits reject overflow', () => { + diagnostic('#' + '한'.repeat(11000), /32768 UTF-8 bytes/); + diagnostic(Array.from({ length: 33 }, (_, i) => `state s${i}=0`).join('\n'), /32 states/); + diagnostic(Array.from({ length: 9 }, (_, i) => `input i${i}=Digit${i}`).join('\n'), /8 inputs/); + diagnostic(Array.from({ length: 25 }, (_, i) => `text t${i}(0,0,0)`).join('\n'), /24 views/); + diagnostic('on frame {}\n'.repeat(65), /64 handlers/); + diagnostic(`state x=0\non frame {\n${'x=1\n'.repeat(257)}}`, /512 instructions/); + assert.equal(compile(`state x=0\non frame {\n${'x=1\n'.repeat(256)}}`).instructions.length, LIMITS.instructions); + diagnostic(`state x=0\non frame { x=${'('.repeat(65)}1${')'.repeat(65)} }`, /nesting exceeds 64/); + diagnostic(`on frame { ${'if 1 { '.repeat(65)}${'}\n'.repeat(66)}`, /nesting exceeds 64/); + // Right-nested clamp's two waiting operands exceed the stack before its depth limit. + let deep = '0'; + for (let i = 0; i < 33; i++) deep = `clamp(0,0,${deep})`; + diagnostic(`text stack(0,0,${deep})`, /stack exceeds 64/); +}); + +test('every supported input maps to a bounded ordered event ID', () => { + const keys = ['Space', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyZ', 'Digit9']; + const program = compile(keys.map((key, i) => `input i${i}=${key}\non i${i}.down {}\non i${i}.up {}`).join('\n')); + assert.deepEqual(program.handlers.map(handler => handler.event), Array.from({ length: 16 }, (_, i) => i + 1)); +}); + +test('optional examples compile and charge behavior is entirely authored', async () => { + for (const name of ['counter', 'charge', 'toggle']) { + const program = compile(await readFile(new URL(`./examples/${name}.pure`, import.meta.url), 'utf8')); + assert.ok(program.instructions.length <= LIMITS.instructions); + assert.ok(program.views.length > 0); + if (name === 'charge') { + const values = program.states.map(state => state.initial); + dispatch(program, 1, values); + for (let frame = 0; frame < 90; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[0], 1); assert.equal(values[1], 1); assert.equal(values[2], 240); + dispatch(program, 2, values); assert.deepEqual(values, [0, 0, 240, -460]); + dispatch(program, 0, values, 1 / 60); assert.ok(values[2] < 240); + for (let frame = 0; frame < 120; frame++) dispatch(program, 0, values, 1 / 60); + assert.equal(values[2], 240); assert.equal(values[3], 0); + } + } +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/core.zig b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/core.zig new file mode 100644 index 0000000..9b5e213 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/core.zig @@ -0,0 +1,999 @@ +//! Browser ABI and regression tests for the explicit-instance scalar engine. +const std = @import("std"); +const impl = @import("engine.zig"); +const Engine = impl.Engine; +const Code = impl.Code; +const max_states = impl.max_states; +const max_instructions = impl.max_instructions; +const max_stack = impl.max_stack; +const no_pc = impl.no_pc; +const frame_dt = impl.frame_dt; +const Pending = impl.Pending; +const Workspace = impl.Workspace; +const Tables = impl.Tables; + +var engine: Engine = .{}; + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +export fn reset_all() void { + engine.reset_all(); +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + return engine.begin_program(state_count, input_count); +} + +export fn abort_program() void { + engine.abort_program(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + return engine.put_state(id, initial, old_id); +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + return engine.put_instruction(op, arg, value); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + return engine.put_handler(event, start, length); +} + +export fn bind_handler(handler: u32, base: u32, count: u32) u32 { + return engine.bind_handler(handler, base, count); +} + +export fn put_pointer_target(input: u32, base: u32, count: u32) u32 { + return engine.put_pointer_target(input, base, count); +} + +export fn bind_target_handler(handler: u32, count: u32) u32 { + return engine.bind_target_handler(handler, count); +} + +export fn put_restored_value(id: u32, value: f64) u32 { + return engine.put_restored_value(id, value); +} + +export fn commit_program() u32 { + return engine.commit_program(); +} + +export fn commit_restored_program() u32 { + return engine.commit_restored_program(); +} + +export fn reset_state() void { + engine.reset_state(); +} + +export fn dispatch(event: u32, dt: f64) u32 { + return engine.dispatch(event, dt); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return engine.dispatch_point(event, x, y); +} + +export fn dispatch_target(event: u32, target: u32, x: f64, y: f64) u32 { + return engine.dispatch_target(event, target, x, y); +} + +export fn dispatch_value(event: u32, value: f64) u32 { + return engine.dispatch_value(event, value); +} + +export fn eval_at(start: u32, length: u32) f64 { + return engine.eval_at(start, length); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return engine.configure_evaluation(cache, history); +} + +export fn forget_evaluation(flags: u32) void { + engine.forget_evaluation(flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.begin_evaluation(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + return engine.eval_bounded(start, length); +} + +export fn evaluation_info(id: u32) u32 { + return engine.evaluation_info(id); +} + +export fn get_state(id: u32) f64 { + return engine.get_state(id); +} + +export fn info(id: u32) u32 { + return engine.info(id); +} + +export fn layout(id: u32) u32 { + return engine.layout(id); +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 16), layout(3)); + try std.testing.expectEqual(@as(u32, 80), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "shared handler bodies access their own bound state slots in source order" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try emit(24, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(1, 1, 1)); + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(2, 0, 1)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(f64, 21), get_state(1)); + try std.testing.expectEqual(@as(u32, 4), info(6)); // Three bindings, one body. + try std.testing.expectEqual(@as(u32, 12), info(3)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); +} + +test "failure in a later shared binding rolls back all earlier bindings" { + reset_all(); + try ok(begin_program(4, 1)); + for ([_]f64{ 8, 2, 12, 0 }, 0..) |value, i| try ok(put_state(@intCast(i), value, -1)); + try emit(24, 0, 0); + try emit(24, 1, 0); + try emit(6, 0, 0); + try emit(25, 0, 0); + for (0..2) |i| { + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(@intCast(i), @intCast(i * 2), 2)); + } + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(2)); + try std.testing.expectEqual(@as(u32, 0), info(9)); +} + +test "shared bindings reject missing arguments partial overlaps and invalid slot mappings" { + reset_all(); + try counterProgram(); + const revision = info(4); + for (0..5) |mode| { + try ok(begin_program(1, 1)); + try ok(put_state(0, 9, -1)); + try emit(24, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(1, 0, 2)); + switch (mode) { + 0 => {}, // Parameter op without a binding. + 1 => { + try ok(bind_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), bind_handler(0, 0, 1)); + }, + 2 => { + try std.testing.expectEqual(public(.bad_state), bind_handler(0, 0, 2)); + }, + 3 => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 1, 1)); + try ok(bind_handler(1, 0, 1)); + }, + else => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 2)); // Another owner missing its mapping. + }, + } + try std.testing.expect(commit_program() != 0); + abort_program(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + } +} + +test "one routed handler changes only its event target and rejects missing or foreign routes" { + reset_all(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try ok(put_pointer_target(0, 0, 1)); + try ok(put_pointer_target(0, 1, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try ok(commit_program()); + try ok(dispatch_target(17, 1, 77, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(3)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(17, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, 2, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, no_pc, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(18, 0, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(0, 0, 99, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_target(17, 0, std.math.inf(f64), 0)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 3), layout(8)); + try std.testing.expectEqual(@as(u32, 2), layout(9)); + try std.testing.expectEqual(@as(u32, 54), layout(4)); +} + +test "routes reject invalid mappings and bindings without replacing active work" { + for (0..7) |mode| { + reset_all(); + try counterProgram(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + switch (mode) { + 0 => try std.testing.expectEqual(public(.bad_state), put_pointer_target(0, 1, 2)), + 1 => try std.testing.expectEqual(public(.bad_event), put_pointer_target(2, 0, 1)), + 2 => { + try ok(put_pointer_target(0, 0, 2)); + try std.testing.expectEqual(public(.invalid), put_pointer_target(1, 1, 1)); + }, + else => { + if (mode != 3) try ok(put_pointer_target(0, 0, 1)); + try emit(0, 0, 2); + try emit(25, 0, 0); + try ok(put_handler(if (mode == 4) 0 else 1, 0, 2)); + try ok(bind_target_handler(0, if (mode == 5) 2 else 1)); + if (mode == 6) try std.testing.expectEqual(public(.invalid), bind_target_handler(0, 1)); + }, + } + try std.testing.expect(commit_program() != 0); + // Active program still executes while the failed candidate occupies staging. + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 0), layout(9)); + } + reset_all(); + try ok(begin_program(32, 1)); + for (0..32) |i| try ok(put_state(@intCast(i), 0, -1)); + for (0..24) |i| try ok(put_pointer_target(0, @intCast(i), 1)); + try std.testing.expectEqual(public(.capacity), put_pointer_target(0, 24, 1)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "routed event rollback includes globals and earlier handlers and survives staged route replacement" { + reset_all(); + try ok(begin_program(3, 1)); + for (0..3) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + try ok(put_pointer_target(0, 1, 1)); + try ok(put_pointer_target(0, 2, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try emit(0, 0, 1); + try emit(23, 0, 0); + try emit(6, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 2, 6)); + try ok(bind_target_handler(1, 1)); + try ok(commit_program()); + try ok(begin_program(1, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_pointer_target(0, 0, 1)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_target(17, 1, 88, 0)); + try std.testing.expectEqual(@as(f64, 0), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 2), get_state(2)); + abort_program(); + try ok(dispatch_target(17, 1, 88, 2)); + try std.testing.expectEqual(@as(f64, 99), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(2)); +} + +test "scalar inputs carry a finite ephemeral value without adding state or pointer ownership" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 50, -1)); + try emit(26, 0, 0); + try emit(0, 0, 20); + try emit(0, 0, 80); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(33, 0, 5)); + try ok(commit_program()); + try ok(dispatch_value(33, 200)); + try std.testing.expectEqual(@as(f64, 80), get_state(0)); + try ok(dispatch_value(33, -200)); + try std.testing.expectEqual(@as(f64, 20), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), info(5)); + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqual(@as(u32, 2), info(9)); + for ([_]u32{ 0, 1, 17, 25, 34, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_value(event, 60)); + try std.testing.expectEqual(public(.bad_event), dispatch(33, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(33, 60, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_value(33, std.math.nan(f64))); + try std.testing.expectEqual(@as(f64, 20), get_state(0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "scalar input failure rolls back earlier writes and old scalar events survive staging" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(26, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(33, 0, 2)); + try emit(0, 0, 1); + try emit(26, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(33, 2, 4)); + try ok(commit_program()); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_value(33, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch_value(33, 2)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 1), info(9)); +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/engine.zig b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/engine.zig new file mode 100644 index 0000000..5410a7c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/engine.zig @@ -0,0 +1,601 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +pub const std = @import("std"); +pub const Evaluation = @import("evaluation.zig").Evaluation; +pub const Tables = @import("evaluation.zig").Tables; + +pub const max_states = 32; +pub const max_inputs = 8; +pub const max_instructions = 512; +pub const max_handlers = 64; +pub const max_stack = 64; +pub const max_pointer_targets = 24; +pub const event_target = 255; +pub const no_pc = std.math.maxInt(u32); +pub const frame_dt: f64 = 1.0 / 60.0; + +pub const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +pub const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +pub const Handler = struct { + event: u32 = 0, + start: u32 = 0, + length: u32 = 0, + parameter_base: u8 = 0, + parameter_count: u8 = 0, +}; +pub const PointerTarget = struct { input: u8, base: u8, count: u8 }; +pub const Program = struct { + pointer_targets: [max_pointer_targets]PointerTarget = undefined, + pointer_target_count: u8 = 0, + routed_inputs: u8 = 0, + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +pub const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +pub const Workspace = union { staging: Pending, reuse: Tables }; +pub const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, + + pub fn reuseTables(self: *Engine) ?*Tables { + return if (self.staging_open) null else &self.workspace.reuse; + } + + pub fn finishStaging(self: *Engine) void { + self.workspace = .{ .reuse = .{} }; + self.staging_open = false; + self.evaluation.forget(null, 3); + } + + pub fn public(_: *Engine, code: Code) u32 { + return @intFromEnum(code); + } + + pub fn stagingFail(self: *Engine, code: Code) u32 { + if (self.staging_error == .ok) self.staging_error = code; + return self.public(code); + } + + pub fn reset_all(self: *Engine) void { + self.* = .{}; + } + + pub fn begin_program(self: *Engine, state_count: u32, input_count: u32) u32 { + self.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (self.staging_open) self.finishStaging(); + return self.stagingFail(.capacity); + } + self.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + self.staging_open = true; + self.evaluation.forget(null, 3); + return 0; + } + + pub fn abort_program(self: *Engine) void { + if (self.staging_open) self.finishStaging(); + } + + pub fn put_state(self: *Engine, id: u32, initial: f64, old_id: i32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + if (id >= self.workspace.staging.program.state_count or old_id < -1) return self.stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= self.active.state_count) return self.stagingFail(.bad_state); + if (self.workspace.staging.program.declared[id]) return self.stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return self.stagingFail(.non_finite); + self.workspace.staging.program.initials[id] = initial; + self.workspace.staging.program.old_ids[id] = old_id; + self.workspace.staging.program.declared[id] = true; + return 0; + } + + pub fn put_instruction(self: *Engine, op: u32, arg: u32, value: f64) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.instruction_count == max_instructions) return self.stagingFail(.capacity); + if (op > 26) return self.stagingFail(.invalid); + if (!std.math.isFinite(value)) return self.stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; + } + + pub fn validEvent(_: *Engine, event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs) or + (event >= 33 and event - 33 < inputs); + } + + pub fn put_handler(self: *Engine, event: u32, start: u32, length: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.handler_count == max_handlers) return self.stagingFail(.capacity); + if (!self.validEvent(event, p.input_count)) return self.stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; + } + + pub fn bind_handler(self: *Engine, handler: u32, base: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return self.stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return self.stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return self.stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; + } + + // A route is a static program-local index, never a persistent object identity. + pub fn put_pointer_target(self: *Engine, input: u32, base: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (p.pointer_target_count == max_pointer_targets) return self.stagingFail(.capacity); + if (input >= p.input_count) return self.stagingFail(.bad_event); + if (base >= p.state_count or count == 0 or count > 8 or count > p.state_count - base) return self.stagingFail(.bad_state); + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (base < @as(u32, t.base) + t.count and t.base < base + count) return self.stagingFail(.invalid); + if (t.input == input and t.count != count) return self.stagingFail(.bad_state); + } + p.pointer_targets[p.pointer_target_count] = .{ .input = @intCast(input), .base = @intCast(base), .count = @intCast(count) }; + p.pointer_target_count += 1; + p.routed_inputs |= @as(u8, 1) << @as(u3, @intCast(input)); + return 0; + } + + pub fn bind_target_handler(self: *Engine, handler: u32, count: u32) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const p = &self.workspace.staging.program; + if (handler >= p.handler_count or count == 0 or count > 8) return self.stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return self.stagingFail(.invalid); + h.parameter_base = event_target; + h.parameter_count = @intCast(count); + return 0; + } + + pub fn eventInput(_: *Engine, event: u32) u32 { + return if (event >= 33) event - 33 else if (event >= 25) event - 25 else if (event >= 17) event - 17 else (event - 1) / 2; + } + + pub fn validate(self: *Engine, p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= self.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!self.validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_base == event_target) { + if (h.event == 0 or h.event >= 33 or h.parameter_count == 0 or h.parameter_count > 8) return .bad_event; + var found = false; + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (t.input != self.eventInput(h.event)) continue; + if (t.count != h.parameter_count) return .bad_state; + found = true; + } + if (!found) return .bad_event; + } else if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 26) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; + } + + pub fn put_restored_value(self: *Engine, id: u32, value: f64) u32 { + if (!self.staging_open) return self.stagingFail(.invalid); + const pending = &self.workspace.staging; + if (id >= pending.program.state_count) return self.stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return self.stagingFail(.bad_state); + if (!std.math.isFinite(value)) return self.stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; + } + + pub fn commit_program(self: *Engine) u32 { + return self.commit(false); + } + + pub fn commit_restored_program(self: *Engine) u32 { + return self.commit(true); + } + + pub fn commit(self: *Engine, restoring: bool) u32 { + if (!self.staging_open) return self.public(.invalid); + if (self.staging_error != .ok) return self.public(self.staging_error); + const count = self.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (self.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return self.stagingFail(.bad_state); + const code = self.validate(&self.workspace.staging.program); + if (code != .ok) return self.stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..self.workspace.staging.program.state_count) |i| { + const old_id = self.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) self.workspace.staging.values[i] else if (old_id < 0) self.workspace.staging.program.initials[i] else self.values[@intCast(old_id)]; + } + self.active = self.workspace.staging.program; + self.values = next; + self.finishStaging(); + if (restoring) { + self.tick = 0; + self.successful_events = 0; + } + self.revision +%= 1; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + return 0; + } + + pub fn reset_state(self: *Engine) void { + self.values = self.active.initials; + self.evaluation.forget(self.reuseTables(), 3); + self.tick = 0; + self.successful_events = 0; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + } + + const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, + }; + + pub fn executionFail(_: *Engine, result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; + } + + const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false, scalar: bool = false, target: ?u32 = null }; + + pub fn execute(self: *Engine, values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &self.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return self.executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return self.executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return self.executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24, 26 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return self.executionFail(result, .invalid, pc), + }; + if (sp < need) return self.executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24 or inst.op == 26) { + if (sp == max_stack) return self.executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return self.executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return self.executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return self.executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 26 => if (context.scalar) context.x else return self.executionFail(result, .bad_event, pc), + 2 => context.dt, + 22, 23 => if (!context.point) return self.executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return self.executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return self.executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return self.executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return self.executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return self.executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return self.executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return self.executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return self.executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; + } + + pub fn dispatch(self: *Engine, event: u32, dt: f64) u32 { + return self.dispatchContext(event, .{ .dt = dt }); + } + + pub fn dispatch_point(self: *Engine, event: u32, x: f64, y: f64) u32 { + return self.dispatchContext(event, .{ .x = x, .y = y, .point = true }); + } + + pub fn dispatch_target(self: *Engine, event: u32, target: u32, x: f64, y: f64) u32 { + return self.dispatchContext(event, .{ .x = x, .y = y, .point = true, .target = target }); + } + + pub fn dispatch_value(self: *Engine, event: u32, value: f64) u32 { + return self.dispatchContext(event, .{ .x = value, .scalar = true }); + } + + pub fn dispatchContext(self: *Engine, event: u32, context: EventContext) u32 { + const dt = context.dt; + self.last_error = .ok; + self.error_pc = no_pc; + self.last_ops = 0; + if (!self.validEvent(event, self.active.input_count) or + ((context.point or context.scalar) and event == 0) or + (context.scalar != (event >= 33)) or + (!context.point and !context.scalar and event > self.active.input_count * 2)) + { + self.last_error = .bad_event; + return self.public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + self.last_error = .non_finite; + return self.public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + self.last_error = .bad_frame; + return self.public(.bad_frame); + } + var route: ?PointerTarget = null; + const routed = event != 0 and self.active.routed_inputs & (@as(u8, 1) << @as(u3, @intCast(self.eventInput(event)))) != 0; + if (context.target) |target| { + if (!routed or target >= self.active.pointer_target_count or + self.active.pointer_targets[target].input != self.eventInput(event)) + { + self.last_error = .bad_event; + return self.public(.bad_event); + } + route = self.active.pointer_targets[target]; + } else if (routed) { + self.last_error = .bad_event; + return self.public(.bad_event); + } + var pending = self.values; + for (self.active.handlers[0..self.active.handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = if (h.parameter_base == event_target) route.?.base else h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = self.execute(&pending, h.start, h.length, bound, false, null); + self.last_ops += result.ops; + if (result.code != .ok) { + self.last_error = result.code; + self.error_pc = result.pc; + return self.public(result.code); + } + } + if (self.evaluation.cache_count != 0 and !self.staging_open) { + var changed: u32 = 0; + for (0..self.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(self.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + self.evaluation.changed(self.reuseTables(), changed); + } + self.values = pending; + if (event == 0) self.tick +%= 1; + self.successful_events +%= 1; + return 0; + } + + pub fn eval_at(self: *Engine, start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = self.values; + const result = self.execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); + } + + pub fn configure_evaluation(self: *Engine, cache: u32, history: u32) u32 { + return if (self.evaluation.configure(self.reuseTables(), cache, history)) 0 else self.public(.capacity); + } + + pub fn forget_evaluation(self: *Engine, flags: u32) void { + self.evaluation.forget(self.reuseTables(), flags); + } + + pub fn begin_evaluation(self: *Engine, limit: u32) void { + self.evaluation.begin(limit); + } + + pub fn eval_bounded(self: *Engine, start: u32, length: u32) f64 { + const e = &self.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(self.reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = self.values; + const result = self.execute(&values, start, length, .{}, true, e); + e.status = self.public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(self.reuseTables(), start, length, result.value, result.dependencies); + return result.value; + } + + pub fn evaluation_info(self: *Engine, id: u32) u32 { + const e = &self.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!self.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; + } + + pub fn get_state(self: *Engine, id: u32) f64 { + return if (id < self.active.state_count) self.values[id] else std.math.nan(f64); + } + + pub fn info(self: *Engine, id: u32) u32 { + return switch (id) { + 0 => self.tick, + 1 => self.public(self.last_error), + 2 => self.error_pc, + 3 => self.last_ops, + 4 => self.revision, + 5 => self.active.state_count, + 6 => self.active.instruction_count, + 7 => self.active.handler_count, + 8 => self.active.input_count, + 9 => self.successful_events, + else => 0, + }; + } + + pub fn layout(self: *Engine, id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => self.active.instruction_count * @sizeOf(Instruction) + self.active.handler_count * @sizeOf(Handler) + @as(u32, self.active.pointer_target_count) * @sizeOf(PointerTarget), + 5 => self.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + 8 => @sizeOf(PointerTarget), + 9 => self.active.pointer_target_count, + else => 0, + }; + } +}; diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation-check.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation-check.mjs new file mode 100644 index 0000000..bb3d616 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation-check.mjs @@ -0,0 +1,76 @@ +// A bounded adoption check on existing authored files, not a platform benchmark. +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { platform, release, arch, cpus } from 'node:os'; +import { performance } from 'node:perf_hooks'; +import { createRuntime } from './web/runtime.mjs'; + +const args = process.argv.slice(2); +if (args.length !== 2 || args[0] !== '--output') throw new Error('Use --output NEW.json'); +const root = new URL('.', import.meta.url); +const wasm = await readFile(new URL('web/core.wasm', root)); +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +const evidence = { purpose: 'Choose whether to enable binding reuse by default', + utc: new Date().toISOString(), host: { platform: platform(), release: release(), + arch: arch(), cpu: cpus()[0]?.model, node: process.version, affinityControlled: false }, + sourceSha256: {}, cases: [], + timingScope: 'Event dispatch plus snapshot construction. Excludes loading, compilation, DOM, equality checks and statistics reads. Five batch samples are not latency percentiles.', +}; +for (const path of ['web/core.wasm', 'core.zig', 'evaluation.zig', 'web/runtime.mjs', + 'web/compiler.mjs', 'evaluation-check.mjs', 'evaluation.test.mjs']) + evidence.sourceSha256[path] = hash(await readFile(new URL(path, root))); + +for (const name of ['counter', 'toggle', 'charge']) { + const path = `examples/${name}.pure`; + const source = await readFile(new URL(path, root), 'utf8'); + evidence.sourceSha256[path] = hash(source); + const plain = await createRuntime(wasm), cached = await createRuntime(wasm, {cacheEntries:32}); + const program = plain.apply(source); cached.apply(source); + const input = program.inputs[0]?.name; + const trace = Array.from({length:100}, () => 'show'); + for (let i = 0; i < 600; i++) { + if (input && i % 60 === 0) trace.push('down'); + if (input && i % 60 === 15) trace.push('up'); + trace.push('frame'); + } + function action(runtime, kind) { + if (kind === 'frame') assert.equal(runtime.frame(), 0); + else if (kind !== 'show') assert.equal(runtime[kind](input, 'replay'), 0); + return runtime.snapshot(); + } + const totals = {plain:{work:0,instructions:0,cacheHits:0},cached:{work:0,instructions:0,cacheHits:0}}; + for (const kind of trace) { + const left = action(plain, kind), right = action(cached, kind); + assert.equal(left.lastError, 0); assert.deepEqual(right, left); + for (const [key, runtime] of Object.entries({plain,cached})) { + const stats = runtime.evaluationStats(); + for (const field of Object.keys(totals[key])) totals[key][field] += stats[field]; + } + } + function replay(runtime) { + runtime.reset(); + for (const kind of trace) { + if (kind === 'frame') runtime.frame(); + else if (kind !== 'show') runtime[kind](input, 'replay'); + runtime.snapshot(); + } + } + for (let warm = 0; warm < 3; warm++) { replay(plain); replay(cached); } + const samples = []; + for (let trial = 0; trial < 5; trial++) { + for (const key of trial % 2 ? ['cached','plain'] : ['plain','cached']) { + const runtime = key === 'plain' ? plain : cached; + const start = performance.now(); replay(runtime); + samples.push({trial,key,elapsedMs:performance.now()-start}); + } + assert.deepEqual(cached.snapshot(), plain.snapshot()); + } + const median = key => samples.filter(s=>s.key===key).map(s=>s.elapsedMs).sort((a,b)=>a-b)[2]; + evidence.cases.push({name,checkedSnapshots:trace.length, traceSha256:hash(JSON.stringify(trace)), + totals, memory:cached.snapshot().memory, evaluation:cached.evaluationStats(), samples, + medianMs:{plain:median('plain'),cached:median('cached')}, cachedOverPlain:median('cached')/median('plain')}); +} +await writeFile(args[1], JSON.stringify(evidence,null,2)+'\n', {flag:'wx'}); +for (const c of evidence.cases) console.log(JSON.stringify({name:c.name,checkedSnapshots:c.checkedSnapshots, + totals:c.totals,medianMs:c.medianMs,cachedOverPlain:c.cachedOverPlain})); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.test.mjs new file mode 100644 index 0000000..21a9590 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.test.mjs @@ -0,0 +1,133 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const make = (options = {}) => createRuntime(wasm, options); +const source = `state x = 3 +state spare = 0 +input next = KeyN +input other = KeyO +input fail = KeyF +text value(0, 0, (x + 1) * (x + 2) / 2) +on next.down { x = x + 0.25 } +on other.down { spare = spare + 1 } +on fail.down { x = 900; spare = 1 / 0 } +`; + +test('cache reuse, unrelated changes and discarded records preserve authored values', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + cached.apply(source); plain.apply(source); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(cached.evaluationStats().cacheHits > 0); + assert(cached.evaluationStats().instructions < plain.evaluationStats().instructions); + for (let i = 0; i < 100; i++) { + const name = i % 3 ? 'other' : 'next'; + for (const runtime of [cached, plain]) { runtime.down(name); runtime.up(name); } + if (i % 7 === 0) cached.forgetEvaluation('history'); + if (i % 11 === 0) cached.forgetEvaluation('values'); + if (i % 13 === 0) cached.forgetEvaluation('all'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + } + // Omit observations long enough to overwrite every change record. + for (let i = 0; i < 20; i++) for (const runtime of [cached, plain]) { + runtime.down('next'); runtime.up('next'); + } + assert.deepEqual(cached.snapshot(), plain.snapshot()); +}); + +test('committed changes only: failed events and failed edits preserve reusable output', async () => { + const cached = await make({ cacheEntries: 32 }); cached.apply(source); + const before = cached.snapshot(); + cached.down('fail'); cached.up('fail'); + const failed = cached.snapshot(); + assert.deepEqual(failed.states, before.states); + assert.deepEqual(failed.views, before.views); + assert.equal(failed.lastError, 4); + assert.equal(cached.evaluationStats().instructions, 0); + assert.throws(() => cached.apply('state x = missing')); + assert.deepEqual(cached.snapshot(), failed); + assert.equal(cached.evaluationStats().instructions, 0); +}); + +test('program edits, state reordering and reset cannot reuse old definitions', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + for (const r of [cached, plain]) { r.apply(source); r.down('next'); r.up('next'); r.snapshot(); } + const changed = source.replace('state x = 3\nstate spare = 0', 'state spare = 9\nstate x = 300') + .replace('(x + 1) * (x + 2) / 2', '(x + 10) * (x + 20) / 2'); + for (const r of [cached, plain]) r.apply(changed); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.reset(); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().states.x, 300); +}); + +test('finite float semantics include signed zero, high state bit and arithmetic errors', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + const program = `${Array.from({ length: 32 }, (_, i) => `state s${i} = 0`).join('\n')} +input flip = KeyF +text sign(0, 0, s31) +text error(0, 0, 1 / s31) +on flip.down { s31 = -s31 } +on flip.up { s31 = 2 } +`; + for (const r of [cached, plain]) r.apply(program); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.down('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(Object.is(cached.snapshot().views[0].value, -0)); + assert.equal(cached.snapshot().displayError.code, 100); + for (const r of [cached, plain]) r.up('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().displayError, null); +}); + +test('one budget covers the whole scene; failures publish no partial scene or state rollback', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + r.down('next'); r.up('next'); + const good = r.snapshot(); + r.configureEvaluation({ workLimit: 5 }); + const limited = r.snapshot(); + assert.equal(limited.lastError, 101); + assert.deepEqual(limited.views, []); + assert.deepEqual(limited.states, good.states); + assert.equal(limited.successfulEvents, good.successfulEvents); + assert(r.evaluationStats().work <= 5); + r.down('fail'); r.up('fail'); + assert.equal(r.snapshot().lastError, 4); + assert.equal(r.snapshot().displayError.code, 101); + r.configureEvaluation({ workLimit: 2048 }); + assert.deepEqual(r.snapshot().views, good.views); + assert.equal(r.snapshot().lastError, 4); +}); + +test('small caches, no history and every small work limit give exact results or explicit failure', async () => { + const plain = await make(); plain.apply(source); + for (const cacheEntries of [0, 1, 32]) for (const historyEntries of [0, 1, 8]) { + const r = await make({ cacheEntries, historyEntries }); r.apply(source); + for (let workLimit = 0; workLimit < 100; workLimit++) { + r.configureEvaluation({ workLimit }); + const actual = r.snapshot(); + assert(r.evaluationStats().work <= workLimit); + if (actual.lastError) { + assert.equal(actual.lastError, 101); + assert.deepEqual(actual.views, []); + } else assert.deepEqual(actual, plain.snapshot()); + } + } +}); + +test('invalid settings and forget names preserve policy and state', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + const before = r.snapshot(), policy = r.evaluationStats(); + for (const option of [{cacheEntries:33}, {historyEntries:9}, {workLimit:-1}, + {workLimit:0x100000000}, {workLimit:NaN}, {cacheEntries:0.5}]) { + assert.throws(() => r.configureEvaluation(option)); + assert.deepEqual(r.evaluationStats(), policy); + } + for (const name of ['unknown', '__proto__', 'constructor']) assert.throws(() => r.forgetEvaluation(name)); + assert.deepEqual(r.snapshot(), before); + assert.equal(r.snapshot().memory.linearBytes, 65536); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.zig b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/charge.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/charge.pure new file mode 100644 index 0000000..eac55c4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/charge.pure @@ -0,0 +1,34 @@ +# All holding, charging, gravity, jumping, and floor behavior is authored here. +state holding = 0 +state charge = 0 +state y = 240 +state velocity = 0 +input jump = Space +box floor(40, 261, 560, 4, "#356548") +circle actor(180, y, 20, "#c78638") +box meter(80, 80, charge * 200, 12, "#c78638") +text amount(80, 60, charge, "Charge: ") +on jump.down { + if y >= 240 { + holding = 1 + charge = 0 + } +} +on jump.up { + if holding { + velocity = -(160 + charge * 300) + holding = 0 + charge = 0 + } +} +on frame { + if holding { + charge = clamp(charge + dt, 0, 1) + } + velocity = velocity + 600 * dt + y = y + velocity * dt + if y >= 240 { + y = 240 + velocity = 0 + } +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/counter.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/counter.pure new file mode 100644 index 0000000..ca5f18e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/counter.pure @@ -0,0 +1,8 @@ +# Space or the tap control increments a state that is displayed and drawn. +state count = 0 +input tap = Space +text label(80, 90, count, "Count: ") +box tile(80, 150, 40 + min(count, 20) * 8, 60, "#356548") +on tap.down { + count = count + 1 +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/direct.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/document.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/document.pure new file mode 100644 index 0000000..6d78f8c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/document.pure @@ -0,0 +1,6 @@ +# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다. +state ratio = 50 +input split = value +input reset = KeyR +on split.change { ratio = clamp(event_value, 20, 80) } +on reset.down { ratio = 50 } diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/layout.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/layout.pure new file mode 100644 index 0000000..41cf149 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/layout.pure @@ -0,0 +1,92 @@ +# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요. +# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다. +state selected = 0 +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +state x1 = 80 +state y1 = 120 +state ox1 = 0 +state oy1 = 0 +state sx1 = 80 +state sy1 = 120 +state held1 = 0 +input drag1 = pointer(tile1) +state x2 = 280 +state y2 = 160 +state ox2 = 0 +state oy2 = 0 +state sx2 = 280 +state sy2 = 160 +state held2 = 0 +input drag2 = pointer(tile2) +state x3 = 450 +state y3 = 220 +state ox3 = 0 +state oy3 = 0 +state sx3 = 450 +state sy3 = 220 +state held3 = 0 +input drag3 = pointer(tile3) +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box tile1(x1,y1,56,44,"#c9f08b") +box tile2(x2,y2,56,44,"#74c4d4") +box tile3(x3,y3,56,44,"#dfb184") +on drag1.down { + selected = 1 + sx1 = x1; sy1 = y1 + ox1 = event_x - x1; oy1 = event_y - y1 + held1 = 1 +} +on drag1.move { + x1 = clamp(event_x - ox1, 20, 564) + y1 = clamp(event_y - oy1, 90, 286) +} +on drag1.up { held1 = 0 } +on drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 } +on drag2.down { + selected = 2 + sx2 = x2; sy2 = y2 + ox2 = event_x - x2; oy2 = event_y - y2 + held2 = 1 +} +on drag2.move { + x2 = clamp(event_x - ox2, 20, 564) + y2 = clamp(event_y - oy2, 90, 286) +} +on drag2.up { held2 = 0 } +on drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 } +on drag3.down { + selected = 3 + sx3 = x3; sy3 = y3 + ox3 = event_x - x3; oy3 = event_y - y3 + held3 = 1 +} +on drag3.move { + x3 = clamp(event_x - ox3, 20, 564) + y3 = clamp(event_y - oy3, 90, 286) +} +on drag3.up { held3 = 0 } +on drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 } +on left.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) } +} +on right.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) } +} +on up.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) } +} +on down.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) } +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/routed-layout.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/routed-layout.pure new file mode 100644 index 0000000..8933c70 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/routed-layout.pure @@ -0,0 +1,70 @@ +# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다. +state selected = 0 +record Tile { + x = 0; y = 0 + ox = 0; oy = 0 + sx = 0; sy = 0 + held = 0; id = 0 +} +item green = Tile(x=80, y=120, sx=80, sy=120, id=1) +item blue = Tile(x=280, y=160, sx=280, sy=160, id=2) +item amber = Tile(x=450, y=220, sx=450, sy=220, id=3) +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +input drag = pointer { + green_box: green + blue_box: blue + amber_box: amber +} +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +box amber_box(amber.x,amber.y,56,44,"#dfb184") + +rule grab(t: Tile) { + selected = t.id + t.sx = t.x; t.sy = t.y + t.ox = event_x - t.x; t.oy = event_y - t.y + t.held = 1 +} +rule move(t: Tile) { + t.x = clamp(event_x - t.ox, 20, 564) + t.y = clamp(event_y - t.oy, 90, 286) +} +rule drop(t: Tile) { t.held = 0 } +rule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 } +rule go_left(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) } +} +rule go_right(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) } +} +rule go_up(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) } +} +rule go_down(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) } +} + +on drag.down use grab(drag.target) +on drag.move use move(drag.target) +on drag.up use drop(drag.target) +on drag.cancel use cancel(drag.target) + +on left.down use go_left(green) +on right.down use go_right(green) +on up.down use go_up(green) +on down.down use go_down(green) + +on left.down use go_left(blue) +on right.down use go_right(blue) +on up.down use go_up(blue) +on down.down use go_down(blue) + +on left.down use go_left(amber) +on right.down use go_right(amber) +on up.down use go_up(amber) +on down.down use go_down(amber) diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/shared-layout.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/shared-layout.pure new file mode 100644 index 0000000..38c001d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/shared-layout.pure @@ -0,0 +1,75 @@ +# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다. +state selected = 0 +record Tile { + x = 0; y = 0 + ox = 0; oy = 0 + sx = 0; sy = 0 + held = 0; id = 0 +} +item green = Tile(x=80, y=120, sx=80, sy=120, id=1) +item blue = Tile(x=280, y=160, sx=280, sy=160, id=2) +item amber = Tile(x=450, y=220, sx=450, sy=220, id=3) +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +input drag_amber = pointer(amber_box) +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +box amber_box(amber.x,amber.y,56,44,"#dfb184") + +rule grab(t: Tile) { + selected = t.id + t.sx = t.x; t.sy = t.y + t.ox = event_x - t.x; t.oy = event_y - t.y + t.held = 1 +} +rule move(t: Tile) { + t.x = clamp(event_x - t.ox, 20, 564) + t.y = clamp(event_y - t.oy, 90, 286) +} +rule drop(t: Tile) { t.held = 0 } +rule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 } +rule go_left(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) } +} +rule go_right(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) } +} +rule go_up(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) } +} +rule go_down(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) } +} + +on drag_green.down use grab(green) +on drag_green.move use move(green) +on drag_green.up use drop(green) +on drag_green.cancel use cancel(green) +on left.down use go_left(green) +on right.down use go_right(green) +on up.down use go_up(green) +on down.down use go_down(green) + +on drag_blue.down use grab(blue) +on drag_blue.move use move(blue) +on drag_blue.up use drop(blue) +on drag_blue.cancel use cancel(blue) +on left.down use go_left(blue) +on right.down use go_right(blue) +on up.down use go_up(blue) +on down.down use go_down(blue) + +on drag_amber.down use grab(amber) +on drag_amber.move use move(amber) +on drag_amber.up use drop(amber) +on drag_amber.cancel use cancel(amber) +on left.down use go_left(amber) +on right.down use go_right(amber) +on up.down use go_up(amber) +on down.down use go_down(amber) diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/toggle.pure b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/toggle.pure new file mode 100644 index 0000000..a29f38f --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/examples/toggle.pure @@ -0,0 +1,17 @@ +# Space toggles the circle. Right arrow moves the tile. +state visible = 1 +state position = 120 +input toggle = Space +input move = ArrowRight +circle marker(position, 140, visible * 24, "#c78638") +box tile(position - 24, 190, 48, 48, "#356548") +text value(80, 80, visible, "Visible: ") +on toggle.down { + visible = !visible +} +on move.down { + position = position + 40 + if position > 560 { + position = 80 + } +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/manage.py b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/manage.py new file mode 100644 index 0000000..ef89d42 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/manage.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Build the authoring VM or serve its local browser adapter.""" +import argparse +from datetime import datetime, timezone +from functools import partial +import hashlib +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import shutil +import subprocess + +ROOT = Path(__file__).resolve().parent + + +def sync_examples(): + target = ROOT / 'web/examples' + target.mkdir(exist_ok=True) + for source in (ROOT / 'examples').glob('*.pure'): + shutil.copyfile(source, target / source.name) + + +def build(zig): + compiler = shutil.which(zig) + if not compiler: + raise SystemExit('Zig 0.15.2가 필요합니다. --zig /path/to/zig로 지정할 수 있습니다.') + version = subprocess.check_output([compiler, 'version'], text=True).strip() + if version != '0.15.2': + raise SystemExit(f'이 실험은 Zig 0.15.2로 고정했습니다. 현재 {version}') + folder = ROOT / 'build' + folder.mkdir(exist_ok=True) + cache = ['--cache-dir', str(folder / 'cache'), '--global-cache-dir', str(folder / 'global-cache')] + source = ROOT / 'core.zig' + result = subprocess.run([compiler, 'test', str(source), *cache], capture_output=True, text=True) + print(result.stdout + result.stderr, end='', flush=True) + result.check_returncode() + flags = ['-target', 'wasm32-freestanding', '-O', 'ReleaseSafe', '-fstrip', '-fno-entry', + '-rdynamic', '--export-memory', '--stack', '16384', + '--initial-memory=65536', '--max-memory=65536'] + artifact = ROOT / 'web/core.wasm' + subprocess.run([compiler, 'build-exe', str(source), *flags, f'-femit-bin={artifact}', *cache], check=True) + sync_examples() + sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() + (folder / 'build.json').write_text(json.dumps({ + 'builtUtc': datetime.now(timezone.utc).isoformat(), 'zigVersion': version, + 'sourceSha256': sha(source), 'wasmSha256': sha(artifact), + 'evaluationSourceSha256': sha(ROOT / 'evaluation.zig'), + 'engineSourceSha256': sha(ROOT / 'engine.zig'), + 'wasmFileBytes': artifact.stat().st_size, 'flags': flags, + 'nativeTests': result.stdout + result.stderr, + }, indent=2) + '\n') + print(f'Built {artifact} ({artifact.stat().st_size:,} bytes)') + + +def native(zig, source, output, cc): + compiler = shutil.which(zig) + if not compiler or subprocess.check_output([compiler, 'version'], text=True).strip() != '0.15.2': + raise SystemExit('Zig 0.15.2가 필요합니다. --zig /path/to/zig로 지정하세요.') + folder = ROOT / 'build/native' + folder.mkdir(parents=True, exist_ok=True) + cache = ['--cache-dir', str(ROOT / 'build/cache'), '--global-cache-dir', str(ROOT / 'build/global-cache')] + library = folder / 'libpure-scalar.a' + subprocess.run([compiler, 'build-lib', str(ROOT / 'native.zig'), '-O', 'ReleaseSafe', + '-fno-compiler-rt', f'-femit-bin={library}', *cache], check=True) + prepared = folder / 'application.c' + subprocess.run(['node', str(ROOT / 'native/prepare.mjs'), str(source.resolve()), str(prepared)], check=True) + result = output.resolve() if output else folder / 'pure-native' + result.parent.mkdir(parents=True, exist_ok=True) + connections = ROOT.parent / 'connections' + inputs = [prepared, ROOT / 'native/main.c', ROOT / 'native/app.c', ROOT / 'native/plan.c', + *(connections / name for name in ('runtime.c', 'evaluate.c', 'store.c', 'store-port.c')), library] + flags = ['-std=c17', '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror'] + subprocess.run([cc, *flags, '-I' + str(ROOT / 'native'), *(str(p) for p in inputs), '-lm', '-o', str(result)], check=True) + sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() + (folder / 'build.json').write_text(json.dumps({'source': str(source.resolve()), 'sourceSha256': sha(source), + 'zigVersion': '0.15.2', 'zigFlags': ['-O', 'ReleaseSafe', '-fno-compiler-rt'], 'cFlags': flags, + 'binary': str(result), 'binarySha256': sha(result), 'librarySha256': sha(library), + 'preparedSha256': sha(prepared), + 'implementationSha256': {str(p.relative_to(ROOT.parent.parent)): sha(p) for p in [ + ROOT / 'engine.zig', ROOT / 'evaluation.zig', ROOT / 'native.zig', ROOT / 'native/prepare.mjs', + ROOT / 'native/scalar.h', ROOT / 'native/plan.h', ROOT / 'native/app.h', ROOT / 'web/compiler.mjs', + *(p for p in inputs if p.suffix == '.c' and p != prepared)]}}, indent=2) + '\n') + print(f'Built native application: {result}') + + +class Handler(SimpleHTTPRequestHandler): + extensions_map = {**SimpleHTTPRequestHandler.extensions_map, '.wasm': 'application/wasm', '.mjs': 'text/javascript', '.pure': 'text/plain'} + + def end_headers(self): + self.send_header('Cache-Control', 'no-store') + super().end_headers() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='action', required=True) + b = sub.add_parser('build') + b.add_argument('--zig', default='zig') + n = sub.add_parser('native', help='prepare an existing .pure definition as a standalone native CLI') + n.add_argument('source', type=Path) + n.add_argument('--zig', default='zig') + n.add_argument('--cc', default='cc') + n.add_argument('--output', type=Path) + s = sub.add_parser('serve') + s.add_argument('--port', type=int, default=8766) + args = parser.parse_args() + if args.action == 'build': + build(args.zig) + return + if args.action == 'native': + native(args.zig, args.source, args.output, args.cc) + return + if not 1 <= args.port <= 65535: + parser.error('port must be 1..65535') + if not (ROOT / 'web/core.wasm').is_file(): + parser.error('먼저 manage.py build를 실행하세요.') + sync_examples() + with ThreadingHTTPServer(('127.0.0.1', args.port), partial(Handler, directory=str(ROOT / 'web'))) as server: + print(f'Pure workbench: http://127.0.0.1:{args.port}', flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + main() diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native.zig b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native.zig new file mode 100644 index 0000000..30d15d4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native.zig @@ -0,0 +1,127 @@ +//! Native C ABI. The caller supplies aligned instance storage; no global VM, +//! allocator, OS call or browser dependency is part of this selected module. +const Engine = @import("engine.zig").Engine; + +// Keep ReleaseSafe bounds/overflow checks. Unexpected internal invariant +// failures trap without importing a process-wide stack unwinder or I/O layer. +// Expected authored errors still return the ordinary scalar status codes. +pub const panic = @import("std").debug.no_panic; + +export fn pure_scalar_bytes() usize { + return @sizeOf(Engine); +} +export fn pure_scalar_alignment() usize { + return @alignOf(Engine); +} + +export fn pure_scalar_initialize(memory: ?*anyopaque, size: usize) ?*Engine { + const pointer = memory orelse return null; + if (size < @sizeOf(Engine) or @intFromPtr(pointer) % @alignOf(Engine) != 0) return null; + const instance: *Engine = @ptrCast(@alignCast(pointer)); + instance.* = .{}; + return instance; +} + +export fn pure_scalar_reset_all(instance: *Engine) void { + instance.reset_all(); +} + +export fn pure_scalar_begin_program(instance: *Engine, state_count: u32, input_count: u32) u32 { + return instance.begin_program(state_count, input_count); +} + +export fn pure_scalar_abort_program(instance: *Engine) void { + instance.abort_program(); +} + +export fn pure_scalar_put_state(instance: *Engine, id: u32, initial: f64, old_id: i32) u32 { + return instance.put_state(id, initial, old_id); +} + +export fn pure_scalar_put_instruction(instance: *Engine, op: u32, arg: u32, value: f64) u32 { + return instance.put_instruction(op, arg, value); +} + +export fn pure_scalar_put_handler(instance: *Engine, event: u32, start: u32, length: u32) u32 { + return instance.put_handler(event, start, length); +} + +export fn pure_scalar_bind_handler(instance: *Engine, handler: u32, base: u32, count: u32) u32 { + return instance.bind_handler(handler, base, count); +} + +export fn pure_scalar_put_pointer_target(instance: *Engine, input: u32, base: u32, count: u32) u32 { + return instance.put_pointer_target(input, base, count); +} + +export fn pure_scalar_bind_target_handler(instance: *Engine, handler: u32, count: u32) u32 { + return instance.bind_target_handler(handler, count); +} + +export fn pure_scalar_put_restored_value(instance: *Engine, id: u32, value: f64) u32 { + return instance.put_restored_value(id, value); +} + +export fn pure_scalar_commit_program(instance: *Engine) u32 { + return instance.commit_program(); +} + +export fn pure_scalar_commit_restored_program(instance: *Engine) u32 { + return instance.commit_restored_program(); +} + +export fn pure_scalar_reset_state(instance: *Engine) void { + instance.reset_state(); +} + +export fn pure_scalar_dispatch(instance: *Engine, event: u32, dt: f64) u32 { + return instance.dispatch(event, dt); +} + +export fn pure_scalar_dispatch_point(instance: *Engine, event: u32, x: f64, y: f64) u32 { + return instance.dispatch_point(event, x, y); +} + +export fn pure_scalar_dispatch_target(instance: *Engine, event: u32, target: u32, x: f64, y: f64) u32 { + return instance.dispatch_target(event, target, x, y); +} + +export fn pure_scalar_dispatch_value(instance: *Engine, event: u32, value: f64) u32 { + return instance.dispatch_value(event, value); +} + +export fn pure_scalar_eval_at(instance: *Engine, start: u32, length: u32) f64 { + return instance.eval_at(start, length); +} + +export fn pure_scalar_configure_evaluation(instance: *Engine, cache: u32, history: u32) u32 { + return instance.configure_evaluation(cache, history); +} + +export fn pure_scalar_forget_evaluation(instance: *Engine, flags: u32) void { + instance.forget_evaluation(flags); +} + +export fn pure_scalar_begin_evaluation(instance: *Engine, limit: u32) void { + instance.begin_evaluation(limit); +} + +export fn pure_scalar_eval_bounded(instance: *Engine, start: u32, length: u32) f64 { + return instance.eval_bounded(start, length); +} + +export fn pure_scalar_evaluation_info(instance: *Engine, id: u32) u32 { + return instance.evaluation_info(id); +} + +export fn pure_scalar_get_state(instance: *Engine, id: u32) f64 { + return instance.get_state(id); +} + +export fn pure_scalar_info(instance: *Engine, id: u32) u32 { + return instance.info(id); +} + +export fn pure_scalar_layout(instance: *Engine, id: u32) u32 { + return instance.layout(id); +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.c new file mode 100644 index 0000000..38da4ab --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.c @@ -0,0 +1,208 @@ +#include "app.h" +#include "../../connections/store-port.h" +#include "../../connections/store.h" +#include +#include +#include + +_Static_assert(sizeof(double) == 8 && FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024, + "the current snapshot boundary requires IEEE binary64"); + +static const PureProgram connections = {.read = {.to_end = true}, .write = {.to_end = true}}; +static const unsigned char marker[8] = {'P','U','R','E','S','0','0','1'}; +enum { SNAPSHOT_HEADER = 44 }; + +static void release(void *memory, size_t size) { (void)size; free(memory); } +static bool allocate(void *context, size_t size, PureResource *out) { + (void)context; + void *memory = malloc(size); + if (!memory) return false; + *out = (PureResource){.body = {memory, size}, .allocation = memory, + .allocation_size = size, .release = release}; + return true; +} +static uint64_t decode(const unsigned char *bytes, unsigned count) { + uint64_t value = 0; + for (unsigned i = 0; i < count; ++i) value |= (uint64_t)bytes[i] << (8 * i); + return value; +} +static void encode(unsigned char *bytes, uint64_t value, unsigned count) { + for (unsigned i = 0; i < count; ++i) bytes[i] = (unsigned char)(value >> (8 * i)); +} + +static PureResult read_value(void *context, const char *key, PureRegion region, size_t minimum, + size_t available, PureResource *out, int64_t *revision) { + PureNativeApp *app = context; + if (!strcmp(key, "load")) { + PureHost store = pure_store_host(app->directory); + return store.read(store.context, app->document, region, minimum, available, out, revision); + } + if (strcmp(key, "snapshot")) return PURE_INVALID; + if (app->held) return PURE_BUSY; + size_t size = SNAPSHOT_HEADER + 8u * app->plan->state_count; + /* This host contract currently exports the complete typed snapshot. */ + if (minimum > size || !region.to_end || region.offset) return PURE_LIMIT; + if (size > available) return PURE_BUDGET; + if (!allocate(NULL, size, out)) return PURE_ALLOCATION; + unsigned char *bytes = out->allocation; + memcpy(bytes, marker, sizeof(marker)); + memcpy(bytes + 8, app->plan->identity, 32); + encode(bytes + 40, app->plan->state_count, 4); + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + double value = pure_scalar_get_state(app->scalar, i); + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + encode(bytes + SNAPSHOT_HEADER + i * 8, bits, 8); + } + *revision = app->revision; + return PURE_OK; +} + +static PureResult action(PureNativeApp *app, PureAction a) { + if (a.phase == PURE_RESET) { + if (app->held) return PURE_BUSY; + pure_scalar_reset_state(app->scalar); + return PURE_OK; + } + if (a.phase == PURE_FRAME) { + app->last_scalar_error = pure_scalar_dispatch(app->scalar, 0, 1.0 / 60.0); + return app->last_scalar_error ? PURE_INVALID : PURE_OK; + } + if (a.input >= app->plan->input_count || a.input >= 8) return PURE_INVALID; + PureScalarInputKind kind = app->plan->inputs[a.input].kind; + uint32_t bit = 1u << a.input, event; + if (kind == PURE_VALUE) { + if (a.phase != PURE_CHANGE) return PURE_INVALID; + app->last_scalar_error = pure_scalar_dispatch_value(app->scalar, 33 + a.input, a.x); + return app->last_scalar_error ? PURE_INVALID : PURE_OK; + } + if (kind == PURE_KEY) { + if ((a.phase != PURE_DOWN && a.phase != PURE_UP) || a.source >= 32) return PURE_INVALID; + uint32_t source = 1u << a.source, before = app->key_sources[a.input]; + if (a.phase == PURE_DOWN) { + app->key_sources[a.input] |= source; + app->held |= bit; + if (before) return PURE_OK; + } else { + if (!(before & source)) return PURE_OK; + app->key_sources[a.input] &= ~source; + if (app->key_sources[a.input]) return PURE_OK; + app->held &= ~bit; + } + event = a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u); + app->last_scalar_error = pure_scalar_dispatch(app->scalar, event, 0); + } else { + if (a.phase > PURE_CANCEL) return PURE_INVALID; + uint32_t slot = a.input; + if (kind == PURE_ROUTED) { + if (a.target >= app->plan->target_count || a.target >= 24 || app->plan->targets[a.target].input != a.input) return PURE_INVALID; + slot = 8 + a.target; + } + bit = 1u << slot; + if (a.phase == PURE_DOWN) { + if (app->held & bit) return app->pointer_owners[slot] == a.source ? PURE_OK : PURE_BUSY; + app->held |= bit; + app->pointer_owners[slot] = a.source; + } else { + if (!(app->held & bit) || app->pointer_owners[slot] != a.source) return PURE_INVALID; + if (a.phase == PURE_UP || a.phase == PURE_CANCEL) app->held &= ~bit; + } + event = a.phase == PURE_MOVE ? 17 + a.input : a.phase == PURE_CANCEL ? 25 + a.input + : a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u); + app->last_scalar_error = kind == PURE_ROUTED + ? pure_scalar_dispatch_target(app->scalar, event, a.target, a.x, a.y) + : pure_scalar_dispatch_point(app->scalar, event, a.x, a.y); + } + /* Input ownership follows the physical phases even if an authored handler + * fails; its scalar writes remain atomic. Each routed target has its owner. */ + return app->last_scalar_error ? PURE_INVALID : PURE_OK; +} + +static PureResult write_value(void *context, const char *key, int64_t expected, + PureSpan bytes, int64_t *revision) { + PureNativeApp *app = context; + if (!strcmp(key, "save")) return pure_store_put(app->directory, app->document, expected, bytes, revision); + *revision = app->revision; + if (expected != app->revision) return PURE_CONFLICT; + if (app->revision == INT64_MAX) return PURE_LIMIT; + app->last_scalar_error = 0; + if (!strcmp(key, "event")) { + if (bytes.size != sizeof(PureAction)) return PURE_LIMIT; + PureAction input; + memcpy(&input, bytes.data, sizeof(input)); + PureResult status = action(app, input); + if (status != PURE_OK) return status; + } else if (!strcmp(key, "restore")) { + if (app->held) return PURE_BUSY; + if (bytes.size != SNAPSHOT_HEADER + 8u * app->plan->state_count || + memcmp(bytes.data, marker, sizeof(marker)) || memcmp(bytes.data + 8, app->plan->identity, 32) || + decode(bytes.data + 40, 4) != app->plan->state_count) return PURE_INVALID; + double values[32]; + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + uint64_t bits = decode(bytes.data + SNAPSHOT_HEADER + 8 * i, 8); + memcpy(values + i, &bits, sizeof(bits)); + } + app->last_scalar_error = pure_scalar_load(app->scalar, app->plan, NULL, values); + if (app->last_scalar_error) return PURE_INVALID; + } else return PURE_INVALID; + *revision = ++app->revision; + return PURE_OK; +} + +PureResult pure_native_app_init(PureNativeApp *app, const PureScalarPlan *plan, void *memory, + size_t size, const char *directory, const char *key, size_t budget) { + if (!app || !plan || !directory || !key || !*key || strlen(key) > 64 || plan->state_count > 32) return PURE_INVALID; + for (const char *p = key; *p; ++p) + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')) return PURE_INVALID; + PureScalar *scalar = pure_scalar_initialize(memory, size); + if (!scalar) return PURE_INVALID; + if (pure_scalar_load(scalar, plan, NULL, NULL)) return PURE_INVALID; + PureResult status = pure_store_init(directory); + if (status != PURE_OK) return status; + *app = (PureNativeApp){.scalar = scalar, .plan = plan, .directory = directory}; + strcpy(app->document, key); + PureHost host = {.context = app, .allocate = allocate, .read = read_value, .write = write_value}; + return pure_runtime_init(&app->execution, app->slots, 2, &connections, host, budget) ? PURE_OK : PURE_INVALID; +} + +static PureOutcome finish(PureNativeApp *app, PureTicket ticket) { + PureOutcome result; + while (!pure_runtime_result(ticket, &result)) { + if (!pure_runtime_step(&app->execution)) return (PureOutcome){.status = PURE_INVALID}; + } + return result; +} + +PureOutcome pure_native_app_action(PureNativeApp *app, PureAction input) { + PureTicket ticket; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_WRITE, "event", app->revision, sizeof(input), &ticket); + if (status != PURE_OK) return (PureOutcome){.status = status}; + memcpy(pure_runtime_ingress(ticket, NULL), &input, sizeof(input)); + pure_runtime_ready(ticket); + PureOutcome result = finish(app, ticket); + pure_runtime_release(ticket); + return result; +} + +static PureOutcome transfer(PureNativeApp *app, bool restore) { + PureTicket source, destination; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_READ, restore ? "load" : "snapshot", 0, 0, &source); + if (status != PURE_OK) return (PureOutcome){.status = status}; + PureOutcome result = finish(app, source); + int64_t stored = result.revision; + if (result.status == PURE_OK) { + status = pure_runtime_forward(source, &app->execution, restore ? "restore" : "save", + restore ? app->revision : app->stored_revision, &destination); + pure_runtime_release(source); + if (status != PURE_OK) return (PureOutcome){.status = status}; + result = finish(app, destination); + pure_runtime_release(destination); + if (result.status == PURE_OK) app->stored_revision = restore ? stored : result.revision; + } else pure_runtime_release(source); + /* Bodies are consumed internally before this API returns. */ + result.body = (PureSpan){0}; + return result; +} +PureOutcome pure_native_app_save(PureNativeApp *app) { return transfer(app, false); } +PureOutcome pure_native_app_restore(PureNativeApp *app) { return transfer(app, true); } diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.h new file mode 100644 index 0000000..bde0db5 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/app.h @@ -0,0 +1,27 @@ +#ifndef PURE_NATIVE_APP_H +#define PURE_NATIVE_APP_H +#include "plan.h" +#include "../../connections/runtime.h" + +typedef enum { PURE_DOWN, PURE_UP, PURE_MOVE, PURE_CANCEL, PURE_CHANGE, PURE_FRAME, PURE_RESET } PureActionPhase; +/* In-process event value, never a persistent or network representation. */ +typedef struct { uint32_t input, phase, target, source; double x, y; } PureAction; +typedef struct { + PureScalar *scalar; + const PureScalarPlan *plan; + const char *directory; + char document[65]; + PureRuntime execution; + PureWork slots[2]; + int64_t revision, stored_revision; + uint32_t last_scalar_error, held, pointer_owners[32], key_sources[8]; +} PureNativeApp; + +/* Caller supplies instance memory and keeps plan/directory alive. This selected + * host uses the existing SQL-free store. Single owner; effects are synchronous. */ +PureResult pure_native_app_init(PureNativeApp *, const PureScalarPlan *, void *memory, + size_t size, const char *directory, const char *key, size_t budget); +PureOutcome pure_native_app_action(PureNativeApp *, PureAction); +PureOutcome pure_native_app_save(PureNativeApp *); +PureOutcome pure_native_app_restore(PureNativeApp *); +#endif diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/check.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/check.mjs new file mode 100644 index 0000000..4985dfa --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/check.mjs @@ -0,0 +1,129 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { resolve, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { compile } from '../web/compiler.mjs'; +import { createRuntime } from '../web/runtime.mjs'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const repo = resolve(root, '../..'); +const connections = resolve(root, '../connections'); +const args = process.argv.slice(2); +const sanitized = args.includes('--sanitize'); +const index = args.indexOf('--output'); +const output = index >= 0 ? resolve(args[index + 1]) : null; +const temporary = await mkdtemp(join(tmpdir(), 'pure-native-parity-')); +const library = resolve(root, 'build/native/libpure-scalar.a'); +const wasm = await readFile(resolve(root, 'web/core.wasm')); +const hash = data => createHash('sha256').update(data).digest('hex'); +const compiler = process.env.CC || 'cc'; +const flags = ['-std=c17', sanitized ? '-O1' : '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror', + ...(sanitized ? ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] : []), '-I' + resolve(root, 'native')]; +function run(command, arguments_, options = {}) { + const p = spawnSync(command, arguments_, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, timeout: 60000, ...options }); + assert.ifError(p.error); + return p; +} +function successful(command, arguments_, options) { + const p = run(command, arguments_, options); + assert.equal(p.status, 0, p.stdout + p.stderr); + return p.stdout; +} +const units = [resolve(root, 'native/app.c'), resolve(root, 'native/plan.c'), + ...['runtime.c', 'evaluate.c', 'store.c', 'store-port.c'].map(name => resolve(connections, name))]; +const objects = []; +const cases = []; +let unitResult; +try { + for (const [i, path] of units.entries()) { + const object = join(temporary, `unit-${i}.o`); + successful(compiler, [...flags, '-c', path, '-o', object]); + objects.push(object); + } + const main = join(temporary, 'main.o'); + successful(compiler, [...flags, '-c', resolve(root, 'native/main.c'), '-o', main]); + const definition = name => readFile(resolve(root, 'examples', name + '.pure'), 'utf8'); + const repeat = (line, n) => Array(n).fill(line); + const definitions = [ + ['counter', await definition('counter'), ['key tap down', 'key tap down', 'key tap up', 'key tap down', 'key tap up']], + ['toggle', await definition('toggle'), ['key toggle down', 'key toggle up', ...Array.from({length: 13}, () => ['key move down', 'key move up']).flat()]], + ['charge', await definition('charge'), ['key jump down', ...repeat('frame', 12), 'key jump up', ...repeat('frame', 90)]], + ['direct', await definition('direct'), ['point drag down tile 90 180', 'point drag move tile 250 220', 'point drag up tile 250 220']], + ['routed', await definition('routed-layout'), ['point drag down green_box 90 130', 'point drag down blue_box 290 170', + 'point drag move green_box 200 210', 'point drag move blue_box 500 270', 'point drag cancel green_box 200 210', + 'point drag up blue_box 500 270', 'key left down', 'key left up']], + ['document', await definition('document'), ['value split 99', 'value split -10', 'value split 61.5', 'key reset down', 'key reset up']], + ['atomic-error', 'state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,"value: ")', + ['value n 0', 'value n 4']], + ['blank', '', []], + ['literal-boundary', 'state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,"한글 \\\" \\\\ \\n")', ['value n -0']], + ]; + for (const [name, source, commands] of definitions) { + const sourcePath = join(temporary, name + '.pure'), prepared = join(temporary, name + '.c'), binary = join(temporary, name); + await writeFile(sourcePath, source); + successful(process.execPath, [resolve(root, 'native/prepare.mjs'), sourcePath, prepared]); + successful(compiler, [...flags, prepared, main, ...objects, library, '-lm', '-o', binary]); + const native = run(binary, [join(temporary, name + '-store'), 'project'], { input: commands.concat(['save', 'quit']).join('\n') + '\n' }); + const rows = native.stdout.trim().split('\n').map(line => JSON.parse(line)); + const runtime = await createRuntime(wasm); + const plan = compile(source); + runtime.apply(source); + function equivalent(row) { + const snapshot = runtime.snapshot(); + assert.deepEqual(row.states, snapshot.states, name); + const views = Object.fromEntries(snapshot.views.map(view => [view.id, Object.fromEntries(Object.keys(plan.views.find(v => v.id === view.id).bindings).map(key => [key, view[key]]))])); + assert.deepEqual(row.views, views, name); + } + equivalent(rows[0]); + let failures = 0; + commands.forEach((command, i) => { + const [verb, input, phase, target, x, y] = command.split(' '); + let code = 0; + if (verb === 'key') code = phase === 'down' ? runtime.down(input) : runtime.up(input); + else if (verb === 'frame') code = runtime.frame(1); + else if (verb === 'value') code = runtime.change(input, Number(phase)); + else if (verb === 'point') { + const routed = plan.inputs.find(p => p.name === input).routes; + code = runtime.point(input, phase, Number(x), Number(y), target, routed ? target : undefined); + } + if (code) ++failures; + assert.equal(rows[i + 1].status === 0, code === 0, command); + if (code) assert.equal(rows[i + 1].scalar_error, code, command); + equivalent(rows[i + 1]); + }); + assert.equal(rows.length, commands.length + 2); + assert.equal(rows.at(-1).status, 0); + assert.equal(native.status, failures ? 1 : 0, native.stderr); + const restored = JSON.parse(successful(binary, [join(temporary, name + '-store'), 'project'], {input: 'load\nquit\n'}).trim().split('\n').at(-1)); + equivalent(restored); + assert.equal(restored.stored_revision, '1'); + if (name === 'routed') { + const unit = join(temporary, 'test-native'); + successful(compiler, [...flags, prepared, resolve(root, 'native/test.c'), ...objects, library, '-lm', '-o', unit]); + unitResult = JSON.parse(successful(unit, [])); + } + cases.push({name, commands: commands.length, failed_events: failures, states_and_views_match: true, + fresh_process_restore: true, source, source_sha256: hash(source), prepared_sha256: hash(await readFile(prepared)), + native_binary_sha256: hash(await readFile(binary)), native_binary_bytes: (await readFile(binary)).length}); + } + const paths = [resolve(root, 'core.zig'), resolve(root, 'engine.zig'), resolve(root, 'evaluation.zig'), resolve(root, 'native.zig'), + resolve(root, 'manage.py'), ...['scalar.h','plan.h','plan.c','app.h','app.c','main.c','prepare.mjs','test.c','check.mjs'].map(name => resolve(root, 'native', name)), + ...['compiler.mjs','runtime.mjs','project.mjs','core.wasm'].map(name => resolve(root, 'web', name)), + ...['runtime.c','runtime.h','evaluate.c','program.h','flow.h','result.h','store.c','store.h','store-port.c','store-port.h'].map(name => resolve(connections, name))]; + const sourceHashes = Object.fromEntries(await Promise.all(paths.map(async path => [relative(repo, path), hash(await readFile(path))]))); + const result = {kind:'native-instance-and-authored-behavior-correctness-not-performance', sanitized_c_host: sanitized, + zig_mode:'ReleaseSafe; Zig object is not ASan-instrumented', compiler: successful(compiler,['--version']).split('\n')[0], + cases, units: unitResult, source_sha256: sourceHashes, scalar_archive_sha256: hash(await readFile(library)), + scalar_archive_size: successful('size',[library]), scalar_archive_undefined: successful('nm',['-u',library])}; + if (output) { + await mkdir(resolve(output, '..'), {recursive: true}); + await writeFile(output, JSON.stringify(result,null,2)+'\n', {flag:'wx'}); + } + console.log(JSON.stringify({cases:cases.length, event_comparisons:cases.reduce((n,c)=>n+c.commands,0), units:unitResult, sanitized_c_host:sanitized})); +} finally { + await rm(temporary, {recursive:true, force:true}); +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/main.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/main.c new file mode 100644 index 0000000..3756297 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/main.c @@ -0,0 +1,135 @@ +#include "app.h" +#include +#include +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; + +static void string(const char *value) { + putchar('"'); + for (const unsigned char *p = (const unsigned char *)value; *p; ++p) { + if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); } + else if (*p < 32) printf("\\u%04x", *p); + else putchar(*p); + } + putchar('"'); +} +static void number(double value) { + if (isfinite(value)) printf("%.17g", value); else fputs("null", stdout); +} +static void show(PureNativeApp *app, PureOutcome result) { + printf("{\"status\":%u,\"scalar_error\":%u,\"revision\":\"%" PRId64 "\",\"stored_revision\":\"%" PRId64 + "\",\"result_revision\":\"%" PRId64 "\",\"held\":%u,\"states\":{", + (unsigned)result.status, app->last_scalar_error, app->revision, app->stored_revision, result.revision, app->held); + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + if (i) putchar(','); + string(app->plan->states[i].name); putchar(':'); number(pure_scalar_get_state(app->scalar, i)); + } + fputs("},\"views\":{", stdout); + pure_scalar_begin_evaluation(app->scalar, 2048); + uint32_t display_error = 0; + for (uint32_t i = 0; i < app->plan->view_count; ++i) { + const PureScalarView *view = &app->plan->views[i]; + if (i) putchar(','); + string(view->id); fputs(":{", stdout); + const char *fields[] = {"x", "y", !strcmp(view->type, "text") ? "value" : !strcmp(view->type, "circle") ? "radius" : "width", "height"}; + for (uint32_t j = 0; j < view->count; ++j) { + if (j) putchar(','); + string(fields[j]); putchar(':'); + double value = pure_scalar_eval_bounded(app->scalar, view->fields[j].start, view->fields[j].length); + uint32_t code = pure_scalar_evaluation_info(app->scalar, 0); + if (code && !display_error) display_error = code; + if (j >= 2 && strcmp(view->type, "text") && value < 0) { display_error = 100; value = NAN; } + number(value); + } + putchar('}'); + } + printf("},\"display_error\":%u}\n", display_error); + fflush(stdout); +} +static bool real(const char *text, double *out) { + if (!text || !*text) return false; + char *end; + errno = 0; + double value = strtod(text, &end); + if (errno || *end || !isfinite(value)) return false; + *out = value; + return true; +} + +int main(int argc, char **argv) { + if (argc != 3) { + fputs("pure-native STORE_DIRECTORY DOCUMENT_KEY\n" + "Commands: show, key INPUT down|up, value INPUT NUMBER,\n" + "point INPUT down|move|up|cancel TARGET X Y, frame, reset, save, load, quit.\n", stderr); + return 2; + } + size_t bytes = pure_scalar_bytes(); + void *memory = malloc(bytes); + if (!memory) return 2; + PureNativeApp app; + PureResult status = pure_native_app_init(&app, &pure_app_plan, memory, bytes, argv[1], argv[2], 4096); + if (status != PURE_OK) { fprintf(stderr, "initialization failed: %u\n", status); free(memory); return 2; } + show(&app, (PureOutcome){.status = PURE_OK}); + int failed = 0; + char line[512]; + while (fgets(line, sizeof(line), stdin)) { + if (!strchr(line, '\n') && !feof(stdin)) { + int c; + while ((c = getchar()) != '\n' && c != EOF) {} + fputs("command exceeds 510 bytes\n", stderr); failed = 1; continue; + } + char *words[8]; + size_t count = 0; + for (char *token = strtok(line, " \t\r\n"); token; token = strtok(NULL, " \t\r\n")) { + if (count == 8) break; + words[count++] = token; + } + if (!count) continue; + PureOutcome result = {.status = PURE_INVALID}; + if (count == 1) { + if (!strcmp(words[0], "quit")) break; + if (!strcmp(words[0], "show")) result.status = PURE_OK; + else if (!strcmp(words[0], "save")) result = pure_native_app_save(&app); + else if (!strcmp(words[0], "load")) result = pure_native_app_restore(&app); + else if (!strcmp(words[0], "frame") || !strcmp(words[0], "reset")) + result = pure_native_app_action(&app, (PureAction){.phase = !strcmp(words[0], "frame") ? PURE_FRAME : PURE_RESET}); + } else { + uint32_t input = app.plan->input_count; + for (uint32_t i = 0; i < app.plan->input_count; ++i) + if (!strcmp(words[1], app.plan->inputs[i].name)) { input = i; break; } + if (input < app.plan->input_count) { + PureScalarInputKind kind = app.plan->inputs[input].kind; + PureAction action = {.input = input, .phase = UINT32_MAX, .target = UINT32_MAX}; + if (count == 3 && !strcmp(words[0], "key") && kind == PURE_KEY) { + if (!strcmp(words[2], "down")) action.phase = PURE_DOWN; + if (!strcmp(words[2], "up")) action.phase = PURE_UP; + } else if (count == 3 && !strcmp(words[0], "value") && kind == PURE_VALUE) { + if (real(words[2], &action.x)) action.phase = PURE_CHANGE; + } else if (count == 6 && !strcmp(words[0], "point") && (kind == PURE_POINT || kind == PURE_ROUTED)) { + bool target = kind == PURE_POINT && !strcmp(words[3], app.plan->inputs[input].target); + if (kind == PURE_ROUTED) + for (uint32_t i = 0; i < app.plan->target_count; ++i) + if (app.plan->targets[i].input == input && !strcmp(words[3], app.plan->targets[i].view)) { + target = true; action.target = i; break; + } + if (target && real(words[4], &action.x) && real(words[5], &action.y)) { + const char *phases[] = {"down", "up", "move", "cancel"}; + for (uint32_t i = 0; i < 4; ++i) if (!strcmp(words[2], phases[i])) action.phase = i; + } + } + if (action.phase != UINT32_MAX) result = pure_native_app_action(&app, action); + } + } + if (result.status != PURE_OK) failed = 1; + show(&app, result); + } + if (ferror(stdin) || ferror(stdout)) failed = 1; + pure_runtime_stop(&app.execution); + if (app.execution.active || app.execution.retained) failed = 1; + free(memory); + return failed; +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.c new file mode 100644 index 0000000..3ddfeda --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.c @@ -0,0 +1,38 @@ +#include "plan.h" +#include + +uint32_t pure_scalar_load(PureScalar *vm, const PureScalarPlan *plan, + const PureScalarPlan *previous, const double *restored) { + uint32_t code = pure_scalar_begin_program(vm, plan->state_count, plan->input_count); + if (code) return code; + for (uint32_t i = 0; i < plan->state_count; ++i) { + int32_t old = -1; + if (!restored && previous) + for (uint32_t j = 0; j < previous->state_count; ++j) + if (!strcmp(plan->states[i].name, previous->states[j].name)) { old = (int32_t)j; break; } + code = pure_scalar_put_state(vm, i, plan->states[i].initial, old); + if (code) goto fail; + if (restored && (code = pure_scalar_put_restored_value(vm, i, restored[i]))) goto fail; + } + for (uint32_t i = 0; i < plan->instruction_count; ++i) { + const PureScalarInstruction *op = &plan->instructions[i]; + if ((code = pure_scalar_put_instruction(vm, op->op, op->arg, op->value))) goto fail; + } + for (uint32_t i = 0; i < plan->target_count; ++i) { + const PureScalarTarget *target = &plan->targets[i]; + if ((code = pure_scalar_put_pointer_target(vm, target->input, target->base, target->count))) goto fail; + } + for (uint32_t i = 0; i < plan->handler_count; ++i) { + const PureScalarHandler *handler = &plan->handlers[i]; + if ((code = pure_scalar_put_handler(vm, handler->event, handler->start, handler->length))) goto fail; + if (handler->count) { + code = handler->base == 255 ? pure_scalar_bind_target_handler(vm, i, handler->count) + : pure_scalar_bind_handler(vm, i, handler->base, handler->count); + if (code) goto fail; + } + } + return restored ? pure_scalar_commit_restored_program(vm) : pure_scalar_commit_program(vm); +fail: + pure_scalar_abort_program(vm); + return code; +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.h new file mode 100644 index 0000000..8de81df --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/plan.h @@ -0,0 +1,36 @@ +#ifndef PURE_SCALAR_PLAN_H +#define PURE_SCALAR_PLAN_H +#include "scalar.h" +#include + +typedef struct { const char *name; double initial; } PureScalarState; +typedef struct { uint32_t op, arg; double value; } PureScalarInstruction; +typedef struct { uint32_t event, start, length, base, count; } PureScalarHandler; +typedef struct { uint32_t input, base, count; const char *view; } PureScalarTarget; +typedef enum { PURE_KEY, PURE_POINT, PURE_ROUTED, PURE_VALUE } PureScalarInputKind; +typedef struct { const char *name, *key, *target; PureScalarInputKind kind; } PureScalarInput; +typedef struct { uint32_t start, length; } PureScalarBinding; +typedef struct { + const char *id, *type, *decoration; + uint32_t count; + PureScalarBinding fields[4]; +} PureScalarView; +/* Immutable, build-prepared application data. C layout is an internal build + * contract; no pointers or native structs are persisted or sent on the wire. */ +typedef struct { + uint32_t state_count, input_count, instruction_count, handler_count, target_count, view_count; + const PureScalarState *states; + const PureScalarInput *inputs; + const PureScalarInstruction *instructions; + const PureScalarHandler *handlers; + const PureScalarTarget *targets; + const PureScalarView *views; + unsigned char identity[32]; +} PureScalarPlan; + +/* Commit a prepared definition with initials, name-preserved previous values, + * or a complete restored state vector. A failed candidate leaves active state + * intact. The caller ensures the application is idle before replacing it. */ +uint32_t pure_scalar_load(PureScalar *, const PureScalarPlan *, + const PureScalarPlan *previous, const double *restored); +#endif diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/prepare.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/prepare.mjs new file mode 100644 index 0000000..f3ff288 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/prepare.mjs @@ -0,0 +1,35 @@ +#!/usr/bin/env node +// Build tool only: reuse the existing authoring compiler. Its parser, JS runtime +// and source syntax do not become dependencies of the resulting native app. +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { compile } from '../web/compiler.mjs'; + +const [input, output] = process.argv.slice(2); +if (!input || !output || process.argv.length !== 4) throw new Error('prepare.mjs SOURCE.pure OUTPUT.c'); +const source = await readFile(input, 'utf8'); +const p = compile(source); +// Encode strings as byte escapes, not JSON/C escape substitution. No source text +// or user-authored identifier can become executable C through this preparation. +const string = s => '"' + [...Buffer.from(s ?? '', 'utf8')].map(b => `\\${b.toString(8).padStart(3, '0')}`).join('') + '"'; +const number = n => Object.is(n, -0) ? '-0.0' : Number.isFinite(n) ? String(n) : (() => { throw new Error('nonfinite prepared constant'); })(); +const declaration = (type, name, rows) => `static const ${type} ${name}[] = {\n${rows.length ? rows.map(r => ` ${r},`).join('\n') : ' {0},'}\n};\n`; +let text = '#include "plan.h"\n\n'; +text += declaration('PureScalarState', 'states', p.states.map(s => `{${string(s.name)}, ${number(s.initial)}}`)); +text += declaration('PureScalarInput', 'inputs', p.inputs.map(i => `{${string(i.name)}, ${string(i.key)}, ${string(i.target)}, ${i.kind === 'value' ? 'PURE_VALUE' : i.routes ? 'PURE_ROUTED' : i.target ? 'PURE_POINT' : 'PURE_KEY'}}`)); +text += declaration('PureScalarInstruction', 'instructions', p.instructions.map(i => `{${i.op}, ${i.arg}, ${number(i.value)}}`)); +text += declaration('PureScalarHandler', 'handlers', p.handlers.map(h => `{${h.event}, ${h.start}, ${h.length}, ${h.parameterBase ?? 0}, ${h.parameterCount ?? 0}}`)); +text += declaration('PureScalarTarget', 'targets', p.pointerTargets.map((t, index) => { + const route = p.inputs[t.input].routes.find(r => r.route === index); + if (!route) throw new Error('missing prepared pointer target'); + return `{${t.input}, ${t.base}, ${t.count}, ${string(route.target)}}`; +})); +text += declaration('PureScalarView', 'views', p.views.map(v => { + const fields = Object.values(v.bindings); + return `{${string(v.id)}, ${string(v.type)}, ${string(v.color ?? v.prefix)}, ${fields.length}, {${fields.map(b => `{${b.start}, ${b.length}}`).join(', ')}}}`; +})); +// Definition identity protects restore compatibility, not authenticity. Persisted +// values from a different definition are rejected; storage authentication is separate. +const identity = createHash('sha256').update(JSON.stringify(p)).digest(); +text += `\nconst PureScalarPlan pure_app_plan = {\n ${p.states.length}, ${p.inputs.length}, ${p.instructions.length}, ${p.handlers.length}, ${p.pointerTargets.length}, ${p.views.length},\n states, inputs, instructions, handlers, targets, views,\n {${[...identity].join(', ')}}\n};\n`; +await writeFile(output, text); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/scalar.h b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/scalar.h new file mode 100644 index 0000000..dee26fe --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/scalar.h @@ -0,0 +1,46 @@ +#ifndef PURE_SCALAR_H +#define PURE_SCALAR_H +#include +#include +#ifdef __cplusplus +extern "C" { +#endif +/* This selected scalar module has no default instance or allocator. Allocate + * bytes() aligned to alignment(), initialize once, and retain it through all + * calls. Each instance has one owner at a time; independent instances share + * no mutable state. Passing invalid pointers is outside this trusted C ABI. + * This is an in-process API, not a stable wire or persistent representation. */ +typedef struct PureScalar PureScalar; +size_t pure_scalar_bytes(void); +size_t pure_scalar_alignment(void); +PureScalar *pure_scalar_initialize(void *memory, size_t size); +void pure_scalar_reset_all(PureScalar *instance); +uint32_t pure_scalar_begin_program(PureScalar *instance, uint32_t state_count, uint32_t input_count); +void pure_scalar_abort_program(PureScalar *instance); +uint32_t pure_scalar_put_state(PureScalar *instance, uint32_t id, double initial, int32_t old_id); +uint32_t pure_scalar_put_instruction(PureScalar *instance, uint32_t op, uint32_t arg, double value); +uint32_t pure_scalar_put_handler(PureScalar *instance, uint32_t event, uint32_t start, uint32_t length); +uint32_t pure_scalar_bind_handler(PureScalar *instance, uint32_t handler, uint32_t base, uint32_t count); +uint32_t pure_scalar_put_pointer_target(PureScalar *instance, uint32_t input, uint32_t base, uint32_t count); +uint32_t pure_scalar_bind_target_handler(PureScalar *instance, uint32_t handler, uint32_t count); +uint32_t pure_scalar_put_restored_value(PureScalar *instance, uint32_t id, double value); +uint32_t pure_scalar_commit_program(PureScalar *instance); +uint32_t pure_scalar_commit_restored_program(PureScalar *instance); +void pure_scalar_reset_state(PureScalar *instance); +uint32_t pure_scalar_dispatch(PureScalar *instance, uint32_t event, double dt); +uint32_t pure_scalar_dispatch_point(PureScalar *instance, uint32_t event, double x, double y); +uint32_t pure_scalar_dispatch_target(PureScalar *instance, uint32_t event, uint32_t target, double x, double y); +uint32_t pure_scalar_dispatch_value(PureScalar *instance, uint32_t event, double value); +double pure_scalar_eval_at(PureScalar *instance, uint32_t start, uint32_t length); +uint32_t pure_scalar_configure_evaluation(PureScalar *instance, uint32_t cache, uint32_t history); +void pure_scalar_forget_evaluation(PureScalar *instance, uint32_t flags); +void pure_scalar_begin_evaluation(PureScalar *instance, uint32_t limit); +double pure_scalar_eval_bounded(PureScalar *instance, uint32_t start, uint32_t length); +uint32_t pure_scalar_evaluation_info(PureScalar *instance, uint32_t id); +double pure_scalar_get_state(PureScalar *instance, uint32_t id); +uint32_t pure_scalar_info(PureScalar *instance, uint32_t id); +uint32_t pure_scalar_layout(PureScalar *instance, uint32_t id); +#ifdef __cplusplus +} +#endif +#endif diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/test.c b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/test.c new file mode 100644 index 0000000..18a049d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/native/test.c @@ -0,0 +1,120 @@ +#define _POSIX_C_SOURCE 200809L +#include "app.h" +#include "../../connections/store.h" +#include +#include +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; +static void move(PureNativeApp *app, uint32_t target, double x, double y) { + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_DOWN, .target = target, .x = x, .y = y}).status == PURE_OK); + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_MOVE, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK); + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_UP, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK); +} +static void drained(PureNativeApp *app) { + assert(app->execution.active == 0 && app->execution.retained == 0); + assert(app->execution.peak_active <= 2 && app->execution.peak_retained <= app->execution.budget); +} +static void values(PureNativeApp *app, double output[32]) { + for (uint32_t i = 0; i < app->plan->state_count; ++i) output[i] = pure_scalar_get_state(app->scalar, i); +} +static void same(PureNativeApp *app, const double *expected) { + for (uint32_t i = 0; i < app->plan->state_count; ++i) assert(pure_scalar_get_state(app->scalar, i) == expected[i]); +} + +int main(void) { + size_t size = pure_scalar_bytes(); + unsigned char *a = malloc(size), *b = malloc(size), *c = malloc(size); + assert(a && b && c); + assert(!pure_scalar_initialize(NULL, size)); + assert(!pure_scalar_initialize(a, size - 1)); + assert(pure_scalar_alignment() > 1 && !pure_scalar_initialize(a + 1, size)); + char path[] = "/tmp/pure-native-instance-XXXXXX"; + assert(mkdtemp(path)); + PureNativeApp first, second, limited; + assert(pure_native_app_init(&first, &pure_app_plan, a, size, path, "scene", 4096) == PURE_OK); + assert(pure_native_app_init(&second, &pure_app_plan, b, size, path, "scene", 4096) == PURE_OK); + assert(pure_native_app_init(&limited, &pure_app_plan, c, size, path, "limited", sizeof(PureAction) - 1) == PURE_OK); + assert(pure_native_app_action(&limited, (PureAction){.phase = PURE_FRAME}).status == PURE_BUDGET); + assert(pure_scalar_info(limited.scalar, 9) == 0); + + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .x = 90, .y = 130}).status == PURE_OK); + assert(pure_native_app_save(&first).status == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 1, .x = 230, .y = 210}).status == PURE_INVALID); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .x = 230, .y = 210}).status == PURE_OK); + move(&second, 1, 290, 170); + assert(pure_scalar_get_state(second.scalar, 1) == 80 && pure_scalar_get_state(second.scalar, 9) == 350); + assert(pure_scalar_get_state(first.scalar, 1) == 220 && pure_scalar_get_state(first.scalar, 9) == 280); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 0, .x = 230, .y = 210}).status == PURE_OK); + assert(first.held == 0 && second.held == 0); + assert(pure_native_app_save(&first).status == PURE_OK && first.stored_revision == 1); + double second_values[32] = {0}; + values(&second, second_values); + PureOutcome conflict = pure_native_app_save(&second); + assert(conflict.status == PURE_CONFLICT && conflict.revision == 1 && second.stored_revision == 0); + same(&second, second_values); + assert(pure_native_app_restore(&second).status == PURE_OK && second.stored_revision == 1); + assert(pure_scalar_get_state(second.scalar, 1) == 220 && pure_scalar_get_state(second.scalar, 9) == 280); + + /* Staging/error/evaluation state also belongs to the selected instance. */ + assert(pure_scalar_begin_program(first.scalar, 1, 0) == 0); + assert(pure_scalar_put_state(first.scalar, 0, 999, -1) == 0); + assert(pure_scalar_configure_evaluation(first.scalar, 2, 2) == 0); + move(&second, 2, 460, 230); + assert(pure_scalar_put_instruction(first.scalar, 999, 0, 0) != 0); + assert(pure_scalar_commit_program(first.scalar) != 0); + assert(pure_scalar_get_state(first.scalar, 1) == 220); + assert(pure_scalar_get_state(first.scalar, 17) == 450 && pure_scalar_get_state(second.scalar, 17) == 520); + assert(pure_native_app_save(&second).status == PURE_OK && second.stored_revision == 2); + assert(pure_native_app_save(&first).status == PURE_CONFLICT && first.stored_revision == 1); + + /* Mismatched definition and nonfinite restored values never replace state. */ + PureStoredView saved; + assert(pure_store_get(path, "scene", 300, &saved) == PURE_OK); + unsigned char invalid[300]; + assert(saved.body.size <= sizeof(invalid)); + size_t length = saved.body.size; + memcpy(invalid, saved.body.data, length); + pure_store_release(&saved); + invalid[8] ^= 1; + int64_t revision; + assert(pure_store_put(path, "scene", 2, (PureSpan){invalid, length}, &revision) == PURE_OK); + double first_values[32] = {0}; + values(&first, first_values); + assert(pure_native_app_restore(&first).status == PURE_INVALID && first.stored_revision == 1); + same(&first, first_values); + invalid[8] ^= 1; + const unsigned char nan[8] = {0,0,0,0,0,0,248,127}; + memcpy(invalid + 44, nan, 8); + assert(pure_store_put(path, "scene", 3, (PureSpan){invalid, length}, &revision) == PURE_OK); + assert(pure_native_app_restore(&first).status == PURE_INVALID && first.last_scalar_error == 5); + same(&first, first_values); + assert(first.stored_revision == 1); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .source = 11, .x = 230, .y = 210}).status == PURE_OK); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 1, .source = 22, .x = 290, .y = 170}).status == PURE_OK); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .source = 22}).status == PURE_INVALID); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_CANCEL, .target = 0, .source = 11}).status == PURE_OK); + assert(first.held && pure_native_app_save(&first).status == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 1, .source = 22}).status == PURE_OK); + assert(first.held == 0); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 1}).status == PURE_OK); + double after_key = pure_scalar_get_state(first.scalar, 9); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 2}).status == PURE_OK); + assert(pure_scalar_get_state(first.scalar, 9) == after_key); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 1}).status == PURE_OK && first.held); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 2}).status == PURE_OK && !first.held); + drained(&first); drained(&second); drained(&limited); + + char file[256]; + snprintf(file, sizeof(file), "%s/obj-scene", path); assert(unlink(file) == 0); + snprintf(file, sizeof(file), "%s/.writer.lock", path); assert(unlink(file) == 0); + assert(rmdir(path) == 0); + free(a); free(b); free(c); + printf("{\"native_instances\":3,\"checks\":\"independent-state-staging-inputs-errors-budget-snapshot-conflict-restore\"," + "\"scalar_instance_bytes\":%zu,\"scalar_alignment\":%zu,\"app_controller_bytes\":%zu,\"action_bytes\":%zu," + "\"snapshot_bytes\":%zu,\"all_work_drained\":true}\n", size, pure_scalar_alignment(), sizeof(PureNativeApp), sizeof(PureAction), length); + return 0; +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/pointer.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/pointer.test.mjs new file mode 100644 index 0000000..8bf530d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/pointer.test.mjs @@ -0,0 +1,118 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = await readFile(new URL('./examples/direct.pure', import.meta.url), 'utf8'); +const fresh = async () => { const r = await createRuntime(wasm, {cacheEntries:32}); r.apply(source); return r; }; + +test('pointer declarations resolve forward targets and reject ambiguous or contextless inputs', () => { + const p = compile(source); + assert.equal(p.inputs[0].target, 'tile'); + assert(p.handlers.some(h => h.event === 17)); assert(p.handlers.some(h => h.event === 25)); + assert(p.instructions.some(i => i.op === 22)); assert(p.instructions.some(i => i.op === 23)); + for (const [code, message] of [ + ['input p = pointer(missing)', /Unknown pointer target/], + ['input p=pointer(tile)\ninput q=pointer(tile)\ntext tile(0,0,0)', /Duplicate pointer/], + ['input p=Space\non p.move {}', /require a pointer/], + ['input p=Space\non p.cancel {}', /require a pointer/], + ['state x=0\non frame { x=event_x }', /only available/], + ['text v(event_y,0,0)', /only available/], + ['state event_x=0', /Reserved name/], + ]) assert.throws(() => compile(code), message); + const full = compile(Array.from({length:8}, (_, i) => `input p${i}=pointer(v${i})\ntext v${i}(0,0,0)\non p${i}.move {}\non p${i}.cancel {}`).join('\n')); + assert.deepEqual(full.handlers.map(h => h.event), Array.from({length:8}, (_, i) => [17+i,25+i]).flat()); +}); + +test('authored drag keeps grab offset and commits the displayed position even if release coordinates differ', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'pen'); + assert.equal(r.snapshot().states.dragging, 1); + assert.throws(() => r.apply(''), /놓은 뒤/); + r.point('drag', 'move', 360, 220, 'pen'); + let s = r.snapshot(); + assert.equal(s.states.x, 350); assert.equal(s.states.y, 210); + assert.equal(s.views.find(v => v.id === 'region').value, 1); + r.point('drag', 'up', 410, 240, 'pen'); + s = r.snapshot(); + assert.equal(s.states.x, 350); assert.equal(s.states.y, 210); + assert.equal(s.states.dragging, 0); assert.deepEqual(s.held, []); + assert.equal(s.successfulEvents, 3); + assert.equal(s.lastEvent.x, 410); + r.down('left'); r.up('left'); + assert.equal(r.snapshot().states.x, 340); + r.point('drag', 'down', 350, 220, 'pen'); + r.point('drag', 'move', 450, 250, 'pen'); + r.point('drag', 'up', 350, 220, 'pen'); // A host may repeat the press coordinates. + assert.equal(r.snapshot().states.x, 440); + assert.equal(r.snapshot().states.y, 240); + r.apply('state x=0\nstate y=0\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.up { x=event_x\ny=event_y }'); + r.point('p','down',1,2); r.point('p','up',30,40); + assert.deepEqual(r.snapshot().states, {x:30,y:40}); // Payload remains available to authors. +}); + +test('ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'a'); + r.point('drag', 'down', 500, 500, 'a'); + assert.throws(() => r.point('drag', 'down', 90, 170, 'b'), /다른 포인터/); + r.point('drag', 'move', 400, 200, 'b'); + assert.equal(r.snapshot().successfulEvents, 1); + r.point('drag', 'move', 400, 200, 'a'); + r.releaseAll(); + const canceled = r.snapshot(); + assert.equal(canceled.states.x, 80); assert.equal(canceled.states.y, 160); + assert.equal(canceled.states.dragging, 0); assert.equal(canceled.lastEvent.phase, 'cancel'); + assert.deepEqual(canceled.held, []); + r.point('drag', 'cancel', 1, 1, 'a'); r.point('drag', 'move', 500, 500, 'a'); r.releaseAll(); + assert.deepEqual(r.snapshot(), canceled); + r.point('drag', 'down', 90, 170, 'new'); r.reset(); + r.point('drag', 'up', 600, 500, 'new'); + assert.equal(r.snapshot().states.x, 80); assert.deepEqual(r.snapshot().held, []); +}); + +test('invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event', async () => { + const r = await fresh(); r.point('drag', 'down', 90, 170); + const before = r.snapshot(); + for (const invoke of [() => r.point('drag','move',NaN,1), () => r.point('drag','up',1,Infinity), + () => r.point('drag','unknown',1,2), () => r.point('left','down',1,2), + () => r.down('drag'), () => r.up('drag'), () => r.point('drag','move',1,2,'')]) assert.throws(invoke); + assert.deepEqual(r.snapshot(), before); + r.releaseAll(); + r.apply(source.replace('x = clamp(event_x - grab_x, 20, 564)', 'x = event_x\n y = 1 / 0')); + r.point('drag', 'down', 90, 170); + assert.equal(r.point('drag', 'move', 500, 220), 4); + let s = r.snapshot(); assert.equal(s.states.x, 80); assert.equal(s.states.y, 160); + assert.equal(s.lastFailure.phase, 'move'); + r.releaseAll(); s = r.snapshot(); assert.deepEqual(s.held, []); assert.equal(s.lastError, 4); + assert.equal(s.states.dragging, 0); +}); + +test('independent targets share the event pipeline and publication remains non-reentrant', async () => { + const r = await createRuntime(wasm); + r.apply('state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\ntext one(0,0,x)\ntext two(0,0,y)\non a.move { x=event_x }\non b.move { y=event_y }'); + r.point('a','down',1,2,'first'); r.point('b','down',3,4,'second'); + r.point('a','move',10,20,'first'); r.point('b','move',30,40,'second'); + assert.deepEqual(r.snapshot().states,{x:10,y:40}); + r.present({begin(){assert.throws(() => r.point('a','move',0,0,'first'), /출력 전달 중/);},state(){},view(){},value(){},end(){}}); + r.releaseAll(); assert.deepEqual(r.snapshot().held, []); +}); + +test('failing down, up and cancel never prevent terminal ownership cleanup', async () => { + for (const phase of ['down', 'up', 'cancel']) { + const r = await createRuntime(wasm); + r.apply(`state x=7\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.${phase} { x=event_x\nx=1/0 }`); + const code = r.point('p','down',10,20,'owner'); + assert.equal(code, phase === 'down' ? 4 : 0); + assert.deepEqual(r.snapshot().held, ['p']); + if (phase === 'up') assert.equal(r.point('p','up',30,40,'owner'), 4); + else assert.equal(r.releaseAll(), phase === 'cancel' ? 4 : 0); + const s = r.snapshot(); + assert.equal(s.states.x, 7); assert.equal(s.lastError, 4); + assert.equal(s.lastFailure.phase, phase); assert.deepEqual(s.held, []); + r.apply('state x=100'); // Cleanup also unblocks definition changes. + assert.equal(r.snapshot().states.x, 7); + } +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/presentation.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/presentation.test.mjs new file mode 100644 index 0000000..98bef6e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/presentation.test.mjs @@ -0,0 +1,132 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; +import { createRuntime as before } from './results/2026-09-23-presentation/before/web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = `state x = 3 +state radius = 5 +input move = KeyM +input shrink = KeyS +box tile(x, 10, x * 2, 20, "#123456") +circle dot(50, 50, radius, "#abcdef") +text label(0, 0, 10 / x, "Value: ") +on move.down { x = x + 0.25 } +on shrink.down { radius = radius - 10 } +on frame { x = x + dt } +`; + +// The archived adapter predates route diagnostics. These legacy fixtures have +// no routes; keep all original fields in the equality comparison. +function archivedSnapshot(runtime) { + const value = runtime.snapshot(); + value.memory.pointerTargetBytes = 3; + value.memory.pointerTargets = 0; + return value; +} + +// Reconstruct only in tests, for comparison with the archived snapshot path. +function collect(runtime, hooks = {}) { + let states, views, declared; + const result = runtime.present({ + begin(program) { + declared = program; states = {}; views = []; + hooks.begin?.(); + }, + state(i, value) { states[declared.states[i].name] = value; }, + view(i, valid) { + if (!valid) return; + const {id, type, color, prefix} = declared.views[i]; + views.push({id, type, color, prefix}); + }, + value(i, name, value) { views.at(-1)[name] = value; hooks.value?.(); }, + end(count) { assert.equal(views.length, count); }, + }); + return {states, views, ...result}; +} + +test('direct publication matches archived snapshots and work counts through authored changes', async () => { + for (const cacheEntries of [0, 1, 32]) { + const left = await createRuntime(wasm, {cacheEntries}), right = await before(wasm, {cacheEntries}); + const compare = () => { + assert.deepEqual(collect(left), archivedSnapshot(right)); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + }; + compare(); + for (const r of [left, right]) r.apply(source); + compare(); + for (let i = 0; i < 120; i++) { + for (const r of [left, right]) { + r.down('move', 'keyboard'); r.down('move', 'pointer'); + r.up('move', 'keyboard'); r.up('move', 'pointer'); + if (i % 11 === 0) r.frame(2); + if (i % 17 === 0) r.forgetEvaluation('history'); + if (i % 23 === 0) r.reset(); + } + compare(); + } + for (const r of [left, right]) { r.down('shrink'); r.up('shrink'); } + compare(); // invalid circle only; other views still publish + for (const r of [left, right]) assert.throws(() => r.apply('state broken =')); + compare(); + for (const r of [left, right]) r.apply('state radius = 1\nstate x = 0\ntext label(1, 2, x)'); + compare(); + for (const r of [left, right]) r.apply(''); + compare(); + } +}); + +test('whole-scene budget failure never publishes partial bindings and recovers exactly', async () => { + const left = await createRuntime(wasm, {cacheEntries:32}), right = await before(wasm, {cacheEntries:32}); + for (const r of [left, right]) r.apply(source); + for (let workLimit = 0; workLimit <= 80; workLimit++) { + for (const r of [left, right]) r.configureEvaluation({workLimit}); + let calls = 0; + const result = collect(left, {value: () => calls++}); + assert.deepEqual(result, archivedSnapshot(right)); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + if (result.displayError?.code === 101) assert.equal(calls, 0); + else assert.equal(calls, 10); + } +}); + +test('latched event rollback, nonfinite bindings, reset and signed zero keep their semantics', async () => { + const left = await createRuntime(wasm), right = await before(wasm); + const definition = 'state x = 0\ninput fail = Space\ntext value(0, 0, 1 / x)\non fail.down { x = 2\nx = 1 / 0 }'; + for (const r of [left, right]) { r.apply(definition); r.down('fail'); r.up('fail'); } + assert.deepEqual(collect(left), archivedSnapshot(right)); + for (const r of [left, right]) r.configureEvaluation({workLimit:0}); + assert.deepEqual(collect(left), archivedSnapshot(right)); + for (const r of [left, right]) { + r.apply('state x = -0\ntext value(x, 0, x)'); r.reset(); r.configureEvaluation({workLimit:2048}); + } + const result = collect(left); + assert.deepEqual(result, archivedSnapshot(right)); + assert(Object.is(result.states.x, -0)); +}); + +test('publication rejects reentrant runtime changes and reads before they affect the scene', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const calls = [() => runtime.apply(''), () => runtime.reset(), () => runtime.frame(), + () => runtime.down('move'), () => runtime.up('move'), () => runtime.releaseAll(), + () => runtime.configureEvaluation({workLimit:0}), () => runtime.forgetEvaluation(), + () => runtime.snapshot(), () => runtime.present({})]; + const expected = runtime.snapshot(); + assert.deepEqual(collect(runtime, {begin() { + for (const call of calls) assert.throws(call, /출력 전달 중/); + }}), expected); + assert.deepEqual(runtime.snapshot(), expected); +}); + +test('invalid or throwing output adapters do not strand the runtime or alter authored state', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const expected = runtime.snapshot(); + assert.throws(() => runtime.present({}), TypeError); + assert.throws(() => collect(runtime, {value() { throw new Error('destination failed'); }}), /destination failed/); + assert.deepEqual(collect(runtime), expected); + runtime.down('move'); runtime.up('move'); + assert.equal(collect(runtime).states.x, 3.25); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/project.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/project.test.mjs new file mode 100644 index 0000000..ad8a48d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/project.test.mjs @@ -0,0 +1,79 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {createRuntime} from './web/runtime.mjs'; +import {parseProject, MAX_PROJECT_BYTES} from './web/project.mjs'; +const wasm = await readFile(new URL('./web/core.wasm',import.meta.url)); +const layout = await readFile(new URL('./examples/layout.pure',import.meta.url),'utf8'); +const fresh = () => createRuntime(wasm,{cacheEntries:32}); +const roundTrip = value => parseProject(JSON.stringify(value)); + +test('multiple authored placements and a separate invalid draft survive a fresh runtime restore',async()=>{ + const a=await fresh(); a.apply(layout); + for(const [name,x,y,nx,ny] of [['drag1',90,130,180,200],['drag2',290,170,370,210],['drag3',460,230,520,280]]) { + a.point(name,'down',x,y);a.point(name,'move',nx,ny);a.point(name,'up',x,y); + } + a.down('left');a.up('left'); + const before=a.snapshot(), document=roundTrip(a.exportDocument('state unfinished =')); + assert.equal(document.version,2);assert.equal(document.source,layout); + const b=await fresh(); b.apply('state old=99'); b.frame(5); + const restored=b.restoreDocument(document), after=b.snapshot(); + assert.equal(restored.draft,'state unfinished =');assert.equal(restored.source,layout); + assert.deepEqual(after.states,before.states);assert.deepEqual(after.views,before.views); + assert.equal(after.tick,0);assert.equal(after.successfulEvents,0);assert.equal(after.lastEvent,null);assert.deepEqual(after.held,[]); + b.down('right');b.up('right');assert.equal(b.snapshot().states.x3,before.states.x3+10); + b.reset();assert.equal(b.snapshot().states.x1,80);assert.equal(b.snapshot().states.x2,280);assert.equal(b.snapshot().states.x3,450); +}); + +test('malformed files reject before changing active code, values, diagnostics or warm display records',async()=>{ + const r=await fresh();r.apply('state x=5\ntext t(0,0,x)');r.snapshot(); + const good=r.exportDocument(), before=r.snapshot(), active=r.currentProgram; + const bad=[null,{}, {...good,version:99},{...good,draft:null},{...good,source:'state x='},{...good,states:[]}, + {...good,states:[{name:'other',value:2}]},{...good,states:[{name:'x',value:null}]}, + {...good,states:[{name:'x',value:'2'}]},{...good,states:[{name:'x',value:Infinity}]}, + {...good,states:[{name:'x',value:NaN}]},{...good,draft:'x'.repeat(32769)}, + {...good,source:'state x=0\nstate y=0',states:[{name:'x',value:1},{name:'x',value:2}]}]; + for(const doc of bad){assert.throws(()=>r.restoreDocument(doc));assert.equal(r.currentProgram,active);assert.deepEqual(r.snapshot(),before);} + assert(r.evaluationStats().cacheHits>0); + assert.throws(()=>parseProject('{'));assert.throws(()=>parseProject(' '.repeat(MAX_PROJECT_BYTES+1))); +}); + +test('restoration matches names rather than ordering and never reuses stale cached values',async()=>{ + const r=await fresh();r.apply('state x=1\nstate y=2\ntext t(0,0,x+y)');r.snapshot();r.snapshot(); + const doc=r.exportDocument();doc.states=[{name:'y',value:20},{name:'x',value:10}]; + r.restoreDocument(roundTrip(doc));assert.deepEqual(r.snapshot().states,{x:10,y:20});assert.equal(r.snapshot().views[0].value,30); + r.apply('state y=200\nstate x=100');assert.deepEqual(r.snapshot().states,{y:20,x:10}); + r.reset();assert.deepEqual(r.snapshot().states,{y:200,x:100}); +}); + +test('finite float values including negative zero round trip without changing authored reset values',async()=>{ + const r=await fresh();r.apply('state z=-0\nstate tiny=5e-324\nstate large=1.7976931348623157e308\nstate constructor=1'); + const doc=roundTrip(r.exportDocument());assert.equal(doc.states[0].value,'-0'); + r.restoreDocument(doc);const s=r.snapshot().states; + assert(Object.is(s.z,-0));assert.equal(s.tiny,Number.MIN_VALUE);assert.equal(s.large,Number.MAX_VALUE);assert.equal(s.constructor,1); +}); + +test('held and failed interactions cannot be saved and restoration cannot steal an active input',async()=>{ + const r=await fresh();r.apply(layout);const saved=r.exportDocument(); + r.point('drag1','down',90,130);const before=r.snapshot(); + assert.throws(()=>r.exportDocument(),/조작/);assert.throws(()=>r.restoreDocument(saved),/놓은 뒤/);assert.deepEqual(r.snapshot(),before); + r.releaseAll();r.apply('state x=0\ninput fail=Space\non fail.down { x=1/0 }'); + r.down('fail');r.up('fail');assert.throws(()=>r.exportDocument(),/오류/); + r.restoreDocument(saved);assert.equal(r.snapshot().lastError,0); +}); + +test('empty and full documents restore without imposing a new source size or state count',async()=>{ + const r=await fresh();r.apply('');r.restoreDocument(roundTrip(r.exportDocument()));assert.deepEqual(r.snapshot().states,{}); + r.apply(Array.from({length:32},(_,i)=>`state s${i}=${i}`).join('\n'));const doc=r.exportDocument(); + for(const s of doc.states) s.value+=100; + r.restoreDocument(roundTrip(doc));assert.equal(r.snapshot().states.s31,131); + assert.equal(parseProject(JSON.stringify({format:'pure-workbench',version:1,source:'state unfinished ='})).version,1); + assert.throws(()=>r.restoreDocument({format:'pure-workbench',version:1,source:''})); +}); + +test('document APIs respect the output callback boundary and export independent values',async()=>{ + const r=await fresh();r.apply('state x=1');const doc=r.exportDocument();doc.states[0].value=500; + assert.equal(r.snapshot().states.x,1); + r.present({begin(){assert.throws(()=>r.restoreDocument(doc),/출력 전달/);assert.throws(()=>r.exportDocument(),/출력 전달/);},state(){},view(){},value(){},end(){}}); + assert.equal(r.snapshot().states.x,1); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/remote.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/routing.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/routing.test.mjs new file mode 100644 index 0000000..bae04de --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/routing.test.mjs @@ -0,0 +1,114 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {spawnSync} from 'node:child_process'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const shared=await readFile(new URL('./examples/shared-layout.pure',import.meta.url),'utf8'); +const routed=await readFile(new URL('./examples/routed-layout.pure',import.meta.url),'utf8'); +const fresh=()=>createRuntime(wasm,{cacheEntries:32}); +const fixture=n=>`input drag=pointer {\n${Array.from({length:n},(_,i)=>`v${i}:c${i}`).join('\n')}\n}\non drag.move use move(drag.target)\nrule move(t:Cell) {t.x=event_x}\nrecord Cell {x=0}\n`+Array.from({length:n},(_,i)=>`item c${i}=Cell()\nbox v${i}(c${i}.x,0,10,10,"#123456")`).join('\n'); + +test('one input and one handler serve up to 24 static targets without instruction or handler expansion',async()=>{ + for(const n of [1,3,9,24]){ + const p=compile(fixture(n));assert.equal(p.inputs.length,1);assert.equal(p.handlers.length,1); + assert.equal(p.handlers[0].length,2);assert.equal(p.pointerTargets.length,n); + assert.equal(p.handlers[0].parameterBase,255); + const r=await fresh();r.apply(fixture(n)); + r.point('drag','down',0,0,'p',`v${n-1}`);r.point('drag','move',37,0,'p',`v${n-1}`);r.point('drag','up',0,0,'p',`v${n-1}`); + const s=r.snapshot();assert.equal(s.states[`c${n-1}.x`],37); + for(let i=0;i{ + const base=fixture(2); + for(const source of [ + base.replace('v0:c0','missing:c0'),base.replace('v0:c0','v0:missing'), + base.replace('v1:c1','v0:c1'),base.replace('v1:c1','v1:c0'), + base.replace('c1=Cell()','c1=Other()')+'\nrecord Other {x=0}', + base.replace('use move(drag.target)','use move(other.target)'), + base.replace('use move(drag.target)','use move(drag.missing)'), + base+'\ninput another=pointer(v0)', + base+'\ninput other=pointer { v2:c0 }\nbox v2(0,0,1,1,"#000000")', + 'input empty=pointer {}', + base.replace('on drag.move','on frame'), + base.replace('rule move(t:Cell)','rule move(t:Other)')+'\nrecord Other {x=0}', + ])assert.throws(()=>compile(source),source); +}); + +test('grouped layout matches explicit bindings through concurrent capture, partial cancel, keyboard and restore',async()=>{ + const a=await fresh(),b=await fresh();a.apply(shared);b.apply(routed); + const compare=()=>{const x=a.snapshot(),y=b.snapshot();assert.deepEqual(y.states,x.states);assert.deepEqual(y.views,x.views);assert.equal(y.lastError,x.lastError);}; + const point=(name,phase,x,y)=>{a.point(`drag_${name}`,phase,x,y,name);b.point('drag',phase,x,y,name,`${name}_box`);compare();}; + point('green','down',90,130);point('blue','down',290,170); + assert.equal(b.snapshot().held.length,2); + assert.throws(()=>b.apply(routed),/놓은/);assert.throws(()=>b.exportDocument(),/조작/); + assert.throws(()=>b.point('drag','down',0,0,'intruder','green_box'),/다른 포인터/); + b.point('drag','move',999,999,'intruder','green_box');compare(); + point('green','move',200,210);point('blue','move',500,270);point('green','cancel',200,210); + assert.equal(b.snapshot().held.length,1);point('blue','up',290,170); + for(const key of ['left','up','right','down']){a.down(key);b.down(key);a.up(key);b.up(key);compare();} + point('amber','down',460,230);point('amber','move',900,500);point('amber','up',460,230); + const c=await fresh();c.restoreDocument(JSON.parse(JSON.stringify(b.exportDocument()))); + assert.deepEqual(c.snapshot().states,b.snapshot().states); + for(const r of [b,c]){r.point('drag','down',570,290,'new','amber_box');r.point('drag','move',100,150,'new','amber_box');r.releaseAll();} + assert.deepEqual(c.snapshot().states,b.snapshot().states); + assert.throws(()=>b.point('drag','down',0,0),/도형/); + assert.throws(()=>b.point('drag','down',0,0,'p','board'),/도형/); + assert.throws(()=>b.down('drag'),/좌표/); +}); + +test('capture remains bound to its target and releaseAll cancels each target exactly once',async()=>{ + const r=await fresh();r.apply(routed+'\nstate cancelled=0\non drag.cancel {cancelled=cancelled+1}'); + r.point('drag','down',90,130,'a','green_box');r.point('drag','move',200,200,'a','green_box'); + r.point('drag','down',290,170,'b','blue_box');r.point('drag','move',400,250,'b','blue_box'); + r.point('drag','up',0,0,'a','blue_box');assert.equal(r.snapshot().held.length,2); + r.releaseAll();r.releaseAll();const s=r.snapshot();assert.equal(s.held.length,0); + assert.equal(s.states.cancelled,2);assert.equal(s.states['green.x'],80);assert.equal(s.states['blue.x'],280); +}); + +test('reorder and remove/readd rebuild routes by names without persisting physical indices',async()=>{ + const r=await fresh();r.apply(fixture(3)); + r.point('drag','down',0,0,'p','v2');r.point('drag','move',55,0,'p','v2');r.releaseAll(); + const reordered=fixture(3).replace('v0:c0\nv1:c1\nv2:c2','v2:c2\nv1:c1\nv0:c0').replace('item c0=Cell()','').replace('item c2=Cell()','item c2=Cell()\nitem c0=Cell()'); + r.apply(reordered);assert.equal(r.snapshot().states['c2.x'],55); + r.point('drag','down',0,0,'p','v2');r.point('drag','move',66,0,'p','v2');r.releaseAll(); + assert.equal(r.snapshot().states['c2.x'],66);assert.equal(r.snapshot().states['c0.x'],0); + const saved=r.exportDocument();const c=await fresh();c.restoreDocument(saved);assert.deepEqual(c.snapshot().states,r.snapshot().states); + r.apply(fixture(2));r.apply(fixture(3));assert.equal(r.snapshot().states['c2.x'],0); + r.point('drag','move',999,0,'p','v2');assert.equal(r.snapshot().states['c2.x'],0); +}); + +test('routed writes invalidate cached displays and failed handlers rollback without losing another target owner',async()=>{ + const source=fixture(2)+'\nstate total=0\non drag.move {total=total+1; total=1/event_y}'; + const r=await fresh();r.apply(source);r.snapshot();r.snapshot(); + r.point('drag','down',0,0,'a','v0');r.point('drag','down',0,0,'b','v1'); + assert.equal(r.point('drag','move',50,2,'a','v0'),0);assert.equal(r.snapshot().views[0].x,50); + assert.equal(r.point('drag','move',99,0,'b','v1'),4);let s=r.snapshot(); + assert.equal(s.states['c1.x'],0);assert.equal(s.states['c0.x'],50);assert.equal(s.states.total,0.5); + assert.equal(s.lastFailure.target,'v1');r.releaseAll();assert.equal(r.snapshot().lastFailure.status,4); + const previous=r.currentProgram;assert.throws(()=>r.apply(source.replace('v0:c0','v0:missing'))); + assert.equal(r.currentProgram,previous);r.reset();assert.equal(r.snapshot().held.length,0); +}); + +test('CLI sends grouped pointer events and rejects an unknown target',()=>{ + const path=new URL('./examples/routed-layout.pure',import.meta.url).pathname; + const run=input=>spawnSync(process.execPath,[new URL('./cli.mjs',import.meta.url).pathname,path],{input,encoding:'utf8'}); + const ok=run('point-target drag green_box down 90 130\npoint-target drag green_box move 210 220\npoint-target drag green_box up 90 130\nquit\n'); + assert.equal(ok.status,0,ok.stderr);const rows=ok.stdout.trim().split('\n').map(JSON.parse); + assert.equal(rows.at(-1).states['green.x'],200);assert.equal(rows.at(-1).states['blue.x'],280);assert.equal(rows.at(-1).held.length,0); + const bad=run('point-target drag missing down 0 0\nquit\n');assert.equal(bad.status,1);assert.match(bad.stderr,/도형/); +}); + + +test('a route from a different input group cannot be delivered through the named group',async()=>{ + const source=fixture(2).replace('v1:c1','')+'\ninput second=pointer {v1:c1}\non second.move use move(second.target)'; + const r=await fresh();r.apply(source); + assert.throws(()=>r.point('drag','down',0,0,'p','v1'),/도형/); + assert.throws(()=>r.point('second','down',0,0,'p','v0'),/도형/); + r.point('second','down',0,0,'p','v1');r.point('second','move',77,0,'p','v1');r.releaseAll(); + assert.equal(r.snapshot().states['c0.x'],0);assert.equal(r.snapshot().states['c1.x'],77); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/rules.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/rules.test.mjs new file mode 100644 index 0000000..b4c4969 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/rules.test.mjs @@ -0,0 +1,94 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +import {parseProject} from './web/project.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const shared=await readFile(new URL('./examples/shared-layout.pure',import.meta.url),'utf8'); +const explicit=await readFile(new URL('./examples/layout.pure',import.meta.url),'utf8'); +const fresh=()=>createRuntime(wasm,{cacheEntries:32}); +const named=['green','blue','amber']; + +function simple(n){return 'record Cell { value=1 }\ninput tap=Space\nrule inc(t: Cell) { t.value=t.value+1 }\n'+Array.from({length:n},(_,i)=>`item cell${i}=Cell()\non tap.down use inc(cell${i})`).join('\n');} + +test('binding more objects shares one VM body instead of expanding its instructions',()=>{ + for(const n of [1,2,4,8]){ + const p=compile(simple(n)); + assert.equal(p.instructions.length,4);assert.equal(p.handlers.length,n); + assert.deepEqual(p.handlers.map(h=>h.start),Array(n).fill(0)); + assert.deepEqual(p.handlers.map(h=>[h.parameterBase,h.parameterCount]),Array.from({length:n},(_,i)=>[i,1])); + } + assert.equal(compile('record R { value=0 }\nrule unused(t:R) { t.value=t.value+1 }').instructions.length,0); +}); + +test('records and rules resolve forward names and reject invalid types fields and bindings',()=>{ + const p=compile('on tap.down use inc(a)\nitem a=R(v=4)\ninput tap=Space\nrule inc(t:R) { t.v=t.v+1 }\nrecord R { v=0 }'); + assert.equal(p.states[0].name,'a.v');assert.deepEqual([p.handlers[0].parameterBase,p.handlers[0].parameterCount],[0,1]); + for(const code of [ + 'record R {}','record R { x=0; x=1 }','record R { event_x=0 }', + 'item a=Missing()','record R {x=0}\nitem a=R(y=1)', + 'record R {x=0}\nitem a=R(x=1,x=2)','record R {x=0}\nstate a=0\nitem a=R()', + 'record R {x=0}\nrule bad(t:R) {t.missing=0}', + 'record R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {t.y=1}', + 'state value=0\nrecord R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {value=t.y}', + 'rule bad(t:Missing) {}','record R {x=0}\nitem a=R()\ninput k=Space\non k.down use missing(a)', + 'record R {x=0}\nrule r(t:R) {}\ninput k=Space\non k.down use r(missing)', + 'record R {x=0}\nrecord S {x=0}\nitem a=S()\nrule r(t:R) {}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_x}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_y}\non frame use r(a)', + 'record R {x=0}\nitem a=R()\ntext label(0,0,t.x)', + `record R { ${Array.from({length:9},(_,i)=>`f${i}=0`).join(';')} }`, + ]) assert.throws(()=>compile(code),code); + assert.throws(()=>compile(simple(33)),/32 states/); +}); + +test('shared layout matches the existing authored layout across independent and simultaneous gestures',async()=>{ + const a=await fresh(),b=await fresh();a.apply(explicit);b.apply(shared); + const compare=()=>{ + const old=a.snapshot(),next=b.snapshot();assert.equal(next.lastError,old.lastError); + assert.equal(next.states.selected,old.states.selected); + for(let i=1;i<=3;i++)for(const field of ['x','y','ox','oy','sx','sy','held'])assert.equal(next.states[`${named[i-1]}.${field}`],old.states[`${field}${i}`]); + for(let i=0;i{a.point(`drag${i}`,phase,x,y,`p${i}`);b.point(`drag_${named[i-1]}`,phase,x,y,`p${i}`);compare();}; + compare();point(1,'down',90,130);point(2,'down',290,170);point(1,'move',350,200);point(2,'move',500,300);point(2,'up',290,170);point(1,'cancel',350,200); + for(const key of ['left','right','up','down']){a.down(key);b.down(key);a.up(key);b.up(key);compare();} + point(3,'down',460,230);point(3,'move',900,500);point(3,'up',460,230); + const saved=JSON.parse(JSON.stringify(b.exportDocument()));const c=await fresh();c.restoreDocument(saved); + assert.deepEqual(c.snapshot().states,b.snapshot().states);assert.deepEqual(c.snapshot().views,b.snapshot().views); + c.down('left');c.up('left');assert.equal(c.snapshot().states['amber.x'],b.snapshot().states['amber.x']-10); +}); + +test('field names retain identity across schema and instance reorder, removal and restoration',async()=>{ + const r=await fresh(); + const source='record R { x=1;y=2 }\nitem a=R(x=10)\nitem b=R(x=20)\ninput k=Space\nrule step(t:R) {t.x=t.x+1}\non k.down use step(b)'; + r.apply(source);r.down('k');r.up('k'); + r.apply(source.replace('x=1;y=2','y=2;x=1').replace('item a=R(x=10)\nitem b=R(x=20)','item b=R(x=20)\nitem a=R(x=10)')); + r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],22);assert.equal(r.snapshot().states['a.x'],10); + r.apply(source.replace('item a=R(x=10)\n',''));r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); + const saved=r.exportDocument();r.apply(source);assert.equal(r.snapshot().states['a.x'],10); + r.restoreDocument(saved);assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); +}); + +test('bindings share one event transaction including global effects and multiple uses of one item',async()=>{ + const r=await fresh();r.apply('state total=0\nrecord R {x=8;divisor=2}\nitem a=R()\nitem b=R(divisor=0)\ninput k=Space\nrule divide(t:R) {total=total+1\nt.x=t.x/t.divisor}\non k.down use divide(a)\non k.down use divide(b)'); + r.down('k');assert.equal(r.snapshot().lastError,4);assert.equal(r.snapshot().states.total,0);assert.equal(r.snapshot().states['a.x'],8); + r.up('k');r.apply(simple(1)+'\non tap.down use inc(cell0)');r.down('tap');r.up('tap');assert.equal(r.snapshot().states['cell0.value'],3); +}); + +test('bound writes invalidate affected display caches and failed edits preserve active bindings',async()=>{ + const r=await fresh();const source=simple(2)+'\ntext first(0,0,cell0.value)\ntext second(0,0,cell1.value)';r.apply(source);r.snapshot();r.snapshot(); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[2,2]); + const before=r.snapshot();assert.throws(()=>r.apply(source.replace('inc(cell1)','inc(missing)')));assert.deepEqual(r.snapshot(),before); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[3,3]); +}); + +test('generated qualified names cannot make a valid exported document unreadable by its own loader',async()=>{ + const field='f'.repeat(18000); + const source=`record R { ${field}=1 }\n`+Array.from({length:32},(_,i)=>`item i${i}=R()`).join('\n'); + const r=await fresh();r.apply(source); + const saved=JSON.stringify(r.exportDocument('\u0001'.repeat(32768))); + r.restoreDocument(parseProject(saved)); + assert.equal(r.snapshot().states[`i31.${field}`],1); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/runtime.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/runtime.test.mjs new file mode 100644 index 0000000..86bc9ba --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/runtime.test.mjs @@ -0,0 +1,197 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const fresh = () => createRuntime(wasm); +const countSource = `state total = 3 +input add = KeyA +box bar(20, 80, total * 10, 30, "#abcdef") +text label(20, 60, total, "Total: ") +on add.down { total = total + 2 } +on add.up { total = total + 1 } +`; + +test('blank startup and applied empty source contain no application objects or behavior', async () => { + const runtime = await fresh(); + assert.equal(runtime.currentProgram, null); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + runtime.apply(''); + runtime.frame(60); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + assert.equal(runtime.snapshot().tick, 60); + assert.throws(() => runtime.down('jump'), /jump/); +}); + +test('author can add unrelated states inputs views and behavior entirely through source', async () => { + const runtime = await fresh(); + runtime.apply(countSource); + runtime.down('add'); runtime.up('add'); + assert.equal(runtime.snapshot().states.total, 6); + const added = `${countSource}\nstate radius = 12 +input inflate = KeyI +circle bubble(240, 160, radius, "#123456") +on inflate.down { radius = radius + total } +on frame { radius = radius + 60 * dt } +`; + runtime.apply(added); + runtime.down('inflate'); runtime.up('inflate'); runtime.frame(2); + assert.deepEqual(runtime.snapshot().states, { total: 6, radius: 20 }); + assert.equal(runtime.snapshot().views.find(view => view.id === 'bubble').radius, 20); + assert.equal(runtime.snapshot().views.length, 3); +}); + +test('keyboard and pointer sources aggregate first down and last up; repeats are ignored', async () => { + const runtime = await fresh(); runtime.apply(countSource); + runtime.down('add', 'keyboard'); runtime.down('add', 'keyboard'); runtime.down('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 5); + runtime.up('add', 'unknown'); runtime.up('add', 'keyboard'); + assert.equal(runtime.snapshot().states.total, 5); + assert.deepEqual(runtime.snapshot().held, ['add']); + runtime.releaseAll(); runtime.up('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 6); + assert.deepEqual(runtime.snapshot().held, []); + assert.equal(runtime.snapshot().successfulEvents, 2); +}); + +test('held input rejects apply without changing applied source, metadata, or values', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); + const before = runtime.snapshot(); + assert.throws(() => runtime.apply('state changed = 9'), /놓은 뒤/); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + runtime.up('add'); runtime.apply('state changed = 9'); + assert.deepEqual(runtime.snapshot().states, { changed: 9 }); +}); + +test('invalid syntax, unknown references and capacity overflow preserve the previous program', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (const source of ['state broken =', 'state n = 0\non frame { n = missing }', + Array.from({ length: 33 }, (_, i) => `state v${i} = ${i}`).join('\n'), '#'.repeat(32769)]) { + assert.throws(() => runtime.apply(source)); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + } +}); + +test('state preservation follows names across reorder/add/remove; reset uses new initializers', async () => { + const runtime = await fresh(); + runtime.apply('state x = 11\nstate y = 22'); + runtime.apply('state z = 30\nstate y = 2\nstate x = 1'); + assert.deepEqual(runtime.snapshot().states, { z: 30, y: 22, x: 11 }); + runtime.apply('state y = 200\nstate renamed = 9'); + assert.deepEqual(runtime.snapshot().states, { y: 22, renamed: 9 }); + runtime.reset(); + assert.deepEqual(runtime.snapshot().states, { y: 200, renamed: 9 }); + assert.equal(runtime.snapshot().revision, 3); +}); + +test('callers cannot mutate active metadata and corrupt name-based state preservation', async () => { + const runtime = await fresh(); const program = runtime.apply('state x = 11\nstate y = 22'); + assert.throws(() => program.states.reverse(), TypeError); + assert.throws(() => { runtime.currentProgram.states[0].name = 'y'; }, TypeError); + runtime.apply('state x = 0\nstate y = 0'); + assert.deepEqual(runtime.snapshot().states, { x: 11, y: 22 }); +}); + +test('conditions execute once, and assignments and multiple handlers follow source order', async () => { + const runtime = await fresh(); + runtime.apply(`state x = 0 +state seen = 0 +on frame { + if x == 0 { + x = 1 + seen = x + 1 + } + x = x + 3 +} +on frame { seen = seen + x } +`); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 4, seen: 6 }); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 7, seen: 13 }); +}); + +test('a failed event rolls back all handlers and keeps its source diagnostic through release', async () => { + const runtime = await fresh(); + const source = `state x = 7 +state y = 9 +input breakit = Space +on breakit.down { x = 99 } +on breakit.down { + y = 2 + x = 1 / 0 +} +`; + runtime.apply(source); + assert.equal(runtime.down('breakit'), 4); + assert.deepEqual(runtime.snapshot().states, { x: 7, y: 9 }); + assert.equal(runtime.snapshot().successfulEvents, 0); + assert.equal(runtime.snapshot().errorLine, 7); + runtime.up('breakit'); runtime.frame(); + assert.equal(runtime.snapshot().lastEvent.status, 0); + assert.equal(runtime.snapshot().lastFailure.phase, 'down'); + assert.equal(runtime.snapshot().lastError, 4); + assert.equal(runtime.snapshot().errorLine, 7); + assert.throws(() => runtime.apply('bad')); + assert.equal(runtime.snapshot().lastError, 4); + runtime.apply(source.replace('1 / 0', '10')); + assert.equal(runtime.snapshot().lastError, 0); + runtime.down('breakit'); runtime.up('breakit'); + assert.deepEqual(runtime.snapshot().states, { x: 10, y: 2 }); +}); + +test('reset clears held inputs and latched errors; invalid frame counts have no effects', async () => { + const runtime = await fresh(); + runtime.apply('state x = 5\ninput tap = Space\non tap.down { x = 1 / 0 }'); + runtime.down('tap'); runtime.reset(); + const before = runtime.snapshot(); + assert.equal(before.lastError, 0); assert.equal(before.lastFailure, null); + assert.deepEqual(before.held, []); + for (const count of [-1, 601, 0.5, NaN, Infinity]) assert.throws(() => runtime.frame(count)); + assert.deepEqual(runtime.snapshot(), before); +}); + +test('render evaluation is pure and invalid binding is distinct from an event rollback', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (let i = 0; i < 100; i++) assert.deepEqual(runtime.snapshot(), before); + runtime.apply('state n = 2\ncircle bad(20, 20, -n, "#123456")\ntext fine(0, 0, n)'); + const result = runtime.snapshot(); + assert.equal(result.lastError, 100); assert.equal(result.errorLine, 2); + assert.deepEqual(result.states, { n: 2 }); assert.equal(result.views.length, 1); + assert.equal(result.lastFailure, null); + assert.deepEqual(runtime.snapshot(), result); +}); + +test('independent adapters replay the same author program without sharing live state', async () => { + const left = await fresh(), right = await fresh(); + left.apply(countSource); right.apply(countSource); + for (const runtime of [left, right]) { + runtime.down('add', 'keyboard'); runtime.frame(3); runtime.up('add', 'keyboard'); + } + assert.deepEqual(left.snapshot(), right.snapshot()); + left.down('add'); + assert.equal(right.snapshot().states.total, 6); + assert.equal(left.snapshot().states.total, 8); +}); + +test('Wasm has no imports and reports fixed memory and exact active IR/state storage', async () => { + const module = await WebAssembly.compile(wasm); + assert.deepEqual(WebAssembly.Module.imports(module), []); + const runtime = await fresh(); const program = runtime.apply(countSource); + const before = runtime.snapshot().memory; + assert.equal(before.linearBytes, 65536); + assert.equal(before.instructionBytes, 16); assert.equal(before.handlerBytes, 16); + assert.equal(before.activeIRBytes, program.instructions.length * 16 + program.handlers.length * 16); + assert.equal(before.activeStateBytes, program.states.length * 8); + assert(before.stagingReservedBytes < before.reservedEngineBytes); + assert(before.reservedEngineBytes < before.linearBytes); + for (let i = 0; i < 20; i++) runtime.frame(600); + assert.deepEqual(runtime.snapshot().memory, before); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/value-input.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/value-input.test.mjs new file mode 100644 index 0000000..3044048 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/value-input.test.mjs @@ -0,0 +1,50 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {spawnSync} from 'node:child_process'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const source=await readFile(new URL('./examples/document.pure',import.meta.url),'utf8'); + +test('numeric controls use explicit change context and cannot read pointer or frame payloads',()=>{ + const p=compile(source);assert.equal(p.states.length,1);assert.equal(p.views.length,0);assert.equal(p.handlers[0].event,33); + for(const s of [ + 'state x=0\ninput n=value\non n.down {x=1}', + 'state x=0\ninput k=Space\non k.change {x=1}', + 'state x=0\ninput n=value\non n.change {x=event_x}', + 'state x=0\non frame {x=event_value}', + 'input n=value\ntext v(0,0,event_value)', + 'record R {x=0}\nitem a=R()\ninput k=Space\nrule r(t:R){t.x=event_value}\non k.down use r(a)', + 'state event_value=0', + ])assert.throws(()=>compile(s),s); +}); + +test('numeric input handles scalar rules, rollback, cache invalidation and signed zero',async()=>{ + const r=await createRuntime(wasm,{cacheEntries:32}); + const code='record R {x=10}\nitem a=R()\ninput n=value\nrule r(t:R){t.x=event_value}\non n.change use r(a)\ntext v(0,0,a.x)'; + r.apply(code);r.snapshot();r.snapshot();r.change('n',25);assert.equal(r.snapshot().views[0].value,25); + r.change('n',-0);assert(Object.is(r.snapshot().states['a.x'],-0));assert.deepEqual(r.snapshot().held,[]); + const before=r.snapshot();assert.throws(()=>r.change('n',Infinity));assert.throws(()=>r.down('n'));assert.throws(()=>r.point('n','down',1,2)); + assert.equal(r.snapshot().successfulEvents,before.successfulEvents); + r.apply(code+'\non n.change {a.x=1/0}');assert.equal(r.change('n',80),4);assert(Object.is(r.snapshot().states['a.x'],-0)); + assert.equal(r.snapshot().lastFailure.value,80);assert.throws(()=>r.exportDocument(),/오류/); + r.apply(code);const saved=r.exportDocument();const next=await createRuntime(wasm);next.restoreDocument(saved);next.change('n',32);assert.equal(next.snapshot().states['a.x'],32); +}); + +test('the document policy receives range, keyboard and pointer requests without layouts or text in VM state',async()=>{ + const r=await createRuntime(wasm);r.apply(source); + for(const [value,expected] of [[60,60],[1000,80],[-1000,20],[45.5,45.5]]){assert.equal(r.change('split',value,'control'),0);assert.deepEqual(r.snapshot().states,{ratio:expected});} + r.down('reset');r.up('reset');assert.deepEqual(r.snapshot().states,{ratio:50}); + assert.equal(r.snapshot().views.length,0);assert.equal(r.snapshot().memory.activeStateBytes,8); + const published=[];const target={begin(){},view(){assert.fail('no declared views');},value(){assert.fail('no view values');},state(i,v){published.push(v);},end(n){assert.equal(n,0);}}; + r.present(target);assert.deepEqual(published,[50]); + r.apply(source.replace('20, 80','30, 70'));r.change('split',10);assert.equal(r.snapshot().states.ratio,30); + const current=r.currentProgram;assert.throws(()=>r.apply(source+'\non split.down {ratio=0}'));assert.equal(r.currentProgram,current); +}); + +test('CLI supplies the same numeric control value and rejects nonfinite input',()=>{ + const run=input=>spawnSync(process.execPath,[new URL('./cli.mjs',import.meta.url).pathname,new URL('./examples/document.pure',import.meta.url).pathname],{input,encoding:'utf8'}); + const ok=run('change split 95\nquit\n');assert.equal(ok.status,0,ok.stderr);assert.equal(JSON.parse(ok.stdout.trim()).states.ratio,80); + const bad=run('change split NaN\nquit\n');assert.equal(bad.status,1);assert.match(bad.stderr,/유한/); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/baseline.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/baseline.mjs new file mode 100644 index 0000000..3873e31 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/baseline.mjs @@ -0,0 +1,68 @@ +// Handwritten JavaScript reference for examples/charge.pure. +// This fixed app has no source compiler, live apply, or state-name migration. +// Its scene and behavior must be changed by editing this JavaScript file. +export function createChargeModel() { + const held = new Set(); + let holding = 0, charge = 0, y = 240, velocity = 0, tick = 0; + const checkInput = name => { + if (name !== 'jump') throw new Error(`Unknown input: ${name}`); + }; + + const model = { + down(name = 'jump', source = 'external') { + checkInput(name); + const wasDown = held.size > 0; + held.add(source); + if (!wasDown && y >= 240) { + holding = 1; + charge = 0; + } + return 0; + }, + up(name = 'jump', source = 'external') { + checkInput(name); + if (!held.delete(source) || held.size) return 0; + if (holding) { + velocity = -(160 + charge * 300); + holding = 0; + charge = 0; + } + return 0; + }, + frame(count = 1) { + if (!Number.isInteger(count) || count < 0 || count > 600) { + throw new Error('Frame count must be an integer from 0 through 600.'); + } + const dt = 1 / 60; + for (let frame = 0; frame < count; frame++) { + if (holding) charge = Math.min(Math.max(charge + dt, 0), 1); + velocity = velocity + 600 * dt; + y = y + velocity * dt; + if (y >= 240) { y = 240; velocity = 0; } + tick++; + } + return 0; + }, + releaseAll() { + for (const source of [...held]) model.up('jump', source); + return 0; + }, + reset() { + held.clear(); + holding = 0; charge = 0; y = 240; velocity = 0; tick = 0; + }, + snapshot() { + return { + states: { holding, charge, y, velocity }, tick, + held: held.size ? ['jump'] : [], + views: [ + { id: 'floor', type: 'box', x: 40, y: 261, width: 560, height: 4, color: '#356548' }, + { id: 'actor', type: 'circle', x: 180, y, radius: 20, color: '#c78638' }, + { id: 'meter', type: 'box', x: 80, y: 80, width: charge * 200, height: 12, color: '#c78638' }, + { id: 'amount', type: 'text', x: 80, y: 60, value: charge, prefix: 'Charge: ' }, + ], + }; + }, + }; + return Object.freeze(model); +} diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/core.wasm b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/core.wasm new file mode 100644 index 0000000..527c57e Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/project.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/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/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace-check.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace-check.mjs new file mode 100644 index 0000000..063ba42 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace-check.mjs @@ -0,0 +1,62 @@ +// Compare the archived separate-storage build with the shared-workspace build. +// This is a correctness and storage check, not another speed benchmark. +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { createRuntime } from './web/runtime.mjs'; +import { createRuntime as createPrevious } from './results/2026-09-22-evaluation/sources/web/runtime.mjs'; + +const args = process.argv.slice(2); +if (args.length !== 2 || args[0] !== '--output') throw new Error('Use --output NEW.json'); +const root = new URL('.', import.meta.url); +const archived = new URL('./results/2026-09-22-evaluation/sources/', root); +const sha = bytes => createHash('sha256').update(bytes).digest('hex'); +const manifest = JSON.parse(await readFile(new URL('manifest.json', archived), 'utf8')); +for (const [path, expected] of Object.entries(manifest)) + assert.equal(sha(await readFile(new URL(path, archived))), expected, path); +const oldWasm = await readFile(new URL('web/core.wasm', archived)); +const wasm = await readFile(new URL('web/core.wasm', root)); +const record = {utc:new Date().toISOString(),kind:'workspace-reuse-correctness-and-storage', + baselineManifestSha256:sha(await readFile(new URL('manifest.json', archived))), + sourceSha256:{},cases:[],node:process.version,platform:process.platform,arch:process.arch}; +for (const path of ['core.zig','evaluation.zig','web/runtime.mjs','web/compiler.mjs','web/core.wasm','workspace-check.mjs']) + record.sourceSha256[path] = sha(await readFile(new URL(path,root))); + +for (const name of ['counter','toggle','charge']) { + const path = `examples/${name}.pure`, source = await readFile(new URL(path,root),'utf8'); + record.sourceSha256[path] = sha(source); + const old = await createPrevious(oldWasm,{cacheEntries:32}); + const current = await createRuntime(wasm,{cacheEntries:32}); + const program = old.apply(source); current.apply(source); + const input = program.inputs[0].name; + let snapshots = 0; + function check() { + const before = old.snapshot(), after = current.snapshot(); + const {memory:oldMemory,...oldObservable} = before; + const {memory:newMemory,...newObservable} = after; + assert.deepEqual(newObservable,oldObservable); + for (const field of ['work','instructions','cacheHits']) + assert.equal(current.evaluationStats()[field],old.evaluationStats()[field]); + assert.equal(oldMemory.linearBytes,newMemory.linearBytes); + ++snapshots; + } + check(); + for (let i=0;i<256;i++) { + for (const r of [old,current]) { + if (i%23===0) {r.down(input);r.up(input);} + if (i%31===0) r.reset(); + if (i%47===0) r.apply(source+`\n# replacement ${i}\n`); + if (i%7===0) r.forgetEvaluation('history'); + if (i%13===0) r.forgetEvaluation('all'); + assert.equal(r.frame(),0); + } + check(); + } + record.cases.push({name,snapshots,before:old.snapshot().memory,after:current.snapshot().memory, + evaluation:current.evaluationStats()}); +} +record.wasmFileBytes = {before:oldWasm.length,after:wasm.length}; +await writeFile(args[1],JSON.stringify(record,null,2)+'\n',{flag:'wx'}); +console.log(JSON.stringify({checkedSnapshots:record.cases.reduce((sum,c)=>sum+c.snapshots,0), + engineBefore:record.cases[0].before.reservedEngineBytes,engineAfter:record.cases[0].after.reservedEngineBytes, + wasmFileBytes:record.wasmFileBytes,evaluation:record.cases[0].evaluation})); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace.test.mjs b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace.test.mjs new file mode 100644 index 0000000..799f056 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/sources/prototypes/workbench/workspace.test.mjs @@ -0,0 +1,91 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = factor => `state x = 10 +input next = KeyN +text label(0, 0, x * ${factor}) +on next.down { x = x + 0.25 } +`; + +function begin(core, program, preserve) { + assert.equal(core.begin_program(program.states.length, program.inputs.length), 0); + program.states.forEach((state, i) => assert.equal(core.put_state(i, state.initial, preserve ? i : -1), 0)); +} + +function finish(core, program, during = () => {}) { + for (const op of program.instructions) { + assert.equal(core.put_instruction(op.op, op.arg, op.value), 0); + during(); + } + for (const handler of program.handlers) + assert.equal(core.put_handler(handler.event, handler.start, handler.length), 0); + assert.equal(core.commit_program(), 0); +} + +function observe(core, binding) { + core.begin_evaluation(100); + const value = core.eval_bounded(binding.start, binding.length); + assert.equal(core.evaluation_info(0), 0); + return value; +} + +test('Wasm executes old events and output during staged replacement without corrupting either phase', async () => { + const {instance:{exports:core}} = await WebAssembly.instantiate(wasm); + core.reset_all(); + const old = compile(source(2)), next = compile(source(3)); + begin(core, old, false); finish(core, old); + assert.equal(core.configure_evaluation(32, 8), 0); + const binding = old.views[0].bindings.value; + assert.equal(observe(core, binding), 20); + assert.equal(observe(core, binding), 20); + assert.equal(core.evaluation_info(3), 1); + begin(core, next, true); + let value = 10; + finish(core, next, () => { + assert.equal(core.evaluation_info(9), 0); + assert.equal(core.dispatch(1, 0), 0); value += 0.25; + assert.equal(core.configure_evaluation(32, 8), 0); + core.forget_evaluation(3); + assert.equal(observe(core, binding), value * 2); + assert.equal(core.evaluation_info(3), 0); + }); + assert.equal(core.evaluation_info(9), 1); + assert.equal(core.get_state(0), value); + const replaced = next.views[0].bindings.value; + assert.equal(observe(core, replaced), value * 3); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.evaluation_info(3), 1); + + const revision = core.info(4); + begin(core, old, true); + assert.equal(core.put_instruction(0, 0, Infinity), 5); + assert.equal(core.commit_program(), 5); + assert.equal(observe(core, replaced), value * 3); + core.abort_program(); + assert.equal(core.info(4), revision); + assert.equal(core.get_state(0), value); + assert.equal(core.evaluation_info(9), 1); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.memory.buffer.byteLength, 65536); +}); + +test('workspace accounting reports shared payload and safety tag without double-counting', async () => { + const runtime = await createRuntime(wasm, {cacheEntries:32}); + runtime.apply(source(2)); runtime.snapshot(); + const stats = runtime.evaluationStats(), memory = runtime.snapshot().memory; + assert.equal(stats.dedicatedBytes, 48); + assert.equal(stats.sharedBytes, 1152); + assert.equal(stats.workspaceTagBytes, 8); + assert.equal(stats.reservedBytes, stats.dedicatedBytes + stats.sharedBytes); + const restoreStorage = memory.stagingReservedBytes - memory.programReservedBytes; + assert.equal(restoreStorage, 264); // 32 values + mask/alignment, only in pending storage. + assert.equal(memory.reservedEngineBytes, 19072 + stats.dedicatedBytes + stats.workspaceTagBytes + restoreStorage + 2 * 64 * (memory.handlerBytes - 12) + 2 * 80); + // Each program adds 24 three-byte routes, count/mask and alignment (80 B). + assert.equal(memory.pointerTargetBytes, 3); + assert(stats.sharedBytes < memory.stagingReservedBytes); + assert.equal(stats.reuseAvailable, true); +}); diff --git a/prototypes/workbench/results/2026-09-23-native-instances/verification.json b/prototypes/workbench/results/2026-09-23-native-instances/verification.json new file mode 100644 index 0000000..29e827d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-native-instances/verification.json @@ -0,0 +1,39 @@ +{ + "sources_verified_and_frozen": 28, + "native_and_sanitized_source_hashes_match": true, + "c_host_sanitizers_only": true, + "zig_tests_passed": 37, + "existing_browser_and_cli_tests_passed": 87, + "authored_cases_per_build": 9, + "event_state_and_view_comparisons_per_build": 156, + "selected_module": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore", + "scalar_instance_bytes": 20064, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "native_library_sections": " text\t data\t bss\t dec\t hex\tfilename\n 33020\t 0\t 0\t 33020\t 80fc\tprototypes/workbench/build/cache/tmp/3cadbbd6974d5d26/libpure-scalar_zcu.o (ex /home/eunhhu/work/pure/prototypes/workbench/build/native/libpure-scalar.a)\n", + "native_library_externals": "\nprototypes/workbench/build/cache/tmp/3cadbbd6974d5d26/libpure-scalar_zcu.o:\n U memcpy\n U memset\n", + "selected_native_binary": { + "sha256": "6d587d072b6cfc93777fece74b99cde1ea5ff581b3276a6de545568bbcd5c69c", + "debug_file_bytes": 213720, + "stripped_file_bytes": 68000, + "size": " text\t data\t bss\t dec\t hex\tfilename\n 62827\t 2040\t 16\t 64883\t fd73\t/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-native\n", + "direct_dynamic_libraries": [ + "0x0000000000000001 (NEEDED) Shared library: [libc.so.6]" + ] + }, + "common_execution_core_unchanged": true, + "limits": [ + "No native window yet; CLI output adapter only", + "Node and Zig are build tools; not runtime dependencies", + "Existing scalar capacities remain 32 states, 8 inputs, 24 views, 512 instructions", + "Single-owner APIs; no strong native-code isolation", + "Synchronous storage; no secure peer RPC", + "No macOS execution verification or throughput/RSS claim" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/README.md b/prototypes/workbench/results/2026-09-23-pointer-routing/README.md new file mode 100644 index 0000000..9f9f93b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/README.md @@ -0,0 +1,51 @@ +# Pointer group routing evidence — 2026-09-23 + +This is functional validation and compiled-representation accounting, not a +throughput, latency, browser-heap, or large-application benchmark. + +- `build.json`: Zig 0.15.2 native tests (35), final core/Wasm hashes and build flags. +- `node-tests.log`, `verification.json`: final 83 Node tests, including 8 route tests + and existing compiler/runtime/CLI/display/persistence behavior. +- `representation.json`: shared-layout and routed-layout compiled on the same final + core. Includes used route bytes, fixed engine reservation, and unchanged 64 KiB memory. +- `browser.json`: 30 real Chromium checks on grouped input, including two CDP contacts, + rejecting a second contact on one view, releasing one then moving/canceling the other, + actual mouse placement, source editing/reordering, file download/new-page restoration, + stale file-read rejection and mobile width. +- `authoring.json`: 44 existing authoring-flow checks. +- `legacy-browser.json`: 23 existing separate-pointer-input/shared-rule checks. +- `pure-routed-layout-saved.json`: actual downloaded document, including unfinished draft. +- `pure-routed-layout-{desktop,mobile}.png`: final grouped example screenshots. +- `sources/`: final implementation, contract and relevant test snapshots. +- `before/`: pre-change core/compiler/runtime/Wasm for auditing; no timing comparison. + +Reproduce from repository root: + +```sh +python3 prototypes/workbench/manage.py build --zig /path/to/zig-0.15.2 +node --test prototypes/workbench/*.test.mjs +python3 prototypes/workbench/manage.py serve --port 8766 +``` + +Open a fresh Chromium page with Playwright CLI, then pass the function in +`prototypes/workbench/qa-routed-input.js` to `run-code`. Read +`window.__pureRoutedInputEvidence` after the function completes. Dialog/download +handling may cause CLI to yield early; a CLI return is not proof of completion. +`qa-authoring.js` and `qa-legacy-pointer.js` preserve the regression functions; +run authoring on a blank page. They expose `__pureWorkbenchQAEvidence` and +`__pureSharedLayoutEvidence` respectively. + +The initial partial-release QA used the wrong contact in the CDP touchEnd call. +After correcting it, the following move check also read before pointermove had +arrived. `browser-initial-failure.json` retains that intermediate failure; +`touch-evidence.json` records actual down/up/move events and retained owners. +Final QA identifies the ending contact and waits for observed movement. These were +QA input/observation corrections; product code was not changed to suppress the checks. +`touch-reproduction.js` is a debugging artifact for this Chromium CDP behavior; +its protocol behavior is not a general claim about mobile hardware. + +All successful browser reports are from the final runtime and Wasm. Browser is +Linux Chromium; touch is injected through CDP, not a physical touchscreen or a +user interaction in the Codex embedded browser. Static route addresses are rebuilt +on apply/restore, not persisted as dynamic lifetime identities. Keyboard selection +still uses the authored ID conditions. diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/authoring-result.log b/prototypes/workbench/results/2026-09-23-pointer-routing/authoring-result.log new file mode 100644 index 0000000..4f20964 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/authoring-result.log @@ -0,0 +1,13 @@ +### Result +{"recordedUtc":"2026-09-23T08:33:23.588Z","browser":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36","checks":["initial editor has literally empty source","initial scene has no built-in entities, states, inputs, or running clock","controls require an authored program","authoring a new program is a draft until Apply","new custom program constructs states and multiple views from source","input buttons are generated from the authored declarations","view bindings render the authored state value","Space and arrows in the editor never reach authored inputs","pointer down dispatches once while the clock is paused","pointer up releases without double dispatch from click","keyboard repeat is ignored","keyboard activation on a generated button is one complete tap","assistive click activation is one complete tap","keyboard and pointer sources share one logical input","releasing one source does not release the other source","last source releases the logical input","authored down/frame behavior uses the declared dt without built-in motion","keyup releases even after focus moves into the editor","focus loss sends release and pauses the clock","Play advances and Pause freezes the clock","invalid code preserves the previous executable program and states","compile error exposes a source-line diagnostic","diagnostic location moves the editor caret to the problem","apply preserves matching state names and initializes newly authored states","new input, entity, binding, and behavior work with source changes alone","changed behavior takes effect without resetting existing values","explicit Reset restores active initializers and keeps the draft","Apply is refused while an input is held","clear editor is explicit and does not clear the running scene before Apply","downloaded project restores the exact un-applied draft without execution","invalid JSON project schema cannot replace the draft","failed assistive down event rolls back and its error survives the following successful up","latched runtime failure is visible and labeled separately from the latest input","explicit Reset clears the failure record","applying empty source removes all authored states, inputs, and scene contents","explicit example loading only fills the editor and never auto-applies","authored scene executes in the actual fixed 64 KiB Wasm memory","cost inspector shows measured Wasm memory without claiming browser heap totals","390px mobile viewport has no horizontal overflow","generated mobile input buttons remain usable","ordinary-JavaScript comparison and equivalent Pure example are linked and served","missing Wasm shows an actionable load failure and disables execution controls","fresh load always returns to a blank authoring environment","no uncaught page errors during the browser scenarios"],"authoredSource":"# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n","authoredSnapshot":{"states":{"x":114,"moving":0,"count":2},"views":[{"id":"marker","type":"box","color":"#7ed9ae","x":114,"y":170,"width":48,"height":48},{"id":"reading","type":"text","prefix":"Signals: ","x":40,"y":70,"value":2}],"tick":12,"revision":5,"displayError":null,"lastError":0,"errorMessage":"","errorLine":null,"lastOps":2,"successfulEvents":18,"held":[],"lastEvent":{"name":"advance","phase":"up","source":"keyboard:ArrowRight","status":0,"line":null,"ops":2},"lastFailure":null,"memory":{"linearBytes":65536,"reservedEngineBytes":20064,"programReservedBytes":9728,"instructionBytes":16,"handlerBytes":16,"activeIRBytes":480,"activeStateBytes":24,"stagingReservedBytes":9992,"evalStackBytes":512,"pointerTargetBytes":3,"pointerTargets":0},"ready":true,"source":"# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n","appliedSource":"# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n","dirty":false,"running":false,"droppedTicks":0,"diagnostic":"코드를 적용했습니다. 같은 이름의 상태는 유지됩니다."},"mobile":{"viewport":390,"content":390},"downloaded":{"filename":"my-work.pure.json","path":"output/playwright/pure-routing-authoring-draft.json","expectedSource":"# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n"},"errors":[],"screenshots":["output/playwright/pure-routing-authoring-blank.png","output/playwright/pure-routing-authoring-authored.png","output/playwright/pure-routing-authoring-mobile.png"]} +### Ran Playwright code +```js +await (async page => await page.evaluate(() => window.__pureWorkbenchQAEvidence ?? {pending:true}))(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +- Page Title: Pure · 빈 작업물에서 시작하기 +### Events +- New console entries: .playwright-cli/console-2026-09-23T08-31-21-228Z.log#L1 +- Downloading file my-work.pure.json ... +- Downloaded file my-work.pure.json to ".playwright-cli/my-work-pure.json" diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/authoring-run.log b/prototypes/workbench/results/2026-09-23-pointer-routing/authoring-run.log new file mode 100644 index 0000000..b35bee9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/authoring-run.log @@ -0,0 +1,237 @@ +### Ran Playwright code +```js +await (// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-routing-authoring-blank.png', 'output/playwright/pure-routing-authoring-authored.png', 'output/playwright/pure-routing-authoring-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.reload(); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-routing-authoring-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +### Modal state +- ["beforeunload" dialog with message ""]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/authoring.json b/prototypes/workbench/results/2026-09-23-pointer-routing/authoring.json new file mode 100644 index 0000000..ae4aee2 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/authoring.json @@ -0,0 +1,130 @@ +{ + "recordedUtc": "2026-09-23T08:33:23.588Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20064, + "programReservedBytes": 9728, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 480, + "activeStateBytes": 24, + "stagingReservedBytes": 9992, + "evalStackBytes": 512, + "pointerTargetBytes": 3, + "pointerTargets": 0 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "output/playwright/pure-routing-authoring-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "output/playwright/pure-routing-authoring-blank.png", + "output/playwright/pure-routing-authoring-authored.png", + "output/playwright/pure-routing-authoring-mobile.png" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/before/compiler.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/before/compiler.mjs new file mode 100644 index 0000000..7e8bff3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/before/compiler.mjs @@ -0,0 +1,447 @@ +// 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', '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 === 'pointer') { + 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'].includes(edge.value)) fail(edge, 'Expected down or up, move or cancel'); + } + if (this.is('name', 'use')) { + this.take(); const rule = this.expect('name', 'rule name'); this.expect('('); + const item = this.expect('name', 'item name'); this.expect(')'); + declarations.push({kind:'handler',token,event,edge,rule,item}); + } 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: [], 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.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}`); + } + 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; + 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_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; 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 body = {start,length,point}; bodies.set(rule.name.value,body); return body; + } + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + pointHandler = 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); + if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input'); + event = 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), item = 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; + const body = ruleBody(rule); + 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: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/workbench/results/2026-09-23-pointer-routing/before/core.wasm b/prototypes/workbench/results/2026-09-23-pointer-routing/before/core.wasm new file mode 100755 index 0000000..8c7b1c5 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer-routing/before/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/before/core.zig b/prototypes/workbench/results/2026-09-23-pointer-routing/before/core.zig new file mode 100644 index 0000000..eedeeb5 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/before/core.zig @@ -0,0 +1,1230 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { + event: u32 = 0, start: u32 = 0, length: u32 = 0, + parameter_base: u8 = 0, parameter_count: u8 = 0, +}; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +const Workspace = union { staging: Pending, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.program.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.program.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.program.initials[id] = initial; + engine.workspace.staging.program.old_ids[id] = old_id; + engine.workspace.staging.program.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 25) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +export fn bind_handler(handler: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 25) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; +} + +export fn put_restored_value(id: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const pending = &engine.workspace.staging; + if (id >= pending.program.state_count) return stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return stagingFail(.bad_state); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; +} + +export fn commit_program() u32 { + return commit(false); +} + +export fn commit_restored_program() u32 { + return commit(true); +} + +fn commit(restoring: bool) u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const count = engine.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (engine.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return stagingFail(.bad_state); + const code = validate(&engine.workspace.staging.program); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.program.state_count) |i| { + const old_id = engine.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) engine.workspace.staging.values[i] else if (old_id < 0) engine.workspace.staging.program.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging.program; + engine.values = next; + finishStaging(); + if (restoring) { + engine.tick = 0; + engine.successful_events = 0; + } + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + (context.point and event == 0) or (!context.point and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = execute(&pending, h.start, h.length, bound, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 16), layout(3)); + try std.testing.expectEqual(@as(u32, 80), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "shared handler bodies access their own bound state slots in source order" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try emit(24, 0, 0); try emit(0, 0, 1); try emit(3, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(1, 1, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(2, 0, 1)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(f64, 21), get_state(1)); + try std.testing.expectEqual(@as(u32, 4), info(6)); // Three bindings, one body. + try std.testing.expectEqual(@as(u32, 12), info(3)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); +} + +test "failure in a later shared binding rolls back all earlier bindings" { + reset_all(); + try ok(begin_program(4, 1)); + for ([_]f64{ 8, 2, 12, 0 }, 0..) |value, i| try ok(put_state(@intCast(i), value, -1)); + try emit(24, 0, 0); try emit(24, 1, 0); try emit(6, 0, 0); try emit(25, 0, 0); + for (0..2) |i| { + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(@intCast(i), @intCast(i * 2), 2)); + } + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(2)); + try std.testing.expectEqual(@as(u32, 0), info(9)); +} + +test "shared bindings reject missing arguments partial overlaps and invalid slot mappings" { + reset_all(); + try counterProgram(); + const revision = info(4); + for (0..5) |mode| { + try ok(begin_program(1, 1)); try ok(put_state(0, 9, -1)); + try emit(24, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 2)); + switch (mode) { + 0 => {}, // Parameter op without a binding. + 1 => { try ok(bind_handler(0, 0, 1)); try std.testing.expectEqual(public(.invalid), bind_handler(0, 0, 1)); }, + 2 => { try std.testing.expectEqual(public(.bad_state), bind_handler(0, 0, 2)); }, + 3 => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 1, 1)); try ok(bind_handler(1, 0, 1)); + }, + else => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 2)); // Another owner missing its mapping. + }, + } + try std.testing.expect(commit_program() != 0); abort_program(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/before/runtime.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/before/runtime.mjs new file mode 100644 index 0000000..4a9cd14 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/before/runtime.mjs @@ -0,0 +1,328 @@ +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') + 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 = []; + // 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) { + const status = point ? core.dispatch_point(event, 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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), + }, + }; + } + + 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 [index, handler] of next.handlers.entries()) { + checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + if (handler.parameterCount) + checked(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; + } + 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].target) 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].target) 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); + }, + point(name, phase, x, y, source = 'external') { + available(); + const index = inputIndex(name); + if (!program.inputs[index].target) throw new Error('포인터에 연결된 입력이 아닙니다.'); + 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(name); + if (phase === 'down') { + if (point?.source === source) return 0; + if (point) throw new Error('이 입력은 다른 포인터가 사용 중입니다.'); + point = {source, x, y}; + points.set(name, point); + held.set(name, new Set([source])); + } else { + if (!point || point.source !== source) return 0; + point.x = x; point.y = y; + if (phase === 'up' || phase === 'cancel') { points.delete(name); held.delete(name); } + } + 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 [name, point] of [...points]) { + const status = api.point(name, 'cancel', point.x, point.y, point.source); + 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/workbench/results/2026-09-23-pointer-routing/browser-initial-failure.json b/prototypes/workbench/results/2026-09-23-pointer-routing/browser-initial-failure.json new file mode 100644 index 0000000..e0f1d95 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/browser-initial-failure.json @@ -0,0 +1,32 @@ +{ + "failed": "Error: the remaining contact keeps moving only its own target", + "checks": [ + "placement save requires an applied definition", + "one authored pointer input connects all three view targets", + "DOM destinations retain distinct target identities", + "last input identifies the released target within its group", + "three independently authored targets move and selection directs keyboard adjustment", + "placement download is a separate document file", + "saving neither applies nor discards the unfinished draft", + "reset returns to authored initial positions", + "downloaded file restores all states and visible positions", + "the draft remains separate from the restored executable definition", + "restored work starts stopped with no replayed inputs or clock", + "restored selection and bindings remain editable", + "a fresh browser page reconstructs the saved work without old runtime state", + "invalid saved state cannot partially replace code, draft or scene", + "a delayed file read cannot overwrite a newer draft", + "a delayed restore cannot overwrite placement changes made while reading", + "saving while time advances requests a stopped document", + "saving with a held input is refused", + "editing the shared rule changes the selected amber target", + "the same single rule edit also changes the blue target", + "reordering instance declarations preserves every field by qualified name", + "rebuilt parameter maps still send selection input to blue after reorder", + "simultaneous touches share code but retain independent field bindings", + "shared cancel restores each contact to its own start position", + "a second touch on the same target cannot steal ownership", + "releasing green keeps blue captured within the same input" + ], + "errors": [] +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/browser-result.log b/prototypes/workbench/results/2026-09-23-pointer-routing/browser-result.log new file mode 100644 index 0000000..5b01c2e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/browser-result.log @@ -0,0 +1,12 @@ +### Result +{"checks":["placement save requires an applied definition","one authored pointer input connects all three view targets","DOM destinations retain distinct target identities","last input identifies the released target within its group","three independently authored targets move and selection directs keyboard adjustment","placement download is a separate document file","saving neither applies nor discards the unfinished draft","reset returns to authored initial positions","downloaded file restores all states and visible positions","the draft remains separate from the restored executable definition","restored work starts stopped with no replayed inputs or clock","restored selection and bindings remain editable","a fresh browser page reconstructs the saved work without old runtime state","invalid saved state cannot partially replace code, draft or scene","a delayed file read cannot overwrite a newer draft","a delayed restore cannot overwrite placement changes made while reading","saving while time advances requests a stopped document","saving with a held input is refused","editing the shared rule changes the selected amber target","the same single rule edit also changes the blue target","reordering instance declarations preserves every field by qualified name","rebuilt parameter maps still send selection input to blue after reorder","simultaneous touches share code but retain independent field bindings","shared cancel restores each contact to its own start position","a second touch on the same target cannot steal ownership","releasing green keeps blue captured within the same input","the remaining contact keeps moving only its own target","canceling the remaining target preserves the already released position","extra persistence control fits a mobile viewport","no uncaught page errors"],"errors":[],"saved":"output/playwright/pure-routed-layout-saved.json","expected":{"states":{"selected":3,"green.x":150.00000659728084,"green.y":169.9999983506798,"green.ox":9.999986805438311,"green.oy":10.00000164932024,"green.sx":80,"green.sy":120,"green.held":0,"green.id":1,"blue.x":350.00000659728073,"blue.y":209.9999983506798,"blue.ox":9.999980208157467,"blue.oy":10.00000329864045,"blue.sx":280,"blue.sy":160,"blue.held":0,"blue.id":2,"amber.x":509.999947221753,"amber.y":260.0000016493202,"amber.ox":10.000013194561802,"amber.oy":9.999998350679817,"amber.sx":450,"amber.sy":220,"amber.held":0,"amber.id":3},"views":[{"id":"board","type":"box","color":"#233a31","x":20,"y":90,"width":600,"height":240},{"id":"help","type":"text","prefix":"선택한 도형: ","x":25,"y":40,"value":3},{"id":"green_box","type":"box","color":"#c9f08b","x":150.00000659728084,"y":169.9999983506798,"width":56,"height":44},{"id":"blue_box","type":"box","color":"#74c4d4","x":350.00000659728073,"y":209.9999983506798,"width":56,"height":44},{"id":"amber_box","type":"box","color":"#dfb184","x":509.999947221753,"y":260.0000016493202,"width":56,"height":44}]},"source":"# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n","draft":"# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n\nstate unfinished ="} +### Ran Playwright code +```js +await (async page => await page.evaluate(() => window.__pureRoutedInputEvidence ?? {pending:true}))(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +- Page Title: Pure · 빈 작업물에서 시작하기 +### Events +- Downloading file my-layout.pure.json ... +- Downloaded file my-layout.pure.json to ".playwright-cli/my-layout-pure.json" diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/browser-run.log b/prototypes/workbench/results/2026-09-23-pointer-routing/browser-run.log new file mode 100644 index 0000000..85b489e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/browser-run.log @@ -0,0 +1,172 @@ +### Ran Playwright code +```js +await (// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('routed-layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + check(await page.locator('[data-point-input="drag"]').count()===3,'one authored pointer input connects all three view targets'); + check(await page.locator('[data-point-target]').count()===3,'DOM destinations retain distinct target identities'); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + check((await page.locator('#last-event').textContent()).includes('drag.up → amber_box'),'last input identifies the released target within its group'); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-routed-layout-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states['green.x']===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + // One source edit affects every bound target, without editing input adapters. + const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,'); + await page.locator('#source').fill(edited);await page.locator('#apply').click(); + const beforeEdit=await state(); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target'); + await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10); + const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target'); + const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse(); + const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n'); + const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click(); + const afterReorder=await state(); + check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder'); + // Two contacts execute the same body with different bound fields. + await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + const session=await page.context().newCDPSession(page); + try { + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]}); + await page.waitForFunction(()=>{const s=window.pureWorkbench.snapshot().states;return Math.abs(s['green.x']-150)<0.1&&Math.abs(s['blue.x']-350)<0.1;}); + const touch=await state(); + check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + const canceled=await state(); + check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position'); + + // A real second contact on the same view must not steal its owner. + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,{id:2,...await client(100,140)}]}); + check((await state()).held.length===1,'a second touch on the same target cannot steal ownership'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + // End one actual contact and continue the other in the same logical input. + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + const m1={id:1,...await client(160,180)},m2={id:2,...await client(360,220)}; + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[m1,m2]}); + // In this Chromium, touchEnd's supplied point identifies the contact ending. + // touch-reproduction.js records the resulting pointerup and retained blue owner. + await session.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[m1]}); + let partial=await state(); + check(partial.held.length===1&&partial.states['green.held']===0&&partial.states['blue.held']===1,'releasing green keeps blue captured within the same input'); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:2,...await client(400,250)}]}); + await page.waitForFunction(()=>Math.abs(window.pureWorkbench.snapshot().states['blue.x']-390)<0.1); + partial=await state(); + check(Math.abs(partial.states['green.x']-150)<0.1&&Math.abs(partial.states['blue.x']-390)<0.1,'the remaining contact keeps moving only its own target'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + partial=await state(); + check(Math.abs(partial.states['green.x']-150)<0.1&&partial.states['blue.x']===280&&!partial.held.length,'canceling the remaining target preserves the already released position'); + } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();} + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-routed-layout-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-routed-layout-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +### Modal state +- ["confirm" dialog with message "현재 코드와 배치를 파일의 내용으로 복원할까요? 저장하지 않은 작업은 사라집니다."]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/browser.json b/prototypes/workbench/results/2026-09-23-pointer-routing/browser.json new file mode 100644 index 0000000..4e0f11b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/browser.json @@ -0,0 +1,113 @@ +{ + "checks": [ + "placement save requires an applied definition", + "one authored pointer input connects all three view targets", + "DOM destinations retain distinct target identities", + "last input identifies the released target within its group", + "three independently authored targets move and selection directs keyboard adjustment", + "placement download is a separate document file", + "saving neither applies nor discards the unfinished draft", + "reset returns to authored initial positions", + "downloaded file restores all states and visible positions", + "the draft remains separate from the restored executable definition", + "restored work starts stopped with no replayed inputs or clock", + "restored selection and bindings remain editable", + "a fresh browser page reconstructs the saved work without old runtime state", + "invalid saved state cannot partially replace code, draft or scene", + "a delayed file read cannot overwrite a newer draft", + "a delayed restore cannot overwrite placement changes made while reading", + "saving while time advances requests a stopped document", + "saving with a held input is refused", + "editing the shared rule changes the selected amber target", + "the same single rule edit also changes the blue target", + "reordering instance declarations preserves every field by qualified name", + "rebuilt parameter maps still send selection input to blue after reorder", + "simultaneous touches share code but retain independent field bindings", + "shared cancel restores each contact to its own start position", + "a second touch on the same target cannot steal ownership", + "releasing green keeps blue captured within the same input", + "the remaining contact keeps moving only its own target", + "canceling the remaining target preserves the already released position", + "extra persistence control fits a mobile viewport", + "no uncaught page errors" + ], + "errors": [], + "saved": "output/playwright/pure-routed-layout-saved.json", + "expected": { + "states": { + "selected": 3, + "green.x": 150.00000659728084, + "green.y": 169.9999983506798, + "green.ox": 9.999986805438311, + "green.oy": 10.00000164932024, + "green.sx": 80, + "green.sy": 120, + "green.held": 0, + "green.id": 1, + "blue.x": 350.00000659728073, + "blue.y": 209.9999983506798, + "blue.ox": 9.999980208157467, + "blue.oy": 10.00000329864045, + "blue.sx": 280, + "blue.sy": 160, + "blue.held": 0, + "blue.id": 2, + "amber.x": 509.999947221753, + "amber.y": 260.0000016493202, + "amber.ox": 10.000013194561802, + "amber.oy": 9.999998350679817, + "amber.sx": 450, + "amber.sy": 220, + "amber.held": 0, + "amber.id": 3 + }, + "views": [ + { + "id": "board", + "type": "box", + "color": "#233a31", + "x": 20, + "y": 90, + "width": 600, + "height": 240 + }, + { + "id": "help", + "type": "text", + "prefix": "선택한 도형: ", + "x": 25, + "y": 40, + "value": 3 + }, + { + "id": "green_box", + "type": "box", + "color": "#c9f08b", + "x": 150.00000659728084, + "y": 169.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "blue_box", + "type": "box", + "color": "#74c4d4", + "x": 350.00000659728073, + "y": 209.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "amber_box", + "type": "box", + "color": "#dfb184", + "x": 509.999947221753, + "y": 260.0000016493202, + "width": 56, + "height": 44 + } + ] + }, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "draft": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n\nstate unfinished =" +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/build.json b/prototypes/workbench/results/2026-09-23-pointer-routing/build.json new file mode 100644 index 0000000..48bb44e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/build.json @@ -0,0 +1,23 @@ +{ + "builtUtc": "2026-09-23T07:22:21.403663+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "56b8db87bcf7e49ab2b2e8d4166ade6ed04c77571fbb69c60633e76376b46976", + "wasmSha256": "b494269c1332f71de9725a4c7f0b58c074b669403d5d9dc95ddfeb7b16967c0d", + "evaluationSourceSha256": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "wasmFileBytes": 60774, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/35 core.test.blank program, no hidden application behavior, and reset...OK\n2/35 core.test.arbitrary input counter and unused events...OK\n3/35 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/35 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/35 core.test.branch to program end is valid...OK\n6/35 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/35 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/35 core.test.stack underflow overflow and leftover values reject the event...OK\n9/35 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/35 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/35 core.test.capacity boundaries and sticky load errors...OK\n12/35 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/35 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/35 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/35 core.test.binding evaluation is pure on both success and failure...OK\n16/35 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/35 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/35 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/35 core.test.point coordinates are ephemeral event operands and unavailable to pure evaluation...OK\n20/35 core.test.point event ranges reject holes and invalid payloads without committing...OK\n21/35 core.test.point event failure rolls back writes from earlier matching handlers...OK\n22/35 core.test.loading shares storage with records while old events and bindings remain executable...OK\n23/35 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK\n24/35 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK\n25/35 core.test.restored definition and values commit together without changing reset initializers...OK\n26/35 core.test.missing duplicate nonfinite or invalid restored candidates leave active work untouched...OK\n27/35 core.test.empty and full restored programs validate all values and discard old display records...OK\n28/35 core.test.shared handler bodies access their own bound state slots in source order...OK\n29/35 core.test.failure in a later shared binding rolls back all earlier bindings...OK\n30/35 core.test.shared bindings reject missing arguments partial overlaps and invalid slot mappings...OK\n31/35 core.test.one routed handler changes only its event target and rejects missing or foreign routes...OK\n32/35 core.test.routes reject invalid mappings and bindings without replacing active work...OK\n33/35 core.test.routed event rollback includes globals and earlier handlers and survives staged route replacement...OK\n34/35 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n35/35 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 35 tests passed.\n" +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-browser.json b/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-browser.json new file mode 100644 index 0000000..edfd419 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-browser.json @@ -0,0 +1,106 @@ +{ + "checks": [ + "placement save requires an applied definition", + "three independently authored targets move and selection directs keyboard adjustment", + "placement download is a separate document file", + "saving neither applies nor discards the unfinished draft", + "reset returns to authored initial positions", + "downloaded file restores all states and visible positions", + "the draft remains separate from the restored executable definition", + "restored work starts stopped with no replayed inputs or clock", + "restored selection and bindings remain editable", + "a fresh browser page reconstructs the saved work without old runtime state", + "invalid saved state cannot partially replace code, draft or scene", + "a delayed file read cannot overwrite a newer draft", + "a delayed restore cannot overwrite placement changes made while reading", + "saving while time advances requests a stopped document", + "saving with a held input is refused", + "editing the shared rule changes the selected amber target", + "the same single rule edit also changes the blue target", + "reordering instance declarations preserves every field by qualified name", + "rebuilt parameter maps still send selection input to blue after reorder", + "simultaneous touches share code but retain independent field bindings", + "shared cancel restores each contact to its own start position", + "extra persistence control fits a mobile viewport", + "no uncaught page errors" + ], + "errors": [], + "saved": "output/playwright/pure-routing-legacy-saved.json", + "expected": { + "states": { + "selected": 3, + "green.x": 150.00000659728084, + "green.y": 169.9999983506798, + "green.ox": 9.999986805438311, + "green.oy": 10.00000164932024, + "green.sx": 80, + "green.sy": 120, + "green.held": 0, + "green.id": 1, + "blue.x": 350.00000659728073, + "blue.y": 209.9999983506798, + "blue.ox": 9.999980208157467, + "blue.oy": 10.00000329864045, + "blue.sx": 280, + "blue.sy": 160, + "blue.held": 0, + "blue.id": 2, + "amber.x": 509.999947221753, + "amber.y": 260.0000016493202, + "amber.ox": 10.000013194561802, + "amber.oy": 9.999998350679817, + "amber.sx": 450, + "amber.sy": 220, + "amber.held": 0, + "amber.id": 3 + }, + "views": [ + { + "id": "board", + "type": "box", + "color": "#233a31", + "x": 20, + "y": 90, + "width": 600, + "height": 240 + }, + { + "id": "help", + "type": "text", + "prefix": "선택한 도형: ", + "x": 25, + "y": 40, + "value": 3 + }, + { + "id": "green_box", + "type": "box", + "color": "#c9f08b", + "x": 150.00000659728084, + "y": 169.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "blue_box", + "type": "box", + "color": "#74c4d4", + "x": 350.00000659728073, + "y": 209.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "amber_box", + "type": "box", + "color": "#dfb184", + "x": 509.999947221753, + "y": 260.0000016493202, + "width": 56, + "height": 44 + } + ] + }, + "source": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "draft": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n\nstate unfinished =" +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-result.log b/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-result.log new file mode 100644 index 0000000..d1ef7bd --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-result.log @@ -0,0 +1,12 @@ +### Result +{"checks":["placement save requires an applied definition","three independently authored targets move and selection directs keyboard adjustment","placement download is a separate document file","saving neither applies nor discards the unfinished draft","reset returns to authored initial positions","downloaded file restores all states and visible positions","the draft remains separate from the restored executable definition","restored work starts stopped with no replayed inputs or clock","restored selection and bindings remain editable","a fresh browser page reconstructs the saved work without old runtime state","invalid saved state cannot partially replace code, draft or scene","a delayed file read cannot overwrite a newer draft","a delayed restore cannot overwrite placement changes made while reading","saving while time advances requests a stopped document","saving with a held input is refused","editing the shared rule changes the selected amber target","the same single rule edit also changes the blue target","reordering instance declarations preserves every field by qualified name","rebuilt parameter maps still send selection input to blue after reorder","simultaneous touches share code but retain independent field bindings","shared cancel restores each contact to its own start position","extra persistence control fits a mobile viewport","no uncaught page errors"],"errors":[],"saved":"output/playwright/pure-routing-legacy-saved.json","expected":{"states":{"selected":3,"green.x":150.00000659728084,"green.y":169.9999983506798,"green.ox":9.999986805438311,"green.oy":10.00000164932024,"green.sx":80,"green.sy":120,"green.held":0,"green.id":1,"blue.x":350.00000659728073,"blue.y":209.9999983506798,"blue.ox":9.999980208157467,"blue.oy":10.00000329864045,"blue.sx":280,"blue.sy":160,"blue.held":0,"blue.id":2,"amber.x":509.999947221753,"amber.y":260.0000016493202,"amber.ox":10.000013194561802,"amber.oy":9.999998350679817,"amber.sx":450,"amber.sy":220,"amber.held":0,"amber.id":3},"views":[{"id":"board","type":"box","color":"#233a31","x":20,"y":90,"width":600,"height":240},{"id":"help","type":"text","prefix":"선택한 도형: ","x":25,"y":40,"value":3},{"id":"green_box","type":"box","color":"#c9f08b","x":150.00000659728084,"y":169.9999983506798,"width":56,"height":44},{"id":"blue_box","type":"box","color":"#74c4d4","x":350.00000659728073,"y":209.9999983506798,"width":56,"height":44},{"id":"amber_box","type":"box","color":"#dfb184","x":509.999947221753,"y":260.0000016493202,"width":56,"height":44}]},"source":"# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n","draft":"# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n\nstate unfinished ="} +### Ran Playwright code +```js +await (async page => await page.evaluate(() => window.__pureSharedLayoutEvidence ?? {pending:true}))(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +- Page Title: Pure · 빈 작업물에서 시작하기 +### Events +- Downloading file my-layout.pure.json ... +- Downloaded file my-layout.pure.json to ".playwright-cli/my-layout-pure.json" diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-run.log b/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-run.log new file mode 100644 index 0000000..f997728 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/legacy-run.log @@ -0,0 +1,146 @@ +### Ran Playwright code +```js +await (// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.reload(); + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('shared-layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-routing-legacy-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states['green.x']===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + // One source edit affects every bound target, without editing input adapters. + const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,'); + await page.locator('#source').fill(edited);await page.locator('#apply').click(); + const beforeEdit=await state(); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target'); + await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10); + const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target'); + const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse(); + const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n'); + const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click(); + const afterReorder=await state(); + check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder'); + // Two contacts execute the same body with different bound fields. + await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + const session=await page.context().newCDPSession(page); + try { + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]}); + const touch=await state(); + check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + const canceled=await state(); + check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position'); + } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();} + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-routing-legacy-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-routing-legacy-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +### Modal state +- ["beforeunload" dialog with message ""]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/manifest.json b/prototypes/workbench/results/2026-09-23-pointer-routing/manifest.json new file mode 100644 index 0000000..7673700 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/manifest.json @@ -0,0 +1,47 @@ +{ + "README.md": "62f6481f9b60250875595e3004258fec360212215b8d392c88dfe2561e3346c2", + "authoring-result.log": "5a8a821aa3cbdaf8ea3061bb3187fbc16a147738f577dca8dd10c98cef59ea98", + "authoring-run.log": "08386adebb2e042f9a3a56f32998fb292df07d3f8cef471ac8ee6491027a17c6", + "authoring.json": "97711e1382629589de96c163770cf5c162d2c5a3c7c8e0bf1256daf3c2ae6bd0", + "before/compiler.mjs": "033227dcee1ecfe79aded2a3b5dd5f28b896377e27671347767ef99e2eb0cf56", + "before/core.wasm": "2b1b6d2bbf9005f5c13275d6013cc4c21c155c7474d334c470fcb11094818b52", + "before/core.zig": "630aee1cc20b5803657f75e541cb6b706e2b99dd00218ac71afd78b82259621b", + "before/runtime.mjs": "d4d8fa382f501e1cdb293a1079e5dcf3cf4e65ccfc860f800b51e32cc098cf51", + "browser-initial-failure.json": "1e8d4c0e447640c1b6c521d10f859e8a32bc9bebedce52dfdc30ae961c315be5", + "browser-result.log": "537e5bac498d74c447592e2fe36f80c5640aedee54420e37e965a61700518abb", + "browser-run.log": "4634dde958e1cb0e86a06272a40ebc03830d711569dfe04c3c2eeb005dd27f3d", + "browser.json": "89aa8e088c523b3509b64064a887e478699ccfb88eb3f01cb8637ba476d6cdd6", + "build.json": "257ec939e124b7ab56b5f74d35dd84000b1a0600e4694d9c2f0c178ee2e56530", + "legacy-browser.json": "a661498c06a876cc384d02cb2bba3b624f47d6b4441a8bc63cd29d3c7a496f48", + "legacy-result.log": "2f5610e40f4abbd37e63848c1b8a4a46fff988894cd15a0b35a02417d55390fb", + "legacy-run.log": "0f1b6298cb577c958238ab8b095663bed6d24de539f12042e713e35d85b0a030", + "node-tests.log": "b99e5cbe8f90f8af63557cd624da1407142be305656d7fcf180c9e74896177b8", + "pure-routed-layout-desktop.png": "a8e92dccce424d9888ad7e72bd004b68cb5d09a9bc39bb9c90be96b4b2f71ff7", + "pure-routed-layout-mobile.png": "1bec982d9c6a85f80fe07aa4e4d2e9bfe5eec13f5e379c34c6275994520c0c5b", + "pure-routed-layout-saved.json": "c229dfac9fb29908842a331c6db7d0247f90d48c7ff355342bf711e9c448661c", + "qa-authoring.js": "be0a5fef207cd4fa744228da3cf33d672ca0f69d09b8f522850a8acf1acafe2e", + "qa-legacy-pointer.js": "ecea8183330354da9da20eaac8bca6660ab0d43e2d50b240ea0b098c871c73e5", + "representation.json": "b307c4aba3ff4c31e94ffb04bf2f08ab8fe0454c4fd34e55b77e72ba3673e9f7", + "sources/CONTRACT.md": "58808e2d310bbf1103d42e731e63c26a1f1baf580e3807945bda2eafdae55178", + "sources/cli.mjs": "d5be26735d41e1ac023802e21298f55f6dfc39d011ea810aee1c3bc2d17d11f4", + "sources/core.zig": "56b8db87bcf7e49ab2b2e8d4166ade6ed04c77571fbb69c60633e76376b46976", + "sources/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "sources/examples/routed-layout.pure": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "sources/presentation.test.mjs": "1596733634b31125d81a324ca633c51b90ce848486f7909ecc0619f949ead4d1", + "sources/qa-routed-input.js": "9a27e06027ad12f4a205893eca7dce5d82c5b8011da8692beed5757039c66573", + "sources/routing.test.mjs": "37f6705a496599778eed306e554147b3d9e408447043f4e75abb7225e548999a", + "sources/web/app.js": "1749df4fd0817fa6e503e9474ff5ce59a2a30f5001cf710e12499619ffa86e95", + "sources/web/compiler.mjs": "a9d5d1a863519e86c37df24385ae9cf605384bea5e6b4298f6e67177e6a2a811", + "sources/web/core.wasm": "b494269c1332f71de9725a4c7f0b58c074b669403d5d9dc95ddfeb7b16967c0d", + "sources/web/index.html": "3c312e7b2efcc4f39f0682305e423a5dd10ae77457e988771f7e6e18cba4e1ca", + "sources/web/output.mjs": "fed2d41d4027c4b388174bfcb20e852e9443073afb8b508919371b7da95d6f8b", + "sources/web/runtime.mjs": "cb22adac57452453fe3add6cd885b5636d49fc946b1cd54e9e8ee502d167e7e2", + "sources/web/scene-input.mjs": "88d93c3898ad8ae3236f0ebaede3a51ecb5dd8702fbf7fe9a6ae45f000a40306", + "sources/workspace.test.mjs": "053c3883d69a528c244d5298bd67b95642a5d6e8ceceea3d189dce6c9747f6ac", + "touch-evidence.json": "0adb9592f691461f9ec830353536b42d86ed6d33c5117917bebee5b231bde1fc", + "touch-evidence.log": "e6d3b52b2531334e33032b6705ae1dbe11b35deadeb23c6c8d59421ce3d643a5", + "touch-reproduction.js": "445d04eaba070d885aad602ffc7f7e60a4e782e7d12a08efe7142a1c1ae8123e", + "touch-reproduction.log": "7612d377829dd98edc96dc114e09b2a7c8bb7da5ff1be0cae96732994bc68a09", + "touch-run.log": "6e0082768745024dfee33b613c9f120d2f165a57e76082245a1ed628d33df40c", + "verification.json": "0e756fe566d8bbc5fa3c837020ea9000fc1a231606226c669e37af7ea0583b60" +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/node-tests.log b/prototypes/workbench/results/2026-09-23-pointer-routing/node-tests.log new file mode 100644 index 0000000..fb311d7 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/node-tests.log @@ -0,0 +1,91 @@ +✔ reference begins with the authored states and matching scene (2.771357ms) +✔ press, hold, release, and frame order produce exact expected values (0.650246ms) +✔ charge clamps at one and explicit floor restores rest (1.551179ms) +✔ repeat and concurrent physical sources produce only logical edges (0.734062ms) +✔ airborne presses cannot charge or cause another jump (0.371855ms) +✔ releaseAll emits release and reset clears latches without a jump (0.55832ms) +✔ unknown inputs and invalid frame counts reject without mutation (1.155825ms) +✔ terminal starts empty and produces the same charge trace as the shared runtime (202.764904ms) +✔ invalid terminal commands do not alter state and fail piped execution (71.118981ms) +✔ terminal replays authored pointer movement and cancellation through the shared runtime (132.226616ms) +✔ file load preserves by name, rejected load preserves active work, and save never overwrites (76.761233ms) +✔ terminal exposes arithmetic rollback and retains failure through matching release (89.163691ms) +✔ terminal configures and discards evaluation records and recovers from a display limit (68.518607ms) +✔ terminal saves and restores placement independently of reset and preserves a browser draft (213.096866ms) +✔ invalid restoration leaves terminal code and state unchanged (51.790723ms) +✔ a blank document is an empty program with no implied behavior (6.111015ms) +✔ forward references and separate state/input namespaces preserve declaration IDs (3.324695ms) +✔ ordered assignments, repeated handlers, and condition evaluation once (1.55018ms) +✔ nested branches use absolute forward targets and skip correct bodies (1.790478ms) +✔ operator precedence, associativity, unary and builtin function lowering (2.619967ms) +✔ multiline parentheses, scientific literals, comments, and string escapes (1.105621ms) +✔ all instruction fields are numeric and carry original source line (0.554227ms) +✔ contextual declaration words remain valid names outside reserved builtins (0.491449ms) +✔ unknown and reserved symbols have useful one-based locations (2.928562ms) +✔ duplicates, unsupported keys, initializers, and invalid syntax are rejected (2.478243ms) +✔ document, declaration, instruction, and nesting limits reject overflow (9.074281ms) +✔ every supported input maps to a bounded ordered event ID (0.836026ms) +✔ optional examples compile and charge behavior is entirely authored (30.732504ms) +✔ cache reuse, unrelated changes and discarded records preserve authored values (27.598773ms) +✔ committed changes only: failed events and failed edits preserve reusable output (6.144201ms) +✔ program edits, state reordering and reset cannot reuse old definitions (6.137257ms) +✔ finite float semantics include signed zero, high state bit and arithmetic errors (8.245478ms) +✔ one budget covers the whole scene; failures publish no partial scene or state rollback (3.888533ms) +✔ small caches, no history and every small work limit give exact results or explicit failure (44.507549ms) +✔ invalid settings and forget names preserve policy and state (4.394927ms) +✔ pointer declarations resolve forward targets and reject ambiguous or contextless inputs (18.675568ms) +✔ authored drag keeps grab offset and commits the displayed position even if release coordinates differ (23.653444ms) +✔ ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input (9.043948ms) +✔ invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event (6.245165ms) +✔ independent targets share the event pipeline and publication remains non-reentrant (7.073228ms) +✔ failing down, up and cancel never prevent terminal ownership cleanup (5.839162ms) +✔ direct publication matches archived snapshots and work counts through authored changes (86.430631ms) +✔ whole-scene budget failure never publishes partial bindings and recovers exactly (13.208058ms) +✔ latched event rollback, nonfinite bindings, reset and signed zero keep their semantics (12.962871ms) +✔ publication rejects reentrant runtime changes and reads before they affect the scene (7.829326ms) +✔ invalid or throwing output adapters do not strand the runtime or alter authored state (2.548948ms) +✔ multiple authored placements and a separate invalid draft survive a fresh runtime restore (33.184081ms) +✔ malformed files reject before changing active code, values, diagnostics or warm display records (17.820004ms) +✔ restoration matches names rather than ordering and never reuses stale cached values (6.060256ms) +✔ finite float values including negative zero round trip without changing authored reset values (2.317002ms) +✔ held and failed interactions cannot be saved and restoration cannot steal an active input (7.353748ms) +✔ empty and full documents restore without imposing a new source size or state count (8.226719ms) +✔ document APIs respect the output callback boundary and export independent values (2.919229ms) +✔ one input and one handler serve up to 24 static targets without instruction or handler expansion (40.29442ms) +✔ routes validate forward references, exact types, unique views/items and current input target (4.661207ms) +✔ grouped layout matches explicit bindings through concurrent capture, partial cancel, keyboard and restore (22.094263ms) +✔ capture remains bound to its target and releaseAll cancels each target exactly once (3.617846ms) +✔ reorder and remove/readd rebuild routes by names without persisting physical indices (5.608048ms) +✔ routed writes invalidate cached displays and failed handlers rollback without losing another target owner (4.362797ms) +✔ CLI sends grouped pointer events and rejects an unknown target (160.551933ms) +✔ a route from a different input group cannot be delivered through the named group (3.526419ms) +✔ binding more objects shares one VM body instead of expanding its instructions (9.978419ms) +✔ records and rules resolve forward names and reject invalid types fields and bindings (7.630825ms) +✔ shared layout matches the existing authored layout across independent and simultaneous gestures (35.957197ms) +✔ field names retain identity across schema and instance reorder, removal and restoration (10.715777ms) +✔ bindings share one event transaction including global effects and multiple uses of one item (3.94759ms) +✔ bound writes invalidate affected display caches and failed edits preserve active bindings (5.036266ms) +✔ generated qualified names cannot make a valid exported document unreadable by its own loader (13.463153ms) +✔ blank startup and applied empty source contain no application objects or behavior (20.961254ms) +✔ author can add unrelated states inputs views and behavior entirely through source (9.527507ms) +✔ keyboard and pointer sources aggregate first down and last up; repeats are ignored (4.848931ms) +✔ held input rejects apply without changing applied source, metadata, or values (4.100868ms) +✔ invalid syntax, unknown references and capacity overflow preserve the previous program (4.786707ms) +✔ state preservation follows names across reorder/add/remove; reset uses new initializers (3.01036ms) +✔ callers cannot mutate active metadata and corrupt name-based state preservation (1.409808ms) +✔ conditions execute once, and assignments and multiple handlers follow source order (1.689163ms) +✔ a failed event rolls back all handlers and keeps its source diagnostic through release (2.460484ms) +✔ reset clears held inputs and latched errors; invalid frame counts have no effects (2.042277ms) +✔ render evaluation is pure and invalid binding is distinct from an event rollback (9.750676ms) +✔ independent adapters replay the same author program without sharing live state (4.353464ms) +✔ Wasm has no imports and reports fixed memory and exact active IR/state storage (14.504051ms) +✔ Wasm executes old events and output during staged replacement without corrupting either phase (12.059677ms) +✔ workspace accounting reports shared payload and safety tag without double-counting (5.505491ms) +ℹ tests 83 +ℹ suites 0 +ℹ pass 83 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 1006.837988 diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-desktop.png b/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-desktop.png new file mode 100644 index 0000000..85cf74d Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-desktop.png differ diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-mobile.png b/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-mobile.png new file mode 100644 index 0000000..8d24e2a Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-mobile.png differ diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-saved.json b/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-saved.json new file mode 100644 index 0000000..20d88d4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/pure-routed-layout-saved.json @@ -0,0 +1,108 @@ +{ + "format": "pure-workbench", + "version": 2, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "draft": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n\nstate unfinished =", + "states": [ + { + "name": "selected", + "value": 3 + }, + { + "name": "green.x", + "value": 150.00000659728084 + }, + { + "name": "green.y", + "value": 169.9999983506798 + }, + { + "name": "green.ox", + "value": 9.999986805438311 + }, + { + "name": "green.oy", + "value": 10.00000164932024 + }, + { + "name": "green.sx", + "value": 80 + }, + { + "name": "green.sy", + "value": 120 + }, + { + "name": "green.held", + "value": 0 + }, + { + "name": "green.id", + "value": 1 + }, + { + "name": "blue.x", + "value": 350.00000659728073 + }, + { + "name": "blue.y", + "value": 209.9999983506798 + }, + { + "name": "blue.ox", + "value": 9.999980208157467 + }, + { + "name": "blue.oy", + "value": 10.00000329864045 + }, + { + "name": "blue.sx", + "value": 280 + }, + { + "name": "blue.sy", + "value": 160 + }, + { + "name": "blue.held", + "value": 0 + }, + { + "name": "blue.id", + "value": 2 + }, + { + "name": "amber.x", + "value": 509.999947221753 + }, + { + "name": "amber.y", + "value": 260.0000016493202 + }, + { + "name": "amber.ox", + "value": 10.000013194561802 + }, + { + "name": "amber.oy", + "value": 9.999998350679817 + }, + { + "name": "amber.sx", + "value": 450 + }, + { + "name": "amber.sy", + "value": 220 + }, + { + "name": "amber.held", + "value": 0 + }, + { + "name": "amber.id", + "value": 3 + } + ] +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/qa-authoring.js b/prototypes/workbench/results/2026-09-23-pointer-routing/qa-authoring.js new file mode 100644 index 0000000..ba4906f --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/qa-authoring.js @@ -0,0 +1,228 @@ +// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-routing-authoring-blank.png', 'output/playwright/pure-routing-authoring-authored.png', 'output/playwright/pure-routing-authoring-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-routing-authoring-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/qa-legacy-pointer.js b/prototypes/workbench/results/2026-09-23-pointer-routing/qa-legacy-pointer.js new file mode 100644 index 0000000..5256227 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/qa-legacy-pointer.js @@ -0,0 +1,138 @@ +// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.reload(); + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('shared-layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-routing-legacy-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states['green.x']===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + // One source edit affects every bound target, without editing input adapters. + const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,'); + await page.locator('#source').fill(edited);await page.locator('#apply').click(); + const beforeEdit=await state(); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target'); + await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10); + const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target'); + const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse(); + const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n'); + const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click(); + const afterReorder=await state(); + check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder'); + // Two contacts execute the same body with different bound fields. + await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + const session=await page.context().newCDPSession(page); + try { + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]}); + const touch=await state(); + check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + const canceled=await state(); + check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position'); + } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();} + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-routing-legacy-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-routing-legacy-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/representation.json b/prototypes/workbench/results/2026-09-23-pointer-routing/representation.json new file mode 100644 index 0000000..1bdce06 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/representation.json @@ -0,0 +1,73 @@ +{ + "purpose": "Same final core, compiled storage accounting; no performance benchmark", + "cases": { + "shared-layout": { + "metrics": { + "sourceBytes": 2271, + "sourceLines": 76, + "states": 25, + "inputs": 7, + "views": 5, + "handlers": 24, + "instructions": 117, + "maxStack": 3 + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20064, + "programReservedBytes": 9728, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 2256, + "activeStateBytes": 200, + "stagingReservedBytes": 9992, + "evalStackBytes": 512, + "pointerTargetBytes": 3, + "pointerTargets": 0 + }, + "routes": [] + }, + "routed-layout": { + "metrics": { + "sourceBytes": 1963, + "sourceLines": 71, + "states": 25, + "inputs": 5, + "views": 5, + "handlers": 16, + "instructions": 117, + "maxStack": 3 + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20064, + "programReservedBytes": 9728, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 2137, + "activeStateBytes": 200, + "stagingReservedBytes": 9992, + "evalStackBytes": 512, + "pointerTargetBytes": 3, + "pointerTargets": 3 + }, + "routes": [ + { + "input": 4, + "base": 1, + "count": 8 + }, + { + "input": 4, + "base": 9, + "count": 8 + }, + { + "input": 4, + "base": 17, + "count": 8 + } + ] + } + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/CONTRACT.md b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/CONTRACT.md new file mode 100644 index 0000000..91e9f94 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/CONTRACT.md @@ -0,0 +1,496 @@ +# Workbench experiment contract + +An author starts with empty source. There is no built-in player, physics, jump, +score, or timer behavior. All behavior is declared by the author. This is a +bounded scalar interaction language experiment, not a general language claim. + +## Source language + +```text +state count = 0 +input tap = Space +box tile(80, 150, 60, 60, "#356548") +circle dot(200, 180, 20, "#c78638") +text label(80, 100, count, "Count: ") +on tap.down { + count = count + 1 +} +on frame { + if count > 20 { + count = 0 + } +} +``` + +Declarations and statements end at a newline or semicolon; `#` comments outside +strings. ASCII identifiers, finite numeric literals, state initializers must be +numeric constants (optional unary sign). Input key codes accepted: +Space, Enter, ArrowLeft/Right/Up/Down, KeyA..KeyZ, Digit0..Digit9. Input names +must be unique, key codes unique, names cannot shadow `frame`, `dt`, `min`, `max`, +`clamp`, `event_x`, `event_y`. State and input names use separate namespaces, entity IDs unique. + +Expressions: parentheses, state references, dt (seconds on frame, zero on input), +unary - and !, + - * /, < <= > >= == !=, && ||, min(a,b), max(a,b), clamp(x,lo,hi). +Logical operators evaluate both sides. Division by zero/non-finite results fail +the whole event without committing state changes. No loops, recursion or eval. +An if without else executes its condition once; assignment statements are ordered. +Multiple handlers for the same event execute in source order within one atomic event. +`dt` is allowed in handlers, not view bindings. A frame always uses 1/60 seconds. + +box: x,y,width,height expressions + literal hex color; circle: x,y,radius + color; +text: x,y,value expression + optional literal prefix. Values are world units in a +640x360 view. No implicit clipping/collision/physics. Render expressions are pure. +Unknown identifiers and invalid syntax include 1-based line+column diagnostics. + +Initial scene is empty. Optional examples are explicit insertions, never auto-run. +Successful apply preserves scalar states by exact name; new names use initializers, +removed names disappear. Apply/reset are separate. Failed parse/load changes nothing. +Apply is refused while any input is held; release first. This avoids inventing +release events across changed mappings. Focus loss sends .up to ordinary buttons +and .cancel to pointer inputs. + +## Compiler interface (web/compiler.mjs, browser and Node) + +`compile(source)` returns: +``` +{ + states:[{name,initial,line}], inputs:[{name,key,line} | {name,target,line} | {name,type,routes,line}], + instructions:[{op,arg,value,line}], handlers:[{event,start,length,line}], + views:[{id,type,line,color?,prefix?,bindings:{x:{start,length},...}}], + pointerTargets:[{input,base,count}], metrics:{...} +} +``` +Bindings: box x/y/width/height; circle x/y/radius; text x/y/value. +Compiler throws an Error with .line and .column. Empty source is a valid empty program. +Handlers and binding expressions all occupy instructions (maximum 512 total), but +only handlers execute on events; bindings are evaluated on demand by VM eval_at. +Limits: 32 states, 8 inputs, 24 views, 64 handlers, 512 instructions, stack 64, +source 32768 UTF-8 bytes. State IDs follow declaration order. Input event IDs: +frame=0; input i down=1+2*i, up=2+2*i, move=17+i, cancel=25+i. +Move/cancel handlers require a pointer input in source. States may be referenced before declaration; +symbols must be resolved after gathering top-level declarations. `metrics` can +record source bytes/lines and instruction/handler/view/state counts. + +## VM opcodes + +| op | operation | +|---:|---| +|0| push constant `value` | +|1| push state `arg` | +|2| push dt | +|3..6| add, subtract, multiply, divide | +|7| negate | +|8..13| less, less-equal, greater, greater-equal, equal, not-equal | +|14..16| AND, OR, NOT (nonzero true) | +|17..19| min, max, clamp (push x,lo,hi; invalid lo>hi fails) | +|20| store state `arg` (pop) | +|21| jump-if-zero to absolute instruction index `arg` (pop), forward only | +|22,23| push current pointer event x,y; fail without point context | +|24,25| load/store field at handler parameter_base + arg; handler-bound only | + +Rules start with an empty stack and must end with empty stack. Bindings must finish +with exactly one value and cannot STORE, branch, or read dt/event coordinates. Branch targets may be handler end. + +## Zig exported ABI + +`reset_all()` clears programs and state. `begin_program(state_count,input_count)` +starts staging, returns0 success; `put_state(id,initial,old_id)` old_id=-1 means init; +`put_instruction(op,arg,value)` append; `put_handler(event,start,length)` append; +`commit_program()` all0 success otherwise error code, atomic; candidate is separate +from the active program, but shares its workspace with optional evaluation records. +Preserved IDs read active values only at successful commit. Validate limits, +indices, finite values and forward branch ranges; runtime stack errors are checked. +`reset_state()` restores active initializers, resets tick/event diagnostics. +`dispatch(event,dt)` accepts0..16 and finite dt; event0 dt must equal1/60, input dt0; +returns0 success, other code failure with no state mutation. Tick increments for +successful frame; one event runs all matching handlers in source order. +`dispatch_point(event,x,y)` accepts existing input down/up IDs plus move/cancel IDs +within input_count, never frame0. Coordinates must be finite; dt is implicitly0. +The core only knows input count, not host device kinds. The compiler and host enforce +pointer ownership and device mapping. Calling legacy dispatch on a coordinate-reading +handler fails with bad_event9. Point operands are ephemeral execution context, not +extra persistent state slots. Both dispatch paths use the same atomic commit/history. +The host requires this new export; older Wasm must be rebuilt. +`eval_at(start,length)` returns f64 or NaN on invalid/pure-expression failure; +must not mutate state or event diagnostics. `get_state(id)` returns f64 orNaN. +`info(id)` u32: 0 tick,1 last_error,2 error_pc (0xffffffff none),3 last_executed_ops, +4 revision,5 state_count,6 instruction_count,7 handler_count,8 input_count, +9 successful_event_count. `layout(id)` u32: 0 Engine reserved @sizeOf, +1 Program reserved @sizeOf,2 Instruction @sizeOf,3 Handler @sizeOf, +4 active used IR bytes (instructions+handlers+pointer targets),5 active state-value bytes, +6 staging reserved bytes (subset of Engine),7 evaluation stack capacity bytes,8 pointer target entry bytes,9 active pointer target count. +`error_message` belongs to host map; core error codes documented in core.zig. +No allocation or imported runtime in Wasm. Fixed64KiB linear memory target with +16KiB stack; increase only if actual linker evidence requires and document it. + +## Host interface (web/runtime.mjs) + +`await createRuntime(wasmBytes?, evaluationOptions?)` fetches core.wasm in browser if omitted. +Returns object with apply(source), reset(), frame(count=1), down(name,source), +up(name,source), point(name,phase,x,y,source,target?), releaseAll(), snapshot(), exportProject(), currentProgram. +Multiple sources per input share one logical held state; down emits only first +source, up only last. Source strings identify keyboard vs onscreen pointer. +`apply` returns compiled program or throws with diagnostic. It must fully validate +in host and stage+commit in core. It preserves by name. `reset` keeps active program. +`snapshot` returns {states:{name:value}, views:[{id,type,x,y,width?,height?,radius?, +value?,prefix?,color?}], tick,revision,lastError,errorLine,lastOps,memory:{...},held:[]} +`currentProgram` is the latest successfully applied compiled program or null. +The program and all nested metadata are frozen so readers cannot corrupt state +name mappings or binding ranges. `lastFailure` records the last failed event; +its `lastError`/line remain visible through successful release or focus cleanup +until successful apply or explicit reset. `lastEvent` still describes the latest +event. A view evaluation failure uses host error100 and does not roll back events. +UI draft and applied code are distinct. Browser/CLI share compiler/runtime/wasm; +instances are separate, not a live collaborative session. No arbitrary JS evaluation. + +## Bounded display evaluation (2026-09-22) + +The existing event and float semantics above are unchanged. The forgetful C +prototype's contract is adapted into `evaluation.zig`; its uint64 DAG is not +used as a substitute for this VM's float instructions. + +Each snapshot starts one display budget, shared by all bindings. Default options +are `{cacheEntries:0, historyEntries:8, workLimit:2048}`. A unit is a binding +request, a candidate-cache probe, a change-record probe, or an executed VM +instruction. State extraction, host object creation, event dispatch and DOM work +are outside this budget. It is not a time limit or a bound on whole-app work. +For the current compiler limits, 96 bindings * 10 units plus 512 instructions +is at most 1,472 units; the default admits even an uncached valid scene with +cache lookup/validation enabled. Malformed bindings may fail sooner. + +`configureEvaluation({cacheEntries?,historyEntries?,workLimit?})` accepts integers +0..32, 0..8, and 0..4294967295 respectively. Missing fields retain prior settings. +Valid configuration clears reuse records; invalid configuration changes nothing. +`forgetEvaluation('values'|'history'|'all')` drops optional records without +changing authored state, event diagnostics or program identity. +`evaluationStats()` returns the options plus work, instructions, cacheHits for +the last snapshot, and reservedBytes for the complete fixed evaluation structure. +Statistics are separate from snapshots so repeated observations remain equivalent. + +Memo entries contain the value, instruction range, input dependency bit mask +and validated revision. Direct mapping uses one candidate probe, with collisions +causing recomputation. Only successful computations are cached. Constant results +need no input-change history. All other reuse requires a complete retained +history since validation and no intersection with changed states. Dependencies +are collected from actual VM LOADs, not trusted compiler-supplied annotations. +History loss causes recomputation. All history and memo storage is fixed size; +setting capacities to zero does not return reserved linear memory. + +Only bitwise-different committed states advance change history (including signed +zero). Failed events and net-zero updates do not. Successful apply and reset +discard all reuse records. Parse/host validation failure before staging preserves +records. Beginning core staging discards them, including if that load is later +rejected or aborted. Epoch wrap also clears records. No inputs or effectful event +handlers are replayed to rebuild a binding. + +Budget exhaustion returns an empty views array for the entire snapshot, with +displayError `{code:101,line,message}`. Successfully committed event state remains. +There is no automatic retry or silent return of an old scene. Raising the budget +allows a fresh attempt. Arithmetic/size errors retain the earlier per-view error100 +behavior. `displayError` is null on display success; a latched execution error +still takes precedence in lastError/errorMessage, while displayError independently +reports display failure. Partial successful binding memos may survive a failed +snapshot but no partial scene is published. + +New core exports: `configure_evaluation(cache,history)` returns 0 or capacity1; +`forget_evaluation(flags)` bits 1=values, 2=history; `begin_evaluation(limit)`; +`eval_bounded(start,length)` returns f64 or NaN and consumes that shared budget. +`evaluation_info(id)` returns 0 status (10=work limit), 1 work, 2 instructions, +3 cache hits, 4 cache capacity, 5 history capacity, 6 reserved bytes. These +diagnostics do not replace event info. The unbounded pure `eval_at` remains for +reference checks and is not the host snapshot path. + +## Shared load/reuse workspace (2026-09-23) + +The pending definition/restore values and evaluation Tables occupy one untagged union. The active +Program, current state and evaluation control fields remain separate. All core +calls are single-threaded and synchronous; no callback can reenter a phase change. +No pointer to the reused tables is retained between calls. + +`begin_program` switches the workspace to staging and drops optional records. +Until commit/abort, old events and bindings still run against active. Binding +evaluation bypasses reuse and recomputes within its budget; cache configuration, +forget and reset must not write the staging payload. Old-id state preservation +reads the current values at successful commit, including events/reset during load. + +Successful commit publishes the new Program and then initializes empty Tables in +the shared workspace. `abort_program()` discards a pending definition and restores +empty Tables without changing active program/state/event diagnostics. Inactive +abort is a no-op. A failed commit leaves staging open for explicit abort or the +next begin. An invalid begin closes any previous pending load. Host apply aborts +after a core loading failure. Lost reuse may make a previously warm tiny budget +insufficient; neither a preserved cache nor completion at that budget is promised. + +`evaluation_info` additionally reports 7 dedicated control bytes, 8 shared Tables +bytes, 9 reuse available (false during load), and 10 union safety-tag overhead. +The host exposes these as dedicatedBytes, sharedBytes, reuseAvailable and +workspaceTagBytes. reservedBytes is control + Tables capacity, not incremental +Engine storage. The Tables capacity overlaps stagingReservedBytes and must not +be added to it. Before document restoration was added, the Wasm control was 48 B, shared Tables +1,152 B, union overhead 8 B. That Engine was 19,128 B, only 56 B larger than the +19,072 B pre-evaluation core; linear memory remains 65,536 B. No OS pages are +returned and this is not a measurement of browser/process memory. + +## Direct output publication (2026-09-23) + +`present(target)` evaluates the same bindings, in the same order and shared +budget as `snapshot()`, then calls a synchronous trusted output adapter: + +- `begin(program)`: the frozen current definition, or null at startup. +- `state(index, value)`: each current authored state, without a host state map. +- `view(index, valid)`: every declared view's publication status. +- `value(viewIndex, bindingName, value)`: each binding of a valid view only. +- `end(visibleCount)`: number of valid views. + +It returns the snapshot's diagnostics, held inputs, event information and memory +report, without `states` or `views`. Evaluation finishes before the first adapter +callback. On budget error101 every view is invalid and no binding values are +published; committed state values are still published. Error100 invalidates only +the affected views, as before. No stale value is silently substituted. + +Runtime mutations, `snapshot()` and nested `present()` are rejected during +callbacks. Read-only program/export/statistics access remains available. Adapters +must be synchronous and must not throw. If one does throw, the exception +propagates and the runtime unlocks in a finally block; arbitrary external adapter +effects cannot be rolled back. A later call republishes every value. This is not +a transaction across arbitrary output devices or an untrusted plugin boundary. + +Evaluation uses a private reusable Float64Array(96) and Uint8Array(24), allocated +on first snapshot/presentation. Their payload totals 792 host bytes, outside the +64 KiB Wasm memory. Typed-array object overhead, binding-entry arrays built at +apply, and DOM destination references are additional host memory. The numeric +buffer is a publication barrier, not another authoritative state or public +mutable scene. `snapshot()` remains an independent object representation for +CLI/export/debugging, built only when explicitly requested. + +The browser calls `present()` for normal refreshes. Its output adapter binds DOM +destinations once per successful definition change and compares values with the +existing DOM before writing. Input refreshes reuse scene, state and cost elements. +Invalid views remain allocated but are hidden using SVG `display="none"`; recovery +reveals them. A definition change can replace all scene/state destinations. This +does not skip unchanged binding evaluation, eliminate all host allocations, or +provide measured process-memory/paint-latency savings. + +## Authored direct manipulation (2026-09-23) + +`input drag = pointer(tile)` binds a logical input to a declared view, including a +forward declaration. One input per target; it shares the existing 8-input limit +with keyboard inputs. `on drag.down/move/up/cancel` handlers may read `event_x` +and `event_y` in the same 640x360 world coordinates as output. They are unavailable +in keyboard/frame handlers and display bindings; copy to authored state to retain +them. There is no built-in drag constraint, snap, grab offset, or gesture rollback. +The direct.pure example authors these using ordinary assignments and expressions. + +Host `point` requires a pointer input, valid phase, finite coordinates and nonempty +source string. Each logical input has one owning source; different targets may +have independent active contacts. Same-owner repeated down is a no-op; another +owner's down throws. Move/up/cancel without that owner are no-ops. Down establishes +ownership before dispatch; up/cancel release it before dispatch, even if authored +arithmetic fails. An event failure rolls back that event's state writes, not the +whole gesture. `releaseAll` cancels owned pointers at their last delivered position, +then releases ordinary buttons. `reset` directly clears ownership and resets state. +Last point events include x,y. A failed cancel can leave authored state unchanged, +but cannot strand host ownership; the failure remains visible until reset/apply. + +Browser hit testing selects the declared target on primary-button pointerdown. +The stable SVG root captures that pointer, including movement/release outside the +scene. Client coordinates are transformed through the inverse SVG screen matrix. +Pointerup exposes its own final coordinates to authored handlers. The direct.pure +example updates position only on move and ends dragging on up, preserving the +last displayed position even if a host repeats stale coordinates on release. Termination is observed in the window capture phase, including releases outside +the SVG. A primary mouseup can finish an owned mouse gesture if pointerup was +not delivered; it cannot finish a touch gesture. Duplicate termination is ignored. +Only capture loss of the SVG root ends its owner, not a bubbled descendant event. +Pointercancel, lostpointercapture and +focus loss use the last delivered position and dispatch cancel, never fake up. +Missing/non-invertible coordinate transforms cancel an active gesture. A second +contact on the same target is ignored; contacts on different targets are independent. +Touch scenes disable native panning on the SVG. Mouse/touch share this adapter; +pen-specific pressure, tilt and button meanings are not implemented or validated. + +Every delivered pointermove executes synchronously; this layer adds no event queue, +coalescing, frame gating or newest-only drop policy. This is not a guarantee that +browsers expose every hardware sample. Keyboard commands and pointer coordinates +enter the same authored state/commit/output path. CLI `point NAME PHASE X Y` uses +that host API and core with source `terminal`; it does not replay browser hit testing. + + +## Placement documents (2026-09-23) + +Code-only version1 JSON and .pure files keep their earlier behavior. Version2 +stores a restorable authored document, not a suspended execution image: + +```json +{"format":"pure-workbench","version":2,"source":"state x=80","draft":"state x=80", + "states":[{"name":"x","value":350}]} +``` + +`source` is the applied definition; optional `draft` is separate unapplied editor +text (default source). Each is at most 32768 UTF-8 bytes. Draft syntax may be +unfinished. State records must contain exactly one finite value for every declared +state name, with no extra, missing or duplicate names. Record order is irrelevant. +JSON numbers preserve finite f64 values, except negative zero which is encoded as +the exact string `"-0"`. No other numeric string is accepted. Input latches, running +clock, diagnostics, caches, DOM, compiled IR and external resources are not saved. +The serialized input file limit is 1449984 bytes, including escaped source/draft +and up to 32 generated item.field names (which can repeat a long schema name). + +`exportDocument(draft?)` returns an independent version2 object. It refuses an +unapplied runtime, held input or latched execution failure. It reads core state, +without constructing an output snapshot. `restoreDocument(document)` validates +and compiles before staging, refuses held input, then commits the definition and +all saved values together. It returns `{source,draft}`. Failed validation leaves +active program/values/diagnostics intact. A failed core load is aborted; optional +reuse records may be lost as with ordinary apply. Successful restore discards old +reuse, clears diagnostics, resets tick/event count and starts with no held inputs. +The original source initializers remain intact: explicit reset restores those, +not saved values. Existing `apply` continues to preserve current values by name. +Both new document APIs reject reentry during output publication. + +Core exports `put_restored_value(id,value)` and `commit_restored_program()` extend +the existing begin/put/commit loader. Every declared state needs exactly one finite +restored value; duplicate/out-of-range/nonfinite input poisons staging. Restored +commit checks completeness and program validity before touching active state. +Ordinary commit rejects staged restored values. Pending values live alongside the +pending Program in the same union as optional evaluation Tables; active Program +layout does not grow. `layout(6)` now includes Pending, rather than Program only. +Before shared rule bindings, the Wasm Pending was 9656 B (264 B added for 32 values, mask and alignment), +Engine was 19392 B; linear memory remains fixed at 65536 B. These are build-layout +facts, not a browser RAM or performance improvement claim. `evaluation_info(10)` +continues to report only union tag overhead, excluding restoration payload. + +Browser **배치 저장** stores applied source, all current values and editor draft. +It requires a stopped clock. **파일 열기** restores version2 after replacement +confirmation when existing work is present; it starts stopped. Version1/plain +source still only replaces the editor draft. Async reading checks draft generation, +program identity and successful event count so intervening edits/inputs cannot be +overwritten by a stale read. Invalid files do not alter the working document. +**.pure 저장 / 코드 JSON 저장** remain draft-only exports. + +CLI `save-project PATH` writes version2 with exclusive creation; `restore PATH` +restores it. Loading a .json path (including at startup) distinguishes versions. +A browser draft is retained through CLI save-project, but never applied implicitly. +Existing `save PATH` remains applied-source-only. No filesystem overwrite is added. + + +## Shared rules and named records (2026-09-23) + +```text +record Position { x=0; y=0 } +item green = Position(x=80,y=120) +item blue = Position(x=280,y=160) +rule move(t: Position) { t.x=event_x; t.y=event_y } +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +on drag_green.move use move(green) +on drag_blue.move use move(blue) +``` + +A record defines 1..8 named finite numeric initializers. An item has its own values; +optional literal field overrides replace defaults. Its fields occupy contiguous +slots in the existing scalar state array, with public names `item.field`. They +count toward the same 32-state limit. Schemas and items may be forward-referenced. +Unknown/duplicate fields, types or items fail compilation. Scalar and item names +cannot collide; type and rule names have separate namespaces. There are no runtime +record objects, nested records, record copying, dynamic item creation or deletion. + +A rule takes exactly one named record parameter with an exact schema name match. +Its body uses the existing expressions, ordered assignments and conditional blocks. +Parameter fields read/write the connected item's current slots; globals and other +qualified state names remain accessible. The parameter shadows the same qualified +prefix only inside its body. No rule calls from expressions or rule bodies, recursion, +return values, stack frames, or implicit event loop are introduced. Frame handlers +may use rules; coordinate-reading rules may only bind to pointer events. +Unused rules are name/type checked without emitting a body into the VM program. + +`on input.edge use rule(item)` is one event handler. Each used rule's instructions +are emitted once at its first use. Every use adds handler metadata referencing that +same exact instruction range, with parameterBase/parameterCount. Bindings run in +source order, including repeated uses of one item. All matching bindings and +ordinary handlers participate in the same atomic event: a failure in any rolls +back every state write in that event. Error PCs refer to the shared source body. + +Core `bind_handler(handler_index,base,count)` sets a previously unbound staged +handler's contiguous state range. Counts are 1..8; the entire range must fit current +staged state_count. A duplicate binding poisons staging. All owners of shared code +must have the same nonzero field count and exactly the same range. Partial overlap +remains invalid. Parameter opcodes must belong to a bound handler and use an index +below every owner's field count. Pure display evaluation rejects parameter ops; +view bindings use ordinary resolved state loads and their normal cache dependencies. + +Handler metadata grows from 12 to 16 B (base/count plus alignment). There is no +per-field indirection list or per-event parameter-value copy. At the shared-rules step, Wasm Program +was 9648 B, Pending 9912 B, Engine 19904 B: 512 B more fixed reservation across active +and pending handler arrays. The linear-memory limit stays 64 KiB. The required +bind_handler export makes the host reject older incompatible Wasm. + +Apply preserves each qualified field by name. Reordering schema fields or item +instances rebuilds physical bindings without changing their named values. Removing +an item removes its fields; adding it again initializes new fields. An active +input still blocks definition changes. Version2 documents save qualified field +names, not physical offsets; restoration recompiles all bindings and validates all +values before commit. This is static named-field identity, not a general lifetime +handle for dynamically deleted/reused objects. Example `id` and `selected` are +ordinary author-defined numbers; uniqueness/selection policy is not enforced by +an implicit object system. + + +## Target-routed pointer groups (2026-09-23) + +```text +input drag = pointer { green_box: green; blue_box: blue } +on drag.move use move(drag.target) +``` + +A group resolves named views to named items of exactly one record type. Forward +references work. Every view may be connected only once across single-target inputs +and groups. Every item may occur in only one group route, even across groups; +this prevents two group captures from writing aliased field ranges. Explicit +ordinary handlers and statically bound rules may still write other states as authored. +Groups must be nonempty. There are at most 24 routes in the program, independently +of the existing eight logical inputs. State/view/instruction limits are unchanged. +`input.target` is a contextual rule argument for the current group's event only, +not a scalar expression or persistent object handle. Wrong groups/types are rejected. + +A group handler is emitted once per authored `on`, independent of target count. +It can share its exact body with statically bound handlers. One route consists of +three bytes: logical input index, field base, field count. The compiler's routes +also retain source view/item names for the host. The host builds one bounded view +lookup Map per successful load; target-name lookup does not scan all group targets +on each event. JS Map/metadata allocations are outside the core memory accounting. + +`put_pointer_target(input,base,count)` appends a staged route, with count 1..8 and +an in-bounds, nonoverlapping state range. Same-input routes must have equal counts. +`bind_target_handler(handler,count)` binds a previously unbound staged handler to +the event target, encoded as parameter_base=255. Commit requires a non-frame event +with matching routes/counts. Existing `bind_handler` accepts only concrete bases. +`dispatch_target(event,route,x,y)` validates the active route's membership in the +event's logical input, then supplies its base to the matching handlers. Missing, +foreign, and out-of-range targets fail before any authored writes. Ordinary dispatch +and dispatch_point reject inputs that require a route. All matching static, routed, +and inline handlers still run in source order within one atomic event. +The VM indexes the route directly; it still scans the bounded handler list to match +event IDs. No claim of constant-time complete dispatch is made. + +Host `point(name,phase,x,y,source,target)` requires a group view name in the final +argument. Legacy single-target calls keep the old form and reject an extra target. +Capture/held ownership is `(input,view)`, so two views in a group can be manipulated +concurrently; a second source cannot steal the same view. Continuations from a +non-owner are ignored. Browser capture records retain the initial view across +movement, release outside the view, and cancellation. `releaseAll` cancels every +owned view once. `held` uses `input:view` for groups and the old input names for +legacy inputs; lastEvent/lastFailure add `target` for group events. + +Route indices live only in the active definition. Apply/restore remain blocked +while inputs are held. Reset clears owners; load rebuilds routes by source names. +Documents still store source and named scalar fields, never physical route indices. +Deletion/recreation at runtime and generation-checked dynamic identity are not added. +CLI uses `point-target NAME VIEW PHASE X Y`; existing `point` is unchanged. + +Each Program adds 80 B (24 three-byte routes, mask/count and alignment). Current +Program=9728 B, Pending=9992 B, Engine=20064 B, an additional 160 B of fixed engine +reservation over the shared-rules step. Handler remains 16 B, Instruction 16 B. +The active IR byte count now includes used routes. Linear memory remains 65536 B. +The host requires put_pointer_target, bind_target_handler, and dispatch_target. diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/cli.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/cli.mjs new file mode 100644 index 0000000..100fa89 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/cli.mjs @@ -0,0 +1,98 @@ +#!/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'; + +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 그룹의 대상에 좌표 입력 + 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 저장한 배치를 복원 (초안이 있으면 파일에 보존) + 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; + 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 === '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 === '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/workbench/results/2026-09-23-pointer-routing/sources/core.zig b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/core.zig new file mode 100644 index 0000000..a68f483 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/core.zig @@ -0,0 +1,1398 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const max_pointer_targets = 24; +const event_target = 255; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { + event: u32 = 0, start: u32 = 0, length: u32 = 0, + parameter_base: u8 = 0, parameter_count: u8 = 0, +}; +const PointerTarget = struct { input: u8, base: u8, count: u8 }; +const Program = struct { + pointer_targets: [max_pointer_targets]PointerTarget = undefined, + pointer_target_count: u8 = 0, + routed_inputs: u8 = 0, + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +const Workspace = union { staging: Pending, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.program.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.program.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.program.initials[id] = initial; + engine.workspace.staging.program.old_ids[id] = old_id; + engine.workspace.staging.program.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 25) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +export fn bind_handler(handler: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; +} + +// A route is a static program-local index, never a persistent object identity. +export fn put_pointer_target(input: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.pointer_target_count == max_pointer_targets) return stagingFail(.capacity); + if (input >= p.input_count) return stagingFail(.bad_event); + if (base >= p.state_count or count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (base < @as(u32, t.base) + t.count and t.base < base + count) return stagingFail(.invalid); + if (t.input == input and t.count != count) return stagingFail(.bad_state); + } + p.pointer_targets[p.pointer_target_count] = .{ .input = @intCast(input), .base = @intCast(base), .count = @intCast(count) }; + p.pointer_target_count += 1; + p.routed_inputs |= @as(u8, 1) << @as(u3, @intCast(input)); + return 0; +} + +export fn bind_target_handler(handler: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or count == 0 or count > 8) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = event_target; + h.parameter_count = @intCast(count); + return 0; +} + +fn eventInput(event: u32) u32 { + return if (event >= 25) event - 25 else if (event >= 17) event - 17 else (event - 1) / 2; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_base == event_target) { + if (h.event == 0 or h.parameter_count == 0 or h.parameter_count > 8) return .bad_event; + var found = false; + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (t.input != eventInput(h.event)) continue; + if (t.count != h.parameter_count) return .bad_state; + found = true; + } + if (!found) return .bad_event; + } else if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 25) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; +} + +export fn put_restored_value(id: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const pending = &engine.workspace.staging; + if (id >= pending.program.state_count) return stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return stagingFail(.bad_state); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; +} + +export fn commit_program() u32 { + return commit(false); +} + +export fn commit_restored_program() u32 { + return commit(true); +} + +fn commit(restoring: bool) u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const count = engine.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (engine.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return stagingFail(.bad_state); + const code = validate(&engine.workspace.staging.program); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.program.state_count) |i| { + const old_id = engine.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) engine.workspace.staging.values[i] else if (old_id < 0) engine.workspace.staging.program.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging.program; + engine.values = next; + finishStaging(); + if (restoring) { + engine.tick = 0; + engine.successful_events = 0; + } + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false, target: ?u32 = null }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +export fn dispatch_target(event: u32, target: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true, .target = target }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + (context.point and event == 0) or (!context.point and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var route: ?PointerTarget = null; + const routed = event != 0 and engine.active.routed_inputs & (@as(u8, 1) << @as(u3, @intCast(eventInput(event)))) != 0; + if (context.target) |target| { + if (!routed or target >= engine.active.pointer_target_count or + engine.active.pointer_targets[target].input != eventInput(event)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + route = engine.active.pointer_targets[target]; + } else if (routed) { + engine.last_error = .bad_event; + return public(.bad_event); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = if (h.parameter_base == event_target) route.?.base else h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = execute(&pending, h.start, h.length, bound, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler) + @as(u32, engine.active.pointer_target_count) * @sizeOf(PointerTarget), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + 8 => @sizeOf(PointerTarget), + 9 => engine.active.pointer_target_count, + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 16), layout(3)); + try std.testing.expectEqual(@as(u32, 80), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "shared handler bodies access their own bound state slots in source order" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try emit(24, 0, 0); try emit(0, 0, 1); try emit(3, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(1, 1, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(2, 0, 1)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(f64, 21), get_state(1)); + try std.testing.expectEqual(@as(u32, 4), info(6)); // Three bindings, one body. + try std.testing.expectEqual(@as(u32, 12), info(3)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); +} + +test "failure in a later shared binding rolls back all earlier bindings" { + reset_all(); + try ok(begin_program(4, 1)); + for ([_]f64{ 8, 2, 12, 0 }, 0..) |value, i| try ok(put_state(@intCast(i), value, -1)); + try emit(24, 0, 0); try emit(24, 1, 0); try emit(6, 0, 0); try emit(25, 0, 0); + for (0..2) |i| { + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(@intCast(i), @intCast(i * 2), 2)); + } + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(2)); + try std.testing.expectEqual(@as(u32, 0), info(9)); +} + +test "shared bindings reject missing arguments partial overlaps and invalid slot mappings" { + reset_all(); + try counterProgram(); + const revision = info(4); + for (0..5) |mode| { + try ok(begin_program(1, 1)); try ok(put_state(0, 9, -1)); + try emit(24, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 2)); + switch (mode) { + 0 => {}, // Parameter op without a binding. + 1 => { try ok(bind_handler(0, 0, 1)); try std.testing.expectEqual(public(.invalid), bind_handler(0, 0, 1)); }, + 2 => { try std.testing.expectEqual(public(.bad_state), bind_handler(0, 0, 2)); }, + 3 => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 1, 1)); try ok(bind_handler(1, 0, 1)); + }, + else => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 2)); // Another owner missing its mapping. + }, + } + try std.testing.expect(commit_program() != 0); abort_program(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + } +} + +test "one routed handler changes only its event target and rejects missing or foreign routes" { + reset_all(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try ok(put_pointer_target(0, 0, 1)); + try ok(put_pointer_target(0, 1, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try ok(commit_program()); + try ok(dispatch_target(17, 1, 77, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(3)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(17, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, 2, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, no_pc, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(18, 0, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(0, 0, 99, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_target(17, 0, std.math.inf(f64), 0)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 3), layout(8)); + try std.testing.expectEqual(@as(u32, 2), layout(9)); + try std.testing.expectEqual(@as(u32, 54), layout(4)); +} + +test "routes reject invalid mappings and bindings without replacing active work" { + for (0..7) |mode| { + reset_all(); + try counterProgram(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + switch (mode) { + 0 => try std.testing.expectEqual(public(.bad_state), put_pointer_target(0, 1, 2)), + 1 => try std.testing.expectEqual(public(.bad_event), put_pointer_target(2, 0, 1)), + 2 => { + try ok(put_pointer_target(0, 0, 2)); + try std.testing.expectEqual(public(.invalid), put_pointer_target(1, 1, 1)); + }, + else => { + if (mode != 3) try ok(put_pointer_target(0, 0, 1)); + try emit(0, 0, 2); + try emit(25, 0, 0); + try ok(put_handler(if (mode == 4) 0 else 1, 0, 2)); + try ok(bind_target_handler(0, if (mode == 5) 2 else 1)); + if (mode == 6) try std.testing.expectEqual(public(.invalid), bind_target_handler(0, 1)); + }, + } + try std.testing.expect(commit_program() != 0); + // Active program still executes while the failed candidate occupies staging. + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 0), layout(9)); + } + reset_all(); + try ok(begin_program(32, 1)); + for (0..32) |i| try ok(put_state(@intCast(i), 0, -1)); + for (0..24) |i| try ok(put_pointer_target(0, @intCast(i), 1)); + try std.testing.expectEqual(public(.capacity), put_pointer_target(0, 24, 1)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "routed event rollback includes globals and earlier handlers and survives staged route replacement" { + reset_all(); + try ok(begin_program(3, 1)); + for (0..3) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + try ok(put_pointer_target(0, 1, 1)); + try ok(put_pointer_target(0, 2, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try emit(0, 0, 1); + try emit(23, 0, 0); + try emit(6, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 2, 6)); + try ok(bind_target_handler(1, 1)); + try ok(commit_program()); + try ok(begin_program(1, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_pointer_target(0, 0, 1)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_target(17, 1, 88, 0)); + try std.testing.expectEqual(@as(f64, 0), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 2), get_state(2)); + abort_program(); + try ok(dispatch_target(17, 1, 88, 2)); + try std.testing.expectEqual(@as(f64, 99), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(2)); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/evaluation.zig b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/examples/routed-layout.pure b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/examples/routed-layout.pure new file mode 100644 index 0000000..8933c70 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/examples/routed-layout.pure @@ -0,0 +1,70 @@ +# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다. +state selected = 0 +record Tile { + x = 0; y = 0 + ox = 0; oy = 0 + sx = 0; sy = 0 + held = 0; id = 0 +} +item green = Tile(x=80, y=120, sx=80, sy=120, id=1) +item blue = Tile(x=280, y=160, sx=280, sy=160, id=2) +item amber = Tile(x=450, y=220, sx=450, sy=220, id=3) +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +input drag = pointer { + green_box: green + blue_box: blue + amber_box: amber +} +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +box amber_box(amber.x,amber.y,56,44,"#dfb184") + +rule grab(t: Tile) { + selected = t.id + t.sx = t.x; t.sy = t.y + t.ox = event_x - t.x; t.oy = event_y - t.y + t.held = 1 +} +rule move(t: Tile) { + t.x = clamp(event_x - t.ox, 20, 564) + t.y = clamp(event_y - t.oy, 90, 286) +} +rule drop(t: Tile) { t.held = 0 } +rule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 } +rule go_left(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) } +} +rule go_right(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) } +} +rule go_up(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) } +} +rule go_down(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) } +} + +on drag.down use grab(drag.target) +on drag.move use move(drag.target) +on drag.up use drop(drag.target) +on drag.cancel use cancel(drag.target) + +on left.down use go_left(green) +on right.down use go_right(green) +on up.down use go_up(green) +on down.down use go_down(green) + +on left.down use go_left(blue) +on right.down use go_right(blue) +on up.down use go_up(blue) +on down.down use go_down(blue) + +on left.down use go_left(amber) +on right.down use go_right(amber) +on up.down use go_up(amber) +on down.down use go_down(amber) diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/presentation.test.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/presentation.test.mjs new file mode 100644 index 0000000..98bef6e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/presentation.test.mjs @@ -0,0 +1,132 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; +import { createRuntime as before } from './results/2026-09-23-presentation/before/web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = `state x = 3 +state radius = 5 +input move = KeyM +input shrink = KeyS +box tile(x, 10, x * 2, 20, "#123456") +circle dot(50, 50, radius, "#abcdef") +text label(0, 0, 10 / x, "Value: ") +on move.down { x = x + 0.25 } +on shrink.down { radius = radius - 10 } +on frame { x = x + dt } +`; + +// The archived adapter predates route diagnostics. These legacy fixtures have +// no routes; keep all original fields in the equality comparison. +function archivedSnapshot(runtime) { + const value = runtime.snapshot(); + value.memory.pointerTargetBytes = 3; + value.memory.pointerTargets = 0; + return value; +} + +// Reconstruct only in tests, for comparison with the archived snapshot path. +function collect(runtime, hooks = {}) { + let states, views, declared; + const result = runtime.present({ + begin(program) { + declared = program; states = {}; views = []; + hooks.begin?.(); + }, + state(i, value) { states[declared.states[i].name] = value; }, + view(i, valid) { + if (!valid) return; + const {id, type, color, prefix} = declared.views[i]; + views.push({id, type, color, prefix}); + }, + value(i, name, value) { views.at(-1)[name] = value; hooks.value?.(); }, + end(count) { assert.equal(views.length, count); }, + }); + return {states, views, ...result}; +} + +test('direct publication matches archived snapshots and work counts through authored changes', async () => { + for (const cacheEntries of [0, 1, 32]) { + const left = await createRuntime(wasm, {cacheEntries}), right = await before(wasm, {cacheEntries}); + const compare = () => { + assert.deepEqual(collect(left), archivedSnapshot(right)); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + }; + compare(); + for (const r of [left, right]) r.apply(source); + compare(); + for (let i = 0; i < 120; i++) { + for (const r of [left, right]) { + r.down('move', 'keyboard'); r.down('move', 'pointer'); + r.up('move', 'keyboard'); r.up('move', 'pointer'); + if (i % 11 === 0) r.frame(2); + if (i % 17 === 0) r.forgetEvaluation('history'); + if (i % 23 === 0) r.reset(); + } + compare(); + } + for (const r of [left, right]) { r.down('shrink'); r.up('shrink'); } + compare(); // invalid circle only; other views still publish + for (const r of [left, right]) assert.throws(() => r.apply('state broken =')); + compare(); + for (const r of [left, right]) r.apply('state radius = 1\nstate x = 0\ntext label(1, 2, x)'); + compare(); + for (const r of [left, right]) r.apply(''); + compare(); + } +}); + +test('whole-scene budget failure never publishes partial bindings and recovers exactly', async () => { + const left = await createRuntime(wasm, {cacheEntries:32}), right = await before(wasm, {cacheEntries:32}); + for (const r of [left, right]) r.apply(source); + for (let workLimit = 0; workLimit <= 80; workLimit++) { + for (const r of [left, right]) r.configureEvaluation({workLimit}); + let calls = 0; + const result = collect(left, {value: () => calls++}); + assert.deepEqual(result, archivedSnapshot(right)); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + if (result.displayError?.code === 101) assert.equal(calls, 0); + else assert.equal(calls, 10); + } +}); + +test('latched event rollback, nonfinite bindings, reset and signed zero keep their semantics', async () => { + const left = await createRuntime(wasm), right = await before(wasm); + const definition = 'state x = 0\ninput fail = Space\ntext value(0, 0, 1 / x)\non fail.down { x = 2\nx = 1 / 0 }'; + for (const r of [left, right]) { r.apply(definition); r.down('fail'); r.up('fail'); } + assert.deepEqual(collect(left), archivedSnapshot(right)); + for (const r of [left, right]) r.configureEvaluation({workLimit:0}); + assert.deepEqual(collect(left), archivedSnapshot(right)); + for (const r of [left, right]) { + r.apply('state x = -0\ntext value(x, 0, x)'); r.reset(); r.configureEvaluation({workLimit:2048}); + } + const result = collect(left); + assert.deepEqual(result, archivedSnapshot(right)); + assert(Object.is(result.states.x, -0)); +}); + +test('publication rejects reentrant runtime changes and reads before they affect the scene', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const calls = [() => runtime.apply(''), () => runtime.reset(), () => runtime.frame(), + () => runtime.down('move'), () => runtime.up('move'), () => runtime.releaseAll(), + () => runtime.configureEvaluation({workLimit:0}), () => runtime.forgetEvaluation(), + () => runtime.snapshot(), () => runtime.present({})]; + const expected = runtime.snapshot(); + assert.deepEqual(collect(runtime, {begin() { + for (const call of calls) assert.throws(call, /출력 전달 중/); + }}), expected); + assert.deepEqual(runtime.snapshot(), expected); +}); + +test('invalid or throwing output adapters do not strand the runtime or alter authored state', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const expected = runtime.snapshot(); + assert.throws(() => runtime.present({}), TypeError); + assert.throws(() => collect(runtime, {value() { throw new Error('destination failed'); }}), /destination failed/); + assert.deepEqual(collect(runtime), expected); + runtime.down('move'); runtime.up('move'); + assert.equal(collect(runtime).states.x, 3.25); +}); diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/qa-routed-input.js b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/qa-routed-input.js new file mode 100644 index 0000000..b935499 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/qa-routed-input.js @@ -0,0 +1,164 @@ +// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('routed-layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + check(await page.locator('[data-point-input="drag"]').count()===3,'one authored pointer input connects all three view targets'); + check(await page.locator('[data-point-target]').count()===3,'DOM destinations retain distinct target identities'); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + check((await page.locator('#last-event').textContent()).includes('drag.up → amber_box'),'last input identifies the released target within its group'); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-routed-layout-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states['green.x']===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + // One source edit affects every bound target, without editing input adapters. + const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,'); + await page.locator('#source').fill(edited);await page.locator('#apply').click(); + const beforeEdit=await state(); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target'); + await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10); + const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target'); + const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse(); + const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n'); + const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click(); + const afterReorder=await state(); + check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder'); + // Two contacts execute the same body with different bound fields. + await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + const session=await page.context().newCDPSession(page); + try { + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]}); + await page.waitForFunction(()=>{const s=window.pureWorkbench.snapshot().states;return Math.abs(s['green.x']-150)<0.1&&Math.abs(s['blue.x']-350)<0.1;}); + const touch=await state(); + check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + const canceled=await state(); + check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position'); + + // A real second contact on the same view must not steal its owner. + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,{id:2,...await client(100,140)}]}); + check((await state()).held.length===1,'a second touch on the same target cannot steal ownership'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + // End one actual contact and continue the other in the same logical input. + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + const m1={id:1,...await client(160,180)},m2={id:2,...await client(360,220)}; + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[m1,m2]}); + // In this Chromium, touchEnd's supplied point identifies the contact ending. + // touch-reproduction.js records the resulting pointerup and retained blue owner. + await session.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[m1]}); + let partial=await state(); + check(partial.held.length===1&&partial.states['green.held']===0&&partial.states['blue.held']===1,'releasing green keeps blue captured within the same input'); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:2,...await client(400,250)}]}); + await page.waitForFunction(()=>Math.abs(window.pureWorkbench.snapshot().states['blue.x']-390)<0.1); + partial=await state(); + check(Math.abs(partial.states['green.x']-150)<0.1&&Math.abs(partial.states['blue.x']-390)<0.1,'the remaining contact keeps moving only its own target'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + partial=await state(); + check(Math.abs(partial.states['green.x']-150)<0.1&&partial.states['blue.x']===280&&!partial.held.length,'canceling the remaining target preserves the already released position'); + } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();} + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-routed-layout-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-routed-layout-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/routing.test.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/routing.test.mjs new file mode 100644 index 0000000..bae04de --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/routing.test.mjs @@ -0,0 +1,114 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {spawnSync} from 'node:child_process'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const shared=await readFile(new URL('./examples/shared-layout.pure',import.meta.url),'utf8'); +const routed=await readFile(new URL('./examples/routed-layout.pure',import.meta.url),'utf8'); +const fresh=()=>createRuntime(wasm,{cacheEntries:32}); +const fixture=n=>`input drag=pointer {\n${Array.from({length:n},(_,i)=>`v${i}:c${i}`).join('\n')}\n}\non drag.move use move(drag.target)\nrule move(t:Cell) {t.x=event_x}\nrecord Cell {x=0}\n`+Array.from({length:n},(_,i)=>`item c${i}=Cell()\nbox v${i}(c${i}.x,0,10,10,"#123456")`).join('\n'); + +test('one input and one handler serve up to 24 static targets without instruction or handler expansion',async()=>{ + for(const n of [1,3,9,24]){ + const p=compile(fixture(n));assert.equal(p.inputs.length,1);assert.equal(p.handlers.length,1); + assert.equal(p.handlers[0].length,2);assert.equal(p.pointerTargets.length,n); + assert.equal(p.handlers[0].parameterBase,255); + const r=await fresh();r.apply(fixture(n)); + r.point('drag','down',0,0,'p',`v${n-1}`);r.point('drag','move',37,0,'p',`v${n-1}`);r.point('drag','up',0,0,'p',`v${n-1}`); + const s=r.snapshot();assert.equal(s.states[`c${n-1}.x`],37); + for(let i=0;i{ + const base=fixture(2); + for(const source of [ + base.replace('v0:c0','missing:c0'),base.replace('v0:c0','v0:missing'), + base.replace('v1:c1','v0:c1'),base.replace('v1:c1','v1:c0'), + base.replace('c1=Cell()','c1=Other()')+'\nrecord Other {x=0}', + base.replace('use move(drag.target)','use move(other.target)'), + base.replace('use move(drag.target)','use move(drag.missing)'), + base+'\ninput another=pointer(v0)', + base+'\ninput other=pointer { v2:c0 }\nbox v2(0,0,1,1,"#000000")', + 'input empty=pointer {}', + base.replace('on drag.move','on frame'), + base.replace('rule move(t:Cell)','rule move(t:Other)')+'\nrecord Other {x=0}', + ])assert.throws(()=>compile(source),source); +}); + +test('grouped layout matches explicit bindings through concurrent capture, partial cancel, keyboard and restore',async()=>{ + const a=await fresh(),b=await fresh();a.apply(shared);b.apply(routed); + const compare=()=>{const x=a.snapshot(),y=b.snapshot();assert.deepEqual(y.states,x.states);assert.deepEqual(y.views,x.views);assert.equal(y.lastError,x.lastError);}; + const point=(name,phase,x,y)=>{a.point(`drag_${name}`,phase,x,y,name);b.point('drag',phase,x,y,name,`${name}_box`);compare();}; + point('green','down',90,130);point('blue','down',290,170); + assert.equal(b.snapshot().held.length,2); + assert.throws(()=>b.apply(routed),/놓은/);assert.throws(()=>b.exportDocument(),/조작/); + assert.throws(()=>b.point('drag','down',0,0,'intruder','green_box'),/다른 포인터/); + b.point('drag','move',999,999,'intruder','green_box');compare(); + point('green','move',200,210);point('blue','move',500,270);point('green','cancel',200,210); + assert.equal(b.snapshot().held.length,1);point('blue','up',290,170); + for(const key of ['left','up','right','down']){a.down(key);b.down(key);a.up(key);b.up(key);compare();} + point('amber','down',460,230);point('amber','move',900,500);point('amber','up',460,230); + const c=await fresh();c.restoreDocument(JSON.parse(JSON.stringify(b.exportDocument()))); + assert.deepEqual(c.snapshot().states,b.snapshot().states); + for(const r of [b,c]){r.point('drag','down',570,290,'new','amber_box');r.point('drag','move',100,150,'new','amber_box');r.releaseAll();} + assert.deepEqual(c.snapshot().states,b.snapshot().states); + assert.throws(()=>b.point('drag','down',0,0),/도형/); + assert.throws(()=>b.point('drag','down',0,0,'p','board'),/도형/); + assert.throws(()=>b.down('drag'),/좌표/); +}); + +test('capture remains bound to its target and releaseAll cancels each target exactly once',async()=>{ + const r=await fresh();r.apply(routed+'\nstate cancelled=0\non drag.cancel {cancelled=cancelled+1}'); + r.point('drag','down',90,130,'a','green_box');r.point('drag','move',200,200,'a','green_box'); + r.point('drag','down',290,170,'b','blue_box');r.point('drag','move',400,250,'b','blue_box'); + r.point('drag','up',0,0,'a','blue_box');assert.equal(r.snapshot().held.length,2); + r.releaseAll();r.releaseAll();const s=r.snapshot();assert.equal(s.held.length,0); + assert.equal(s.states.cancelled,2);assert.equal(s.states['green.x'],80);assert.equal(s.states['blue.x'],280); +}); + +test('reorder and remove/readd rebuild routes by names without persisting physical indices',async()=>{ + const r=await fresh();r.apply(fixture(3)); + r.point('drag','down',0,0,'p','v2');r.point('drag','move',55,0,'p','v2');r.releaseAll(); + const reordered=fixture(3).replace('v0:c0\nv1:c1\nv2:c2','v2:c2\nv1:c1\nv0:c0').replace('item c0=Cell()','').replace('item c2=Cell()','item c2=Cell()\nitem c0=Cell()'); + r.apply(reordered);assert.equal(r.snapshot().states['c2.x'],55); + r.point('drag','down',0,0,'p','v2');r.point('drag','move',66,0,'p','v2');r.releaseAll(); + assert.equal(r.snapshot().states['c2.x'],66);assert.equal(r.snapshot().states['c0.x'],0); + const saved=r.exportDocument();const c=await fresh();c.restoreDocument(saved);assert.deepEqual(c.snapshot().states,r.snapshot().states); + r.apply(fixture(2));r.apply(fixture(3));assert.equal(r.snapshot().states['c2.x'],0); + r.point('drag','move',999,0,'p','v2');assert.equal(r.snapshot().states['c2.x'],0); +}); + +test('routed writes invalidate cached displays and failed handlers rollback without losing another target owner',async()=>{ + const source=fixture(2)+'\nstate total=0\non drag.move {total=total+1; total=1/event_y}'; + const r=await fresh();r.apply(source);r.snapshot();r.snapshot(); + r.point('drag','down',0,0,'a','v0');r.point('drag','down',0,0,'b','v1'); + assert.equal(r.point('drag','move',50,2,'a','v0'),0);assert.equal(r.snapshot().views[0].x,50); + assert.equal(r.point('drag','move',99,0,'b','v1'),4);let s=r.snapshot(); + assert.equal(s.states['c1.x'],0);assert.equal(s.states['c0.x'],50);assert.equal(s.states.total,0.5); + assert.equal(s.lastFailure.target,'v1');r.releaseAll();assert.equal(r.snapshot().lastFailure.status,4); + const previous=r.currentProgram;assert.throws(()=>r.apply(source.replace('v0:c0','v0:missing'))); + assert.equal(r.currentProgram,previous);r.reset();assert.equal(r.snapshot().held.length,0); +}); + +test('CLI sends grouped pointer events and rejects an unknown target',()=>{ + const path=new URL('./examples/routed-layout.pure',import.meta.url).pathname; + const run=input=>spawnSync(process.execPath,[new URL('./cli.mjs',import.meta.url).pathname,path],{input,encoding:'utf8'}); + const ok=run('point-target drag green_box down 90 130\npoint-target drag green_box move 210 220\npoint-target drag green_box up 90 130\nquit\n'); + assert.equal(ok.status,0,ok.stderr);const rows=ok.stdout.trim().split('\n').map(JSON.parse); + assert.equal(rows.at(-1).states['green.x'],200);assert.equal(rows.at(-1).states['blue.x'],280);assert.equal(rows.at(-1).held.length,0); + const bad=run('point-target drag missing down 0 0\nquit\n');assert.equal(bad.status,1);assert.match(bad.stderr,/도형/); +}); + + +test('a route from a different input group cannot be delivered through the named group',async()=>{ + const source=fixture(2).replace('v1:c1','')+'\ninput second=pointer {v1:c1}\non second.move use move(second.target)'; + const r=await fresh();r.apply(source); + assert.throws(()=>r.point('drag','down',0,0,'p','v1'),/도형/); + assert.throws(()=>r.point('second','down',0,0,'p','v0'),/도형/); + r.point('second','down',0,0,'p','v1');r.point('second','move',77,0,'p','v1');r.releaseAll(); + assert.equal(r.snapshot().states['c0.x'],0);assert.equal(r.snapshot().states['c1.x'],77); +}); diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/app.js b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/app.js new file mode 100644 index 0000000..4b367de --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/app.js @@ -0,0 +1,403 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; +import { bindSceneInput } from './scene-input.mjs'; +import { parseProject, MAX_PROJECT_BYTES } from './project.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +let draftGeneration = 0; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); +const sceneInput = bindSceneInput($('scene'), () => runtime, safeAction); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +// DOM is the published value. Avoid rewriting unchanged diagnostics/controls. +function text(id, value) { + const node = $(id); + if (node.textContent !== value) node.textContent = value; +} +function pressed(node, value) { + const next = String(value); + if (node.getAttribute('aria-pressed') !== next) node.setAttribute('aria-pressed', next); +} +function draftStatus() { + const changed = dirty(); + text('draft-status', changed ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'); + $('draft-status').classList.toggle('dirty', changed); +} + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + text('cursor-position', `${lines.length}행 · ${lines.at(-1).length + 1}열`); +} + +function draftChanged() { + draftGeneration++; + // Read scroll position before writes; this path runs only for source edits. + const scrollTop = source.scrollTop; + const count = source.value.split('\n').length; + const size = bytes(source.value); + text('line-numbers', Array.from({ length: count }, (_, i) => i + 1).join('\n')); + $('line-numbers').scrollTop = scrollTop; + text('source-size', `${size.toLocaleString('en-US')} / 32,768 bytes`); + $('source-size').style.color = size > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + draftStatus(); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + text('play', '▶ 실행'); + pressed($('play'), false); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + sceneInput.releaseCaptures(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + pressed(button, held.has(name)); + } + text('scene-status', runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'); + text('clock-status', `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`); + text('revision', `revision ${snapshot.revision || 0}`); + for (const id of ['play', 'step', 'reset', 'download-layout']) { + if ($(id).disabled !== !runtime.currentProgram) $(id).disabled = !runtime.currentProgram; + } + const event = snapshot.lastEvent; + text('last-event', event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''}${event.target ? ` → ${event.target}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'); + if ($('runtime-error').hidden !== !snapshot.lastError) $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + let message = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + message += ` · ${snapshot.displayError.message}`; + text('runtime-error', message); + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + if (input.target || input.routes) { + const note = document.createElement('p'); + note.className = 'empty-note'; + note.textContent = `${input.name} · 장면의 ${input.target || input.routes.map(route => route.target).join(', ')}을 잡아 조작하세요.`; + children.push(note); + continue; + } + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + draftStatus(); + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime, message = '편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.') { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic(message); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('download-layout').addEventListener('click', () => { + try { + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 저장하세요.'); + const project = runtime.exportDocument(source.value); + download('my-layout.pure.json', JSON.stringify(project, null, 2) + '\n', 'application/json', + '적용된 코드·현재 배치·미적용 초안을 함께 저장했습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_PROJECT_BYTES) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = draftGeneration; + const active = runtime?.currentProgram; + const events = runtime?.snapshot().successfulEvents; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + const project = parseProject(content); + if (project.version === 2) { + if (!runtime) throw new Error('실행기가 준비되지 않았습니다.'); + if (draftGeneration !== before || runtime.currentProgram !== active || runtime.snapshot().successfulEvents !== events) + throw new Error('파일을 읽는 동안 작업이 바뀌었습니다. 정지한 뒤 다시 불러오세요.'); + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 복원하세요.'); + if ((source.value || appliedSource) && !window.confirm('현재 코드와 배치를 파일의 내용으로 복원할까요? 저장하지 않은 작업은 사라집니다.')) return; + const restored = runtime.restoreDocument(project); + pause(); droppedTicks = 0; + appliedSource = restored.source; + rebuildInputControls(); + replaceDraft(restored.draft, '배치를 복원했습니다. 미적용 초안도 보존했고, 시간은 정지 상태입니다.'); + refresh(); + return; + } + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (draftGeneration !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/compiler.mjs new file mode 100644 index 0000000..bfe671b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/compiler.mjs @@ -0,0 +1,484 @@ +// 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', '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 === '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'].includes(edge.value)) fail(edge, 'Expected down or up, move or cancel'); + } + 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.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; + 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_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; 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 body = {start,length,point}; bodies.set(rule.name.value,body); return body; + } + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + pointHandler = 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); + if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input'); + event = 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; + const body = ruleBody(rule); + 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/workbench/results/2026-09-23-pointer-routing/sources/web/core.wasm b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/core.wasm new file mode 100644 index 0000000..117f25d Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/index.html b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/index.html new file mode 100644 index 0000000..a4bc2af --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/index.html @@ -0,0 +1,94 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

직접 조작
input drag = pointer(tile)로 도형에 입력을 연결합니다. on drag.down / .move / .up / .cancel에서 event_x, event_y는 장면 좌표입니다. 이동 방식과 취소 시 동작도 코드로 정합니다. 초점이나 포인터 캡처를 잃으면 .cancel이 전달됩니다.

값과 규칙 재사용
record Tile { x=0; y=0 }로 필드를 정하고 item green = Tile(x=80)으로 대상을 만듭니다. rule move(t: Tile) { t.x=event_x }를 on drag.move use move(green)으로 연결합니다. 화면에서는 green.x를 읽습니다. 규칙은 연결한 대상의 값을 직접 바꿉니다.

여러 도형의 입력 묶기
input drag = pointer { green_box: green; blue_box: blue }로 연결하고 on drag.move use move(drag.target)으로 현재 조작 대상을 전달합니다. 같은 레코드 타입의 대상끼리 묶으며, 각 도형은 따로 잡고 놓을 수 있습니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/output.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/output.mjs new file mode 100644 index 0000000..694fce6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/output.mjs @@ -0,0 +1,103 @@ +// Bind authored declarations to concrete DOM destinations once per apply. +// The DOM holds the published values; no parallel scene/value model is kept. +const NS = 'http://www.w3.org/2000/svg'; +const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString(); +const attributes = { box: { x: 'x', y: 'y', width: 'width', height: 'height' }, + circle: { x: 'cx', y: 'cy', radius: 'r' }, text: { x: 'x', y: 'y' } }; + +function text(node, value) { + if (node.textContent !== value) node.textContent = value; +} +function attribute(node, name, value) { + const next = String(value); + if (node.getAttribute(name) !== next) node.setAttribute(name, next); +} + +export function createOutput(document) { + const get = id => document.getElementById(id); + const scene = get('scene-content'), states = get('state-values'); + const empty = get('empty-scene'), description = get('scene-description'); + let active = undefined, nodes = [], amounts = []; + const costNames = ['Wasm 선형 메모리', '코어 예약 공간', '사용 중인 명령·연결', + '현재 상태 값', '새 코드 준비 공간 (예약에 포함)', '평가 스택 용량', + '마지막 이벤트 실행 명령', '적용된 소스 크기']; + const costs = costNames.map(label => { + const dt = document.createElement('dt'), dd = document.createElement('dd'); + dt.textContent = label; + return { dt, dd }; + }); + get('cost-values').replaceChildren(...costs.flatMap(({dt, dd}) => [dt, dd])); + + return { + begin(program) { + if (program === active) return; + active = program; + const targets = new Map(); + for (const input of program?.inputs ?? []) { + if (input.target) targets.set(input.target, {name:input.name}); + for (const route of input.routes ?? []) targets.set(route.target, {name:input.name,target:route.target}); + } + scene.ownerSVGElement.classList.toggle('direct-input', targets.size > 0); + nodes = (program?.views ?? []).map(view => { + const node = document.createElementNS(NS, {box:'rect', circle:'circle', text:'text'}[view.type]); + node.setAttribute('data-view-id', view.id); + if (targets.has(view.id)) { + const input = targets.get(view.id); + node.setAttribute('data-point-input', input.name); + if (input.target) node.setAttribute('data-point-target', input.target); + } + node.setAttribute('fill', view.color ?? '#e7ece6'); + if (view.type === 'text') { + node.setAttribute('font-family', 'ui-monospace, monospace'); + node.setAttribute('font-size', '20'); + } + return node; + }); + scene.replaceChildren(...nodes); + const holders = []; + amounts = (program?.states ?? []).map(state => { + const dl = document.createElement('dl'), dt = document.createElement('dt'), dd = document.createElement('dd'); + dl.className = 'state-value'; dt.textContent = state.name; dd.dataset.state = state.name; + dl.append(dt, dd); holders.push(dl); + return dd; + }); + if (!holders.length) { + const note = document.createElement('p'); + note.className = 'empty-note'; note.textContent = '선언한 상태의 값이 표시됩니다.'; + holders.push(note); + } + states.replaceChildren(...holders); + }, + state(index, value) { text(amounts[index], number(value)); }, + view(index, valid) { + const node = nodes[index]; + if (valid) { if (node.hasAttribute('display')) node.removeAttribute('display'); } + else attribute(node, 'display', 'none'); + }, + value(index, name, value) { + const view = active.views[index], node = nodes[index]; + if (name === 'value') text(node, `${view.prefix || ''}${number(value)}`); + else attribute(node, attributes[view.type][name], value); + }, + end(visible) { + const hidden = visible !== 0; + if (empty.hidden !== hidden) empty.hidden = hidden; + const failed = !visible && nodes.length > 0; + text(empty.querySelector('strong'), failed ? '장면을 표시할 수 없습니다' : '아직 아무것도 없습니다'); + text(empty.querySelector('span:last-child'), failed ? '현재 상태 아래의 표시 오류를 확인하세요.' : '도형이나 글자를 선언하고 코드를 적용하세요.'); + text(description, visible ? `사용자가 만든 장면. 도형과 글자 ${visible}개.` : failed ? '표시 오류로 장면을 표시할 수 없습니다.' : '사용자가 코드로 만든 장면. 현재 비어 있습니다.'); + }, + costs(status, metrics, droppedTicks) { + const m = status.memory; + const values = [m.linearBytes, m.reservedEngineBytes, m.activeIRBytes, m.activeStateBytes, + m.stagingReservedBytes, m.evalStackBytes, status.lastOps, metrics?.sourceBytes]; + for (let i = 0; i < values.length; i++) { + const value = values[i], {dt, dd} = costs[i], hidden = !Number.isFinite(value); + if (dt.hidden !== hidden) dt.hidden = hidden; + if (dd.hidden !== hidden) dd.hidden = hidden; + if (!hidden) text(dd, i === 6 ? `${value}개` : `${value.toLocaleString('en-US')} B`); + } + text(get('dropped-ticks'), `시간 지연으로 건너뛴 프레임: ${droppedTicks}`); + }, + }; +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/runtime.mjs new file mode 100644 index 0000000..eb36839 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/runtime.mjs @@ -0,0 +1,339 @@ +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') + 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) { + const status = 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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].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].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); + }, + 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/workbench/results/2026-09-23-pointer-routing/sources/web/scene-input.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/scene-input.mjs new file mode 100644 index 0000000..0daf079 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/web/scene-input.mjs @@ -0,0 +1,84 @@ +// This adapter owns browser capture only. Authored state changes stay in the VM. +export function bindSceneInput(scene, runtime, act) { + const captures = new Map(); + const events = scene.ownerDocument.defaultView; + const coordinates = event => { + const matrix = scene.getScreenCTM(); + if (!matrix) return null; + try { + const p = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse()); + return Number.isFinite(p.x) && Number.isFinite(p.y) ? p : null; + } catch { return null; } + }; + const release = id => { + if (scene.hasPointerCapture(id)) scene.releasePointerCapture(id); + }; + function finish(event, phase, pointerId = event.pointerId) { + const entry = captures.get(pointerId); + if (!entry) return; + event.preventDefault(); + // Capture loss/cancel may not carry a meaningful position. Use the last + // delivered coordinates. Up must use its own final position. + const p = phase === 'up' ? coordinates(event) : null; + if (p) { entry.x = p.x; entry.y = p.y; } + if (phase === 'up' && !p) phase = 'cancel'; + captures.delete(pointerId); + release(pointerId); + act(() => runtime().point(entry.name, phase, entry.x, entry.y, entry.source, entry.target)); + } + scene.addEventListener('pointerdown', event => { + if (event.button !== 0 || !runtime() || captures.has(event.pointerId)) return; + const target = event.target.closest?.('[data-point-input]'); + if (!target || !scene.contains(target)) return; + const name = target.getAttribute('data-point-input'); + const targetId = target.getAttribute('data-point-target') ?? undefined; + // Each target has one coordinate owner. A second contact on it must not + // steal the gesture; other authored targets can be manipulated independently. + if ([...captures.values()].some(entry => entry.name === name && entry.target === targetId)) return; + const p = coordinates(event); + if (!p) return; + event.preventDefault(); scene.focus(); + act(() => { + scene.setPointerCapture(event.pointerId); + const entry = {name, target:targetId, source:`scene:${event.pointerId}`, pointerType:event.pointerType, x:p.x, y:p.y}; + captures.set(event.pointerId, entry); + try { runtime().point(name, 'down', p.x, p.y, entry.source, entry.target); } + catch (error) { captures.delete(event.pointerId); release(event.pointerId); throw error; } + }); + }); + scene.addEventListener('pointermove', event => { + const entry = captures.get(event.pointerId); + if (!entry) return; + const p = coordinates(event); + if (!p) { finish(event, 'cancel'); return; } + event.preventDefault(); + entry.x = p.x; entry.y = p.y; + act(() => runtime().point(entry.name, 'move', p.x, p.y, entry.source, entry.target)); + }); + // A host may deliver release outside the SVG even while it owns capture. + // Observe termination before target handlers or capture cleanup can cancel it. + events.addEventListener('pointerup', event => finish(event, 'up'), true); + events.addEventListener('pointercancel', event => finish(event, 'cancel'), true); + events.addEventListener('mouseup', event => { + if (event.button !== 0) return; + // Mouse-only fallback when the host delivers mouseup without pointerup. + // A normal pointerup already removed the owner; touch compatibility events + // must never finish a different contact. + for (const [id, entry] of captures) { + if (entry.pointerType === 'mouse') finish(event, 'up', id); + } + }, true); + scene.addEventListener('lostpointercapture', event => { + // Descendant capture transitions bubble here but do not release our owner. + if (event.target === scene) finish(event, 'cancel'); + }); + return Object.freeze({ + releaseCaptures() { + const ids = [...captures.keys()]; + captures.clear(); + for (const id of ids) release(id); + // The caller subsequently invokes runtime.releaseAll(), which dispatches + // each pending cancel once. Clearing first makes lostcapture harmless. + }, + }); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/sources/workspace.test.mjs b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/workspace.test.mjs new file mode 100644 index 0000000..799f056 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/sources/workspace.test.mjs @@ -0,0 +1,91 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = factor => `state x = 10 +input next = KeyN +text label(0, 0, x * ${factor}) +on next.down { x = x + 0.25 } +`; + +function begin(core, program, preserve) { + assert.equal(core.begin_program(program.states.length, program.inputs.length), 0); + program.states.forEach((state, i) => assert.equal(core.put_state(i, state.initial, preserve ? i : -1), 0)); +} + +function finish(core, program, during = () => {}) { + for (const op of program.instructions) { + assert.equal(core.put_instruction(op.op, op.arg, op.value), 0); + during(); + } + for (const handler of program.handlers) + assert.equal(core.put_handler(handler.event, handler.start, handler.length), 0); + assert.equal(core.commit_program(), 0); +} + +function observe(core, binding) { + core.begin_evaluation(100); + const value = core.eval_bounded(binding.start, binding.length); + assert.equal(core.evaluation_info(0), 0); + return value; +} + +test('Wasm executes old events and output during staged replacement without corrupting either phase', async () => { + const {instance:{exports:core}} = await WebAssembly.instantiate(wasm); + core.reset_all(); + const old = compile(source(2)), next = compile(source(3)); + begin(core, old, false); finish(core, old); + assert.equal(core.configure_evaluation(32, 8), 0); + const binding = old.views[0].bindings.value; + assert.equal(observe(core, binding), 20); + assert.equal(observe(core, binding), 20); + assert.equal(core.evaluation_info(3), 1); + begin(core, next, true); + let value = 10; + finish(core, next, () => { + assert.equal(core.evaluation_info(9), 0); + assert.equal(core.dispatch(1, 0), 0); value += 0.25; + assert.equal(core.configure_evaluation(32, 8), 0); + core.forget_evaluation(3); + assert.equal(observe(core, binding), value * 2); + assert.equal(core.evaluation_info(3), 0); + }); + assert.equal(core.evaluation_info(9), 1); + assert.equal(core.get_state(0), value); + const replaced = next.views[0].bindings.value; + assert.equal(observe(core, replaced), value * 3); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.evaluation_info(3), 1); + + const revision = core.info(4); + begin(core, old, true); + assert.equal(core.put_instruction(0, 0, Infinity), 5); + assert.equal(core.commit_program(), 5); + assert.equal(observe(core, replaced), value * 3); + core.abort_program(); + assert.equal(core.info(4), revision); + assert.equal(core.get_state(0), value); + assert.equal(core.evaluation_info(9), 1); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.memory.buffer.byteLength, 65536); +}); + +test('workspace accounting reports shared payload and safety tag without double-counting', async () => { + const runtime = await createRuntime(wasm, {cacheEntries:32}); + runtime.apply(source(2)); runtime.snapshot(); + const stats = runtime.evaluationStats(), memory = runtime.snapshot().memory; + assert.equal(stats.dedicatedBytes, 48); + assert.equal(stats.sharedBytes, 1152); + assert.equal(stats.workspaceTagBytes, 8); + assert.equal(stats.reservedBytes, stats.dedicatedBytes + stats.sharedBytes); + const restoreStorage = memory.stagingReservedBytes - memory.programReservedBytes; + assert.equal(restoreStorage, 264); // 32 values + mask/alignment, only in pending storage. + assert.equal(memory.reservedEngineBytes, 19072 + stats.dedicatedBytes + stats.workspaceTagBytes + restoreStorage + 2 * 64 * (memory.handlerBytes - 12) + 2 * 80); + // Each program adds 24 three-byte routes, count/mask and alignment (80 B). + assert.equal(memory.pointerTargetBytes, 3); + assert(stats.sharedBytes < memory.stagingReservedBytes); + assert.equal(stats.reuseAvailable, true); +}); diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.json b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.json new file mode 100644 index 0000000..18c5013 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.json @@ -0,0 +1,246 @@ +[ + { + "label": "start", + "log": [ + { + "type": "pointerdown", + "id": 2, + "x": 808.8055419921875, + "y": 133.63368225097656 + }, + { + "type": "pointerdown", + "id": 3, + "x": 1014.3958129882812, + "y": 174.75173950195312 + } + ], + "held": [ + "drag:green_box", + "drag:blue_box" + ], + "states": { + "selected": 2, + "green.x": 80, + "green.y": 120, + "green.ox": 9.999986805438311, + "green.oy": 10.00000164932024, + "green.sx": 80, + "green.sy": 120, + "green.held": 1, + "green.id": 1, + "blue.x": 280, + "blue.y": 160, + "blue.ox": 9.999980208157467, + "blue.oy": 10.00000329864045, + "blue.sx": 280, + "blue.sy": 160, + "blue.held": 1, + "blue.id": 2, + "amber.x": 450, + "amber.y": 220, + "amber.ox": 0, + "amber.oy": 0, + "amber.sx": 450, + "amber.sy": 220, + "amber.held": 0, + "amber.id": 3 + } + }, + { + "label": "move", + "log": [ + { + "type": "pointerdown", + "id": 2, + "x": 808.8055419921875, + "y": 133.63368225097656 + }, + { + "type": "pointerdown", + "id": 3, + "x": 1014.3958129882812, + "y": 174.75173950195312 + }, + { + "type": "pointermove", + "id": 3, + "x": 1086.3524169921875, + "y": 226.14930725097656 + }, + { + "type": "pointermove", + "id": 2, + "x": 880.7621459960938, + "y": 185.03125 + } + ], + "held": [ + "drag:green_box", + "drag:blue_box" + ], + "states": { + "selected": 2, + "green.x": 150.00000659728084, + "green.y": 169.9999983506798, + "green.ox": 9.999986805438311, + "green.oy": 10.00000164932024, + "green.sx": 80, + "green.sy": 120, + "green.held": 1, + "green.id": 1, + "blue.x": 350.00000659728073, + "blue.y": 209.9999983506798, + "blue.ox": 9.999980208157467, + "blue.oy": 10.00000329864045, + "blue.sx": 280, + "blue.sy": 160, + "blue.held": 1, + "blue.id": 2, + "amber.x": 450, + "amber.y": 220, + "amber.ox": 0, + "amber.oy": 0, + "amber.sx": 450, + "amber.sy": 220, + "amber.held": 0, + "amber.id": 3 + } + }, + { + "label": "remaining blue", + "log": [ + { + "type": "pointerdown", + "id": 2, + "x": 808.8055419921875, + "y": 133.63368225097656 + }, + { + "type": "pointerdown", + "id": 3, + "x": 1014.3958129882812, + "y": 174.75173950195312 + }, + { + "type": "pointermove", + "id": 3, + "x": 1086.3524169921875, + "y": 226.14930725097656 + }, + { + "type": "pointermove", + "id": 2, + "x": 880.7621459960938, + "y": 185.03125 + }, + { + "type": "pointerup", + "id": 2, + "x": 880.7621459960938, + "y": 185.03125 + } + ], + "held": [ + "drag:blue_box" + ], + "states": { + "selected": 2, + "green.x": 150.00000659728084, + "green.y": 169.9999983506798, + "green.ox": 9.999986805438311, + "green.oy": 10.00000164932024, + "green.sx": 80, + "green.sy": 120, + "green.held": 0, + "green.id": 1, + "blue.x": 350.00000659728073, + "blue.y": 209.9999983506798, + "blue.ox": 9.999980208157467, + "blue.oy": 10.00000329864045, + "blue.sx": 280, + "blue.sy": 160, + "blue.held": 1, + "blue.id": 2, + "amber.x": 450, + "amber.y": 220, + "amber.ox": 0, + "amber.oy": 0, + "amber.sx": 450, + "amber.sy": 220, + "amber.held": 0, + "amber.id": 3 + } + }, + { + "label": "move remaining blue", + "log": [ + { + "type": "pointerdown", + "id": 2, + "x": 808.8055419921875, + "y": 133.63368225097656 + }, + { + "type": "pointerdown", + "id": 3, + "x": 1014.3958129882812, + "y": 174.75173950195312 + }, + { + "type": "pointermove", + "id": 3, + "x": 1086.3524169921875, + "y": 226.14930725097656 + }, + { + "type": "pointermove", + "id": 2, + "x": 880.7621459960938, + "y": 185.03125 + }, + { + "type": "pointerup", + "id": 2, + "x": 880.7621459960938, + "y": 185.03125 + }, + { + "type": "pointermove", + "id": 3, + "x": 1127.470458984375, + "y": 256.98785400390625 + } + ], + "held": [ + "drag:blue_box" + ], + "states": { + "selected": 2, + "green.x": 150.00000659728084, + "green.y": 169.9999983506798, + "green.ox": 9.999986805438311, + "green.oy": 10.00000164932024, + "green.sx": 80, + "green.sy": 120, + "green.held": 0, + "green.id": 1, + "blue.x": 389.99999340271904, + "blue.y": 240.00000329864045, + "blue.ox": 9.999980208157467, + "blue.oy": 10.00000329864045, + "blue.sx": 280, + "blue.sy": 160, + "blue.held": 1, + "blue.id": 2, + "amber.x": 450, + "amber.y": 220, + "amber.ox": 0, + "amber.oy": 0, + "amber.sx": 450, + "amber.sy": 220, + "amber.held": 0, + "amber.id": 3 + } + } +] diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.log b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.log new file mode 100644 index 0000000..1a55063 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-evidence.log @@ -0,0 +1,12 @@ +### Error +dialog.accept: Cannot accept dialog which is already handled! + at _Page. (evalmachine.:2:25) +### Result +[{"label":"start","log":[{"type":"pointerdown","id":2,"x":808.8055419921875,"y":133.63368225097656},{"type":"pointerdown","id":3,"x":1014.3958129882812,"y":174.75173950195312}],"held":["drag:green_box","drag:blue_box"],"states":{"selected":2,"green.x":80,"green.y":120,"green.ox":9.999986805438311,"green.oy":10.00000164932024,"green.sx":80,"green.sy":120,"green.held":1,"green.id":1,"blue.x":280,"blue.y":160,"blue.ox":9.999980208157467,"blue.oy":10.00000329864045,"blue.sx":280,"blue.sy":160,"blue.held":1,"blue.id":2,"amber.x":450,"amber.y":220,"amber.ox":0,"amber.oy":0,"amber.sx":450,"amber.sy":220,"amber.held":0,"amber.id":3}},{"label":"move","log":[{"type":"pointerdown","id":2,"x":808.8055419921875,"y":133.63368225097656},{"type":"pointerdown","id":3,"x":1014.3958129882812,"y":174.75173950195312},{"type":"pointermove","id":3,"x":1086.3524169921875,"y":226.14930725097656},{"type":"pointermove","id":2,"x":880.7621459960938,"y":185.03125}],"held":["drag:green_box","drag:blue_box"],"states":{"selected":2,"green.x":150.00000659728084,"green.y":169.9999983506798,"green.ox":9.999986805438311,"green.oy":10.00000164932024,"green.sx":80,"green.sy":120,"green.held":1,"green.id":1,"blue.x":350.00000659728073,"blue.y":209.9999983506798,"blue.ox":9.999980208157467,"blue.oy":10.00000329864045,"blue.sx":280,"blue.sy":160,"blue.held":1,"blue.id":2,"amber.x":450,"amber.y":220,"amber.ox":0,"amber.oy":0,"amber.sx":450,"amber.sy":220,"amber.held":0,"amber.id":3}},{"label":"remaining blue","log":[{"type":"pointerdown","id":2,"x":808.8055419921875,"y":133.63368225097656},{"type":"pointerdown","id":3,"x":1014.3958129882812,"y":174.75173950195312},{"type":"pointermove","id":3,"x":1086.3524169921875,"y":226.14930725097656},{"type":"pointermove","id":2,"x":880.7621459960938,"y":185.03125},{"type":"pointerup","id":2,"x":880.7621459960938,"y":185.03125}],"held":["drag:blue_box"],"states":{"selected":2,"green.x":150.00000659728084,"green.y":169.9999983506798,"green.ox":9.999986805438311,"green.oy":10.00000164932024,"green.sx":80,"green.sy":120,"green.held":0,"green.id":1,"blue.x":350.00000659728073,"blue.y":209.9999983506798,"blue.ox":9.999980208157467,"blue.oy":10.00000329864045,"blue.sx":280,"blue.sy":160,"blue.held":1,"blue.id":2,"amber.x":450,"amber.y":220,"amber.ox":0,"amber.oy":0,"amber.sx":450,"amber.sy":220,"amber.held":0,"amber.id":3}},{"label":"move remaining blue","log":[{"type":"pointerdown","id":2,"x":808.8055419921875,"y":133.63368225097656},{"type":"pointerdown","id":3,"x":1014.3958129882812,"y":174.75173950195312},{"type":"pointermove","id":3,"x":1086.3524169921875,"y":226.14930725097656},{"type":"pointermove","id":2,"x":880.7621459960938,"y":185.03125},{"type":"pointerup","id":2,"x":880.7621459960938,"y":185.03125},{"type":"pointermove","id":3,"x":1127.470458984375,"y":256.98785400390625}],"held":["drag:blue_box"],"states":{"selected":2,"green.x":150.00000659728084,"green.y":169.9999983506798,"green.ox":9.999986805438311,"green.oy":10.00000164932024,"green.sx":80,"green.sy":120,"green.held":0,"green.id":1,"blue.x":389.99999340271904,"blue.y":240.00000329864045,"blue.ox":9.999980208157467,"blue.oy":10.00000329864045,"blue.sx":280,"blue.sy":160,"blue.held":1,"blue.id":2,"amber.x":450,"amber.y":220,"amber.ox":0,"amber.oy":0,"amber.sx":450,"amber.sy":220,"amber.held":0,"amber.id":3}}] +### Ran Playwright code +```js +await (async page => await page.evaluate(() => window.touchDebug))(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +- Page Title: Pure · 빈 작업물에서 시작하기 diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.js b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.js new file mode 100644 index 0000000..11ddd85 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.js @@ -0,0 +1,21 @@ +async page => { + page.on('dialog',d=>d.accept()); + await page.reload();await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('routed-layout');await page.locator('#load-example').click();await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + await page.evaluate(()=>{window.pointerLog=[];for(const type of ['pointerdown','pointerup','pointercancel','pointermove'])window.addEventListener(type,e=>window.pointerLog.push({type,id:e.pointerId,x:e.clientX,y:e.clientY}),true);}); + const session=await page.context().newCDPSession(page); + const xy=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y}},{x,y}); + const results=[];const snap=async label=>results.push({label,...await page.evaluate(()=>{const s=window.pureWorkbench.snapshot();return{log:window.pointerLog.slice(),held:s.held,states:s.states};})}); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await xy(90,130)},p2={id:2,...await xy(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]});await snap('start'); + const m1={id:1,...await xy(160,180)},m2={id:2,...await xy(360,220)}; + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[m1,m2]});await snap('move'); + await session.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[m1]});await snap('remaining blue'); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:2,...await xy(400,250)}]});await page.waitForFunction(()=>Math.abs(window.pureWorkbench.snapshot().states['blue.x']-390)<0.1);await snap('move remaining blue'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach(); + await page.evaluate(e=>window.touchDebug=e,results);return results; +} diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.log b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.log new file mode 100644 index 0000000..399b467 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-reproduction.log @@ -0,0 +1,28 @@ +### Ran Playwright code +```js +await (async page => { + page.on('dialog',d=>d.accept()); + await page.reload();await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('routed-layout');await page.locator('#load-example').click();await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + await page.evaluate(()=>{window.pointerLog=[];for(const type of ['pointerdown','pointerup','pointercancel'])window.addEventListener(type,e=>window.pointerLog.push({type,id:e.pointerId}),true);}); + const session=await page.context().newCDPSession(page); + const xy=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y}},{x,y}); + const results=[];const snap=async label=>results.push({label,...await page.evaluate(()=>{const s=window.pureWorkbench.snapshot();return{log:window.pointerLog.slice(),held:s.held,states:s.states};})}); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await xy(90,130)},p2={id:2,...await xy(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]});await snap('start'); + const m1={id:1,...await xy(160,180)},m2={id:2,...await xy(360,220)}; + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[m1,m2]});await snap('move'); + await session.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[m1]});await snap('remaining blue'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach(); + await page.evaluate(e=>window.touchDebug=e,results);return results; +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +### Modal state +- ["beforeunload" dialog with message ""]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/touch-run.log b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-run.log new file mode 100644 index 0000000..06bee36 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/touch-run.log @@ -0,0 +1,29 @@ +### Ran Playwright code +```js +await (async page => { + page.on('dialog',d=>d.accept()); + await page.reload();await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('routed-layout');await page.locator('#load-example').click();await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + await page.evaluate(()=>{window.pointerLog=[];for(const type of ['pointerdown','pointerup','pointercancel','pointermove'])window.addEventListener(type,e=>window.pointerLog.push({type,id:e.pointerId,x:e.clientX,y:e.clientY}),true);}); + const session=await page.context().newCDPSession(page); + const xy=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y}},{x,y}); + const results=[];const snap=async label=>results.push({label,...await page.evaluate(()=>{const s=window.pureWorkbench.snapshot();return{log:window.pointerLog.slice(),held:s.held,states:s.states};})}); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await xy(90,130)},p2={id:2,...await xy(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]});await snap('start'); + const m1={id:1,...await xy(160,180)},m2={id:2,...await xy(360,220)}; + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[m1,m2]});await snap('move'); + await session.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[m1]});await snap('remaining blue'); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:2,...await xy(400,250)}]});await page.waitForFunction(()=>Math.abs(window.pureWorkbench.snapshot().states['blue.x']-390)<0.1);await snap('move remaining blue'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach(); + await page.evaluate(e=>window.touchDebug=e,results);return results; +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +### Modal state +- ["beforeunload" dialog with message ""]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-pointer-routing/verification.json b/prototypes/workbench/results/2026-09-23-pointer-routing/verification.json new file mode 100644 index 0000000..1f5189a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer-routing/verification.json @@ -0,0 +1,13 @@ +{ + "nodeTests": 83, + "nativeTests": 35, + "command": "node --test prototypes/workbench/*.test.mjs", + "nodeLog": "node-tests.log", + "sourceSha256": "56b8db87bcf7e49ab2b2e8d4166ade6ed04c77571fbb69c60633e76376b46976", + "wasmSha256": "b494269c1332f71de9725a4c7f0b58c074b669403d5d9dc95ddfeb7b16967c0d", + "browserChecks": { + "grouped": 30, + "authoring": 44, + "legacyPointers": 23 + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/README.md b/prototypes/workbench/results/2026-09-23-pointer/README.md new file mode 100644 index 0000000..5185e35 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/README.md @@ -0,0 +1,23 @@ +# Pointer input verification — 2026-09-23 + +Functional validation, not a speed or memory comparison. + +- `build.json`: exact source/Wasm hashes and 26 passing native tests. +- `verification.json`: 59 passing Node tests; each suite runs with child-process support. +- `pointer-browser.json`: 26 direct manipulation checks, no page errors. +- `authoring-browser.json`: 44 existing authoring checks, no page errors. +- `before/`, `after/`, `manifest.json`: source artifacts and SHA-256 identities. +- `qa-pointer.js`, `qa-browser.js`: executed browser scripts. The authoring script + changes only artifact path prefixes to avoid overwriting earlier evidence. +- `pure-pointer-desktop.png`, `pure-pointer-mobile.png`: actual browser captures. + +Build using Zig 0.15.2 with `python3 prototypes/workbench/manage.py build --zig PATH`. +Run `node --test prototypes/workbench/*.test.mjs` with Node child-process support. +Serve with `python3 prototypes/workbench/manage.py serve --port 8766` and execute +`qa-pointer.js` as a Playwright CLI run-code expression against Chromium. +Browser tests exercise real mouse input and CDP touch emulation, including two +contacts. Blur/single-pointer cancellation use explicit event injection. They do +not validate physical touchscreen hardware or every browser engine. + +Test runner elapsed times and build artifact sizes are incidental reproducibility +metadata, not performance conclusions. No throughput/latency benchmark was run. diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/CONTRACT.md b/prototypes/workbench/results/2026-09-23-pointer/after/CONTRACT.md new file mode 100644 index 0000000..3de96b3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/CONTRACT.md @@ -0,0 +1,308 @@ +# Workbench experiment contract + +An author starts with empty source. There is no built-in player, physics, jump, +score, or timer behavior. All behavior is declared by the author. This is a +bounded scalar interaction language experiment, not a general language claim. + +## Source language + +```text +state count = 0 +input tap = Space +box tile(80, 150, 60, 60, "#356548") +circle dot(200, 180, 20, "#c78638") +text label(80, 100, count, "Count: ") +on tap.down { + count = count + 1 +} +on frame { + if count > 20 { + count = 0 + } +} +``` + +Declarations and statements end at a newline or semicolon; `#` comments outside +strings. ASCII identifiers, finite numeric literals, state initializers must be +numeric constants (optional unary sign). Input key codes accepted: +Space, Enter, ArrowLeft/Right/Up/Down, KeyA..KeyZ, Digit0..Digit9. Input names +must be unique, key codes unique, names cannot shadow `frame`, `dt`, `min`, `max`, +`clamp`, `event_x`, `event_y`. State and input names use separate namespaces, entity IDs unique. + +Expressions: parentheses, state references, dt (seconds on frame, zero on input), +unary - and !, + - * /, < <= > >= == !=, && ||, min(a,b), max(a,b), clamp(x,lo,hi). +Logical operators evaluate both sides. Division by zero/non-finite results fail +the whole event without committing state changes. No loops, recursion or eval. +An if without else executes its condition once; assignment statements are ordered. +Multiple handlers for the same event execute in source order within one atomic event. +`dt` is allowed in handlers, not view bindings. A frame always uses 1/60 seconds. + +box: x,y,width,height expressions + literal hex color; circle: x,y,radius + color; +text: x,y,value expression + optional literal prefix. Values are world units in a +640x360 view. No implicit clipping/collision/physics. Render expressions are pure. +Unknown identifiers and invalid syntax include 1-based line+column diagnostics. + +Initial scene is empty. Optional examples are explicit insertions, never auto-run. +Successful apply preserves scalar states by exact name; new names use initializers, +removed names disappear. Apply/reset are separate. Failed parse/load changes nothing. +Apply is refused while any input is held; release first. This avoids inventing +release events across changed mappings. Focus loss sends .up to ordinary buttons +and .cancel to pointer inputs. + +## Compiler interface (web/compiler.mjs, browser and Node) + +`compile(source)` returns: +``` +{ + states:[{name,initial,line}], inputs:[{name,key,line} | {name,target,line}], + instructions:[{op,arg,value,line}], handlers:[{event,start,length,line}], + views:[{id,type,line,color?,prefix?,bindings:{x:{start,length},...}}], + metrics:{...} +} +``` +Bindings: box x/y/width/height; circle x/y/radius; text x/y/value. +Compiler throws an Error with .line and .column. Empty source is a valid empty program. +Handlers and binding expressions all occupy instructions (maximum 512 total), but +only handlers execute on events; bindings are evaluated on demand by VM eval_at. +Limits: 32 states, 8 inputs, 24 views, 64 handlers, 512 instructions, stack 64, +source 32768 UTF-8 bytes. State IDs follow declaration order. Input event IDs: +frame=0; input i down=1+2*i, up=2+2*i, move=17+i, cancel=25+i. +Move/cancel handlers require a pointer input in source. States may be referenced before declaration; +symbols must be resolved after gathering top-level declarations. `metrics` can +record source bytes/lines and instruction/handler/view/state counts. + +## VM opcodes + +| op | operation | +|---:|---| +|0| push constant `value` | +|1| push state `arg` | +|2| push dt | +|3..6| add, subtract, multiply, divide | +|7| negate | +|8..13| less, less-equal, greater, greater-equal, equal, not-equal | +|14..16| AND, OR, NOT (nonzero true) | +|17..19| min, max, clamp (push x,lo,hi; invalid lo>hi fails) | +|20| store state `arg` (pop) | +|21| jump-if-zero to absolute instruction index `arg` (pop), forward only | +|22,23| push current pointer event x,y; fail without point context | + +Rules start with an empty stack and must end with empty stack. Bindings must finish +with exactly one value and cannot STORE, branch, or read dt/event coordinates. Branch targets may be handler end. + +## Zig exported ABI + +`reset_all()` clears programs and state. `begin_program(state_count,input_count)` +starts staging, returns0 success; `put_state(id,initial,old_id)` old_id=-1 means init; +`put_instruction(op,arg,value)` append; `put_handler(event,start,length)` append; +`commit_program()` all0 success otherwise error code, atomic; candidate is separate +from the active program, but shares its workspace with optional evaluation records. +Preserved IDs read active values only at successful commit. Validate limits, +indices, finite values and forward branch ranges; runtime stack errors are checked. +`reset_state()` restores active initializers, resets tick/event diagnostics. +`dispatch(event,dt)` accepts0..16 and finite dt; event0 dt must equal1/60, input dt0; +returns0 success, other code failure with no state mutation. Tick increments for +successful frame; one event runs all matching handlers in source order. +`dispatch_point(event,x,y)` accepts existing input down/up IDs plus move/cancel IDs +within input_count, never frame0. Coordinates must be finite; dt is implicitly0. +The core only knows input count, not host device kinds. The compiler and host enforce +pointer ownership and device mapping. Calling legacy dispatch on a coordinate-reading +handler fails with bad_event9. Point operands are ephemeral execution context, not +extra persistent state slots. Both dispatch paths use the same atomic commit/history. +The host requires this new export; older Wasm must be rebuilt. +`eval_at(start,length)` returns f64 or NaN on invalid/pure-expression failure; +must not mutate state or event diagnostics. `get_state(id)` returns f64 orNaN. +`info(id)` u32: 0 tick,1 last_error,2 error_pc (0xffffffff none),3 last_executed_ops, +4 revision,5 state_count,6 instruction_count,7 handler_count,8 input_count, +9 successful_event_count. `layout(id)` u32: 0 Engine reserved @sizeOf, +1 Program reserved @sizeOf,2 Instruction @sizeOf,3 Handler @sizeOf, +4 active used IR bytes (instructions+handlers),5 active state-value bytes, +6 staging reserved bytes (subset of Engine),7 evaluation stack capacity bytes. +`error_message` belongs to host map; core error codes documented in core.zig. +No allocation or imported runtime in Wasm. Fixed64KiB linear memory target with +16KiB stack; increase only if actual linker evidence requires and document it. + +## Host interface (web/runtime.mjs) + +`await createRuntime(wasmBytes?, evaluationOptions?)` fetches core.wasm in browser if omitted. +Returns object with apply(source), reset(), frame(count=1), down(name,source), +up(name,source), point(name,phase,x,y,source), releaseAll(), snapshot(), exportProject(), currentProgram. +Multiple sources per input share one logical held state; down emits only first +source, up only last. Source strings identify keyboard vs onscreen pointer. +`apply` returns compiled program or throws with diagnostic. It must fully validate +in host and stage+commit in core. It preserves by name. `reset` keeps active program. +`snapshot` returns {states:{name:value}, views:[{id,type,x,y,width?,height?,radius?, +value?,prefix?,color?}], tick,revision,lastError,errorLine,lastOps,memory:{...},held:[]} +`currentProgram` is the latest successfully applied compiled program or null. +The program and all nested metadata are frozen so readers cannot corrupt state +name mappings or binding ranges. `lastFailure` records the last failed event; +its `lastError`/line remain visible through successful release or focus cleanup +until successful apply or explicit reset. `lastEvent` still describes the latest +event. A view evaluation failure uses host error100 and does not roll back events. +UI draft and applied code are distinct. Browser/CLI share compiler/runtime/wasm; +instances are separate, not a live collaborative session. No arbitrary JS evaluation. + +## Bounded display evaluation (2026-09-22) + +The existing event and float semantics above are unchanged. The forgetful C +prototype's contract is adapted into `evaluation.zig`; its uint64 DAG is not +used as a substitute for this VM's float instructions. + +Each snapshot starts one display budget, shared by all bindings. Default options +are `{cacheEntries:0, historyEntries:8, workLimit:2048}`. A unit is a binding +request, a candidate-cache probe, a change-record probe, or an executed VM +instruction. State extraction, host object creation, event dispatch and DOM work +are outside this budget. It is not a time limit or a bound on whole-app work. +For the current compiler limits, 96 bindings * 10 units plus 512 instructions +is at most 1,472 units; the default admits even an uncached valid scene with +cache lookup/validation enabled. Malformed bindings may fail sooner. + +`configureEvaluation({cacheEntries?,historyEntries?,workLimit?})` accepts integers +0..32, 0..8, and 0..4294967295 respectively. Missing fields retain prior settings. +Valid configuration clears reuse records; invalid configuration changes nothing. +`forgetEvaluation('values'|'history'|'all')` drops optional records without +changing authored state, event diagnostics or program identity. +`evaluationStats()` returns the options plus work, instructions, cacheHits for +the last snapshot, and reservedBytes for the complete fixed evaluation structure. +Statistics are separate from snapshots so repeated observations remain equivalent. + +Memo entries contain the value, instruction range, input dependency bit mask +and validated revision. Direct mapping uses one candidate probe, with collisions +causing recomputation. Only successful computations are cached. Constant results +need no input-change history. All other reuse requires a complete retained +history since validation and no intersection with changed states. Dependencies +are collected from actual VM LOADs, not trusted compiler-supplied annotations. +History loss causes recomputation. All history and memo storage is fixed size; +setting capacities to zero does not return reserved linear memory. + +Only bitwise-different committed states advance change history (including signed +zero). Failed events and net-zero updates do not. Successful apply and reset +discard all reuse records. Parse/host validation failure before staging preserves +records. Beginning core staging discards them, including if that load is later +rejected or aborted. Epoch wrap also clears records. No inputs or effectful event +handlers are replayed to rebuild a binding. + +Budget exhaustion returns an empty views array for the entire snapshot, with +displayError `{code:101,line,message}`. Successfully committed event state remains. +There is no automatic retry or silent return of an old scene. Raising the budget +allows a fresh attempt. Arithmetic/size errors retain the earlier per-view error100 +behavior. `displayError` is null on display success; a latched execution error +still takes precedence in lastError/errorMessage, while displayError independently +reports display failure. Partial successful binding memos may survive a failed +snapshot but no partial scene is published. + +New core exports: `configure_evaluation(cache,history)` returns 0 or capacity1; +`forget_evaluation(flags)` bits 1=values, 2=history; `begin_evaluation(limit)`; +`eval_bounded(start,length)` returns f64 or NaN and consumes that shared budget. +`evaluation_info(id)` returns 0 status (10=work limit), 1 work, 2 instructions, +3 cache hits, 4 cache capacity, 5 history capacity, 6 reserved bytes. These +diagnostics do not replace event info. The unbounded pure `eval_at` remains for +reference checks and is not the host snapshot path. + +## Shared load/reuse workspace (2026-09-23) + +The pending Program and evaluation Tables occupy one untagged union. The active +Program, current state and evaluation control fields remain separate. All core +calls are single-threaded and synchronous; no callback can reenter a phase change. +No pointer to the reused tables is retained between calls. + +`begin_program` switches the workspace to staging and drops optional records. +Until commit/abort, old events and bindings still run against active. Binding +evaluation bypasses reuse and recomputes within its budget; cache configuration, +forget and reset must not write the staging payload. Old-id state preservation +reads the current values at successful commit, including events/reset during load. + +Successful commit publishes the new Program and then initializes empty Tables in +the shared workspace. `abort_program()` discards a pending definition and restores +empty Tables without changing active program/state/event diagnostics. Inactive +abort is a no-op. A failed commit leaves staging open for explicit abort or the +next begin. An invalid begin closes any previous pending load. Host apply aborts +after a core loading failure. Lost reuse may make a previously warm tiny budget +insufficient; neither a preserved cache nor completion at that budget is promised. + +`evaluation_info` additionally reports 7 dedicated control bytes, 8 shared Tables +bytes, 9 reuse available (false during load), and 10 union safety-tag overhead. +The host exposes these as dedicatedBytes, sharedBytes, reuseAvailable and +workspaceTagBytes. reservedBytes is control + Tables capacity, not incremental +Engine storage. The Tables capacity overlaps stagingReservedBytes and must not +be added to it. In the current Wasm build the control is 48 B, shared Tables +1,152 B, union overhead 8 B. The Engine is 19,128 B, only 56 B larger than the +19,072 B pre-evaluation core; linear memory remains 65,536 B. No OS pages are +returned and this is not a measurement of browser/process memory. + +## Direct output publication (2026-09-23) + +`present(target)` evaluates the same bindings, in the same order and shared +budget as `snapshot()`, then calls a synchronous trusted output adapter: + +- `begin(program)`: the frozen current definition, or null at startup. +- `state(index, value)`: each current authored state, without a host state map. +- `view(index, valid)`: every declared view's publication status. +- `value(viewIndex, bindingName, value)`: each binding of a valid view only. +- `end(visibleCount)`: number of valid views. + +It returns the snapshot's diagnostics, held inputs, event information and memory +report, without `states` or `views`. Evaluation finishes before the first adapter +callback. On budget error101 every view is invalid and no binding values are +published; committed state values are still published. Error100 invalidates only +the affected views, as before. No stale value is silently substituted. + +Runtime mutations, `snapshot()` and nested `present()` are rejected during +callbacks. Read-only program/export/statistics access remains available. Adapters +must be synchronous and must not throw. If one does throw, the exception +propagates and the runtime unlocks in a finally block; arbitrary external adapter +effects cannot be rolled back. A later call republishes every value. This is not +a transaction across arbitrary output devices or an untrusted plugin boundary. + +Evaluation uses a private reusable Float64Array(96) and Uint8Array(24), allocated +on first snapshot/presentation. Their payload totals 792 host bytes, outside the +64 KiB Wasm memory. Typed-array object overhead, binding-entry arrays built at +apply, and DOM destination references are additional host memory. The numeric +buffer is a publication barrier, not another authoritative state or public +mutable scene. `snapshot()` remains an independent object representation for +CLI/export/debugging, built only when explicitly requested. + +The browser calls `present()` for normal refreshes. Its output adapter binds DOM +destinations once per successful definition change and compares values with the +existing DOM before writing. Input refreshes reuse scene, state and cost elements. +Invalid views remain allocated but are hidden using SVG `display="none"`; recovery +reveals them. A definition change can replace all scene/state destinations. This +does not skip unchanged binding evaluation, eliminate all host allocations, or +provide measured process-memory/paint-latency savings. + +## Authored direct manipulation (2026-09-23) + +`input drag = pointer(tile)` binds a logical input to a declared view, including a +forward declaration. One input per target; it shares the existing 8-input limit +with keyboard inputs. `on drag.down/move/up/cancel` handlers may read `event_x` +and `event_y` in the same 640x360 world coordinates as output. They are unavailable +in keyboard/frame handlers and display bindings; copy to authored state to retain +them. There is no built-in drag constraint, snap, grab offset, or gesture rollback. +The direct.pure example authors these using ordinary assignments and expressions. + +Host `point` requires a pointer input, valid phase, finite coordinates and nonempty +source string. Each logical input has one owning source; different targets may +have independent active contacts. Same-owner repeated down is a no-op; another +owner's down throws. Move/up/cancel without that owner are no-ops. Down establishes +ownership before dispatch; up/cancel release it before dispatch, even if authored +arithmetic fails. An event failure rolls back that event's state writes, not the +whole gesture. `releaseAll` cancels owned pointers at their last delivered position, +then releases ordinary buttons. `reset` directly clears ownership and resets state. +Last point events include x,y. A failed cancel can leave authored state unchanged, +but cannot strand host ownership; the failure remains visible until reset/apply. + +Browser hit testing selects the declared target on primary-button pointerdown. +The stable SVG root captures that pointer, including movement/release outside the +scene. Client coordinates are transformed through the inverse SVG screen matrix. +Pointerup uses its own final coordinates. Pointercancel, lostpointercapture and +focus loss use the last delivered position and dispatch cancel, never fake up. +Missing/non-invertible coordinate transforms cancel an active gesture. A second +contact on the same target is ignored; contacts on different targets are independent. +Touch scenes disable native panning on the SVG. Mouse/touch share this adapter; +pen-specific pressure, tilt and button meanings are not implemented or validated. + +Every delivered pointermove executes synchronously; this layer adds no event queue, +coalescing, frame gating or newest-only drop policy. This is not a guarantee that +browsers expose every hardware sample. Keyboard commands and pointer coordinates +enter the same authored state/commit/output path. CLI `point NAME PHASE X Y` uses +that host API and core with source `terminal`; it does not replay browser hit testing. diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/cli.mjs b/prototypes/workbench/results/2026-09-23-pointer/after/cli.mjs new file mode 100644 index 0000000..835fd35 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/cli.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; +import { createInterface } from 'node:readline'; +import { createRuntime } from './web/runtime.mjs'; + +const help = `Pure authoring workbench · 같은 정의와 Zig 실행기 + load PATH .pure 파일을 적용 (같은 이름의 상태 보존) + down NAME 정의한 입력 누르기 + up NAME 정의한 입력 놓기 + point NAME PHASE X Y 좌표 입력 (down / move / up / cancel) + 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 적용한 정의를 저장 + 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))); + runtime.apply(args[0] ? await readFile(args[0], 'utf8') : ''); + 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) runtime.apply(await readFile(arg, 'utf8')); + 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 === '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/workbench/results/2026-09-23-pointer/after/cli.test.mjs b/prototypes/workbench/results/2026-09-23-pointer/after/cli.test.mjs new file mode 100644 index 0000000..8dbabd7 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/cli.test.mjs @@ -0,0 +1,97 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => { + const file = join(root, 'examples/direct.pure'); + const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]]) + runtime.point('drag', phase, x, y, 'terminal'); + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert.equal(result.snapshots.at(-1).states.x, 400); + assert.equal(result.snapshots.at(-1).states.y, 230); + assert.deepEqual(result.snapshots.at(-1).held, []); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/core.zig b/prototypes/workbench/results/2026-09-23-pointer/after/core.zig new file mode 100644 index 0000000..2e3fd75 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/core.zig @@ -0,0 +1,1027 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { event: u32 = 0, start: u32 = 0, length: u32 = 0 }; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Workspace = union { staging: Program, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .state_count = state_count, .input_count = input_count } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.initials[id] = initial; + engine.workspace.staging.old_ids[id] = old_id; + engine.workspace.staging.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 23) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) return .invalid; + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 23) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + break; + } + } + const h = owner orelse return .invalid; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + } + } + return .ok; +} + +export fn commit_program() u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const code = validate(&engine.workspace.staging); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.state_count) |i| { + const old_id = engine.workspace.staging.old_ids[i]; + next[i] = if (old_id < 0) engine.workspace.staging.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging; + engine.values = next; + finishStaging(); + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, point: bool = false }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23 => 0, + 7, 16, 20, 21 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20) { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[inst.arg] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + (context.point and event == 0) or (!context.point and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + const result = execute(&pending, h.start, h.length, context, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Program), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Program), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 12), layout(3)); + try std.testing.expectEqual(@as(u32, 76), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(layout(1), layout(6)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Program)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Program) + @alignOf(Program)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/evaluation.zig b/prototypes/workbench/results/2026-09-23-pointer/after/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/examples/direct.pure b/prototypes/workbench/results/2026-09-23-pointer/after/examples/direct.pure new file mode 100644 index 0000000..ecc54d5 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/examples/direct.pure @@ -0,0 +1,48 @@ +# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다. +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 { + x = clamp(event_x - grab_x, 20, 564) + y = clamp(event_y - grab_y, 100, 276) + 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/workbench/results/2026-09-23-pointer/after/pointer.test.mjs b/prototypes/workbench/results/2026-09-23-pointer/after/pointer.test.mjs new file mode 100644 index 0000000..1c0e43c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/pointer.test.mjs @@ -0,0 +1,110 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = await readFile(new URL('./examples/direct.pure', import.meta.url), 'utf8'); +const fresh = async () => { const r = await createRuntime(wasm, {cacheEntries:32}); r.apply(source); return r; }; + +test('pointer declarations resolve forward targets and reject ambiguous or contextless inputs', () => { + const p = compile(source); + assert.equal(p.inputs[0].target, 'tile'); + assert(p.handlers.some(h => h.event === 17)); assert(p.handlers.some(h => h.event === 25)); + assert(p.instructions.some(i => i.op === 22)); assert(p.instructions.some(i => i.op === 23)); + for (const [code, message] of [ + ['input p = pointer(missing)', /Unknown pointer target/], + ['input p=pointer(tile)\ninput q=pointer(tile)\ntext tile(0,0,0)', /Duplicate pointer/], + ['input p=Space\non p.move {}', /require a pointer/], + ['input p=Space\non p.cancel {}', /require a pointer/], + ['state x=0\non frame { x=event_x }', /only available/], + ['text v(event_y,0,0)', /only available/], + ['state event_x=0', /Reserved name/], + ]) assert.throws(() => compile(code), message); + const full = compile(Array.from({length:8}, (_, i) => `input p${i}=pointer(v${i})\ntext v${i}(0,0,0)\non p${i}.move {}\non p${i}.cancel {}`).join('\n')); + assert.deepEqual(full.handlers.map(h => h.event), Array.from({length:8}, (_, i) => [17+i,25+i]).flat()); +}); + +test('authored drag keeps grab offset, uses final up coordinates, and updates linked output', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'pen'); + assert.equal(r.snapshot().states.dragging, 1); + assert.throws(() => r.apply(''), /놓은 뒤/); + r.point('drag', 'move', 360, 220, 'pen'); + let s = r.snapshot(); + assert.equal(s.states.x, 350); assert.equal(s.states.y, 210); + assert.equal(s.views.find(v => v.id === 'region').value, 1); + r.point('drag', 'up', 410, 240, 'pen'); + s = r.snapshot(); + assert.equal(s.states.x, 400); assert.equal(s.states.y, 230); + assert.equal(s.states.dragging, 0); assert.deepEqual(s.held, []); + assert.equal(s.successfulEvents, 3); + assert.equal(s.lastEvent.x, 410); + r.down('left'); r.up('left'); + assert.equal(r.snapshot().states.x, 390); +}); + +test('ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'a'); + r.point('drag', 'down', 500, 500, 'a'); + assert.throws(() => r.point('drag', 'down', 90, 170, 'b'), /다른 포인터/); + r.point('drag', 'move', 400, 200, 'b'); + assert.equal(r.snapshot().successfulEvents, 1); + r.point('drag', 'move', 400, 200, 'a'); + r.releaseAll(); + const canceled = r.snapshot(); + assert.equal(canceled.states.x, 80); assert.equal(canceled.states.y, 160); + assert.equal(canceled.states.dragging, 0); assert.equal(canceled.lastEvent.phase, 'cancel'); + assert.deepEqual(canceled.held, []); + r.point('drag', 'cancel', 1, 1, 'a'); r.point('drag', 'move', 500, 500, 'a'); r.releaseAll(); + assert.deepEqual(r.snapshot(), canceled); + r.point('drag', 'down', 90, 170, 'new'); r.reset(); + r.point('drag', 'up', 600, 500, 'new'); + assert.equal(r.snapshot().states.x, 80); assert.deepEqual(r.snapshot().held, []); +}); + +test('invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event', async () => { + const r = await fresh(); r.point('drag', 'down', 90, 170); + const before = r.snapshot(); + for (const invoke of [() => r.point('drag','move',NaN,1), () => r.point('drag','up',1,Infinity), + () => r.point('drag','unknown',1,2), () => r.point('left','down',1,2), + () => r.down('drag'), () => r.up('drag'), () => r.point('drag','move',1,2,'')]) assert.throws(invoke); + assert.deepEqual(r.snapshot(), before); + r.releaseAll(); + r.apply(source.replace('x = clamp(event_x - grab_x, 20, 564)', 'x = event_x\n y = 1 / 0')); + r.point('drag', 'down', 90, 170); + assert.equal(r.point('drag', 'move', 500, 220), 4); + let s = r.snapshot(); assert.equal(s.states.x, 80); assert.equal(s.states.y, 160); + assert.equal(s.lastFailure.phase, 'move'); + r.releaseAll(); s = r.snapshot(); assert.deepEqual(s.held, []); assert.equal(s.lastError, 4); + assert.equal(s.states.dragging, 0); +}); + +test('independent targets share the event pipeline and publication remains non-reentrant', async () => { + const r = await createRuntime(wasm); + r.apply('state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\ntext one(0,0,x)\ntext two(0,0,y)\non a.move { x=event_x }\non b.move { y=event_y }'); + r.point('a','down',1,2,'first'); r.point('b','down',3,4,'second'); + r.point('a','move',10,20,'first'); r.point('b','move',30,40,'second'); + assert.deepEqual(r.snapshot().states,{x:10,y:40}); + r.present({begin(){assert.throws(() => r.point('a','move',0,0,'first'), /출력 전달 중/);},state(){},view(){},value(){},end(){}}); + r.releaseAll(); assert.deepEqual(r.snapshot().held, []); +}); + +test('failing down, up and cancel never prevent terminal ownership cleanup', async () => { + for (const phase of ['down', 'up', 'cancel']) { + const r = await createRuntime(wasm); + r.apply(`state x=7\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.${phase} { x=event_x\nx=1/0 }`); + const code = r.point('p','down',10,20,'owner'); + assert.equal(code, phase === 'down' ? 4 : 0); + assert.deepEqual(r.snapshot().held, ['p']); + if (phase === 'up') assert.equal(r.point('p','up',30,40,'owner'), 4); + else assert.equal(r.releaseAll(), phase === 'cancel' ? 4 : 0); + const s = r.snapshot(); + assert.equal(s.states.x, 7); assert.equal(s.lastError, 4); + assert.equal(s.lastFailure.phase, phase); assert.deepEqual(s.held, []); + r.apply('state x=100'); // Cleanup also unblocks definition changes. + assert.equal(r.snapshot().states.x, 7); + } +}); diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/qa-pointer.js b/prototypes/workbench/results/2026-09-23-pointer/after/qa-pointer.js new file mode 100644 index 0000000..9a12cc4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/qa-pointer.js @@ -0,0 +1,120 @@ +// Playwright CLI run-code: real mouse / CDP touch input; no timing benchmark. +async (page) => { + const checks = [], errors = []; + const check = (condition, label) => { if (!condition) throw new Error(label); checks.push(label); }; + const close = (a, b) => Math.abs(a - b) < 0.1; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const client = (x, y) => page.evaluate(({x,y}) => { + const p = new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM()); + return {x:p.x,y:p.y}; + }, {x,y}); + const move = async (x,y) => { const p = await client(x,y); await page.mouse.move(p.x,p.y); }; + const start = async (x,y) => { await move(x,y); await page.mouse.down(); }; + const reset = async () => { await page.locator('#reset').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const apply = async source => { await page.locator('#source').fill(source); await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); page.on('dialog', onDialog); + let session; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('direct'); + await page.locator('#load-example').click(); + const source = await page.locator('#source').inputValue(); + check((await state()).views.length === 0, 'loading the pointer example leaves the running program unchanged until Apply'); + await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); + check(await page.locator('[data-point-input="drag"]').count() === 1, 'authored pointer declaration binds the visible target'); + check(await page.locator('#input-controls button').count() === 2, 'keyboard alternatives remain available without a fake pointer button'); + await page.evaluate(() => { + window.__pointerTile = document.querySelector('[data-point-input="drag"]'); + document.querySelector('#scene').addEventListener('pointerdown', e => { window.__pointerID = e.pointerId; }); + }); + await start(90,170); + let s = await state(); + check(s.states.dragging === 1 && s.held.includes('drag'), 'real mouse down reaches the authored rule'); + await move(370,230); s = await state(); + check(close(s.states.x,360) && close(s.states.y,220), 'drag retains grab offset in scene coordinates'); + check(s.views.find(v => v.id === 'region').value === 1 && await page.locator('[data-view-id="region"]').textContent() === '오른쪽 영역: 1', 'linked output updates when the authored boundary is crossed'); + check(await page.evaluate(() => window.__pointerTile === document.querySelector('[data-point-input="drag"]')), 'drag keeps its bound DOM target'); + // Physical mouse outside the SVG still reaches the stable captured scene. + await move(750,400); s = await state(); + check(close(s.states.x,564) && close(s.states.y,276), 'capture delivers movement outside the scene and authored clamp applies'); + await page.mouse.up(); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'release outside the scene ends the gesture once'); + await reset(); + await start(90,170); await move(380,240); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + s = await state(); + check(close(s.states.x,80) && close(s.states.y,160) && s.states.dragging === 0 && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'focus loss invokes authored rollback and clears ownership'); + const ended = s.successfulEvents; + await page.mouse.up(); + check((await state()).successfulEvents === ended, 'late mouse release after cancel does not dispatch a second terminal event'); + await start(90,170); await move(350,220); + await page.evaluate(() => document.querySelector('#scene').releasePointerCapture(window.__pointerID)); + await move(360,230); s = await state(); + check(close(s.states.x,80) && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'explicit capture loss cancels instead of committing a drop'); + await page.mouse.up(); + await start(90,170); await move(300,220); + await page.evaluate(() => document.querySelector('#scene').dispatchEvent(new PointerEvent('pointercancel', {pointerId:window.__pointerID,bubbles:true}))); + check((await state()).held.length === 0 && close((await state()).states.x,80), 'browser cancellation routes to the authored cancel handler'); + await page.mouse.up(); + // A source edit, not a renderer option, changes the movement constraint. + await apply(source.replaceAll('20, 564','20, 200')); + await start(90,170); await move(500,220); await page.mouse.up(); s = await state(); + check(close(s.states.x,200), 'editing the authored rule changes the drag limit'); + await apply(source + '\non drag.move { x = 1 / 0 }\n'); await reset(); + await start(90,170); await move(300,220); s = await state(); + check(s.lastError === 4 && close(s.states.x,80) && close(s.states.y,160), 'later handler failure rolls back all earlier movement writes'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); await page.mouse.up(); + s = await state(); + check(s.lastError === 4 && s.held.length === 0 && s.states.dragging === 0, 'cleanup releases a failed gesture while preserving its error'); + await apply(source); await reset(); + await page.screenshot({path:'output/playwright/pure-pointer-desktop.png',fullPage:true}); + + await page.setViewportSize({width:390,height:844}); + await page.locator('#scene').scrollIntoViewIfNeeded(); + session = await page.context().newCDPSession(page); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const touch = async (type, points) => session.send('Input.dispatchTouchEvent',{type,touchPoints:points}); + const a = await client(90,170), b = await client(270,210); + const scroll = await page.evaluate(() => window.scrollY); + await touch('touchStart',[{id:1,...a}]); + check((await state()).states.dragging === 1, 'touch contact starts the same authored interaction'); + await touch('touchMove',[{id:1,...b}]); s = await state(); + check(close(s.states.x,260) && close(s.states.y,200), 'mobile scaled SVG maps touch coordinates back to world coordinates'); + check(await page.evaluate(() => window.scrollY) === scroll, 'touch drag manipulates the target without scrolling the page'); + await touch('touchEnd',[]); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'touch release clears capture ownership'); + await page.screenshot({path:'output/playwright/pure-pointer-mobile.png',fullPage:true}); + // Independent contacts are not collapsed into one global drag state. + const two = 'state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\nbox one(20,100,100,80,"#c9f08b")\nbox two(300,100,100,80,"#c9f08b")\non a.move { x=event_x }\non b.move { y=event_y }'; + await apply(two); + const p1 = {id:1,...await client(50,140)}, p2 = {id:2,...await client(330,140)}; + await touch('touchStart',[p1]); await touch('touchStart',[p1,p2]); + check((await state()).held.length === 2, 'two contacts on different authored targets retain separate owners'); + await touch('touchMove',[{id:1,...await client(180,160)},{id:2,...await client(430,200)}]); s = await state(); + check(close(s.states.x,180) && close(s.states.y,200), 'independent touch payloads reach their own rules'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'touch cancellation releases both targets'); + // A second contact on the same target cannot steal the first one. + await touch('touchStart',[p1]); + await touch('touchStart',[p1,{id:2,...await client(70,150)}]); + check((await state()).held.length === 1, 'second contact on an already owned target does not steal it'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'ignored second contact cannot strand a logical input'); + check(errors.length === 0,'no browser page errors'); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); + await session.detach(); session = null; + await page.setViewportSize({width:1440,height:1000}); await apply(source); await reset(); + await page.evaluate(result => { window.__purePointerEvidence = result; }, {checks,errors,kind:'functional input/output checks; no performance benchmark'}); + return {checks:checks.length,errors}; + } catch (error) { + await page.evaluate(result => { window.__purePointerEvidence = result; }, {failed:String(error),checks,errors}); + throw error; + } finally { + if (session) { await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); await session.detach(); } + page.off('pageerror',onError); page.off('dialog',onDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/web/app.js b/prototypes/workbench/results/2026-09-23-pointer/after/web/app.js new file mode 100644 index 0000000..3f35a7d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/web/app.js @@ -0,0 +1,378 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; +import { bindSceneInput } from './scene-input.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); +const sceneInput = bindSceneInput($('scene'), () => runtime, safeAction); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +// DOM is the published value. Avoid rewriting unchanged diagnostics/controls. +function text(id, value) { + const node = $(id); + if (node.textContent !== value) node.textContent = value; +} +function pressed(node, value) { + const next = String(value); + if (node.getAttribute('aria-pressed') !== next) node.setAttribute('aria-pressed', next); +} +function draftStatus() { + const changed = dirty(); + text('draft-status', changed ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'); + $('draft-status').classList.toggle('dirty', changed); +} + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + text('cursor-position', `${lines.length}행 · ${lines.at(-1).length + 1}열`); +} + +function draftChanged() { + // Read scroll position before writes; this path runs only for source edits. + const scrollTop = source.scrollTop; + const count = source.value.split('\n').length; + const size = bytes(source.value); + text('line-numbers', Array.from({ length: count }, (_, i) => i + 1).join('\n')); + $('line-numbers').scrollTop = scrollTop; + text('source-size', `${size.toLocaleString('en-US')} / 32,768 bytes`); + $('source-size').style.color = size > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + draftStatus(); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + text('play', '▶ 실행'); + pressed($('play'), false); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + sceneInput.releaseCaptures(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + pressed(button, held.has(name)); + } + text('scene-status', runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'); + text('clock-status', `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`); + text('revision', `revision ${snapshot.revision || 0}`); + for (const id of ['play', 'step', 'reset']) { + if ($(id).disabled !== !runtime.currentProgram) $(id).disabled = !runtime.currentProgram; + } + const event = snapshot.lastEvent; + text('last-event', event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'); + if ($('runtime-error').hidden !== !snapshot.lastError) $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + let message = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + message += ` · ${snapshot.displayError.message}`; + text('runtime-error', message); + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + if (input.target) { + const note = document.createElement('p'); + note.className = 'empty-note'; + note.textContent = `${input.name} · 장면의 ${input.target}을 잡아 조작하세요.`; + children.push(note); + continue; + } + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + draftStatus(); + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime) { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic('편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.'); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_SOURCE_BYTES * 7 + 1024) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = source.value; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + let project; + try { project = JSON.parse(content); } catch { throw new Error('올바른 JSON 파일이 아닙니다.'); } + if (!project || typeof project !== 'object' || Array.isArray(project) || project.format !== 'pure-workbench' || project.version !== 1 || typeof project.source !== 'string') throw new Error('Pure 작업물 형식이 아닙니다. format, version, source를 확인하세요.'); + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-pointer/after/web/compiler.mjs new file mode 100644 index 0000000..1ec0c7c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/web/compiler.mjs @@ -0,0 +1,326 @@ +// 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', '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(); + } + 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: token.value }); + } 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 { + this.expect('='); + statements.push({ kind: 'assign', token, name: token.value, 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('='); + let sign = 1; + if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1; + const value = this.expect('number', 'numeric state initializer'); + declarations.push({ kind, token, name, initial: sign * value.value }); + } 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 === 'pointer') { + 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'].includes(edge.value)) fail(edge, 'Expected down or up, move or cancel'); + } + 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: [], metrics: {} }; + const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(), targets = new Set(); + 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 === '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.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}`); + } + let pointHandler = false; + 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_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 (!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 (!states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`); + expression(statement.expression); + emit(20, statement.token, states.get(statement.name), 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; + } + } + }; + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + pointHandler = 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); + if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input'); + event = edge === 'move' ? 17 + index : edge === 'cancel' ? 25 + index : 1 + index * 2 + (edge === 'up' ? 1 : 0); + } + 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); + } + } + 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/workbench/results/2026-09-23-pointer/after/web/core.wasm b/prototypes/workbench/results/2026-09-23-pointer/after/web/core.wasm new file mode 100644 index 0000000..b2f2cd1 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer/after/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/web/index.html b/prototypes/workbench/results/2026-09-23-pointer/after/web/index.html new file mode 100644 index 0000000..85c6493 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/web/index.html @@ -0,0 +1,93 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

직접 조작
input drag = pointer(tile)로 도형에 입력을 연결합니다. on drag.down / .move / .up / .cancel에서 event_x, event_y는 장면 좌표입니다. 이동 방식과 취소 시 동작도 코드로 정합니다. 초점이나 포인터 캡처를 잃으면 .cancel이 전달됩니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/web/output.mjs b/prototypes/workbench/results/2026-09-23-pointer/after/web/output.mjs new file mode 100644 index 0000000..93d771a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/web/output.mjs @@ -0,0 +1,95 @@ +// Bind authored declarations to concrete DOM destinations once per apply. +// The DOM holds the published values; no parallel scene/value model is kept. +const NS = 'http://www.w3.org/2000/svg'; +const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString(); +const attributes = { box: { x: 'x', y: 'y', width: 'width', height: 'height' }, + circle: { x: 'cx', y: 'cy', radius: 'r' }, text: { x: 'x', y: 'y' } }; + +function text(node, value) { + if (node.textContent !== value) node.textContent = value; +} +function attribute(node, name, value) { + const next = String(value); + if (node.getAttribute(name) !== next) node.setAttribute(name, next); +} + +export function createOutput(document) { + const get = id => document.getElementById(id); + const scene = get('scene-content'), states = get('state-values'); + const empty = get('empty-scene'), description = get('scene-description'); + let active = undefined, nodes = [], amounts = []; + const costNames = ['Wasm 선형 메모리', '코어 예약 공간', '사용 중인 명령·핸들러', + '현재 상태 값', '새 코드 준비 공간 (예약에 포함)', '평가 스택 용량', + '마지막 이벤트 실행 명령', '적용된 소스 크기']; + const costs = costNames.map(label => { + const dt = document.createElement('dt'), dd = document.createElement('dd'); + dt.textContent = label; + return { dt, dd }; + }); + get('cost-values').replaceChildren(...costs.flatMap(({dt, dd}) => [dt, dd])); + + return { + begin(program) { + if (program === active) return; + active = program; + const targets = new Map((program?.inputs ?? []).filter(input => input.target).map(input => [input.target, input.name])); + scene.ownerSVGElement.classList.toggle('direct-input', targets.size > 0); + nodes = (program?.views ?? []).map(view => { + const node = document.createElementNS(NS, {box:'rect', circle:'circle', text:'text'}[view.type]); + node.setAttribute('data-view-id', view.id); + if (targets.has(view.id)) node.setAttribute('data-point-input', targets.get(view.id)); + node.setAttribute('fill', view.color ?? '#e7ece6'); + if (view.type === 'text') { + node.setAttribute('font-family', 'ui-monospace, monospace'); + node.setAttribute('font-size', '20'); + } + return node; + }); + scene.replaceChildren(...nodes); + const holders = []; + amounts = (program?.states ?? []).map(state => { + const dl = document.createElement('dl'), dt = document.createElement('dt'), dd = document.createElement('dd'); + dl.className = 'state-value'; dt.textContent = state.name; dd.dataset.state = state.name; + dl.append(dt, dd); holders.push(dl); + return dd; + }); + if (!holders.length) { + const note = document.createElement('p'); + note.className = 'empty-note'; note.textContent = '선언한 상태의 값이 표시됩니다.'; + holders.push(note); + } + states.replaceChildren(...holders); + }, + state(index, value) { text(amounts[index], number(value)); }, + view(index, valid) { + const node = nodes[index]; + if (valid) { if (node.hasAttribute('display')) node.removeAttribute('display'); } + else attribute(node, 'display', 'none'); + }, + value(index, name, value) { + const view = active.views[index], node = nodes[index]; + if (name === 'value') text(node, `${view.prefix || ''}${number(value)}`); + else attribute(node, attributes[view.type][name], value); + }, + end(visible) { + const hidden = visible !== 0; + if (empty.hidden !== hidden) empty.hidden = hidden; + const failed = !visible && nodes.length > 0; + text(empty.querySelector('strong'), failed ? '장면을 표시할 수 없습니다' : '아직 아무것도 없습니다'); + text(empty.querySelector('span:last-child'), failed ? '현재 상태 아래의 표시 오류를 확인하세요.' : '도형이나 글자를 선언하고 코드를 적용하세요.'); + text(description, visible ? `사용자가 만든 장면. 도형과 글자 ${visible}개.` : failed ? '표시 오류로 장면을 표시할 수 없습니다.' : '사용자가 코드로 만든 장면. 현재 비어 있습니다.'); + }, + costs(status, metrics, droppedTicks) { + const m = status.memory; + const values = [m.linearBytes, m.reservedEngineBytes, m.activeIRBytes, m.activeStateBytes, + m.stagingReservedBytes, m.evalStackBytes, status.lastOps, metrics?.sourceBytes]; + for (let i = 0; i < values.length; i++) { + const value = values[i], {dt, dd} = costs[i], hidden = !Number.isFinite(value); + if (dt.hidden !== hidden) dt.hidden = hidden; + if (dd.hidden !== hidden) dd.hidden = hidden; + if (!hidden) text(dd, i === 6 ? `${value}개` : `${value.toLocaleString('en-US')} B`); + } + text(get('dropped-ticks'), `시간 지연으로 건너뛴 프레임: ${droppedTicks}`); + }, + }; +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-pointer/after/web/runtime.mjs new file mode 100644 index 0000000..28648cc --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/web/runtime.mjs @@ -0,0 +1,298 @@ +import { compile } from './compiler.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') + 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 = []; + // 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) { + const status = point ? core.dispatch_point(event, 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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), + }, + }; + } + + 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)); + const oldIds = new Map(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); + } + for (const op of next.instructions) checked(core.put_instruction(op.op, op.arg, op.value), op.line); + for (const handler of next.handlers) checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + checked(core.commit_program()); + } catch (error) { + core.abort_program(); + throw error; + } + program = next; + bindings = next.views.map(view => Object.entries(view.bindings)); + appliedSource = source; + lastEvent = null; + lastFailure = null; + return next; + }, + 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].target) 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].target) 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); + }, + point(name, phase, x, y, source = 'external') { + available(); + const index = inputIndex(name); + if (!program.inputs[index].target) throw new Error('포인터에 연결된 입력이 아닙니다.'); + 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(name); + if (phase === 'down') { + if (point?.source === source) return 0; + if (point) throw new Error('이 입력은 다른 포인터가 사용 중입니다.'); + point = {source, x, y}; + points.set(name, point); + held.set(name, new Set([source])); + } else { + if (!point || point.source !== source) return 0; + point.x = x; point.y = y; + if (phase === 'up' || phase === 'cancel') { points.delete(name); held.delete(name); } + } + 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 [name, point] of [...points]) { + const status = api.point(name, 'cancel', point.x, point.y, point.source); + 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/workbench/results/2026-09-23-pointer/after/web/scene-input.mjs b/prototypes/workbench/results/2026-09-23-pointer/after/web/scene-input.mjs new file mode 100644 index 0000000..bd6dfd8 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/web/scene-input.mjs @@ -0,0 +1,68 @@ +// This adapter owns browser capture only. Authored state changes stay in the VM. +export function bindSceneInput(scene, runtime, act) { + const captures = new Map(); + const coordinates = event => { + const matrix = scene.getScreenCTM(); + if (!matrix) return null; + try { + const p = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse()); + return Number.isFinite(p.x) && Number.isFinite(p.y) ? p : null; + } catch { return null; } + }; + const release = id => { + if (scene.hasPointerCapture(id)) scene.releasePointerCapture(id); + }; + function finish(event, phase) { + const entry = captures.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + // Capture loss/cancel may not carry a meaningful position. Use the last + // delivered coordinates. Up must use its own final position. + const p = phase === 'up' ? coordinates(event) : null; + if (p) { entry.x = p.x; entry.y = p.y; } + if (phase === 'up' && !p) phase = 'cancel'; + captures.delete(event.pointerId); + release(event.pointerId); + act(() => runtime().point(entry.name, phase, entry.x, entry.y, entry.source)); + } + scene.addEventListener('pointerdown', event => { + if (event.button !== 0 || !runtime() || captures.has(event.pointerId)) return; + const target = event.target.closest?.('[data-point-input]'); + if (!target || !scene.contains(target)) return; + const name = target.getAttribute('data-point-input'); + // A logical input has one coordinate owner. A second contact on it must not + // steal the gesture; other authored targets can be manipulated independently. + if ([...captures.values()].some(entry => entry.name === name)) return; + const p = coordinates(event); + if (!p) return; + event.preventDefault(); scene.focus(); + act(() => { + scene.setPointerCapture(event.pointerId); + const entry = {name, source:`scene:${event.pointerId}`, x:p.x, y:p.y}; + captures.set(event.pointerId, entry); + try { runtime().point(name, 'down', p.x, p.y, entry.source); } + catch (error) { captures.delete(event.pointerId); release(event.pointerId); throw error; } + }); + }); + scene.addEventListener('pointermove', event => { + const entry = captures.get(event.pointerId); + if (!entry) return; + const p = coordinates(event); + if (!p) { finish(event, 'cancel'); return; } + event.preventDefault(); + entry.x = p.x; entry.y = p.y; + act(() => runtime().point(entry.name, 'move', p.x, p.y, entry.source)); + }); + scene.addEventListener('pointerup', event => finish(event, 'up')); + scene.addEventListener('pointercancel', event => finish(event, 'cancel')); + scene.addEventListener('lostpointercapture', event => finish(event, 'cancel')); + return Object.freeze({ + releaseCaptures() { + const ids = [...captures.keys()]; + captures.clear(); + for (const id of ids) release(id); + // The caller subsequently invokes runtime.releaseAll(), which dispatches + // each pending cancel once. Clearing first makes lostcapture harmless. + }, + }); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/after/web/style.css b/prototypes/workbench/results/2026-09-23-pointer/after/web/style.css new file mode 100644 index 0000000..702a6e4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/after/web/style.css @@ -0,0 +1,10 @@ +:root{color-scheme:dark;--bg:#151918;--panel:#1d2220;--field:#151a18;--line:#353e38;--text:#e7ece6;--muted:#a9b6aa;--faint:#7b8e7e;--accent:#c9f08b;--accent-dark:#222e18;--danger:#f6afa0;--radius:14px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-synthesis:none} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text)}button,input,select,textarea{font:inherit}button,select{border:1px solid var(--line);border-radius:7px;background:#282f2a;color:var(--text);padding:9px 12px;cursor:pointer;font-size:12px;font-weight:600;min-height:36px}button:hover:not(:disabled){background:#344035;border-color:#70836d}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button.primary{background:var(--accent);color:var(--accent-dark);border-color:var(--accent);padding:11px 19px;font-size:14px;display:flex;gap:28px;justify-content:space-between;align-items:center}button.primary:hover:not(:disabled){background:#d8f7ad}button.secondary{background:#c9f08b16;border-color:#637545;color:#d7f0b5}button.quiet{background:transparent;border-color:transparent;color:var(--muted);padding:6px 8px;min-height:30px}button.quiet:hover{color:var(--text)}.masthead{height:76px;padding:0 4.5%;display:flex;align-items:center;gap:28px;border-bottom:1px solid var(--line)}.wordmark{font-size:23px;font-weight:750;letter-spacing:-1px;color:var(--accent);text-decoration:none}.wordmark span{font-size:13px;color:var(--muted);letter-spacing:0;font-weight:400}.experiment{font-size:11px;color:var(--faint);border-left:1px solid var(--line);padding-left:24px}.connection{margin-left:auto;display:flex;gap:9px;align-items:center;font-size:11px;color:var(--muted)}.dot{width:6px;height:6px;border-radius:100%;background:#aa8e5c}.dot.ready{background:var(--accent)}.dot.failed{background:var(--danger)}main{max-width:1540px;margin:auto;padding:38px 4.5% 0}.intro{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin:0 0 27px}.eyebrow{font-size:10px;letter-spacing:1.4px;font-weight:600;color:var(--accent);margin:0 0 10px}h1{font-size:clamp(23px,2.5vw,35px);letter-spacing:-1px;margin:0;font-weight:600}.intro>p{font-size:12px;line-height:1.9;color:var(--muted);margin:0 0 2px}.intro strong{color:var(--text);font-weight:500}.workspace{display:grid;grid-template-columns:minmax(340px,1fr) minmax(380px,1.05fr);gap:22px;align-items:start}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.panel-heading{height:56px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line)}h2{margin:0;font-size:13px;font-weight:650;display:flex;gap:10px;align-items:center}.section-index{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;font-weight:400;color:var(--faint)}.badge{font-size:10px;border:1px solid var(--line);border-radius:4px;padding:4px 7px;white-space:nowrap;color:var(--muted)}.badge.dirty{color:var(--accent);border-color:#5f7247}.file-tools{display:flex;align-items:center;gap:3px;padding:8px 10px;border-bottom:1px solid var(--line);flex-wrap:wrap}.code-editor{height:514px;display:flex;overflow:hidden;background:var(--field);position:relative}.line-numbers{padding:20px 10px 20px 16px;min-width:45px;text-align:right;color:#667567;user-select:none;white-space:pre;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:hidden;flex-shrink:0;border-right:1px solid #2a322b}textarea#source{border:0;border-radius:0;outline:0;resize:none;padding:20px 16px;display:block;width:100%;height:100%;color:#d4e3ca;background:transparent;white-space:pre;overflow:auto;tab-size:2;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;caret-color:var(--accent)}textarea#source:focus{background:#171e19}textarea#source::placeholder{color:#7c8b7d;font:12px/1.9 ui-sans-serif,system-ui,sans-serif;white-space:pre-wrap}textarea#source[aria-invalid=true]{box-shadow:inset 3px 0 0 var(--danger)}.editor-footer{padding:8px 15px;font-size:10px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--faint);display:flex;justify-content:space-between;border-top:1px solid var(--line)}.apply-row{display:flex;gap:18px;align-items:center;padding:16px 18px 12px}.apply-row p{font-size:10px;color:var(--muted);line-height:1.7;margin:0;max-width:185px}.diagnostic{margin:0 18px 17px;font-size:11px;line-height:1.8;min-height:22px;color:var(--muted);display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}.diagnostic.error{color:var(--danger)}.diagnostic.success{color:#c7dea9}.right-column{display:grid;gap:18px}.stage-wrap{position:relative;background-color:#121613;background-image:linear-gradient(#a6c39907 1px,transparent 1px),linear-gradient(90deg,#a6c39907 1px,transparent 1px);background-size:24px 24px}#scene{width:100%;height:auto;aspect-ratio:640/360;display:block;overflow:hidden}.empty-scene{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;gap:9px;color:var(--faint);padding:24px;text-align:center}.empty-scene[hidden]{display:none}.empty-scene strong{font-size:13px;color:var(--muted);font-weight:400}.empty-scene>span:not(.empty-cross){font-size:11px}.empty-cross{font-size:31px;color:#52614d;font-weight:200;margin-bottom:7px}.stage-dimensions{position:absolute;right:14px;bottom:10px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#6d7f6d;pointer-events:none}.transport{display:flex;gap:7px;align-items:center;padding:14px 16px 0;flex-wrap:wrap}.transport button{font-size:11px;padding:7px 10px}.transport #clock-status{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:var(--muted);margin-left:auto}.control-note,.keyboard-note{font-size:10px;line-height:1.7;color:var(--faint);margin:10px 18px 15px}.input-heading{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:0 18px;padding-top:13px;border-top:1px solid var(--line)}h3{font-size:11px;font-weight:550;margin:0}.input-heading>span{font-size:9px;color:var(--faint)}.input-controls{display:flex;flex-wrap:wrap;gap:7px;padding:11px 17px 0;min-height:39px}.input-button{display:flex;align-items:center;gap:14px;touch-action:none;user-select:none;-webkit-user-select:none}.input-button kbd{font:9px ui-monospace,SFMono-Regular,Consolas,monospace;border:1px solid #5a6957;border-radius:3px;padding:3px 4px;color:var(--muted);min-width:22px;text-align:center}.input-button.held{background:var(--accent);border-color:var(--accent);color:var(--accent-dark)}.input-button.held kbd{color:var(--accent-dark);border-color:#829a62}.empty-note{font-size:11px;line-height:1.7;color:var(--faint);margin:0}.keyboard-note{margin-bottom:13px;font-size:9px}.state-values{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));padding:15px 19px;gap:13px;max-height:210px;overflow:auto}.state-value{display:flex;flex-direction:column;gap:6px;min-width:0}.state-value dt{font:10px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);overflow-wrap:anywhere}.state-value dd{font:17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--accent);margin:0;overflow-wrap:anywhere}.state-value dl{margin:0}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.subdued{color:var(--muted);font-size:10px}.event-line{padding:0 19px 13px;display:flex;gap:16px;align-items:baseline;flex-wrap:wrap}.event-line #last-event{color:var(--muted);font-size:10px;overflow-wrap:anywhere}.runtime-error{margin:0 18px 14px;padding:10px;border:1px solid #705049;border-radius:5px;background:#432c272e;color:var(--danger);font-size:11px;line-height:1.6}.cost-details{border-top:1px solid var(--line);padding:12px 19px;font-size:10px;color:var(--muted)}summary{cursor:pointer;font-size:11px;color:var(--muted)}.cost-details p{font-size:10px;line-height:1.8;color:var(--faint)}.cost-values{display:grid;grid-template-columns:1fr auto;gap:8px 20px;margin:16px 0}.cost-values dt,.cost-values dd{margin:0}.cost-values dd{text-align:right;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.reference{margin-top:25px}.reference-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));padding:22px 20px;gap:24px}.reference-grid article{min-width:0;display:flex;flex-direction:column;align-items:flex-start}.reference-grid h3{font-size:12px;color:#d2ddc8}.reference-grid p{font-size:10px;line-height:1.8;color:var(--muted);min-height:36px;margin:8px 0 10px}.reference-grid pre{font:10px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;background:#151b16;color:#bacead;width:100%;padding:10px;overflow:auto;border-radius:6px;min-height:115px;margin:0 0 8px}.reference-grid .quiet{font-size:10px;padding:5px 0}.syntax-details{padding:16px 20px;border-top:1px solid var(--line)}.syntax-columns{display:grid;grid-template-columns:1fr 1fr;gap:35px;margin-top:12px}.syntax-columns p{font-size:11px;line-height:1.95;color:var(--muted)}.syntax-columns strong{color:var(--text);font-weight:500}.syntax-columns code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:#d0dcbe;overflow-wrap:anywhere}.example-row{padding:18px 20px;border-top:1px solid var(--line);display:flex;align-items:center;gap:10px}.example-row>div{margin-right:auto}.example-row label{font-size:11px;color:var(--text)}.example-row p{font-size:10px;color:var(--faint);margin:5px 0 0}.example-row button,.example-row select{font-size:10px}.page-footer{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:26px 0 30px;color:var(--faint);font-size:10px}.page-footer>span{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:9px}.page-footer p{margin:0;line-height:1.8}.page-footer strong{font-weight:400;color:var(--muted)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important} +.baseline-row{border-top:1px solid var(--line);padding:15px 20px;display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center}.baseline-row a{font-size:11px;color:var(--accent);text-underline-offset:3px}.baseline-row span{font-size:10px;color:var(--faint)}#scene:focus-visible{outline:2px solid var(--accent);outline-offset:-3px} +@media(min-width:1450px){.code-editor{height:558px}.reference-grid pre{font-size:11px}} +@media(max-width:1050px){.masthead{padding:0 25px}.experiment{display:none}main{padding:29px 25px 0}.workspace{grid-template-columns:minmax(300px,1fr) minmax(340px,1fr);gap:15px}.intro>p{font-size:11px;max-width:290px}.reference-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:25px}.reference-grid p{min-height:0}.reference-grid pre{min-height:110px;font-size:11px}.transport #clock-status{width:100%;margin:3px 0 0}.code-editor{height:533px}.panel-heading{padding:0 16px}.apply-row{gap:10px;padding-left:14px;padding-right:14px}.apply-row p{font-size:9px}.input-heading{align-items:flex-start;flex-direction:column;gap:5px}.intro{gap:20px}button.primary{gap:14px;padding:11px 13px}} +@media(max-width:760px){.masthead{height:63px;padding:0 20px}.wordmark{font-size:21px}.wordmark span{font-size:11px}.connection{font-size:10px}main{padding:25px 18px 0}.intro{display:block;margin-bottom:22px}.intro h1{font-size:28px}.intro>p{margin-top:14px;max-width:none;font-size:11px}.workspace{grid-template-columns:minmax(0,1fr);gap:17px}.code-editor{height:340px}.editor-panel{width:100%}.transport #clock-status{width:auto;margin-left:auto}.right-column{gap:17px}.input-heading{flex-direction:row;align-items:center}.reference{margin-top:20px}.reference-grid{padding:20px 16px;gap:24px 17px}.reference-grid pre{font-size:10px}.reference>.panel-heading{height:auto;min-height:57px;padding-top:12px;padding-bottom:12px;align-items:flex-start;flex-direction:column;gap:6px}.syntax-columns{grid-template-columns:1fr;gap:0}.example-row{flex-wrap:wrap;padding:17px 16px}.example-row>div{width:100%;margin-bottom:4px}.example-row select{flex:1}.page-footer{align-items:flex-start;flex-direction:column;gap:10px;padding:23px 0}.apply-row{padding:14px 16px 10px;gap:17px}.apply-row p{font-size:10px;max-width:none}.diagnostic{margin-left:16px}.reference-grid article{overflow:hidden}} +@media(max-width:420px){main{padding-left:13px;padding-right:13px}.masthead{padding:0 15px}.wordmark span{font-size:10px}.connection{font-size:9px;gap:6px}.intro h1{font-size:25px}.transport{gap:5px;padding-left:12px;padding-right:12px}.transport button{font-size:10px;padding:7px 8px}.transport #clock-status{font-size:9px}.input-heading>span{font-size:8px}.reference-grid{grid-template-columns:minmax(0,1fr)}.reference-grid pre{min-height:0;font-size:11px}.reference-grid p{margin-bottom:8px}.file-tools{gap:0;padding:7px 5px}.file-tools .quiet{font-size:10px;padding:6px 7px}.apply-row p{font-size:9px}.line-numbers{padding-left:10px;min-width:36px}textarea#source{padding-left:12px;font-size:11px}.line-numbers{font-size:11px}} +@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}} + +#scene.direct-input{touch-action:none;user-select:none;-webkit-user-select:none}#scene [data-point-input]{cursor:grab} diff --git a/prototypes/workbench/results/2026-09-23-pointer/authoring-browser.json b/prototypes/workbench/results/2026-09-23-pointer/authoring-browser.json new file mode 100644 index 0000000..419bafd --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/authoring-browser.json @@ -0,0 +1,128 @@ +{ + "recordedUtc": "2026-09-23T06:18:07.245Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 464, + "activeStateBytes": 24, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "output/playwright/pure-pointer-workbench-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "output/playwright/pure-pointer-workbench-blank.png", + "output/playwright/pure-pointer-workbench-authored.png", + "output/playwright/pure-pointer-workbench-mobile.png" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/cli.mjs b/prototypes/workbench/results/2026-09-23-pointer/before/cli.mjs new file mode 100644 index 0000000..aafcd51 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/cli.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; +import { createInterface } from 'node:readline'; +import { createRuntime } from './web/runtime.mjs'; + +const help = `Pure authoring workbench · 같은 정의와 Zig 실행기 + load PATH .pure 파일을 적용 (같은 이름의 상태 보존) + down NAME 정의한 입력 누르기 + up NAME 정의한 입력 놓기 + 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 적용한 정의를 저장 + 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))); + runtime.apply(args[0] ? await readFile(args[0], 'utf8') : ''); + 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) runtime.apply(await readFile(arg, 'utf8')); + 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 === '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/workbench/results/2026-09-23-pointer/before/core.zig b/prototypes/workbench/results/2026-09-23-pointer/before/core.zig new file mode 100644 index 0000000..84812a3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/core.zig @@ -0,0 +1,942 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { event: u32 = 0, start: u32 = 0, length: u32 = 0 }; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Workspace = union { staging: Program, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .state_count = state_count, .input_count = input_count } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.initials[id] = initial; + engine.workspace.staging.old_ids[id] = old_id; + engine.workspace.staging.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 21) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (event > p.input_count * 2) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (h.event > 2 * p.input_count) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) return .invalid; + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 21) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op == 20 or inst.op == 21) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + break; + } + } + const h = owner orelse return .invalid; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + } + } + return .ok; +} + +export fn commit_program() u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const code = validate(&engine.workspace.staging); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.state_count) |i| { + const old_id = engine.workspace.staging.old_ids[i]; + next[i] = if (old_id < 0) engine.workspace.staging.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging; + engine.values = next; + finishStaging(); + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +fn execute(values: *[max_states]f64, start: u32, length: u32, dt: f64, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op == 20 or inst.op == 21)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2 => 0, + 7, 16, 20, 21 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 2 => dt, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20) { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[inst.arg] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (event > engine.active.input_count * 2) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + const result = execute(&pending, h.start, h.length, dt, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, 0, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, 0, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Program), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Program), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 12), layout(3)); + try std.testing.expectEqual(@as(u32, 76), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(layout(1), layout(6)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Program)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Program) + @alignOf(Program)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/evaluation.zig b/prototypes/workbench/results/2026-09-23-pointer/before/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/manifest.json b/prototypes/workbench/results/2026-09-23-pointer/before/manifest.json new file mode 100644 index 0000000..1a6fe94 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/manifest.json @@ -0,0 +1,12 @@ +{ + "web/app.js": "f91c076ff5242b10b390b57b4213d09b1ae74e61ea221845603428ecec5ea3f9", + "web/runtime.mjs": "d490c54fb2b422dfe89b90a594a8da9ebc95bb007910211cb4686ccb83ed75c7", + "web/output.mjs": "1d22c44955e812d671f1a3c3ae4b2f0642a05ebe614017bb041bdddc8c363be3", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "web/index.html": "4fff6eb67d8cff098302465449251d17f0efb39d9144e87d15fefcc152e5d97e", + "web/style.css": "43301298028c5bc431eecd5ff30d7362fae83cce1fd0fdd7e8bea858100c22b9", + "core.zig": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "cli.mjs": "2e0627ef84a45c8a9e91fb28c0010673212d459d0b0d09a5342856a049060030" +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/web/app.js b/prototypes/workbench/results/2026-09-23-pointer/before/web/app.js new file mode 100644 index 0000000..9327e53 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/web/app.js @@ -0,0 +1,368 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +// DOM is the published value. Avoid rewriting unchanged diagnostics/controls. +function text(id, value) { + const node = $(id); + if (node.textContent !== value) node.textContent = value; +} +function pressed(node, value) { + const next = String(value); + if (node.getAttribute('aria-pressed') !== next) node.setAttribute('aria-pressed', next); +} +function draftStatus() { + const changed = dirty(); + text('draft-status', changed ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'); + $('draft-status').classList.toggle('dirty', changed); +} + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + text('cursor-position', `${lines.length}행 · ${lines.at(-1).length + 1}열`); +} + +function draftChanged() { + // Read scroll position before writes; this path runs only for source edits. + const scrollTop = source.scrollTop; + const count = source.value.split('\n').length; + const size = bytes(source.value); + text('line-numbers', Array.from({ length: count }, (_, i) => i + 1).join('\n')); + $('line-numbers').scrollTop = scrollTop; + text('source-size', `${size.toLocaleString('en-US')} / 32,768 bytes`); + $('source-size').style.color = size > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + draftStatus(); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + text('play', '▶ 실행'); + pressed($('play'), false); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + pressed(button, held.has(name)); + } + text('scene-status', runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'); + text('clock-status', `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`); + text('revision', `revision ${snapshot.revision || 0}`); + for (const id of ['play', 'step', 'reset']) { + if ($(id).disabled !== !runtime.currentProgram) $(id).disabled = !runtime.currentProgram; + } + const event = snapshot.lastEvent; + text('last-event', event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'); + if ($('runtime-error').hidden !== !snapshot.lastError) $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + let message = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + message += ` · ${snapshot.displayError.message}`; + text('runtime-error', message); + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + draftStatus(); + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime) { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic('편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.'); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_SOURCE_BYTES * 7 + 1024) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = source.value; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + let project; + try { project = JSON.parse(content); } catch { throw new Error('올바른 JSON 파일이 아닙니다.'); } + if (!project || typeof project !== 'object' || Array.isArray(project) || project.format !== 'pure-workbench' || project.version !== 1 || typeof project.source !== 'string') throw new Error('Pure 작업물 형식이 아닙니다. format, version, source를 확인하세요.'); + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-pointer/before/web/compiler.mjs new file mode 100644 index 0000000..016ea9e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/web/compiler.mjs @@ -0,0 +1,303 @@ +// 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', '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(); + } + 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: token.value }); + } 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 { + this.expect('='); + statements.push({ kind: 'assign', token, name: token.value, 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('='); + let sign = 1; + if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1; + const value = this.expect('number', 'numeric state initializer'); + declarations.push({ kind, token, name, initial: sign * value.value }); + } else if (kind === 'input') { + const name = this.expect('name', 'input name'); this.expect('='); + const key = this.expect('name', 'keyboard code'); + 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 or up'); + if (edge.value !== 'down' && edge.value !== 'up') fail(edge, 'Expected down or up'); + } + 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: [], metrics: {} }; + const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(); + 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 === '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 (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'); + } + 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 === 'dt') { + if (binding) fail(token, 'dt is only available in handlers'); + emit(2, token, 0, 0, 1); + } else { + 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 (!states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`); + expression(statement.expression); + emit(20, statement.token, states.get(statement.name), 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; + } + } + }; + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + if (declaration.event.value !== 'frame') { + if (!inputs.has(declaration.event.value)) fail(declaration.event, `Unknown input ${declaration.event.value}`); + event = 1 + inputs.get(declaration.event.value) * 2 + (declaration.edge.value === 'up' ? 1 : 0); + } + 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); + } + } + 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/workbench/results/2026-09-23-pointer/before/web/core.wasm b/prototypes/workbench/results/2026-09-23-pointer/before/web/core.wasm new file mode 100644 index 0000000..0eb3fe5 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer/before/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/web/index.html b/prototypes/workbench/results/2026-09-23-pointer/before/web/index.html new file mode 100644 index 0000000..d93589e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/web/index.html @@ -0,0 +1,93 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/web/output.mjs b/prototypes/workbench/results/2026-09-23-pointer/before/web/output.mjs new file mode 100644 index 0000000..53cae48 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/web/output.mjs @@ -0,0 +1,92 @@ +// Bind authored declarations to concrete DOM destinations once per apply. +// The DOM holds the published values; no parallel scene/value model is kept. +const NS = 'http://www.w3.org/2000/svg'; +const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString(); +const attributes = { box: { x: 'x', y: 'y', width: 'width', height: 'height' }, + circle: { x: 'cx', y: 'cy', radius: 'r' }, text: { x: 'x', y: 'y' } }; + +function text(node, value) { + if (node.textContent !== value) node.textContent = value; +} +function attribute(node, name, value) { + const next = String(value); + if (node.getAttribute(name) !== next) node.setAttribute(name, next); +} + +export function createOutput(document) { + const get = id => document.getElementById(id); + const scene = get('scene-content'), states = get('state-values'); + const empty = get('empty-scene'), description = get('scene-description'); + let active = undefined, nodes = [], amounts = []; + const costNames = ['Wasm 선형 메모리', '코어 예약 공간', '사용 중인 명령·핸들러', + '현재 상태 값', '새 코드 준비 공간 (예약에 포함)', '평가 스택 용량', + '마지막 이벤트 실행 명령', '적용된 소스 크기']; + const costs = costNames.map(label => { + const dt = document.createElement('dt'), dd = document.createElement('dd'); + dt.textContent = label; + return { dt, dd }; + }); + get('cost-values').replaceChildren(...costs.flatMap(({dt, dd}) => [dt, dd])); + + return { + begin(program) { + if (program === active) return; + active = program; + nodes = (program?.views ?? []).map(view => { + const node = document.createElementNS(NS, {box:'rect', circle:'circle', text:'text'}[view.type]); + node.setAttribute('data-view-id', view.id); + node.setAttribute('fill', view.color ?? '#e7ece6'); + if (view.type === 'text') { + node.setAttribute('font-family', 'ui-monospace, monospace'); + node.setAttribute('font-size', '20'); + } + return node; + }); + scene.replaceChildren(...nodes); + const holders = []; + amounts = (program?.states ?? []).map(state => { + const dl = document.createElement('dl'), dt = document.createElement('dt'), dd = document.createElement('dd'); + dl.className = 'state-value'; dt.textContent = state.name; dd.dataset.state = state.name; + dl.append(dt, dd); holders.push(dl); + return dd; + }); + if (!holders.length) { + const note = document.createElement('p'); + note.className = 'empty-note'; note.textContent = '선언한 상태의 값이 표시됩니다.'; + holders.push(note); + } + states.replaceChildren(...holders); + }, + state(index, value) { text(amounts[index], number(value)); }, + view(index, valid) { + const node = nodes[index]; + if (valid) { if (node.hasAttribute('display')) node.removeAttribute('display'); } + else attribute(node, 'display', 'none'); + }, + value(index, name, value) { + const view = active.views[index], node = nodes[index]; + if (name === 'value') text(node, `${view.prefix || ''}${number(value)}`); + else attribute(node, attributes[view.type][name], value); + }, + end(visible) { + const hidden = visible !== 0; + if (empty.hidden !== hidden) empty.hidden = hidden; + const failed = !visible && nodes.length > 0; + text(empty.querySelector('strong'), failed ? '장면을 표시할 수 없습니다' : '아직 아무것도 없습니다'); + text(empty.querySelector('span:last-child'), failed ? '현재 상태 아래의 표시 오류를 확인하세요.' : '도형이나 글자를 선언하고 코드를 적용하세요.'); + text(description, visible ? `사용자가 만든 장면. 도형과 글자 ${visible}개.` : failed ? '표시 오류로 장면을 표시할 수 없습니다.' : '사용자가 코드로 만든 장면. 현재 비어 있습니다.'); + }, + costs(status, metrics, droppedTicks) { + const m = status.memory; + const values = [m.linearBytes, m.reservedEngineBytes, m.activeIRBytes, m.activeStateBytes, + m.stagingReservedBytes, m.evalStackBytes, status.lastOps, metrics?.sourceBytes]; + for (let i = 0; i < values.length; i++) { + const value = values[i], {dt, dd} = costs[i], hidden = !Number.isFinite(value); + if (dt.hidden !== hidden) dt.hidden = hidden; + if (dd.hidden !== hidden) dd.hidden = hidden; + if (!hidden) text(dd, i === 6 ? `${value}개` : `${value.toLocaleString('en-US')} B`); + } + text(get('dropped-ticks'), `시간 지연으로 건너뛴 프레임: ${droppedTicks}`); + }, + }; +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/before/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-pointer/before/web/runtime.mjs new file mode 100644 index 0000000..96634b8 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/web/runtime.mjs @@ -0,0 +1,267 @@ +import { compile } from './compiler.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') + 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(); + let presenting = false; + let bindings = []; + // 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) { + const status = 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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), + }, + }; + } + + 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)); + const oldIds = new Map(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); + } + for (const op of next.instructions) checked(core.put_instruction(op.op, op.arg, op.value), op.line); + for (const handler of next.handlers) checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + checked(core.commit_program()); + } catch (error) { + core.abort_program(); + throw error; + } + program = next; + bindings = next.views.map(view => Object.entries(view.bindings)); + appliedSource = source; + lastEvent = null; + lastFailure = null; + return next; + }, + reset() { + available(); + held.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); + 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); + 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); + }, + releaseAll() { + available(); + let firstError = 0; + 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/workbench/results/2026-09-23-pointer/before/web/style.css b/prototypes/workbench/results/2026-09-23-pointer/before/web/style.css new file mode 100644 index 0000000..819c84c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/before/web/style.css @@ -0,0 +1,8 @@ +:root{color-scheme:dark;--bg:#151918;--panel:#1d2220;--field:#151a18;--line:#353e38;--text:#e7ece6;--muted:#a9b6aa;--faint:#7b8e7e;--accent:#c9f08b;--accent-dark:#222e18;--danger:#f6afa0;--radius:14px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-synthesis:none} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text)}button,input,select,textarea{font:inherit}button,select{border:1px solid var(--line);border-radius:7px;background:#282f2a;color:var(--text);padding:9px 12px;cursor:pointer;font-size:12px;font-weight:600;min-height:36px}button:hover:not(:disabled){background:#344035;border-color:#70836d}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button.primary{background:var(--accent);color:var(--accent-dark);border-color:var(--accent);padding:11px 19px;font-size:14px;display:flex;gap:28px;justify-content:space-between;align-items:center}button.primary:hover:not(:disabled){background:#d8f7ad}button.secondary{background:#c9f08b16;border-color:#637545;color:#d7f0b5}button.quiet{background:transparent;border-color:transparent;color:var(--muted);padding:6px 8px;min-height:30px}button.quiet:hover{color:var(--text)}.masthead{height:76px;padding:0 4.5%;display:flex;align-items:center;gap:28px;border-bottom:1px solid var(--line)}.wordmark{font-size:23px;font-weight:750;letter-spacing:-1px;color:var(--accent);text-decoration:none}.wordmark span{font-size:13px;color:var(--muted);letter-spacing:0;font-weight:400}.experiment{font-size:11px;color:var(--faint);border-left:1px solid var(--line);padding-left:24px}.connection{margin-left:auto;display:flex;gap:9px;align-items:center;font-size:11px;color:var(--muted)}.dot{width:6px;height:6px;border-radius:100%;background:#aa8e5c}.dot.ready{background:var(--accent)}.dot.failed{background:var(--danger)}main{max-width:1540px;margin:auto;padding:38px 4.5% 0}.intro{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin:0 0 27px}.eyebrow{font-size:10px;letter-spacing:1.4px;font-weight:600;color:var(--accent);margin:0 0 10px}h1{font-size:clamp(23px,2.5vw,35px);letter-spacing:-1px;margin:0;font-weight:600}.intro>p{font-size:12px;line-height:1.9;color:var(--muted);margin:0 0 2px}.intro strong{color:var(--text);font-weight:500}.workspace{display:grid;grid-template-columns:minmax(340px,1fr) minmax(380px,1.05fr);gap:22px;align-items:start}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.panel-heading{height:56px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line)}h2{margin:0;font-size:13px;font-weight:650;display:flex;gap:10px;align-items:center}.section-index{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;font-weight:400;color:var(--faint)}.badge{font-size:10px;border:1px solid var(--line);border-radius:4px;padding:4px 7px;white-space:nowrap;color:var(--muted)}.badge.dirty{color:var(--accent);border-color:#5f7247}.file-tools{display:flex;align-items:center;gap:3px;padding:8px 10px;border-bottom:1px solid var(--line);flex-wrap:wrap}.code-editor{height:514px;display:flex;overflow:hidden;background:var(--field);position:relative}.line-numbers{padding:20px 10px 20px 16px;min-width:45px;text-align:right;color:#667567;user-select:none;white-space:pre;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:hidden;flex-shrink:0;border-right:1px solid #2a322b}textarea#source{border:0;border-radius:0;outline:0;resize:none;padding:20px 16px;display:block;width:100%;height:100%;color:#d4e3ca;background:transparent;white-space:pre;overflow:auto;tab-size:2;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;caret-color:var(--accent)}textarea#source:focus{background:#171e19}textarea#source::placeholder{color:#7c8b7d;font:12px/1.9 ui-sans-serif,system-ui,sans-serif;white-space:pre-wrap}textarea#source[aria-invalid=true]{box-shadow:inset 3px 0 0 var(--danger)}.editor-footer{padding:8px 15px;font-size:10px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--faint);display:flex;justify-content:space-between;border-top:1px solid var(--line)}.apply-row{display:flex;gap:18px;align-items:center;padding:16px 18px 12px}.apply-row p{font-size:10px;color:var(--muted);line-height:1.7;margin:0;max-width:185px}.diagnostic{margin:0 18px 17px;font-size:11px;line-height:1.8;min-height:22px;color:var(--muted);display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}.diagnostic.error{color:var(--danger)}.diagnostic.success{color:#c7dea9}.right-column{display:grid;gap:18px}.stage-wrap{position:relative;background-color:#121613;background-image:linear-gradient(#a6c39907 1px,transparent 1px),linear-gradient(90deg,#a6c39907 1px,transparent 1px);background-size:24px 24px}#scene{width:100%;height:auto;aspect-ratio:640/360;display:block;overflow:hidden}.empty-scene{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;gap:9px;color:var(--faint);padding:24px;text-align:center}.empty-scene[hidden]{display:none}.empty-scene strong{font-size:13px;color:var(--muted);font-weight:400}.empty-scene>span:not(.empty-cross){font-size:11px}.empty-cross{font-size:31px;color:#52614d;font-weight:200;margin-bottom:7px}.stage-dimensions{position:absolute;right:14px;bottom:10px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#6d7f6d;pointer-events:none}.transport{display:flex;gap:7px;align-items:center;padding:14px 16px 0;flex-wrap:wrap}.transport button{font-size:11px;padding:7px 10px}.transport #clock-status{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:var(--muted);margin-left:auto}.control-note,.keyboard-note{font-size:10px;line-height:1.7;color:var(--faint);margin:10px 18px 15px}.input-heading{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:0 18px;padding-top:13px;border-top:1px solid var(--line)}h3{font-size:11px;font-weight:550;margin:0}.input-heading>span{font-size:9px;color:var(--faint)}.input-controls{display:flex;flex-wrap:wrap;gap:7px;padding:11px 17px 0;min-height:39px}.input-button{display:flex;align-items:center;gap:14px;touch-action:none;user-select:none;-webkit-user-select:none}.input-button kbd{font:9px ui-monospace,SFMono-Regular,Consolas,monospace;border:1px solid #5a6957;border-radius:3px;padding:3px 4px;color:var(--muted);min-width:22px;text-align:center}.input-button.held{background:var(--accent);border-color:var(--accent);color:var(--accent-dark)}.input-button.held kbd{color:var(--accent-dark);border-color:#829a62}.empty-note{font-size:11px;line-height:1.7;color:var(--faint);margin:0}.keyboard-note{margin-bottom:13px;font-size:9px}.state-values{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));padding:15px 19px;gap:13px;max-height:210px;overflow:auto}.state-value{display:flex;flex-direction:column;gap:6px;min-width:0}.state-value dt{font:10px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);overflow-wrap:anywhere}.state-value dd{font:17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--accent);margin:0;overflow-wrap:anywhere}.state-value dl{margin:0}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.subdued{color:var(--muted);font-size:10px}.event-line{padding:0 19px 13px;display:flex;gap:16px;align-items:baseline;flex-wrap:wrap}.event-line #last-event{color:var(--muted);font-size:10px;overflow-wrap:anywhere}.runtime-error{margin:0 18px 14px;padding:10px;border:1px solid #705049;border-radius:5px;background:#432c272e;color:var(--danger);font-size:11px;line-height:1.6}.cost-details{border-top:1px solid var(--line);padding:12px 19px;font-size:10px;color:var(--muted)}summary{cursor:pointer;font-size:11px;color:var(--muted)}.cost-details p{font-size:10px;line-height:1.8;color:var(--faint)}.cost-values{display:grid;grid-template-columns:1fr auto;gap:8px 20px;margin:16px 0}.cost-values dt,.cost-values dd{margin:0}.cost-values dd{text-align:right;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.reference{margin-top:25px}.reference-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));padding:22px 20px;gap:24px}.reference-grid article{min-width:0;display:flex;flex-direction:column;align-items:flex-start}.reference-grid h3{font-size:12px;color:#d2ddc8}.reference-grid p{font-size:10px;line-height:1.8;color:var(--muted);min-height:36px;margin:8px 0 10px}.reference-grid pre{font:10px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;background:#151b16;color:#bacead;width:100%;padding:10px;overflow:auto;border-radius:6px;min-height:115px;margin:0 0 8px}.reference-grid .quiet{font-size:10px;padding:5px 0}.syntax-details{padding:16px 20px;border-top:1px solid var(--line)}.syntax-columns{display:grid;grid-template-columns:1fr 1fr;gap:35px;margin-top:12px}.syntax-columns p{font-size:11px;line-height:1.95;color:var(--muted)}.syntax-columns strong{color:var(--text);font-weight:500}.syntax-columns code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:#d0dcbe;overflow-wrap:anywhere}.example-row{padding:18px 20px;border-top:1px solid var(--line);display:flex;align-items:center;gap:10px}.example-row>div{margin-right:auto}.example-row label{font-size:11px;color:var(--text)}.example-row p{font-size:10px;color:var(--faint);margin:5px 0 0}.example-row button,.example-row select{font-size:10px}.page-footer{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:26px 0 30px;color:var(--faint);font-size:10px}.page-footer>span{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:9px}.page-footer p{margin:0;line-height:1.8}.page-footer strong{font-weight:400;color:var(--muted)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important} +.baseline-row{border-top:1px solid var(--line);padding:15px 20px;display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center}.baseline-row a{font-size:11px;color:var(--accent);text-underline-offset:3px}.baseline-row span{font-size:10px;color:var(--faint)}#scene:focus-visible{outline:2px solid var(--accent);outline-offset:-3px} +@media(min-width:1450px){.code-editor{height:558px}.reference-grid pre{font-size:11px}} +@media(max-width:1050px){.masthead{padding:0 25px}.experiment{display:none}main{padding:29px 25px 0}.workspace{grid-template-columns:minmax(300px,1fr) minmax(340px,1fr);gap:15px}.intro>p{font-size:11px;max-width:290px}.reference-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:25px}.reference-grid p{min-height:0}.reference-grid pre{min-height:110px;font-size:11px}.transport #clock-status{width:100%;margin:3px 0 0}.code-editor{height:533px}.panel-heading{padding:0 16px}.apply-row{gap:10px;padding-left:14px;padding-right:14px}.apply-row p{font-size:9px}.input-heading{align-items:flex-start;flex-direction:column;gap:5px}.intro{gap:20px}button.primary{gap:14px;padding:11px 13px}} +@media(max-width:760px){.masthead{height:63px;padding:0 20px}.wordmark{font-size:21px}.wordmark span{font-size:11px}.connection{font-size:10px}main{padding:25px 18px 0}.intro{display:block;margin-bottom:22px}.intro h1{font-size:28px}.intro>p{margin-top:14px;max-width:none;font-size:11px}.workspace{grid-template-columns:minmax(0,1fr);gap:17px}.code-editor{height:340px}.editor-panel{width:100%}.transport #clock-status{width:auto;margin-left:auto}.right-column{gap:17px}.input-heading{flex-direction:row;align-items:center}.reference{margin-top:20px}.reference-grid{padding:20px 16px;gap:24px 17px}.reference-grid pre{font-size:10px}.reference>.panel-heading{height:auto;min-height:57px;padding-top:12px;padding-bottom:12px;align-items:flex-start;flex-direction:column;gap:6px}.syntax-columns{grid-template-columns:1fr;gap:0}.example-row{flex-wrap:wrap;padding:17px 16px}.example-row>div{width:100%;margin-bottom:4px}.example-row select{flex:1}.page-footer{align-items:flex-start;flex-direction:column;gap:10px;padding:23px 0}.apply-row{padding:14px 16px 10px;gap:17px}.apply-row p{font-size:10px;max-width:none}.diagnostic{margin-left:16px}.reference-grid article{overflow:hidden}} +@media(max-width:420px){main{padding-left:13px;padding-right:13px}.masthead{padding:0 15px}.wordmark span{font-size:10px}.connection{font-size:9px;gap:6px}.intro h1{font-size:25px}.transport{gap:5px;padding-left:12px;padding-right:12px}.transport button{font-size:10px;padding:7px 8px}.transport #clock-status{font-size:9px}.input-heading>span{font-size:8px}.reference-grid{grid-template-columns:minmax(0,1fr)}.reference-grid pre{min-height:0;font-size:11px}.reference-grid p{margin-bottom:8px}.file-tools{gap:0;padding:7px 5px}.file-tools .quiet{font-size:10px;padding:6px 7px}.apply-row p{font-size:9px}.line-numbers{padding-left:10px;min-width:36px}textarea#source{padding-left:12px;font-size:11px}.line-numbers{font-size:11px}} +@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}} diff --git a/prototypes/workbench/results/2026-09-23-pointer/build.json b/prototypes/workbench/results/2026-09-23-pointer/build.json new file mode 100644 index 0000000..5cddc11 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/build.json @@ -0,0 +1,23 @@ +{ + "builtUtc": "2026-09-23T06:14:20.758373+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "e253ccf62c10386f70edb8c84e299eb0b5eb59f8d35772150c37b3035e9047d4", + "wasmSha256": "5e733dbbb87bfc7f5b0c7714f176d5655cca4f617d11b92b07e638ae320474a6", + "evaluationSourceSha256": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "wasmFileBytes": 54511, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/26 core.test.blank program, no hidden application behavior, and reset...OK\n2/26 core.test.arbitrary input counter and unused events...OK\n3/26 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/26 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/26 core.test.branch to program end is valid...OK\n6/26 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/26 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/26 core.test.stack underflow overflow and leftover values reject the event...OK\n9/26 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/26 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/26 core.test.capacity boundaries and sticky load errors...OK\n12/26 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/26 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/26 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/26 core.test.binding evaluation is pure on both success and failure...OK\n16/26 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/26 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/26 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/26 core.test.point coordinates are ephemeral event operands and unavailable to pure evaluation...OK\n20/26 core.test.point event ranges reject holes and invalid payloads without committing...OK\n21/26 core.test.point event failure rolls back writes from earlier matching handlers...OK\n22/26 core.test.loading shares storage with records while old events and bindings remain executable...OK\n23/26 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK\n24/26 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK\n25/26 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n26/26 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 26 tests passed.\n" +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/manifest.json b/prototypes/workbench/results/2026-09-23-pointer/manifest.json new file mode 100644 index 0000000..645a8f3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/manifest.json @@ -0,0 +1,23 @@ +{ + "recordedUtc": "2026-09-23T06:20:26.666681+00:00", + "host": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "purpose": "Functional input/output correctness, not a performance comparison", + "sha256": { + "core.zig": "e253ccf62c10386f70edb8c84e299eb0b5eb59f8d35772150c37b3035e9047d4", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "cli.mjs": "a64681e25259c4b639f6f0dfd0c39e771096619bcae8639706633a01b3aa0ba9", + "pointer.test.mjs": "dcbea73a1d9506698c1559f141f0aea671e98c420eaa11ccf499fd40a7049cff", + "cli.test.mjs": "e10724ccbe76ec8930546ac58e4e7148c062eae44162ecb65fd6a8d8a1f68270", + "qa-pointer.js": "4e684038e149e8f71bf1b4d7189eecab79c16f7d06db6b3f3273a5ef441e5e87", + "CONTRACT.md": "bdc013e85a8c0b304f444e107d208309c8e9ae5bbc8d54f01349da911f0843f0", + "examples/direct.pure": "ba555e4591e92ec524f9d785333a3b8201e335abcdd2c08d9bbcc11aa84ef6f4", + "web/app.js": "96739c52c67398b250958a42509f1ad883d321f042d5b842e92f17995df5b3f9", + "web/compiler.mjs": "676f67135c2f659a6840a4bfc2eb46d808a08b026e0401022a1a10ff48f59733", + "web/runtime.mjs": "db026b48cd976a4b05304f56df9386b76a68a9174f5edf66f6f69b6ac7c28bf7", + "web/output.mjs": "98bfa97dcf5a2481fcf22dba749541dc63cac961cf1800f24c8679caab1d2353", + "web/scene-input.mjs": "4f65c523c31c1bc2a07dc08d7d5b384f4dd7a53b5e8298a729cbc2e39ea66a2e", + "web/index.html": "6fa0ff41f847fd1ab07c6c2efc777081672eaa53c434e51d7991beded5fe4765", + "web/style.css": "c9cf8e7983b0b11e17afa539de69b654b8e9d27962d8b16142e91ec303692cc6", + "web/core.wasm": "5e733dbbb87bfc7f5b0c7714f176d5655cca4f617d11b92b07e638ae320474a6" + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/pointer-browser.json b/prototypes/workbench/results/2026-09-23-pointer/pointer-browser.json new file mode 100644 index 0000000..7f74840 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/pointer-browser.json @@ -0,0 +1,32 @@ +{ + "checks": [ + "loading the pointer example leaves the running program unchanged until Apply", + "authored pointer declaration binds the visible target", + "keyboard alternatives remain available without a fake pointer button", + "real mouse down reaches the authored rule", + "drag retains grab offset in scene coordinates", + "linked output updates when the authored boundary is crossed", + "drag keeps its bound DOM target", + "capture delivers movement outside the scene and authored clamp applies", + "release outside the scene ends the gesture once", + "focus loss invokes authored rollback and clears ownership", + "late mouse release after cancel does not dispatch a second terminal event", + "explicit capture loss cancels instead of committing a drop", + "browser cancellation routes to the authored cancel handler", + "editing the authored rule changes the drag limit", + "later handler failure rolls back all earlier movement writes", + "cleanup releases a failed gesture while preserving its error", + "touch contact starts the same authored interaction", + "mobile scaled SVG maps touch coordinates back to world coordinates", + "touch drag manipulates the target without scrolling the page", + "touch release clears capture ownership", + "two contacts on different authored targets retain separate owners", + "independent touch payloads reach their own rules", + "touch cancellation releases both targets", + "second contact on an already owned target does not steal it", + "ignored second contact cannot strand a logical input", + "no browser page errors" + ], + "errors": [], + "kind": "functional input/output checks; no performance benchmark" +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/pure-pointer-desktop.png b/prototypes/workbench/results/2026-09-23-pointer/pure-pointer-desktop.png new file mode 100644 index 0000000..c3d9ab7 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer/pure-pointer-desktop.png differ diff --git a/prototypes/workbench/results/2026-09-23-pointer/pure-pointer-mobile.png b/prototypes/workbench/results/2026-09-23-pointer/pure-pointer-mobile.png new file mode 100644 index 0000000..83b65f3 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-pointer/pure-pointer-mobile.png differ diff --git a/prototypes/workbench/results/2026-09-23-pointer/qa-browser.js b/prototypes/workbench/results/2026-09-23-pointer/qa-browser.js new file mode 100644 index 0000000..9c2514a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/qa-browser.js @@ -0,0 +1,228 @@ +// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-pointer-workbench-blank.png', 'output/playwright/pure-pointer-workbench-authored.png', 'output/playwright/pure-pointer-workbench-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-pointer-workbench-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/qa-pointer.js b/prototypes/workbench/results/2026-09-23-pointer/qa-pointer.js new file mode 100644 index 0000000..9a12cc4 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/qa-pointer.js @@ -0,0 +1,120 @@ +// Playwright CLI run-code: real mouse / CDP touch input; no timing benchmark. +async (page) => { + const checks = [], errors = []; + const check = (condition, label) => { if (!condition) throw new Error(label); checks.push(label); }; + const close = (a, b) => Math.abs(a - b) < 0.1; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const client = (x, y) => page.evaluate(({x,y}) => { + const p = new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM()); + return {x:p.x,y:p.y}; + }, {x,y}); + const move = async (x,y) => { const p = await client(x,y); await page.mouse.move(p.x,p.y); }; + const start = async (x,y) => { await move(x,y); await page.mouse.down(); }; + const reset = async () => { await page.locator('#reset').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const apply = async source => { await page.locator('#source').fill(source); await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); page.on('dialog', onDialog); + let session; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('direct'); + await page.locator('#load-example').click(); + const source = await page.locator('#source').inputValue(); + check((await state()).views.length === 0, 'loading the pointer example leaves the running program unchanged until Apply'); + await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); + check(await page.locator('[data-point-input="drag"]').count() === 1, 'authored pointer declaration binds the visible target'); + check(await page.locator('#input-controls button').count() === 2, 'keyboard alternatives remain available without a fake pointer button'); + await page.evaluate(() => { + window.__pointerTile = document.querySelector('[data-point-input="drag"]'); + document.querySelector('#scene').addEventListener('pointerdown', e => { window.__pointerID = e.pointerId; }); + }); + await start(90,170); + let s = await state(); + check(s.states.dragging === 1 && s.held.includes('drag'), 'real mouse down reaches the authored rule'); + await move(370,230); s = await state(); + check(close(s.states.x,360) && close(s.states.y,220), 'drag retains grab offset in scene coordinates'); + check(s.views.find(v => v.id === 'region').value === 1 && await page.locator('[data-view-id="region"]').textContent() === '오른쪽 영역: 1', 'linked output updates when the authored boundary is crossed'); + check(await page.evaluate(() => window.__pointerTile === document.querySelector('[data-point-input="drag"]')), 'drag keeps its bound DOM target'); + // Physical mouse outside the SVG still reaches the stable captured scene. + await move(750,400); s = await state(); + check(close(s.states.x,564) && close(s.states.y,276), 'capture delivers movement outside the scene and authored clamp applies'); + await page.mouse.up(); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'release outside the scene ends the gesture once'); + await reset(); + await start(90,170); await move(380,240); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + s = await state(); + check(close(s.states.x,80) && close(s.states.y,160) && s.states.dragging === 0 && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'focus loss invokes authored rollback and clears ownership'); + const ended = s.successfulEvents; + await page.mouse.up(); + check((await state()).successfulEvents === ended, 'late mouse release after cancel does not dispatch a second terminal event'); + await start(90,170); await move(350,220); + await page.evaluate(() => document.querySelector('#scene').releasePointerCapture(window.__pointerID)); + await move(360,230); s = await state(); + check(close(s.states.x,80) && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'explicit capture loss cancels instead of committing a drop'); + await page.mouse.up(); + await start(90,170); await move(300,220); + await page.evaluate(() => document.querySelector('#scene').dispatchEvent(new PointerEvent('pointercancel', {pointerId:window.__pointerID,bubbles:true}))); + check((await state()).held.length === 0 && close((await state()).states.x,80), 'browser cancellation routes to the authored cancel handler'); + await page.mouse.up(); + // A source edit, not a renderer option, changes the movement constraint. + await apply(source.replaceAll('20, 564','20, 200')); + await start(90,170); await move(500,220); await page.mouse.up(); s = await state(); + check(close(s.states.x,200), 'editing the authored rule changes the drag limit'); + await apply(source + '\non drag.move { x = 1 / 0 }\n'); await reset(); + await start(90,170); await move(300,220); s = await state(); + check(s.lastError === 4 && close(s.states.x,80) && close(s.states.y,160), 'later handler failure rolls back all earlier movement writes'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); await page.mouse.up(); + s = await state(); + check(s.lastError === 4 && s.held.length === 0 && s.states.dragging === 0, 'cleanup releases a failed gesture while preserving its error'); + await apply(source); await reset(); + await page.screenshot({path:'output/playwright/pure-pointer-desktop.png',fullPage:true}); + + await page.setViewportSize({width:390,height:844}); + await page.locator('#scene').scrollIntoViewIfNeeded(); + session = await page.context().newCDPSession(page); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const touch = async (type, points) => session.send('Input.dispatchTouchEvent',{type,touchPoints:points}); + const a = await client(90,170), b = await client(270,210); + const scroll = await page.evaluate(() => window.scrollY); + await touch('touchStart',[{id:1,...a}]); + check((await state()).states.dragging === 1, 'touch contact starts the same authored interaction'); + await touch('touchMove',[{id:1,...b}]); s = await state(); + check(close(s.states.x,260) && close(s.states.y,200), 'mobile scaled SVG maps touch coordinates back to world coordinates'); + check(await page.evaluate(() => window.scrollY) === scroll, 'touch drag manipulates the target without scrolling the page'); + await touch('touchEnd',[]); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'touch release clears capture ownership'); + await page.screenshot({path:'output/playwright/pure-pointer-mobile.png',fullPage:true}); + // Independent contacts are not collapsed into one global drag state. + const two = 'state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\nbox one(20,100,100,80,"#c9f08b")\nbox two(300,100,100,80,"#c9f08b")\non a.move { x=event_x }\non b.move { y=event_y }'; + await apply(two); + const p1 = {id:1,...await client(50,140)}, p2 = {id:2,...await client(330,140)}; + await touch('touchStart',[p1]); await touch('touchStart',[p1,p2]); + check((await state()).held.length === 2, 'two contacts on different authored targets retain separate owners'); + await touch('touchMove',[{id:1,...await client(180,160)},{id:2,...await client(430,200)}]); s = await state(); + check(close(s.states.x,180) && close(s.states.y,200), 'independent touch payloads reach their own rules'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'touch cancellation releases both targets'); + // A second contact on the same target cannot steal the first one. + await touch('touchStart',[p1]); + await touch('touchStart',[p1,{id:2,...await client(70,150)}]); + check((await state()).held.length === 1, 'second contact on an already owned target does not steal it'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'ignored second contact cannot strand a logical input'); + check(errors.length === 0,'no browser page errors'); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); + await session.detach(); session = null; + await page.setViewportSize({width:1440,height:1000}); await apply(source); await reset(); + await page.evaluate(result => { window.__purePointerEvidence = result; }, {checks,errors,kind:'functional input/output checks; no performance benchmark'}); + return {checks:checks.length,errors}; + } catch (error) { + await page.evaluate(result => { window.__purePointerEvidence = result; }, {failed:String(error),checks,errors}); + throw error; + } finally { + if (session) { await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); await session.detach(); } + page.off('pageerror',onError); page.off('dialog',onDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-pointer/verification.json b/prototypes/workbench/results/2026-09-23-pointer/verification.json new file mode 100644 index 0000000..c4f3ab6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-pointer/verification.json @@ -0,0 +1,50 @@ +[ + { + "file": "prototypes/workbench/baseline.test.mjs", + "exitCode": 0, + "stdout": "\u2714 reference begins with the authored states and matching scene (2.305482ms)\n\u2714 press, hold, release, and frame order produce exact expected values (0.527815ms)\n\u2714 charge clamps at one and explicit floor restores rest (1.250815ms)\n\u2714 repeat and concurrent physical sources produce only logical edges (0.412074ms)\n\u2714 airborne presses cannot charge or cause another jump (0.291222ms)\n\u2714 releaseAll emits release and reset clears latches without a jump (0.422408ms)\n\u2714 unknown inputs and invalid frame counts reject without mutation (0.72439ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 81.548794\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/cli.test.mjs", + "exitCode": 0, + "stdout": "\u2714 terminal starts empty and produces the same charge trace as the shared runtime (112.05504ms)\n\u2714 invalid terminal commands do not alter state and fail piped execution (49.240343ms)\n\u2714 terminal replays authored pointer movement and cancellation through the shared runtime (54.859307ms)\n\u2714 file load preserves by name, rejected load preserves active work, and save never overwrites (55.085715ms)\n\u2714 terminal exposes arithmetic rollback and retains failure through matching release (56.135751ms)\n\u2714 terminal configures and discards evaluation records and recovers from a display limit (51.887603ms)\n\u2139 tests 6\n\u2139 suites 0\n\u2139 pass 6\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 450.784885\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/compiler.test.mjs", + "exitCode": 0, + "stdout": "\u2714 a blank document is an empty program with no implied behavior (4.256704ms)\n\u2714 forward references and separate state/input namespaces preserve declaration IDs (1.963593ms)\n\u2714 ordered assignments, repeated handlers, and condition evaluation once (0.883148ms)\n\u2714 nested branches use absolute forward targets and skip correct bodies (0.576167ms)\n\u2714 operator precedence, associativity, unary and builtin function lowering (2.148353ms)\n\u2714 multiline parentheses, scientific literals, comments, and string escapes (0.726093ms)\n\u2714 all instruction fields are numeric and carry original source line (1.483741ms)\n\u2714 contextual declaration words remain valid names outside reserved builtins (0.465ms)\n\u2714 unknown and reserved symbols have useful one-based locations (1.551241ms)\n\u2714 duplicates, unsupported keys, initializers, and invalid syntax are rejected (1.354833ms)\n\u2714 document, declaration, instruction, and nesting limits reject overflow (5.992483ms)\n\u2714 every supported input maps to a bounded ordered event ID (0.696482ms)\n\u2714 optional examples compile and charge behavior is entirely authored (17.239448ms)\n\u2139 tests 13\n\u2139 suites 0\n\u2139 pass 13\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 109.878985\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/evaluation.test.mjs", + "exitCode": 0, + "stdout": "\u2714 cache reuse, unrelated changes and discarded records preserve authored values (26.550024ms)\n\u2714 committed changes only: failed events and failed edits preserve reusable output (3.965408ms)\n\u2714 program edits, state reordering and reset cannot reuse old definitions (6.722354ms)\n\u2714 finite float semantics include signed zero, high state bit and arithmetic errors (4.888334ms)\n\u2714 one budget covers the whole scene; failures publish no partial scene or state rollback (2.441686ms)\n\u2714 small caches, no history and every small work limit give exact results or explicit failure (34.833599ms)\n\u2714 invalid settings and forget names preserve policy and state (2.774686ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 176.339238\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/pointer.test.mjs", + "exitCode": 0, + "stdout": "\u2714 pointer declarations resolve forward targets and reject ambiguous or contextless inputs (13.505164ms)\n\u2714 authored drag keeps grab offset, uses final up coordinates, and updates linked output (28.893605ms)\n\u2714 ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input (14.600108ms)\n\u2714 invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event (10.681516ms)\n\u2714 independent targets share the event pipeline and publication remains non-reentrant (10.878331ms)\n\u2714 failing down, up and cancel never prevent terminal ownership cleanup (10.156146ms)\n\u2139 tests 6\n\u2139 suites 0\n\u2139 pass 6\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 230.99603\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/presentation.test.mjs", + "exitCode": 0, + "stdout": "\u2714 direct publication matches archived snapshots and work counts through authored changes (52.238325ms)\n\u2714 whole-scene budget failure never publishes partial bindings and recovers exactly (6.839798ms)\n\u2714 latched event rollback, nonfinite bindings, reset and signed zero keep their semantics (3.393075ms)\n\u2714 publication rejects reentrant runtime changes and reads before they affect the scene (5.349038ms)\n\u2714 invalid or throwing output adapters do not strand the runtime or alter authored state (2.23126ms)\n\u2139 tests 5\n\u2139 suites 0\n\u2139 pass 5\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 145.248954\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/runtime.test.mjs", + "exitCode": 0, + "stdout": "\u2714 blank startup and applied empty source contain no application objects or behavior (10.791113ms)\n\u2714 author can add unrelated states inputs views and behavior entirely through source (6.443242ms)\n\u2714 keyboard and pointer sources aggregate first down and last up; repeats are ignored (2.079686ms)\n\u2714 held input rejects apply without changing applied source, metadata, or values (2.284056ms)\n\u2714 invalid syntax, unknown references and capacity overflow preserve the previous program (3.138815ms)\n\u2714 state preservation follows names across reorder/add/remove; reset uses new initializers (1.600112ms)\n\u2714 callers cannot mutate active metadata and corrupt name-based state preservation (1.649259ms)\n\u2714 conditions execute once, and assignments and multiple handlers follow source order (2.004259ms)\n\u2714 a failed event rolls back all handlers and keeps its source diagnostic through release (1.672019ms)\n\u2714 reset clears held inputs and latched errors; invalid frame counts have no effects (1.742611ms)\n\u2714 render evaluation is pure and invalid binding is distinct from an event rollback (4.822723ms)\n\u2714 independent adapters replay the same author program without sharing live state (2.826889ms)\n\u2714 Wasm has no imports and reports fixed memory and exact active IR/state storage (7.826798ms)\n\u2139 tests 13\n\u2139 suites 0\n\u2139 pass 13\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 126.815099\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/workspace.test.mjs", + "exitCode": 0, + "stdout": "\u2714 Wasm executes old events and output during staged replacement without corrupting either phase (8.926576ms)\n\u2714 workspace accounting reports shared payload and safety tag without double-counting (3.193686ms)\n\u2139 tests 2\n\u2139 suites 0\n\u2139 pass 2\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 82.501368\n", + "stderr": "" + } +] diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/core.zig b/prototypes/workbench/results/2026-09-23-presentation/before/core.zig new file mode 100644 index 0000000..84812a3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/core.zig @@ -0,0 +1,942 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { event: u32 = 0, start: u32 = 0, length: u32 = 0 }; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Workspace = union { staging: Program, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .state_count = state_count, .input_count = input_count } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.initials[id] = initial; + engine.workspace.staging.old_ids[id] = old_id; + engine.workspace.staging.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 21) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (event > p.input_count * 2) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (h.event > 2 * p.input_count) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) return .invalid; + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 21) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op == 20 or inst.op == 21) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + break; + } + } + const h = owner orelse return .invalid; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + } + } + return .ok; +} + +export fn commit_program() u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const code = validate(&engine.workspace.staging); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.state_count) |i| { + const old_id = engine.workspace.staging.old_ids[i]; + next[i] = if (old_id < 0) engine.workspace.staging.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging; + engine.values = next; + finishStaging(); + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +fn execute(values: *[max_states]f64, start: u32, length: u32, dt: f64, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op == 20 or inst.op == 21)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2 => 0, + 7, 16, 20, 21 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 2 => dt, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20) { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[inst.arg] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (event > engine.active.input_count * 2) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + const result = execute(&pending, h.start, h.length, dt, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, 0, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, 0, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Program), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Program), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 12), layout(3)); + try std.testing.expectEqual(@as(u32, 76), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(layout(1), layout(6)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Program)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Program) + @alignOf(Program)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/evaluation.zig b/prototypes/workbench/results/2026-09-23-presentation/before/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/manifest.json b/prototypes/workbench/results/2026-09-23-presentation/before/manifest.json new file mode 100644 index 0000000..5a8a5bb --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/manifest.json @@ -0,0 +1,10 @@ +{ + "web/runtime.mjs": "8516b0d7d89f5fd483b8c48cfc33bbb2bcbe8c27db3aa35b5f6cf892e89faa6e", + "web/app.js": "6d63f3bfdfe212146894dbb1772509eb4ecc2f3be3b106675f0fc10cdeb5bfd3", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "web/index.html": "4fff6eb67d8cff098302465449251d17f0efb39d9144e87d15fefcc152e5d97e", + "web/style.css": "43301298028c5bc431eecd5ff30d7362fae83cce1fd0fdd7e8bea858100c22b9", + "core.zig": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165" +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/web/app.js b/prototypes/workbench/results/2026-09-23-presentation/before/web/app.js new file mode 100644 index 0000000..c931b1a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/web/app.js @@ -0,0 +1,427 @@ +import { createRuntime } from './runtime.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const NS = 'http://www.w3.org/2000/svg'; +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString(); +const formatBytes = value => `${Number(value).toLocaleString('en-US')} B`; + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + $('cursor-position').textContent = `${lines.length}행 · ${lines.at(-1).length + 1}열`; +} + +function draftChanged() { + const count = source.value.split('\n').length; + $('line-numbers').textContent = Array.from({ length: count }, (_, i) => i + 1).join('\n'); + $('line-numbers').scrollTop = source.scrollTop; + $('source-size').textContent = `${bytes(source.value).toLocaleString('en-US')} / 32,768 bytes`; + $('source-size').style.color = bytes(source.value) > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + $('draft-status').textContent = dirty() ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'; + $('draft-status').classList.toggle('dirty', dirty()); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + $('play').textContent = '▶ 실행'; + $('play').setAttribute('aria-pressed', 'false'); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function svgElement(type, attrs, text) { + const node = document.createElementNS(NS, type); + for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); + if (text !== undefined) node.textContent = text; + return node; +} + +function renderScene(views) { + const elements = []; + for (const view of views) { + let element; + const attrs = { 'data-view-id': view.id }; + if (view.type === 'box') element = svgElement('rect', { ...attrs, x: view.x, y: view.y, width: Math.max(0, view.width), height: Math.max(0, view.height), fill: view.color }); + else if (view.type === 'circle') element = svgElement('circle', { ...attrs, cx: view.x, cy: view.y, r: Math.max(0, view.radius), fill: view.color }); + else if (view.type === 'text') element = svgElement('text', { ...attrs, x: view.x, y: view.y, fill: '#e7ece6', 'font-family': 'ui-monospace, monospace', 'font-size': 20 }, `${view.prefix || ''}${number(view.value)}`); + if (element) elements.push(element); + } + $('scene-content').replaceChildren(...elements); + $('empty-scene').hidden = elements.length !== 0; + $('scene-description').textContent = elements.length ? `사용자가 만든 장면. 도형과 글자 ${elements.length}개.` : '사용자가 코드로 만든 장면. 현재 비어 있습니다.'; +} + +function renderStates(states) { + const children = []; + for (const [name, value] of Object.entries(states)) { + const holder = document.createElement('dl'); + holder.className = 'state-value'; + const label = document.createElement('dt'); + label.textContent = name; + const amount = document.createElement('dd'); + amount.textContent = number(value); + amount.dataset.state = name; + holder.append(label, amount); + children.push(holder); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '선언한 상태의 값이 표시됩니다.'; + children.push(message); + } + $('state-values').replaceChildren(...children); +} + +function renderCosts(snapshot) { + const memory = snapshot.memory || {}; + const metrics = runtime.currentProgram?.metrics || {}; + const rows = [ + ['Wasm 선형 메모리', memory.linearBytes], + ['코어 예약 공간', memory.reservedEngineBytes], + ['사용 중인 명령·핸들러', memory.activeIRBytes], + ['현재 상태 값', memory.activeStateBytes], + ['새 코드 준비 공간 (예약에 포함)', memory.stagingReservedBytes], + ['평가 스택 용량', memory.evalStackBytes], + ]; + const children = []; + for (const [label, amount] of rows) { + if (!Number.isFinite(amount)) continue; + const dt = document.createElement('dt'); dt.textContent = label; + const dd = document.createElement('dd'); dd.textContent = formatBytes(amount); + children.push(dt, dd); + } + const dt = document.createElement('dt'); dt.textContent = '마지막 이벤트 실행 명령'; + const dd = document.createElement('dd'); dd.textContent = `${snapshot.lastOps || 0}개`; + children.push(dt, dd); + const sourceBytes = metrics.sourceBytes; + if (Number.isFinite(sourceBytes)) { + const a = document.createElement('dt'); a.textContent = '적용된 소스 크기'; + const b = document.createElement('dd'); b.textContent = formatBytes(sourceBytes); + children.push(a, b); + } + $('cost-values').replaceChildren(...children); + $('dropped-ticks').textContent = `시간 지연으로 건너뛴 프레임: ${droppedTicks}`; +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.snapshot(); + if (snapshot.lastError) pause(); + renderScene(snapshot.views || []); + renderStates(snapshot.states || {}); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + button.setAttribute('aria-pressed', String(held.has(name))); + } + $('scene-status').textContent = runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'; + $('clock-status').textContent = `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`; + $('revision').textContent = `revision ${snapshot.revision || 0}`; + for (const id of ['play', 'step', 'reset']) $(id).disabled = !runtime.currentProgram; + const event = snapshot.lastEvent; + $('last-event').textContent = event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'; + $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + $('runtime-error').textContent = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + $('runtime-error').textContent += ` · ${snapshot.displayError.message}`; + renderCosts(snapshot); + draftChanged(); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime) { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic('편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.'); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_SOURCE_BYTES * 7 + 1024) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = source.value; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + let project; + try { project = JSON.parse(content); } catch { throw new Error('올바른 JSON 파일이 아닙니다.'); } + if (!project || typeof project !== 'object' || Array.isArray(project) || project.format !== 'pure-workbench' || project.version !== 1 || typeof project.source !== 'string') throw new Error('Pure 작업물 형식이 아닙니다. format, version, source를 확인하세요.'); + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-presentation/before/web/compiler.mjs new file mode 100644 index 0000000..016ea9e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/web/compiler.mjs @@ -0,0 +1,303 @@ +// 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', '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(); + } + 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: token.value }); + } 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 { + this.expect('='); + statements.push({ kind: 'assign', token, name: token.value, 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('='); + let sign = 1; + if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1; + const value = this.expect('number', 'numeric state initializer'); + declarations.push({ kind, token, name, initial: sign * value.value }); + } else if (kind === 'input') { + const name = this.expect('name', 'input name'); this.expect('='); + const key = this.expect('name', 'keyboard code'); + 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 or up'); + if (edge.value !== 'down' && edge.value !== 'up') fail(edge, 'Expected down or up'); + } + 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: [], metrics: {} }; + const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(); + 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 === '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 (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'); + } + 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 === 'dt') { + if (binding) fail(token, 'dt is only available in handlers'); + emit(2, token, 0, 0, 1); + } else { + 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 (!states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`); + expression(statement.expression); + emit(20, statement.token, states.get(statement.name), 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; + } + } + }; + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + if (declaration.event.value !== 'frame') { + if (!inputs.has(declaration.event.value)) fail(declaration.event, `Unknown input ${declaration.event.value}`); + event = 1 + inputs.get(declaration.event.value) * 2 + (declaration.edge.value === 'up' ? 1 : 0); + } + 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); + } + } + 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/workbench/results/2026-09-23-presentation/before/web/core.wasm b/prototypes/workbench/results/2026-09-23-presentation/before/web/core.wasm new file mode 100644 index 0000000..0eb3fe5 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-presentation/before/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/web/index.html b/prototypes/workbench/results/2026-09-23-presentation/before/web/index.html new file mode 100644 index 0000000..d93589e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/web/index.html @@ -0,0 +1,93 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-presentation/before/web/runtime.mjs new file mode 100644 index 0000000..02b4e7c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/web/runtime.mjs @@ -0,0 +1,205 @@ +import { compile } from './compiler.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') + 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(); + 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) { + const status = 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + const api = { + get currentProgram() { return program; }, + configureEvaluation({ cacheEntries = evaluationPolicy.cacheEntries, + historyEntries = evaluationPolicy.historyEntries, workLimit = evaluationPolicy.workLimit } = {}) { + 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') { + 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) { + 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)); + const oldIds = new Map(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); + } + for (const op of next.instructions) checked(core.put_instruction(op.op, op.arg, op.value), op.line); + for (const handler of next.handlers) checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + checked(core.commit_program()); + } catch (error) { + core.abort_program(); + throw error; + } + program = next; + appliedSource = source; + lastEvent = null; + lastFailure = null; + return next; + }, + reset() { + held.clear(); + core.reset_state(); + lastEvent = null; + lastFailure = null; + }, + frame(count = 1) { + 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') { + const index = inputIndex(name); + 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') { + const index = inputIndex(name); + 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); + }, + releaseAll() { + let firstError = 0; + 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 }; + }, + snapshot() { + core.begin_evaluation(evaluationPolicy.workLimit); + const states = Object.fromEntries((program?.states ?? []).map((state, index) => [state.name, core.get_state(index)])); + const views = []; + let viewError = null; + viewLoop: for (const view of program?.views ?? []) { + const rendered = { id: view.id, type: view.type, color: view.color, prefix: view.prefix }; + let valid = true; + for (const [name, binding] of Object.entries(view.bindings)) { + const value = core.eval_bounded(binding.start, binding.length); + if (core.evaluation_info(0) === 10) { + // A snapshot must not mix complete views with budget-truncated ones. + views.length = 0; + viewError = { code: 101, line: view.line, + message: '표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.' }; + break viewLoop; + } + if (!Number.isFinite(value) || (['width', 'height', 'radius'].includes(name) && value < 0)) { + viewError ??= { line: view.line, message: `${view.id}.${name}: 표시할 수 없는 값입니다. 0 나눗셈이나 음수 크기를 확인하세요.` }; + valid = false; + } + rendered[name] = value; + } + if (valid) views.push(rendered); + } + const executionError = lastFailure?.status || 0; + return { + states, views, 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), + }, + }; + }, + }; + api.configureEvaluation(evaluationOptions); + return Object.freeze(api); +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/before/web/style.css b/prototypes/workbench/results/2026-09-23-presentation/before/web/style.css new file mode 100644 index 0000000..819c84c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/before/web/style.css @@ -0,0 +1,8 @@ +:root{color-scheme:dark;--bg:#151918;--panel:#1d2220;--field:#151a18;--line:#353e38;--text:#e7ece6;--muted:#a9b6aa;--faint:#7b8e7e;--accent:#c9f08b;--accent-dark:#222e18;--danger:#f6afa0;--radius:14px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-synthesis:none} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text)}button,input,select,textarea{font:inherit}button,select{border:1px solid var(--line);border-radius:7px;background:#282f2a;color:var(--text);padding:9px 12px;cursor:pointer;font-size:12px;font-weight:600;min-height:36px}button:hover:not(:disabled){background:#344035;border-color:#70836d}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button.primary{background:var(--accent);color:var(--accent-dark);border-color:var(--accent);padding:11px 19px;font-size:14px;display:flex;gap:28px;justify-content:space-between;align-items:center}button.primary:hover:not(:disabled){background:#d8f7ad}button.secondary{background:#c9f08b16;border-color:#637545;color:#d7f0b5}button.quiet{background:transparent;border-color:transparent;color:var(--muted);padding:6px 8px;min-height:30px}button.quiet:hover{color:var(--text)}.masthead{height:76px;padding:0 4.5%;display:flex;align-items:center;gap:28px;border-bottom:1px solid var(--line)}.wordmark{font-size:23px;font-weight:750;letter-spacing:-1px;color:var(--accent);text-decoration:none}.wordmark span{font-size:13px;color:var(--muted);letter-spacing:0;font-weight:400}.experiment{font-size:11px;color:var(--faint);border-left:1px solid var(--line);padding-left:24px}.connection{margin-left:auto;display:flex;gap:9px;align-items:center;font-size:11px;color:var(--muted)}.dot{width:6px;height:6px;border-radius:100%;background:#aa8e5c}.dot.ready{background:var(--accent)}.dot.failed{background:var(--danger)}main{max-width:1540px;margin:auto;padding:38px 4.5% 0}.intro{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin:0 0 27px}.eyebrow{font-size:10px;letter-spacing:1.4px;font-weight:600;color:var(--accent);margin:0 0 10px}h1{font-size:clamp(23px,2.5vw,35px);letter-spacing:-1px;margin:0;font-weight:600}.intro>p{font-size:12px;line-height:1.9;color:var(--muted);margin:0 0 2px}.intro strong{color:var(--text);font-weight:500}.workspace{display:grid;grid-template-columns:minmax(340px,1fr) minmax(380px,1.05fr);gap:22px;align-items:start}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.panel-heading{height:56px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line)}h2{margin:0;font-size:13px;font-weight:650;display:flex;gap:10px;align-items:center}.section-index{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;font-weight:400;color:var(--faint)}.badge{font-size:10px;border:1px solid var(--line);border-radius:4px;padding:4px 7px;white-space:nowrap;color:var(--muted)}.badge.dirty{color:var(--accent);border-color:#5f7247}.file-tools{display:flex;align-items:center;gap:3px;padding:8px 10px;border-bottom:1px solid var(--line);flex-wrap:wrap}.code-editor{height:514px;display:flex;overflow:hidden;background:var(--field);position:relative}.line-numbers{padding:20px 10px 20px 16px;min-width:45px;text-align:right;color:#667567;user-select:none;white-space:pre;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:hidden;flex-shrink:0;border-right:1px solid #2a322b}textarea#source{border:0;border-radius:0;outline:0;resize:none;padding:20px 16px;display:block;width:100%;height:100%;color:#d4e3ca;background:transparent;white-space:pre;overflow:auto;tab-size:2;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;caret-color:var(--accent)}textarea#source:focus{background:#171e19}textarea#source::placeholder{color:#7c8b7d;font:12px/1.9 ui-sans-serif,system-ui,sans-serif;white-space:pre-wrap}textarea#source[aria-invalid=true]{box-shadow:inset 3px 0 0 var(--danger)}.editor-footer{padding:8px 15px;font-size:10px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--faint);display:flex;justify-content:space-between;border-top:1px solid var(--line)}.apply-row{display:flex;gap:18px;align-items:center;padding:16px 18px 12px}.apply-row p{font-size:10px;color:var(--muted);line-height:1.7;margin:0;max-width:185px}.diagnostic{margin:0 18px 17px;font-size:11px;line-height:1.8;min-height:22px;color:var(--muted);display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}.diagnostic.error{color:var(--danger)}.diagnostic.success{color:#c7dea9}.right-column{display:grid;gap:18px}.stage-wrap{position:relative;background-color:#121613;background-image:linear-gradient(#a6c39907 1px,transparent 1px),linear-gradient(90deg,#a6c39907 1px,transparent 1px);background-size:24px 24px}#scene{width:100%;height:auto;aspect-ratio:640/360;display:block;overflow:hidden}.empty-scene{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;gap:9px;color:var(--faint);padding:24px;text-align:center}.empty-scene[hidden]{display:none}.empty-scene strong{font-size:13px;color:var(--muted);font-weight:400}.empty-scene>span:not(.empty-cross){font-size:11px}.empty-cross{font-size:31px;color:#52614d;font-weight:200;margin-bottom:7px}.stage-dimensions{position:absolute;right:14px;bottom:10px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#6d7f6d;pointer-events:none}.transport{display:flex;gap:7px;align-items:center;padding:14px 16px 0;flex-wrap:wrap}.transport button{font-size:11px;padding:7px 10px}.transport #clock-status{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:var(--muted);margin-left:auto}.control-note,.keyboard-note{font-size:10px;line-height:1.7;color:var(--faint);margin:10px 18px 15px}.input-heading{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:0 18px;padding-top:13px;border-top:1px solid var(--line)}h3{font-size:11px;font-weight:550;margin:0}.input-heading>span{font-size:9px;color:var(--faint)}.input-controls{display:flex;flex-wrap:wrap;gap:7px;padding:11px 17px 0;min-height:39px}.input-button{display:flex;align-items:center;gap:14px;touch-action:none;user-select:none;-webkit-user-select:none}.input-button kbd{font:9px ui-monospace,SFMono-Regular,Consolas,monospace;border:1px solid #5a6957;border-radius:3px;padding:3px 4px;color:var(--muted);min-width:22px;text-align:center}.input-button.held{background:var(--accent);border-color:var(--accent);color:var(--accent-dark)}.input-button.held kbd{color:var(--accent-dark);border-color:#829a62}.empty-note{font-size:11px;line-height:1.7;color:var(--faint);margin:0}.keyboard-note{margin-bottom:13px;font-size:9px}.state-values{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));padding:15px 19px;gap:13px;max-height:210px;overflow:auto}.state-value{display:flex;flex-direction:column;gap:6px;min-width:0}.state-value dt{font:10px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);overflow-wrap:anywhere}.state-value dd{font:17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--accent);margin:0;overflow-wrap:anywhere}.state-value dl{margin:0}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.subdued{color:var(--muted);font-size:10px}.event-line{padding:0 19px 13px;display:flex;gap:16px;align-items:baseline;flex-wrap:wrap}.event-line #last-event{color:var(--muted);font-size:10px;overflow-wrap:anywhere}.runtime-error{margin:0 18px 14px;padding:10px;border:1px solid #705049;border-radius:5px;background:#432c272e;color:var(--danger);font-size:11px;line-height:1.6}.cost-details{border-top:1px solid var(--line);padding:12px 19px;font-size:10px;color:var(--muted)}summary{cursor:pointer;font-size:11px;color:var(--muted)}.cost-details p{font-size:10px;line-height:1.8;color:var(--faint)}.cost-values{display:grid;grid-template-columns:1fr auto;gap:8px 20px;margin:16px 0}.cost-values dt,.cost-values dd{margin:0}.cost-values dd{text-align:right;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.reference{margin-top:25px}.reference-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));padding:22px 20px;gap:24px}.reference-grid article{min-width:0;display:flex;flex-direction:column;align-items:flex-start}.reference-grid h3{font-size:12px;color:#d2ddc8}.reference-grid p{font-size:10px;line-height:1.8;color:var(--muted);min-height:36px;margin:8px 0 10px}.reference-grid pre{font:10px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;background:#151b16;color:#bacead;width:100%;padding:10px;overflow:auto;border-radius:6px;min-height:115px;margin:0 0 8px}.reference-grid .quiet{font-size:10px;padding:5px 0}.syntax-details{padding:16px 20px;border-top:1px solid var(--line)}.syntax-columns{display:grid;grid-template-columns:1fr 1fr;gap:35px;margin-top:12px}.syntax-columns p{font-size:11px;line-height:1.95;color:var(--muted)}.syntax-columns strong{color:var(--text);font-weight:500}.syntax-columns code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:#d0dcbe;overflow-wrap:anywhere}.example-row{padding:18px 20px;border-top:1px solid var(--line);display:flex;align-items:center;gap:10px}.example-row>div{margin-right:auto}.example-row label{font-size:11px;color:var(--text)}.example-row p{font-size:10px;color:var(--faint);margin:5px 0 0}.example-row button,.example-row select{font-size:10px}.page-footer{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:26px 0 30px;color:var(--faint);font-size:10px}.page-footer>span{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:9px}.page-footer p{margin:0;line-height:1.8}.page-footer strong{font-weight:400;color:var(--muted)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important} +.baseline-row{border-top:1px solid var(--line);padding:15px 20px;display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center}.baseline-row a{font-size:11px;color:var(--accent);text-underline-offset:3px}.baseline-row span{font-size:10px;color:var(--faint)}#scene:focus-visible{outline:2px solid var(--accent);outline-offset:-3px} +@media(min-width:1450px){.code-editor{height:558px}.reference-grid pre{font-size:11px}} +@media(max-width:1050px){.masthead{padding:0 25px}.experiment{display:none}main{padding:29px 25px 0}.workspace{grid-template-columns:minmax(300px,1fr) minmax(340px,1fr);gap:15px}.intro>p{font-size:11px;max-width:290px}.reference-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:25px}.reference-grid p{min-height:0}.reference-grid pre{min-height:110px;font-size:11px}.transport #clock-status{width:100%;margin:3px 0 0}.code-editor{height:533px}.panel-heading{padding:0 16px}.apply-row{gap:10px;padding-left:14px;padding-right:14px}.apply-row p{font-size:9px}.input-heading{align-items:flex-start;flex-direction:column;gap:5px}.intro{gap:20px}button.primary{gap:14px;padding:11px 13px}} +@media(max-width:760px){.masthead{height:63px;padding:0 20px}.wordmark{font-size:21px}.wordmark span{font-size:11px}.connection{font-size:10px}main{padding:25px 18px 0}.intro{display:block;margin-bottom:22px}.intro h1{font-size:28px}.intro>p{margin-top:14px;max-width:none;font-size:11px}.workspace{grid-template-columns:minmax(0,1fr);gap:17px}.code-editor{height:340px}.editor-panel{width:100%}.transport #clock-status{width:auto;margin-left:auto}.right-column{gap:17px}.input-heading{flex-direction:row;align-items:center}.reference{margin-top:20px}.reference-grid{padding:20px 16px;gap:24px 17px}.reference-grid pre{font-size:10px}.reference>.panel-heading{height:auto;min-height:57px;padding-top:12px;padding-bottom:12px;align-items:flex-start;flex-direction:column;gap:6px}.syntax-columns{grid-template-columns:1fr;gap:0}.example-row{flex-wrap:wrap;padding:17px 16px}.example-row>div{width:100%;margin-bottom:4px}.example-row select{flex:1}.page-footer{align-items:flex-start;flex-direction:column;gap:10px;padding:23px 0}.apply-row{padding:14px 16px 10px;gap:17px}.apply-row p{font-size:10px;max-width:none}.diagnostic{margin-left:16px}.reference-grid article{overflow:hidden}} +@media(max-width:420px){main{padding-left:13px;padding-right:13px}.masthead{padding:0 15px}.wordmark span{font-size:10px}.connection{font-size:9px;gap:6px}.intro h1{font-size:25px}.transport{gap:5px;padding-left:12px;padding-right:12px}.transport button{font-size:10px;padding:7px 8px}.transport #clock-status{font-size:9px}.input-heading>span{font-size:8px}.reference-grid{grid-template-columns:minmax(0,1fr)}.reference-grid pre{min-height:0;font-size:11px}.reference-grid p{margin-bottom:8px}.file-tools{gap:0;padding:7px 5px}.file-tools .quiet{font-size:10px;padding:6px 7px}.apply-row p{font-size:9px}.line-numbers{padding-left:10px;min-width:36px}textarea#source{padding-left:12px;font-size:11px}.line-numbers{font-size:11px}} +@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}} diff --git a/prototypes/workbench/results/2026-09-23-presentation/browser-authoring.json b/prototypes/workbench/results/2026-09-23-presentation/browser-authoring.json new file mode 100644 index 0000000..996591e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/browser-authoring.json @@ -0,0 +1,131 @@ +{ + "recordedUtc": "2026-09-23T05:42:05.485Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 464, + "activeStateBytes": 24, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "output/playwright/pure-presentation-workbench-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "output/playwright/pure-presentation-workbench-blank.png", + "output/playwright/pure-presentation-workbench-authored.png", + "output/playwright/pure-presentation-workbench-mobile.png" + ], + "qaSourceSha256": "0d4c155b3575481c60f175b71acdc332f1c0d8489297794deca6f55999ead1b8", + "executedScriptSha256": "b70a26bdc9d84e85b459da697670e8c122d5447a5bca04cecba6a54a317a7fec", + "scriptChange": "Artifact filename prefix (screenshots and downloaded draft) changed from pure-workbench- to pure-presentation-workbench- only." +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/browser-limit.json b/prototypes/workbench/results/2026-09-23-presentation/browser-limit.json new file mode 100644 index 0000000..a85692e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/browser-limit.json @@ -0,0 +1,242 @@ +{ + "utc": "2026-09-23T05:43:00.130Z", + "checks": [ + "authored browser input commits fractional state", + "bounded evaluation renders the exact authored expression", + "code replacement preserves state and changes binding result", + "browser Wasm reuses completed bindings", + "browser runs the shared-workspace build with only 56 additional dedicated bytes", + "discarding records preserves the browser snapshot", + "browser API publishes no partial scene after budget exhaustion", + "raising the budget recovers the browser snapshot", + "configured UI keeps committed state while withholding incomplete display", + "UI distinguishes display limit from event rollback", + "normal configuration reloads to a clean blank work", + "no uncaught browser errors" + ], + "api": { + "before": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "warm": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 6, + "instructions": 0, + "cacheHits": 3, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + }, + "forgotten": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limited": { + "states": { + "x": 3 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limitStats": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 1, + "work": 1, + "instructions": 0, + "cacheHits": 0, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + }, + "recovered": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + } + }, + "limitedUi": { + "states": { + "x": 3.25 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 2, + "held": [], + "lastEvent": { + "name": "next", + "phase": "up", + "source": "pointer:1", + "status": 0, + "line": null, + "ops": 0 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "appliedSource": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "message": "표시 오류 · 3행 · 표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요. — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.", + "errors": [], + "uiLimitSetup": "Test-only module response overrides default workLimit to 1; production files are unchanged.", + "screenshot": "output/playwright/pure-presentation-limit.png", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "qaSourceSha256": "d83577c0851b465c3bd43c256b53659aef9df96f3c3dffe7e59f5d7c98db80c6", + "executedScriptSha256": "b1e1a18f540720252e3d2e6bb00907b343aee6f23ab339e704e96bf2e81229a9", + "scriptChange": "Screenshot path changed from pure-workspace-limit.png to pure-presentation-limit.png only." +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/browser.json b/prototypes/workbench/results/2026-09-23-presentation/browser.json new file mode 100644 index 0000000..d42f835 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/browser.json @@ -0,0 +1,1536 @@ +{ + "utc": "2026-09-23T05:40:52.800Z", + "checks": [ + "real keyboard event reaches the authored output", + "real pointer input and keyboard input use the same authored behavior", + "the displayed element survives an input update", + "failed editing preserves active values and output destinations", + "invalid view is hidden while valid views remain published", + "reset recovers a hidden destination without recreating the scene", + "successful editing replaces changed output structure", + "applying empty work removes all old destinations", + "all six archived/current runs have identical final authored state and views", + "current hot path creates no DOM elements and no full snapshots", + "archived hot path reproduces full snapshots and element replacement", + "each tap changes only the moving x attribute and label text in the scene", + "no uncaught browser errors" + ], + "errors": [], + "samples": [ + { + "variant": "before", + "taps": 100, + "events": 200, + "elapsedMs": 941.1999999955297, + "createdElements": 9200, + "snapshotCalls": 200, + "mutations": { + "scene-content": { + "records": 200, + "childList": 200, + "attributes": 0 + }, + "state-values": { + "records": 200, + "childList": 200, + "attributes": 0 + }, + "cost-values": { + "records": 200, + "childList": 200, + "attributes": 0 + } + }, + "identityPreserved": false, + "final": { + "states": { + "x": 70, + "radius": 12 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "x: ", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "bubble", + "type": "circle", + "color": "#c9f08b", + "x": 500, + "y": 150, + "radius": 12 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 280, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240 + }, + "scene": "x: 70" + }, + { + "variant": "after", + "taps": 100, + "events": 200, + "elapsedMs": 751.3999999985099, + "createdElements": 0, + "snapshotCalls": 0, + "mutations": { + "scene-content": { + "records": 200, + "childList": 100, + "attributes": 100 + }, + "state-values": { + "records": 100, + "childList": 100, + "attributes": 0 + }, + "cost-values": { + "records": 200, + "childList": 200, + "attributes": 0 + } + }, + "identityPreserved": true, + "final": { + "states": { + "x": 70, + "radius": 12 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "x: ", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "bubble", + "type": "circle", + "color": "#c9f08b", + "x": 500, + "y": 150, + "radius": 12 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 280, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240 + }, + "scene": "x: 70" + }, + { + "variant": "after", + "taps": 100, + "events": 200, + "elapsedMs": 579.1000000014901, + "createdElements": 0, + "snapshotCalls": 0, + "mutations": { + "scene-content": { + "records": 200, + "childList": 100, + "attributes": 100 + }, + "state-values": { + "records": 100, + "childList": 100, + "attributes": 0 + }, + "cost-values": { + "records": 200, + "childList": 200, + "attributes": 0 + } + }, + "identityPreserved": true, + "final": { + "states": { + "x": 70, + "radius": 12 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "x: ", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "bubble", + "type": "circle", + "color": "#c9f08b", + "x": 500, + "y": 150, + "radius": 12 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 280, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240 + }, + "scene": "x: 70" + }, + { + "variant": "before", + "taps": 100, + "events": 200, + "elapsedMs": 648.6999999955297, + "createdElements": 9200, + "snapshotCalls": 200, + "mutations": { + "scene-content": { + "records": 200, + "childList": 200, + "attributes": 0 + }, + "state-values": { + "records": 200, + "childList": 200, + "attributes": 0 + }, + "cost-values": { + "records": 200, + "childList": 200, + "attributes": 0 + } + }, + "identityPreserved": false, + "final": { + "states": { + "x": 70, + "radius": 12 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "x: ", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "bubble", + "type": "circle", + "color": "#c9f08b", + "x": 500, + "y": 150, + "radius": 12 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 280, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240 + }, + "scene": "x: 70" + }, + { + "variant": "before", + "taps": 100, + "events": 200, + "elapsedMs": 633.6000000014901, + "createdElements": 9200, + "snapshotCalls": 200, + "mutations": { + "scene-content": { + "records": 200, + "childList": 200, + "attributes": 0 + }, + "state-values": { + "records": 200, + "childList": 200, + "attributes": 0 + }, + "cost-values": { + "records": 200, + "childList": 200, + "attributes": 0 + } + }, + "identityPreserved": false, + "final": { + "states": { + "x": 70, + "radius": 12 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "x: ", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "bubble", + "type": "circle", + "color": "#c9f08b", + "x": 500, + "y": 150, + "radius": 12 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 280, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240 + }, + "scene": "x: 70" + }, + { + "variant": "after", + "taps": 100, + "events": 200, + "elapsedMs": 733.0999999940395, + "createdElements": 0, + "snapshotCalls": 0, + "mutations": { + "scene-content": { + "records": 200, + "childList": 100, + "attributes": 100 + }, + "state-values": { + "records": 100, + "childList": 100, + "attributes": 0 + }, + "cost-values": { + "records": 200, + "childList": 200, + "attributes": 0 + } + }, + "identityPreserved": true, + "final": { + "states": { + "x": 70, + "radius": 12 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "x: ", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "bubble", + "type": "circle", + "color": "#c9f08b", + "x": 500, + "y": 150, + "radius": 12 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 240, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 280, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 280, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240 + }, + "scene": "x: 70" + } + ], + "source": "state x = 40\nstate radius = 12\ninput move = KeyM\ninput shrink = KeyS\nbox moving(x, 100, 30, 30, \"#7ed9ae\")\ntext label(20, 40, x, \"x: \")\ncircle bubble(500, 150, radius, \"#c9f08b\")\nbox static0(20, 200, 20, 20, \"#8da5bb\")\nbox static1(90, 200, 20, 20, \"#8da5bb\")\nbox static2(160, 200, 20, 20, \"#8da5bb\")\nbox static3(230, 200, 20, 20, \"#8da5bb\")\nbox static4(300, 200, 20, 20, \"#8da5bb\")\nbox static5(370, 200, 20, 20, \"#8da5bb\")\nbox static6(440, 200, 20, 20, \"#8da5bb\")\nbox static7(20, 240, 20, 20, \"#8da5bb\")\nbox static8(90, 240, 20, 20, \"#8da5bb\")\nbox static9(160, 240, 20, 20, \"#8da5bb\")\nbox static10(230, 240, 20, 20, \"#8da5bb\")\nbox static11(300, 240, 20, 20, \"#8da5bb\")\nbox static12(370, 240, 20, 20, \"#8da5bb\")\nbox static13(440, 240, 20, 20, \"#8da5bb\")\nbox static14(20, 280, 20, 20, \"#8da5bb\")\nbox static15(90, 280, 20, 20, \"#8da5bb\")\nbox static16(160, 280, 20, 20, \"#8da5bb\")\nbox static17(230, 280, 20, 20, \"#8da5bb\")\nbox static18(300, 280, 20, 20, \"#8da5bb\")\nbox static19(370, 280, 20, 20, \"#8da5bb\")\nbox static20(440, 280, 20, 20, \"#8da5bb\")\non move.down { x = x + 0.25 }\non shrink.down { radius = radius - 24 }", + "timingScope": "100 synchronous synthetic keyboard taps after 20 warm-up taps; JS dispatch to DOM submission only, including instrumentation; not paint/input latency.", + "setup": "Archived app/runtime are supplied by test-only routing; snapshot calls and element creation are instrumented. Same Wasm/compiler and production configuration.", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "screenshot": "output/playwright/pure-presentation.png", + "sourceSha256": { + "web/runtime.mjs": "d490c54fb2b422dfe89b90a594a8da9ebc95bb007910211cb4686ccb83ed75c7", + "web/app.js": "a50e6d99306461f690530eedf5797b2c86c3367a704d24dc5cc86a1c1d2035ea", + "web/output.mjs": "1d22c44955e812d671f1a3c3ae4b2f0642a05ebe614017bb041bdddc8c363be3", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "qa-presentation.js": "04b0135983ea718d3744b56b706c9bf3266972a5180c744ac791ef787a29aa05", + "qa-presentation.py": "bec45426ec297896ed5c87b152148956bc6c5730ff188aea0e041a0ec69ae1c3" + }, + "baselineManifestSha256": "6068e6ca3630e240484abfa2cddbb9d3fb877b3df1886ae500dc4caa485d4f6a" +} diff --git a/prototypes/workbench/results/2026-09-23-presentation/verification.json b/prototypes/workbench/results/2026-09-23-presentation/verification.json new file mode 100644 index 0000000..eb007bb --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-presentation/verification.json @@ -0,0 +1,58 @@ +{ + "utc": "2026-09-23T05:41:43.833092+00:00", + "javascript": [ + { + "file": "compiler.test.mjs", + "tests": 13, + "sha256": "6aa442d9d1eeb69453082f1780ff4597e46b1fc09d8ba3cf955545bc94d81431", + "output": "✔ a blank document is an empty program with no implied behavior (4.119065ms)\n✔ forward references and separate state/input namespaces preserve declaration IDs (2.599791ms)\n✔ ordered assignments, repeated handlers, and condition evaluation once (0.981294ms)\n✔ nested branches use absolute forward targets and skip correct bodies (0.595425ms)\n✔ operator precedence, associativity, unary and builtin function lowering (1.513886ms)\n✔ multiline parentheses, scientific literals, comments, and string escapes (0.671276ms)\n✔ all instruction fields are numeric and carry original source line (0.349295ms)\n✔ contextual declaration words remain valid names outside reserved builtins (0.541406ms)\n✔ unknown and reserved symbols have useful one-based locations (1.579459ms)\n✔ duplicates, unsupported keys, initializers, and invalid syntax are rejected (1.378719ms)\n✔ document, declaration, instruction, and nesting limits reject overflow (6.561671ms)\n✔ every supported input maps to a bounded ordered event ID (0.715887ms)\n✔ optional examples compile and charge behavior is entirely authored (10.511884ms)\nℹ tests 13\nℹ suites 0\nℹ pass 13\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 41.185946\n" + }, + { + "file": "runtime.test.mjs", + "tests": 13, + "sha256": "5fe66115aab0c178a283dcb592376a2e7209308368004865e6c0a1cc58fa7f62", + "output": "✔ blank startup and applied empty source contain no application objects or behavior (7.223873ms)\n✔ author can add unrelated states inputs views and behavior entirely through source (4.612638ms)\n✔ keyboard and pointer sources aggregate first down and last up; repeats are ignored (2.630605ms)\n✔ held input rejects apply without changing applied source, metadata, or values (1.535145ms)\n✔ invalid syntax, unknown references and capacity overflow preserve the previous program (2.099848ms)\n✔ state preservation follows names across reorder/add/remove; reset uses new initializers (1.048794ms)\n✔ callers cannot mutate active metadata and corrupt name-based state preservation (0.86472ms)\n✔ conditions execute once, and assignments and multiple handlers follow source order (0.828498ms)\n✔ a failed event rolls back all handlers and keeps its source diagnostic through release (2.229514ms)\n✔ reset clears held inputs and latched errors; invalid frame counts have no effects (1.298812ms)\n✔ render evaluation is pure and invalid binding is distinct from an event rollback (4.6701ms)\n✔ independent adapters replay the same author program without sharing live state (2.174384ms)\n✔ Wasm has no imports and reports fixed memory and exact active IR/state storage (9.03348ms)\nℹ tests 13\nℹ suites 0\nℹ pass 13\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 51.721812\n" + }, + { + "file": "baseline.test.mjs", + "tests": 7, + "sha256": "3ea469a29d301b95701bc6f6ffafa2471ac0f334c5bd462c0cbc7c320bdc0764", + "output": "✔ reference begins with the authored states and matching scene (2.419773ms)\n✔ press, hold, release, and frame order produce exact expected values (0.596573ms)\n✔ charge clamps at one and explicit floor restores rest (1.552126ms)\n✔ repeat and concurrent physical sources produce only logical edges (0.600425ms)\n✔ airborne presses cannot charge or cause another jump (0.332166ms)\n✔ releaseAll emits release and reset clears latches without a jump (0.530518ms)\n✔ unknown inputs and invalid frame counts reject without mutation (0.836813ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 16.923982\n" + }, + { + "file": "cli.test.mjs", + "tests": 5, + "sha256": "655c6d80b51af818fa7cdeb27cb5462962a0e1aef70f609ef5e2ddc85f4a6bc7", + "output": "✔ terminal starts empty and produces the same charge trace as the shared runtime (112.599177ms)\n✔ invalid terminal commands do not alter state and fail piped execution (50.710481ms)\n✔ file load preserves by name, rejected load preserves active work, and save never overwrites (58.74187ms)\n✔ terminal exposes arithmetic rollback and retains failure through matching release (54.053511ms)\n✔ terminal configures and discards evaluation records and recovers from a display limit (49.682483ms)\nℹ tests 5\nℹ suites 0\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 333.929744\n" + }, + { + "file": "evaluation.test.mjs", + "tests": 7, + "sha256": "d3026ab5c6bffce56c0bde3155d7dddd6fa442a5688656ee710626c762258ff1", + "output": "✔ cache reuse, unrelated changes and discarded records preserve authored values (17.225666ms)\n✔ committed changes only: failed events and failed edits preserve reusable output (2.575309ms)\n✔ program edits, state reordering and reset cannot reuse old definitions (2.738642ms)\n✔ finite float semantics include signed zero, high state bit and arithmetic errors (2.509513ms)\n✔ one budget covers the whole scene; failures publish no partial scene or state rollback (1.276608ms)\n✔ small caches, no history and every small work limit give exact results or explicit failure (30.326359ms)\n✔ invalid settings and forget names preserve policy and state (2.301254ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 69.267384\n" + }, + { + "file": "workspace.test.mjs", + "tests": 2, + "sha256": "7ea664736e4c9d791d72cf64de0fbc230b8d849910cc2a456d4d0e959acb9857", + "output": "✔ Wasm executes old events and output during staged replacement without corrupting either phase (6.30019ms)\n✔ workspace accounting reports shared payload and safety tag without double-counting (2.077181ms)\nℹ tests 2\nℹ suites 0\nℹ pass 2\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 14.960097\n" + }, + { + "file": "presentation.test.mjs", + "tests": 5, + "sha256": "604858a3f601d2a67c601ff477415025dcb766cdfd092bafed2d9dcfda39abd5", + "output": "✔ direct publication matches archived snapshots and work counts through authored changes (48.77004ms)\n✔ whole-scene budget failure never publishes partial bindings and recovers exactly (5.804043ms)\n✔ latched event rollback, nonfinite bindings, reset and signed zero keep their semantics (3.292808ms)\n✔ publication rejects reentrant runtime changes and reads before they affect the scene (3.694584ms)\n✔ invalid or throwing output adapters do not strand the runtime or alter authored state (7.631298ms)\nℹ tests 5\nℹ suites 0\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 82.836799\n" + } + ], + "javascriptTestCount": 52, + "sourceSha256": { + "web/runtime.mjs": "d490c54fb2b422dfe89b90a594a8da9ebc95bb007910211cb4686ccb83ed75c7", + "web/output.mjs": "1d22c44955e812d671f1a3c3ae4b2f0642a05ebe614017bb041bdddc8c363be3", + "web/app.js": "a50e6d99306461f690530eedf5797b2c86c3367a704d24dc5cc86a1c1d2035ea", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "core.zig": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165" + }, + "nativeNote": "Zig and Wasm are unchanged; native tests are not rerun. Prior build record remains in 2026-09-23-workspace/verification.json." +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/core.zig b/prototypes/workbench/results/2026-09-23-refresh/before/core.zig new file mode 100644 index 0000000..84812a3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/core.zig @@ -0,0 +1,942 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { event: u32 = 0, start: u32 = 0, length: u32 = 0 }; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Workspace = union { staging: Program, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .state_count = state_count, .input_count = input_count } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.initials[id] = initial; + engine.workspace.staging.old_ids[id] = old_id; + engine.workspace.staging.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 21) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (event > p.input_count * 2) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (h.event > 2 * p.input_count) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) return .invalid; + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 21) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op == 20 or inst.op == 21) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + break; + } + } + const h = owner orelse return .invalid; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + } + } + return .ok; +} + +export fn commit_program() u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const code = validate(&engine.workspace.staging); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.state_count) |i| { + const old_id = engine.workspace.staging.old_ids[i]; + next[i] = if (old_id < 0) engine.workspace.staging.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging; + engine.values = next; + finishStaging(); + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +fn execute(values: *[max_states]f64, start: u32, length: u32, dt: f64, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op == 20 or inst.op == 21)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2 => 0, + 7, 16, 20, 21 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 2 => dt, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20) { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[inst.arg] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (event > engine.active.input_count * 2) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + const result = execute(&pending, h.start, h.length, dt, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, 0, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, 0, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Program), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Program), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 12), layout(3)); + try std.testing.expectEqual(@as(u32, 76), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(layout(1), layout(6)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Program)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Program) + @alignOf(Program)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/evaluation.zig b/prototypes/workbench/results/2026-09-23-refresh/before/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/manifest.json b/prototypes/workbench/results/2026-09-23-refresh/before/manifest.json new file mode 100644 index 0000000..6284e49 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/manifest.json @@ -0,0 +1,14 @@ +{ + "web/app.js": "a50e6d99306461f690530eedf5797b2c86c3367a704d24dc5cc86a1c1d2035ea", + "web/output.mjs": "1d22c44955e812d671f1a3c3ae4b2f0642a05ebe614017bb041bdddc8c363be3", + "web/runtime.mjs": "d490c54fb2b422dfe89b90a594a8da9ebc95bb007910211cb4686ccb83ed75c7", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "web/index.html": "4fff6eb67d8cff098302465449251d17f0efb39d9144e87d15fefcc152e5d97e", + "web/style.css": "43301298028c5bc431eecd5ff30d7362fae83cce1fd0fdd7e8bea858100c22b9", + "core.zig": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "qa-presentation.js": "04b0135983ea718d3744b56b706c9bf3266972a5180c744ac791ef787a29aa05", + "qa-presentation.py": "bec45426ec297896ed5c87b152148956bc6c5730ff188aea0e041a0ec69ae1c3", + "presentation.test.mjs": "604858a3f601d2a67c601ff477415025dcb766cdfd092bafed2d9dcfda39abd5" +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/presentation.test.mjs b/prototypes/workbench/results/2026-09-23-refresh/before/presentation.test.mjs new file mode 100644 index 0000000..207905d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/presentation.test.mjs @@ -0,0 +1,123 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; +import { createRuntime as before } from './results/2026-09-23-presentation/before/web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = `state x = 3 +state radius = 5 +input move = KeyM +input shrink = KeyS +box tile(x, 10, x * 2, 20, "#123456") +circle dot(50, 50, radius, "#abcdef") +text label(0, 0, 10 / x, "Value: ") +on move.down { x = x + 0.25 } +on shrink.down { radius = radius - 10 } +on frame { x = x + dt } +`; + +// Reconstruct only in tests, for comparison with the archived snapshot path. +function collect(runtime, hooks = {}) { + let states, views, declared; + const result = runtime.present({ + begin(program) { + declared = program; states = {}; views = []; + hooks.begin?.(); + }, + state(i, value) { states[declared.states[i].name] = value; }, + view(i, valid) { + if (!valid) return; + const {id, type, color, prefix} = declared.views[i]; + views.push({id, type, color, prefix}); + }, + value(i, name, value) { views.at(-1)[name] = value; hooks.value?.(); }, + end(count) { assert.equal(views.length, count); }, + }); + return {states, views, ...result}; +} + +test('direct publication matches archived snapshots and work counts through authored changes', async () => { + for (const cacheEntries of [0, 1, 32]) { + const left = await createRuntime(wasm, {cacheEntries}), right = await before(wasm, {cacheEntries}); + const compare = () => { + assert.deepEqual(collect(left), right.snapshot()); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + }; + compare(); + for (const r of [left, right]) r.apply(source); + compare(); + for (let i = 0; i < 120; i++) { + for (const r of [left, right]) { + r.down('move', 'keyboard'); r.down('move', 'pointer'); + r.up('move', 'keyboard'); r.up('move', 'pointer'); + if (i % 11 === 0) r.frame(2); + if (i % 17 === 0) r.forgetEvaluation('history'); + if (i % 23 === 0) r.reset(); + } + compare(); + } + for (const r of [left, right]) { r.down('shrink'); r.up('shrink'); } + compare(); // invalid circle only; other views still publish + for (const r of [left, right]) assert.throws(() => r.apply('state broken =')); + compare(); + for (const r of [left, right]) r.apply('state radius = 1\nstate x = 0\ntext label(1, 2, x)'); + compare(); + for (const r of [left, right]) r.apply(''); + compare(); + } +}); + +test('whole-scene budget failure never publishes partial bindings and recovers exactly', async () => { + const left = await createRuntime(wasm, {cacheEntries:32}), right = await before(wasm, {cacheEntries:32}); + for (const r of [left, right]) r.apply(source); + for (let workLimit = 0; workLimit <= 80; workLimit++) { + for (const r of [left, right]) r.configureEvaluation({workLimit}); + let calls = 0; + const result = collect(left, {value: () => calls++}); + assert.deepEqual(result, right.snapshot()); + assert.deepEqual(left.evaluationStats(), right.evaluationStats()); + if (result.displayError?.code === 101) assert.equal(calls, 0); + else assert.equal(calls, 10); + } +}); + +test('latched event rollback, nonfinite bindings, reset and signed zero keep their semantics', async () => { + const left = await createRuntime(wasm), right = await before(wasm); + const definition = 'state x = 0\ninput fail = Space\ntext value(0, 0, 1 / x)\non fail.down { x = 2\nx = 1 / 0 }'; + for (const r of [left, right]) { r.apply(definition); r.down('fail'); r.up('fail'); } + assert.deepEqual(collect(left), right.snapshot()); + for (const r of [left, right]) r.configureEvaluation({workLimit:0}); + assert.deepEqual(collect(left), right.snapshot()); + for (const r of [left, right]) { + r.apply('state x = -0\ntext value(x, 0, x)'); r.reset(); r.configureEvaluation({workLimit:2048}); + } + const result = collect(left); + assert.deepEqual(result, right.snapshot()); + assert(Object.is(result.states.x, -0)); +}); + +test('publication rejects reentrant runtime changes and reads before they affect the scene', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const calls = [() => runtime.apply(''), () => runtime.reset(), () => runtime.frame(), + () => runtime.down('move'), () => runtime.up('move'), () => runtime.releaseAll(), + () => runtime.configureEvaluation({workLimit:0}), () => runtime.forgetEvaluation(), + () => runtime.snapshot(), () => runtime.present({})]; + const expected = runtime.snapshot(); + assert.deepEqual(collect(runtime, {begin() { + for (const call of calls) assert.throws(call, /출력 전달 중/); + }}), expected); + assert.deepEqual(runtime.snapshot(), expected); +}); + +test('invalid or throwing output adapters do not strand the runtime or alter authored state', async () => { + const runtime = await createRuntime(wasm); + runtime.apply(source); + const expected = runtime.snapshot(); + assert.throws(() => runtime.present({}), TypeError); + assert.throws(() => collect(runtime, {value() { throw new Error('destination failed'); }}), /destination failed/); + assert.deepEqual(collect(runtime), expected); + runtime.down('move'); runtime.up('move'); + assert.equal(collect(runtime).states.x, 3.25); +}); diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.js b/prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.js new file mode 100644 index 0000000..90595e3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.js @@ -0,0 +1,141 @@ +// Playwright CLI run-code function. The Python runner injects the archived +// app/runtime source at BASELINE_SOURCES; production files are never replaced. +async (page) => { + const baseline = /* BASELINE_SOURCES */ null; + const checks = [], errors = [], samples = []; + const check = (ok, name) => { if (!ok) throw new Error(name); checks.push(name); }; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); page.on('dialog', onDialog); + let variant = 'after'; + const source = [ + 'state x = 40', 'state radius = 12', 'input move = KeyM', 'input shrink = KeyS', + 'box moving(x, 100, 30, 30, "#7ed9ae")', 'text label(20, 40, x, "x: ")', + 'circle bubble(500, 150, radius, "#c9f08b")', + ...Array.from({length:21}, (_, i) => `box static${i}(${20 + (i % 7) * 70}, ${200 + Math.floor(i / 7) * 40}, 20, 20, "#8da5bb")`), + 'on move.down { x = x + 0.25 }', 'on shrink.down { radius = radius - 24 }', + ].join('\n'); + const apply = async value => { await page.locator('#source').fill(value); await page.locator('#apply').click(); }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const reload = async () => { + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); + }; + const routeApp = async route => { + const response = await route.fetch(); + await route.fulfill({response, body: variant === 'before' ? baseline.app : await response.text()}); + }; + const routeRuntime = async route => { + const response = await route.fetch(); + const body = variant === 'before' ? baseline.runtime : await response.text(); + if (!body.includes('snapshot() {')) throw new Error('snapshot instrumentation does not match'); + await route.fulfill({response, body:body.replace('snapshot() {', 'snapshot() { globalThis.__snapshotCalls = (globalThis.__snapshotCalls || 0) + 1;')}); + }; + try { + if (!baseline) throw new Error('Run through qa-presentation.py to supply the archived baseline.'); + await page.route('**/app.js', routeApp); + await page.route('**/runtime.mjs', routeRuntime); + await reload(); + await page.locator('#scene').focus(); + await page.keyboard.press('KeyM'); + check((await state()).states.x === 40.25, 'real keyboard event reaches the authored output'); + await page.evaluate(() => { window.__retained = document.querySelector('[data-view-id="moving"]'); }); + await page.locator('[data-input="move"]').click(); + check((await state()).states.x === 40.5 && await page.locator('[data-view-id="moving"]').getAttribute('x') === '40.5', + 'real pointer input and keyboard input use the same authored behavior'); + check(await page.evaluate(() => window.__retained === document.querySelector('[data-view-id="moving"]')), + 'the displayed element survives an input update'); + await apply(source + '\nstate broken ='); + check((await state()).states.x === 40.5 && await page.evaluate(() => window.__retained === document.querySelector('[data-view-id="moving"]')), + 'failed editing preserves active values and output destinations'); + await page.locator('[data-input="shrink"]').click(); + check((await state()).lastError === 100 && await page.locator('[data-view-id="bubble"]').getAttribute('display') === 'none' && + await page.locator('[data-view-id="moving"]').getAttribute('display') === null, + 'invalid view is hidden while valid views remain published'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('[data-view-id="bubble"]').getAttribute('display') === null, + 'reset recovers a hidden destination without recreating the scene'); + await apply(source.replace('box moving(x, 100, 30, 30, "#7ed9ae")', 'circle moving(x, 100, 15, "#7ed9ae")')); + check(await page.locator('[data-view-id="moving"]').evaluate(node => node.localName) === 'circle', + 'successful editing replaces changed output structure'); + await apply(''); + check(await page.locator('#scene-content > *').count() === 0 && (await state()).views.length === 0, + 'applying empty work removes all old destinations'); + + // Alternating whole-page runs avoid sharing a runtime or retained DOM across + // variants. Timings cover synchronous dispatch through DOM submission, not + // OS event arrival, layout, paint, or actual display scanout. + for (const mode of ['before', 'after', 'after', 'before', 'before', 'after']) { + variant = mode; + await reload(); + await page.locator('#scene').focus(); + const result = await page.evaluate(() => { + const scene = document.getElementById('scene'); + const tap = () => { + scene.dispatchEvent(new KeyboardEvent('keydown', {code:'KeyM', bubbles:true})); + scene.dispatchEvent(new KeyboardEvent('keyup', {code:'KeyM', bubbles:true})); + }; + for (let i = 0; i < 20; i++) tap(); + const retained = [...document.querySelectorAll('#scene-content > *')]; + const scopes = ['scene-content', 'state-values', 'cost-values']; + const observers = scopes.map(id => { + const observer = new MutationObserver(() => {}); + observer.observe(document.getElementById(id), {subtree:true, childList:true, attributes:true, characterData:true}); + return observer; + }); + const createElement = document.createElement, createElementNS = document.createElementNS; + let createdElements = 0; + document.createElement = function(...args) { createdElements++; return createElement.apply(this, args); }; + document.createElementNS = function(...args) { createdElements++; return createElementNS.apply(this, args); }; + window.__snapshotCalls = 0; + let elapsedMs; + try { + const start = performance.now(); + for (let i = 0; i < 100; i++) tap(); + elapsedMs = performance.now() - start; + } finally { + document.createElement = createElement; document.createElementNS = createElementNS; + } + const snapshotCalls = window.__snapshotCalls; + const mutations = Object.fromEntries(observers.map((observer, i) => { + const records = observer.takeRecords(); observer.disconnect(); + return [scopes[i], {records:records.length, + childList:records.filter(record => record.type === 'childList').length, + attributes:records.filter(record => record.type === 'attributes').length}]; + })); + const nodes = [...document.querySelectorAll('#scene-content > *')]; + const identityPreserved = nodes.every((node, i) => node === retained[i]); + const final = window.pureWorkbench.snapshot(); // outside timed/count interval + return {elapsedMs, createdElements, snapshotCalls, mutations, identityPreserved, + final: {states:final.states, views:final.views, successfulEvents:final.successfulEvents}, + scene:document.getElementById('scene-content').outerHTML}; + }); + samples.push({variant:mode, taps:100, events:200, ...result}); + } + check(samples.every(sample => JSON.stringify(sample.final) === JSON.stringify(samples[0].final)), + 'all six archived/current runs have identical final authored state and views'); + check(samples.filter(s => s.variant === 'after').every(s => s.createdElements === 0 && s.snapshotCalls === 0 && s.identityPreserved), + 'current hot path creates no DOM elements and no full snapshots'); + check(samples.filter(s => s.variant === 'before').every(s => s.createdElements > 0 && s.snapshotCalls === 200 && !s.identityPreserved), + 'archived hot path reproduces full snapshots and element replacement'); + check(samples.filter(s => s.variant === 'after').every(s => s.mutations['scene-content'].attributes === 100 && s.mutations['scene-content'].childList === 100), + 'each tap changes only the moving x attribute and label text in the scene'); + variant = 'after'; await reload(); + await page.screenshot({path:'output/playwright/pure-presentation.png', fullPage:true}); + check(errors.length === 0, 'no uncaught browser errors'); + const evidence = {utc:new Date().toISOString(), checks, errors, samples, source, + timingScope:'100 synchronous synthetic keyboard taps after 20 warm-up taps; JS dispatch to DOM submission only, including instrumentation; not paint/input latency.', + setup:'Archived app/runtime are supplied by test-only routing; snapshot calls and element creation are instrumented. Same Wasm/compiler and production configuration.', + browser:await page.evaluate(() => navigator.userAgent), screenshot:'output/playwright/pure-presentation.png'}; + await page.evaluate(value => { window.__purePresentationEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = {failed:true, message:error.stack || error.message, checks, errors, samples}; + await page.evaluate(value => { window.__purePresentationEvidence = value; }, evidence); + return evidence; + } finally { + await page.unroute('**/app.js', routeApp); await page.unroute('**/runtime.mjs', routeRuntime); + page.off('pageerror', onError); page.off('dialog', onDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.py b/prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.py new file mode 100644 index 0000000..b3adb02 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/qa-presentation.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Run the local browser comparison with the archived implementation injected.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import time + +ROOT = Path(__file__).resolve().parent +parser = argparse.ArgumentParser() +parser.add_argument('--cli', required=True, help='Path to Playwright CLI 0.1.21 JS entry point') +parser.add_argument('--session', default='pure-presentation') +parser.add_argument('--output', required=True, type=Path) +args = parser.parse_args() +if args.output.exists(): + raise SystemExit('Choose a new output file; evidence is never overwritten.') +archive = ROOT / 'results/2026-09-23-presentation/before' +sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest() +manifest = json.loads((archive / 'manifest.json').read_text()) +for name, digest in manifest.items(): + if sha(archive / name) != digest: + raise SystemExit(f'Archived source mismatch: {name}') +baseline = {name: (archive / f'web/{file}').read_text() + for name, file in [('app', 'app.js'), ('runtime', 'runtime.mjs')]} +script = (ROOT / 'qa-presentation.js').read_text().replace('/* BASELINE_SOURCES */ null', json.dumps(baseline)) +command = ['node', args.cli, f'--session={args.session}'] +run = subprocess.run([*command, 'run-code', script], text=True, capture_output=True, check=True) +# Navigation can return early while the run-code function is accepting a +# beforeunload dialog. Collect the published result instead of rerunning it. +for attempt in range(30): + result = subprocess.run([*command, 'eval', 'window.__purePresentationEvidence'], + text=True, capture_output=True, check=True) + if '### Result\n' not in result.stdout: + raise SystemExit(result.stdout + result.stderr) + payload = result.stdout.split('### Result\n', 1)[1].split('\n### ', 1)[0].strip() + if payload != 'undefined': + value = json.loads(payload) + if value.get('failed'): + raise SystemExit(json.dumps(value, ensure_ascii=False, indent=2)) + value['sourceSha256'] = {name: sha(ROOT / name) for name in + ['web/runtime.mjs', 'web/app.js', 'web/output.mjs', 'web/core.wasm', + 'web/compiler.mjs', 'qa-presentation.js', 'qa-presentation.py']} + value['baselineManifestSha256'] = sha(archive / 'manifest.json') + with args.output.open('x') as output: + json.dump(value, output, ensure_ascii=False, indent=2) + output.write('\n') + print(json.dumps({'checks':len(value['checks']), 'samples':[ + {k:s[k] for k in ['variant', 'elapsedMs', 'createdElements', 'snapshotCalls']} + for s in value['samples']]}, indent=2)) + break + time.sleep(0.2) +else: + raise SystemExit('No finished evidence; inspect the browser session before retrying.') diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/web/app.js b/prototypes/workbench/results/2026-09-23-refresh/before/web/app.js new file mode 100644 index 0000000..5241754 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/web/app.js @@ -0,0 +1,349 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + $('cursor-position').textContent = `${lines.length}행 · ${lines.at(-1).length + 1}열`; +} + +function draftChanged() { + const count = source.value.split('\n').length; + $('line-numbers').textContent = Array.from({ length: count }, (_, i) => i + 1).join('\n'); + $('line-numbers').scrollTop = source.scrollTop; + $('source-size').textContent = `${bytes(source.value).toLocaleString('en-US')} / 32,768 bytes`; + $('source-size').style.color = bytes(source.value) > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + $('draft-status').textContent = dirty() ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'; + $('draft-status').classList.toggle('dirty', dirty()); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + $('play').textContent = '▶ 실행'; + $('play').setAttribute('aria-pressed', 'false'); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + button.setAttribute('aria-pressed', String(held.has(name))); + } + $('scene-status').textContent = runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'; + $('clock-status').textContent = `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`; + $('revision').textContent = `revision ${snapshot.revision || 0}`; + for (const id of ['play', 'step', 'reset']) $(id).disabled = !runtime.currentProgram; + const event = snapshot.lastEvent; + $('last-event').textContent = event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'; + $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + $('runtime-error').textContent = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + $('runtime-error').textContent += ` · ${snapshot.displayError.message}`; + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); + draftChanged(); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime) { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic('편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.'); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_SOURCE_BYTES * 7 + 1024) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = source.value; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + let project; + try { project = JSON.parse(content); } catch { throw new Error('올바른 JSON 파일이 아닙니다.'); } + if (!project || typeof project !== 'object' || Array.isArray(project) || project.format !== 'pure-workbench' || project.version !== 1 || typeof project.source !== 'string') throw new Error('Pure 작업물 형식이 아닙니다. format, version, source를 확인하세요.'); + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-refresh/before/web/compiler.mjs new file mode 100644 index 0000000..016ea9e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/web/compiler.mjs @@ -0,0 +1,303 @@ +// 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', '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(); + } + 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: token.value }); + } 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 { + this.expect('='); + statements.push({ kind: 'assign', token, name: token.value, 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('='); + let sign = 1; + if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1; + const value = this.expect('number', 'numeric state initializer'); + declarations.push({ kind, token, name, initial: sign * value.value }); + } else if (kind === 'input') { + const name = this.expect('name', 'input name'); this.expect('='); + const key = this.expect('name', 'keyboard code'); + 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 or up'); + if (edge.value !== 'down' && edge.value !== 'up') fail(edge, 'Expected down or up'); + } + 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: [], metrics: {} }; + const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(); + 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 === '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 (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'); + } + 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 === 'dt') { + if (binding) fail(token, 'dt is only available in handlers'); + emit(2, token, 0, 0, 1); + } else { + 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 (!states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`); + expression(statement.expression); + emit(20, statement.token, states.get(statement.name), 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; + } + } + }; + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + if (declaration.event.value !== 'frame') { + if (!inputs.has(declaration.event.value)) fail(declaration.event, `Unknown input ${declaration.event.value}`); + event = 1 + inputs.get(declaration.event.value) * 2 + (declaration.edge.value === 'up' ? 1 : 0); + } + 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); + } + } + 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/workbench/results/2026-09-23-refresh/before/web/core.wasm b/prototypes/workbench/results/2026-09-23-refresh/before/web/core.wasm new file mode 100644 index 0000000..0eb3fe5 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-refresh/before/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/web/index.html b/prototypes/workbench/results/2026-09-23-refresh/before/web/index.html new file mode 100644 index 0000000..d93589e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/web/index.html @@ -0,0 +1,93 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/web/output.mjs b/prototypes/workbench/results/2026-09-23-refresh/before/web/output.mjs new file mode 100644 index 0000000..53cae48 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/web/output.mjs @@ -0,0 +1,92 @@ +// Bind authored declarations to concrete DOM destinations once per apply. +// The DOM holds the published values; no parallel scene/value model is kept. +const NS = 'http://www.w3.org/2000/svg'; +const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString(); +const attributes = { box: { x: 'x', y: 'y', width: 'width', height: 'height' }, + circle: { x: 'cx', y: 'cy', radius: 'r' }, text: { x: 'x', y: 'y' } }; + +function text(node, value) { + if (node.textContent !== value) node.textContent = value; +} +function attribute(node, name, value) { + const next = String(value); + if (node.getAttribute(name) !== next) node.setAttribute(name, next); +} + +export function createOutput(document) { + const get = id => document.getElementById(id); + const scene = get('scene-content'), states = get('state-values'); + const empty = get('empty-scene'), description = get('scene-description'); + let active = undefined, nodes = [], amounts = []; + const costNames = ['Wasm 선형 메모리', '코어 예약 공간', '사용 중인 명령·핸들러', + '현재 상태 값', '새 코드 준비 공간 (예약에 포함)', '평가 스택 용량', + '마지막 이벤트 실행 명령', '적용된 소스 크기']; + const costs = costNames.map(label => { + const dt = document.createElement('dt'), dd = document.createElement('dd'); + dt.textContent = label; + return { dt, dd }; + }); + get('cost-values').replaceChildren(...costs.flatMap(({dt, dd}) => [dt, dd])); + + return { + begin(program) { + if (program === active) return; + active = program; + nodes = (program?.views ?? []).map(view => { + const node = document.createElementNS(NS, {box:'rect', circle:'circle', text:'text'}[view.type]); + node.setAttribute('data-view-id', view.id); + node.setAttribute('fill', view.color ?? '#e7ece6'); + if (view.type === 'text') { + node.setAttribute('font-family', 'ui-monospace, monospace'); + node.setAttribute('font-size', '20'); + } + return node; + }); + scene.replaceChildren(...nodes); + const holders = []; + amounts = (program?.states ?? []).map(state => { + const dl = document.createElement('dl'), dt = document.createElement('dt'), dd = document.createElement('dd'); + dl.className = 'state-value'; dt.textContent = state.name; dd.dataset.state = state.name; + dl.append(dt, dd); holders.push(dl); + return dd; + }); + if (!holders.length) { + const note = document.createElement('p'); + note.className = 'empty-note'; note.textContent = '선언한 상태의 값이 표시됩니다.'; + holders.push(note); + } + states.replaceChildren(...holders); + }, + state(index, value) { text(amounts[index], number(value)); }, + view(index, valid) { + const node = nodes[index]; + if (valid) { if (node.hasAttribute('display')) node.removeAttribute('display'); } + else attribute(node, 'display', 'none'); + }, + value(index, name, value) { + const view = active.views[index], node = nodes[index]; + if (name === 'value') text(node, `${view.prefix || ''}${number(value)}`); + else attribute(node, attributes[view.type][name], value); + }, + end(visible) { + const hidden = visible !== 0; + if (empty.hidden !== hidden) empty.hidden = hidden; + const failed = !visible && nodes.length > 0; + text(empty.querySelector('strong'), failed ? '장면을 표시할 수 없습니다' : '아직 아무것도 없습니다'); + text(empty.querySelector('span:last-child'), failed ? '현재 상태 아래의 표시 오류를 확인하세요.' : '도형이나 글자를 선언하고 코드를 적용하세요.'); + text(description, visible ? `사용자가 만든 장면. 도형과 글자 ${visible}개.` : failed ? '표시 오류로 장면을 표시할 수 없습니다.' : '사용자가 코드로 만든 장면. 현재 비어 있습니다.'); + }, + costs(status, metrics, droppedTicks) { + const m = status.memory; + const values = [m.linearBytes, m.reservedEngineBytes, m.activeIRBytes, m.activeStateBytes, + m.stagingReservedBytes, m.evalStackBytes, status.lastOps, metrics?.sourceBytes]; + for (let i = 0; i < values.length; i++) { + const value = values[i], {dt, dd} = costs[i], hidden = !Number.isFinite(value); + if (dt.hidden !== hidden) dt.hidden = hidden; + if (dd.hidden !== hidden) dd.hidden = hidden; + if (!hidden) text(dd, i === 6 ? `${value}개` : `${value.toLocaleString('en-US')} B`); + } + text(get('dropped-ticks'), `시간 지연으로 건너뛴 프레임: ${droppedTicks}`); + }, + }; +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/before/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-refresh/before/web/runtime.mjs new file mode 100644 index 0000000..96634b8 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/web/runtime.mjs @@ -0,0 +1,267 @@ +import { compile } from './compiler.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') + 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(); + let presenting = false; + let bindings = []; + // 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) { + const status = 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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), + }, + }; + } + + 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)); + const oldIds = new Map(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); + } + for (const op of next.instructions) checked(core.put_instruction(op.op, op.arg, op.value), op.line); + for (const handler of next.handlers) checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + checked(core.commit_program()); + } catch (error) { + core.abort_program(); + throw error; + } + program = next; + bindings = next.views.map(view => Object.entries(view.bindings)); + appliedSource = source; + lastEvent = null; + lastFailure = null; + return next; + }, + reset() { + available(); + held.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); + 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); + 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); + }, + releaseAll() { + available(); + let firstError = 0; + 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/workbench/results/2026-09-23-refresh/before/web/style.css b/prototypes/workbench/results/2026-09-23-refresh/before/web/style.css new file mode 100644 index 0000000..819c84c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/before/web/style.css @@ -0,0 +1,8 @@ +:root{color-scheme:dark;--bg:#151918;--panel:#1d2220;--field:#151a18;--line:#353e38;--text:#e7ece6;--muted:#a9b6aa;--faint:#7b8e7e;--accent:#c9f08b;--accent-dark:#222e18;--danger:#f6afa0;--radius:14px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-synthesis:none} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text)}button,input,select,textarea{font:inherit}button,select{border:1px solid var(--line);border-radius:7px;background:#282f2a;color:var(--text);padding:9px 12px;cursor:pointer;font-size:12px;font-weight:600;min-height:36px}button:hover:not(:disabled){background:#344035;border-color:#70836d}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button.primary{background:var(--accent);color:var(--accent-dark);border-color:var(--accent);padding:11px 19px;font-size:14px;display:flex;gap:28px;justify-content:space-between;align-items:center}button.primary:hover:not(:disabled){background:#d8f7ad}button.secondary{background:#c9f08b16;border-color:#637545;color:#d7f0b5}button.quiet{background:transparent;border-color:transparent;color:var(--muted);padding:6px 8px;min-height:30px}button.quiet:hover{color:var(--text)}.masthead{height:76px;padding:0 4.5%;display:flex;align-items:center;gap:28px;border-bottom:1px solid var(--line)}.wordmark{font-size:23px;font-weight:750;letter-spacing:-1px;color:var(--accent);text-decoration:none}.wordmark span{font-size:13px;color:var(--muted);letter-spacing:0;font-weight:400}.experiment{font-size:11px;color:var(--faint);border-left:1px solid var(--line);padding-left:24px}.connection{margin-left:auto;display:flex;gap:9px;align-items:center;font-size:11px;color:var(--muted)}.dot{width:6px;height:6px;border-radius:100%;background:#aa8e5c}.dot.ready{background:var(--accent)}.dot.failed{background:var(--danger)}main{max-width:1540px;margin:auto;padding:38px 4.5% 0}.intro{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin:0 0 27px}.eyebrow{font-size:10px;letter-spacing:1.4px;font-weight:600;color:var(--accent);margin:0 0 10px}h1{font-size:clamp(23px,2.5vw,35px);letter-spacing:-1px;margin:0;font-weight:600}.intro>p{font-size:12px;line-height:1.9;color:var(--muted);margin:0 0 2px}.intro strong{color:var(--text);font-weight:500}.workspace{display:grid;grid-template-columns:minmax(340px,1fr) minmax(380px,1.05fr);gap:22px;align-items:start}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.panel-heading{height:56px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line)}h2{margin:0;font-size:13px;font-weight:650;display:flex;gap:10px;align-items:center}.section-index{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;font-weight:400;color:var(--faint)}.badge{font-size:10px;border:1px solid var(--line);border-radius:4px;padding:4px 7px;white-space:nowrap;color:var(--muted)}.badge.dirty{color:var(--accent);border-color:#5f7247}.file-tools{display:flex;align-items:center;gap:3px;padding:8px 10px;border-bottom:1px solid var(--line);flex-wrap:wrap}.code-editor{height:514px;display:flex;overflow:hidden;background:var(--field);position:relative}.line-numbers{padding:20px 10px 20px 16px;min-width:45px;text-align:right;color:#667567;user-select:none;white-space:pre;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:hidden;flex-shrink:0;border-right:1px solid #2a322b}textarea#source{border:0;border-radius:0;outline:0;resize:none;padding:20px 16px;display:block;width:100%;height:100%;color:#d4e3ca;background:transparent;white-space:pre;overflow:auto;tab-size:2;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;caret-color:var(--accent)}textarea#source:focus{background:#171e19}textarea#source::placeholder{color:#7c8b7d;font:12px/1.9 ui-sans-serif,system-ui,sans-serif;white-space:pre-wrap}textarea#source[aria-invalid=true]{box-shadow:inset 3px 0 0 var(--danger)}.editor-footer{padding:8px 15px;font-size:10px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--faint);display:flex;justify-content:space-between;border-top:1px solid var(--line)}.apply-row{display:flex;gap:18px;align-items:center;padding:16px 18px 12px}.apply-row p{font-size:10px;color:var(--muted);line-height:1.7;margin:0;max-width:185px}.diagnostic{margin:0 18px 17px;font-size:11px;line-height:1.8;min-height:22px;color:var(--muted);display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}.diagnostic.error{color:var(--danger)}.diagnostic.success{color:#c7dea9}.right-column{display:grid;gap:18px}.stage-wrap{position:relative;background-color:#121613;background-image:linear-gradient(#a6c39907 1px,transparent 1px),linear-gradient(90deg,#a6c39907 1px,transparent 1px);background-size:24px 24px}#scene{width:100%;height:auto;aspect-ratio:640/360;display:block;overflow:hidden}.empty-scene{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;gap:9px;color:var(--faint);padding:24px;text-align:center}.empty-scene[hidden]{display:none}.empty-scene strong{font-size:13px;color:var(--muted);font-weight:400}.empty-scene>span:not(.empty-cross){font-size:11px}.empty-cross{font-size:31px;color:#52614d;font-weight:200;margin-bottom:7px}.stage-dimensions{position:absolute;right:14px;bottom:10px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#6d7f6d;pointer-events:none}.transport{display:flex;gap:7px;align-items:center;padding:14px 16px 0;flex-wrap:wrap}.transport button{font-size:11px;padding:7px 10px}.transport #clock-status{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:var(--muted);margin-left:auto}.control-note,.keyboard-note{font-size:10px;line-height:1.7;color:var(--faint);margin:10px 18px 15px}.input-heading{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:0 18px;padding-top:13px;border-top:1px solid var(--line)}h3{font-size:11px;font-weight:550;margin:0}.input-heading>span{font-size:9px;color:var(--faint)}.input-controls{display:flex;flex-wrap:wrap;gap:7px;padding:11px 17px 0;min-height:39px}.input-button{display:flex;align-items:center;gap:14px;touch-action:none;user-select:none;-webkit-user-select:none}.input-button kbd{font:9px ui-monospace,SFMono-Regular,Consolas,monospace;border:1px solid #5a6957;border-radius:3px;padding:3px 4px;color:var(--muted);min-width:22px;text-align:center}.input-button.held{background:var(--accent);border-color:var(--accent);color:var(--accent-dark)}.input-button.held kbd{color:var(--accent-dark);border-color:#829a62}.empty-note{font-size:11px;line-height:1.7;color:var(--faint);margin:0}.keyboard-note{margin-bottom:13px;font-size:9px}.state-values{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));padding:15px 19px;gap:13px;max-height:210px;overflow:auto}.state-value{display:flex;flex-direction:column;gap:6px;min-width:0}.state-value dt{font:10px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);overflow-wrap:anywhere}.state-value dd{font:17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--accent);margin:0;overflow-wrap:anywhere}.state-value dl{margin:0}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.subdued{color:var(--muted);font-size:10px}.event-line{padding:0 19px 13px;display:flex;gap:16px;align-items:baseline;flex-wrap:wrap}.event-line #last-event{color:var(--muted);font-size:10px;overflow-wrap:anywhere}.runtime-error{margin:0 18px 14px;padding:10px;border:1px solid #705049;border-radius:5px;background:#432c272e;color:var(--danger);font-size:11px;line-height:1.6}.cost-details{border-top:1px solid var(--line);padding:12px 19px;font-size:10px;color:var(--muted)}summary{cursor:pointer;font-size:11px;color:var(--muted)}.cost-details p{font-size:10px;line-height:1.8;color:var(--faint)}.cost-values{display:grid;grid-template-columns:1fr auto;gap:8px 20px;margin:16px 0}.cost-values dt,.cost-values dd{margin:0}.cost-values dd{text-align:right;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.reference{margin-top:25px}.reference-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));padding:22px 20px;gap:24px}.reference-grid article{min-width:0;display:flex;flex-direction:column;align-items:flex-start}.reference-grid h3{font-size:12px;color:#d2ddc8}.reference-grid p{font-size:10px;line-height:1.8;color:var(--muted);min-height:36px;margin:8px 0 10px}.reference-grid pre{font:10px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;background:#151b16;color:#bacead;width:100%;padding:10px;overflow:auto;border-radius:6px;min-height:115px;margin:0 0 8px}.reference-grid .quiet{font-size:10px;padding:5px 0}.syntax-details{padding:16px 20px;border-top:1px solid var(--line)}.syntax-columns{display:grid;grid-template-columns:1fr 1fr;gap:35px;margin-top:12px}.syntax-columns p{font-size:11px;line-height:1.95;color:var(--muted)}.syntax-columns strong{color:var(--text);font-weight:500}.syntax-columns code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:#d0dcbe;overflow-wrap:anywhere}.example-row{padding:18px 20px;border-top:1px solid var(--line);display:flex;align-items:center;gap:10px}.example-row>div{margin-right:auto}.example-row label{font-size:11px;color:var(--text)}.example-row p{font-size:10px;color:var(--faint);margin:5px 0 0}.example-row button,.example-row select{font-size:10px}.page-footer{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:26px 0 30px;color:var(--faint);font-size:10px}.page-footer>span{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:9px}.page-footer p{margin:0;line-height:1.8}.page-footer strong{font-weight:400;color:var(--muted)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important} +.baseline-row{border-top:1px solid var(--line);padding:15px 20px;display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center}.baseline-row a{font-size:11px;color:var(--accent);text-underline-offset:3px}.baseline-row span{font-size:10px;color:var(--faint)}#scene:focus-visible{outline:2px solid var(--accent);outline-offset:-3px} +@media(min-width:1450px){.code-editor{height:558px}.reference-grid pre{font-size:11px}} +@media(max-width:1050px){.masthead{padding:0 25px}.experiment{display:none}main{padding:29px 25px 0}.workspace{grid-template-columns:minmax(300px,1fr) minmax(340px,1fr);gap:15px}.intro>p{font-size:11px;max-width:290px}.reference-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:25px}.reference-grid p{min-height:0}.reference-grid pre{min-height:110px;font-size:11px}.transport #clock-status{width:100%;margin:3px 0 0}.code-editor{height:533px}.panel-heading{padding:0 16px}.apply-row{gap:10px;padding-left:14px;padding-right:14px}.apply-row p{font-size:9px}.input-heading{align-items:flex-start;flex-direction:column;gap:5px}.intro{gap:20px}button.primary{gap:14px;padding:11px 13px}} +@media(max-width:760px){.masthead{height:63px;padding:0 20px}.wordmark{font-size:21px}.wordmark span{font-size:11px}.connection{font-size:10px}main{padding:25px 18px 0}.intro{display:block;margin-bottom:22px}.intro h1{font-size:28px}.intro>p{margin-top:14px;max-width:none;font-size:11px}.workspace{grid-template-columns:minmax(0,1fr);gap:17px}.code-editor{height:340px}.editor-panel{width:100%}.transport #clock-status{width:auto;margin-left:auto}.right-column{gap:17px}.input-heading{flex-direction:row;align-items:center}.reference{margin-top:20px}.reference-grid{padding:20px 16px;gap:24px 17px}.reference-grid pre{font-size:10px}.reference>.panel-heading{height:auto;min-height:57px;padding-top:12px;padding-bottom:12px;align-items:flex-start;flex-direction:column;gap:6px}.syntax-columns{grid-template-columns:1fr;gap:0}.example-row{flex-wrap:wrap;padding:17px 16px}.example-row>div{width:100%;margin-bottom:4px}.example-row select{flex:1}.page-footer{align-items:flex-start;flex-direction:column;gap:10px;padding:23px 0}.apply-row{padding:14px 16px 10px;gap:17px}.apply-row p{font-size:10px;max-width:none}.diagnostic{margin-left:16px}.reference-grid article{overflow:hidden}} +@media(max-width:420px){main{padding-left:13px;padding-right:13px}.masthead{padding:0 15px}.wordmark span{font-size:10px}.connection{font-size:9px;gap:6px}.intro h1{font-size:25px}.transport{gap:5px;padding-left:12px;padding-right:12px}.transport button{font-size:10px;padding:7px 8px}.transport #clock-status{font-size:9px}.input-heading>span{font-size:8px}.reference-grid{grid-template-columns:minmax(0,1fr)}.reference-grid pre{min-height:0;font-size:11px}.reference-grid p{margin-bottom:8px}.file-tools{gap:0;padding:7px 5px}.file-tools .quiet{font-size:10px;padding:6px 7px}.apply-row p{font-size:9px}.line-numbers{padding-left:10px;min-width:36px}textarea#source{padding-left:12px;font-size:11px}.line-numbers{font-size:11px}} +@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}} diff --git a/prototypes/workbench/results/2026-09-23-refresh/browser-authoring.json b/prototypes/workbench/results/2026-09-23-refresh/browser-authoring.json new file mode 100644 index 0000000..893db32 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/browser-authoring.json @@ -0,0 +1,131 @@ +{ + "recordedUtc": "2026-09-23T05:54:43.141Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 464, + "activeStateBytes": 24, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "output/playwright/pure-refresh-workbench-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "output/playwright/pure-refresh-workbench-blank.png", + "output/playwright/pure-refresh-workbench-authored.png", + "output/playwright/pure-refresh-workbench-mobile.png" + ], + "qaSourceSha256": "0d4c155b3575481c60f175b71acdc332f1c0d8489297794deca6f55999ead1b8", + "executedScriptSha256": "30103b714560a01bc82e3c1698efd77e2e5424d60973ba0a7096de2ff3b2c821", + "scriptChange": "Artifact filename prefix changed from pure-workbench- to pure-refresh-workbench- only." +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/browser-limit.json b/prototypes/workbench/results/2026-09-23-refresh/browser-limit.json new file mode 100644 index 0000000..97ce00b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/browser-limit.json @@ -0,0 +1,242 @@ +{ + "utc": "2026-09-23T05:56:55.778Z", + "checks": [ + "authored browser input commits fractional state", + "bounded evaluation renders the exact authored expression", + "code replacement preserves state and changes binding result", + "browser Wasm reuses completed bindings", + "browser runs the shared-workspace build with only 56 additional dedicated bytes", + "discarding records preserves the browser snapshot", + "browser API publishes no partial scene after budget exhaustion", + "raising the budget recovers the browser snapshot", + "configured UI keeps committed state while withholding incomplete display", + "UI distinguishes display limit from event rollback", + "normal configuration reloads to a clean blank work", + "no uncaught browser errors" + ], + "api": { + "before": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "warm": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 6, + "instructions": 0, + "cacheHits": 3, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + }, + "forgotten": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limited": { + "states": { + "x": 3 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limitStats": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 1, + "work": 1, + "instructions": 0, + "cacheHits": 0, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + }, + "recovered": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + } + }, + "limitedUi": { + "states": { + "x": 3.25 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 2, + "held": [], + "lastEvent": { + "name": "next", + "phase": "up", + "source": "pointer:1", + "status": 0, + "line": null, + "ops": 0 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "appliedSource": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "message": "표시 오류 · 3행 · 표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요. — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.", + "errors": [], + "uiLimitSetup": "Test-only module response overrides default workLimit to 1; production files are unchanged.", + "screenshot": "output/playwright/pure-refresh-limit.png", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "qaSourceSha256": "d83577c0851b465c3bd43c256b53659aef9df96f3c3dffe7e59f5d7c98db80c6", + "executedScriptSha256": "58addd30823934f2e10aab96abcc1d8291897a8f92401fd606b908c76fe6cb76", + "scriptChange": "Screenshot changed from pure-workspace-limit.png to pure-refresh-limit.png only." +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/browser.json b/prototypes/workbench/results/2026-09-23-refresh/browser.json new file mode 100644 index 0000000..fe41194 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/browser.json @@ -0,0 +1,1487 @@ +{ + "utc": "2026-09-23T05:53:50.616Z", + "checks": [ + "six runs preserve exact authored states, outputs, event counts and rendered position", + "archived path reproduces unrelated editor work on every event", + "runtime input performs no source encoding or editor metadata mutations", + "Unicode source editing updates byte count, line numbers and dirty status", + "keyboard input and frame stepping preserve unapplied editor metadata", + "reset preserves draft and its metadata while restoring active state", + "successful apply updates draft status without requiring a source edit", + "failed apply preserves dirty status and active work with a source diagnostic", + "diagnostic navigation still updates the cursor position", + "programmatic Tab insertion updates source metadata", + "applying an empty program has a correct applied status", + "snippet insertion updates source metadata", + "no uncaught browser errors" + ], + "errors": [], + "samples": [ + { + "variant": "before", + "taps": 100, + "events": 200, + "elapsedMs": 1151.8000000044703, + "encodingCalls": 400, + "encodedBytes": 410800, + "editorMutations": 800, + "metrics": { + "LayoutCount": 201, + "LayoutDuration": 0.547966, + "RecalcStyleCount": 201, + "RecalcStyleDuration": 0.116558, + "ScriptDuration": 0.462013 + }, + "final": { + "states": { + "x": 70 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static21", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 290, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240, + "tick": 0, + "lastError": 0 + }, + "renderedX": "70" + }, + { + "variant": "after", + "taps": 100, + "events": 200, + "elapsedMs": 327.40000000596046, + "encodingCalls": 0, + "encodedBytes": 0, + "editorMutations": 0, + "metrics": { + "LayoutCount": 1, + "LayoutDuration": 0.0006349999999999967, + "RecalcStyleCount": 1, + "RecalcStyleDuration": 0.002024999999999999, + "ScriptDuration": 0.311722 + }, + "final": { + "states": { + "x": 70 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static21", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 290, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240, + "tick": 0, + "lastError": 0 + }, + "renderedX": "70" + }, + { + "variant": "after", + "taps": 100, + "events": 200, + "elapsedMs": 137.80000000447035, + "encodingCalls": 0, + "encodedBytes": 0, + "editorMutations": 0, + "metrics": { + "LayoutCount": 1, + "LayoutDuration": 0.0016250000000000014, + "RecalcStyleCount": 1, + "RecalcStyleDuration": 0.0002489999999999992, + "ScriptDuration": 0.129971 + }, + "final": { + "states": { + "x": 70 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static21", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 290, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240, + "tick": 0, + "lastError": 0 + }, + "renderedX": "70" + }, + { + "variant": "before", + "taps": 100, + "events": 200, + "elapsedMs": 709.6999999955297, + "encodingCalls": 400, + "encodedBytes": 410800, + "editorMutations": 800, + "metrics": { + "LayoutCount": 201, + "LayoutDuration": 0.343894, + "RecalcStyleCount": 201, + "RecalcStyleDuration": 0.07382699999999999, + "ScriptDuration": 0.27855 + }, + "final": { + "states": { + "x": 70 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static21", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 290, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240, + "tick": 0, + "lastError": 0 + }, + "renderedX": "70" + }, + { + "variant": "before", + "taps": 100, + "events": 200, + "elapsedMs": 837.8999999985099, + "encodingCalls": 400, + "encodedBytes": 410800, + "editorMutations": 800, + "metrics": { + "LayoutCount": 201, + "LayoutDuration": 0.387684, + "RecalcStyleCount": 201, + "RecalcStyleDuration": 0.08588600000000002, + "ScriptDuration": 0.351059 + }, + "final": { + "states": { + "x": 70 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static21", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 290, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240, + "tick": 0, + "lastError": 0 + }, + "renderedX": "70" + }, + { + "variant": "after", + "taps": 100, + "events": 200, + "elapsedMs": 152.80000000447035, + "encodingCalls": 0, + "encodedBytes": 0, + "editorMutations": 0, + "metrics": { + "LayoutCount": 1, + "LayoutDuration": 0.0011510000000000062, + "RecalcStyleCount": 1, + "RecalcStyleDuration": 0.00024500000000000216, + "ScriptDuration": 0.142424 + }, + "final": { + "states": { + "x": 70 + }, + "views": [ + { + "id": "moving", + "type": "box", + "color": "#7ed9ae", + "x": 70, + "y": 100, + "width": 30, + "height": 30 + }, + { + "id": "label", + "type": "text", + "prefix": "", + "x": 20, + "y": 40, + "value": 70 + }, + { + "id": "static0", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static1", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static2", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static3", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static4", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static5", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static6", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 200, + "width": 20, + "height": 20 + }, + { + "id": "static7", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static8", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static9", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static10", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static11", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static12", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static13", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 230, + "width": 20, + "height": 20 + }, + { + "id": "static14", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static15", + "type": "box", + "color": "#8da5bb", + "x": 90, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static16", + "type": "box", + "color": "#8da5bb", + "x": 160, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static17", + "type": "box", + "color": "#8da5bb", + "x": 230, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static18", + "type": "box", + "color": "#8da5bb", + "x": 300, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static19", + "type": "box", + "color": "#8da5bb", + "x": 370, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static20", + "type": "box", + "color": "#8da5bb", + "x": 440, + "y": 260, + "width": 20, + "height": 20 + }, + { + "id": "static21", + "type": "box", + "color": "#8da5bb", + "x": 20, + "y": 290, + "width": 20, + "height": 20 + } + ], + "successfulEvents": 240, + "tick": 0, + "lastError": 0 + }, + "renderedX": "70" + } + ], + "source": "state x = 40\ninput move = KeyM\nbox moving(x, 100, 30, 30, \"#7ed9ae\")\ntext label(20, 40, x)\nbox static0(20, 200, 20, 20, \"#8da5bb\")\nbox static1(90, 200, 20, 20, \"#8da5bb\")\nbox static2(160, 200, 20, 20, \"#8da5bb\")\nbox static3(230, 200, 20, 20, \"#8da5bb\")\nbox static4(300, 200, 20, 20, \"#8da5bb\")\nbox static5(370, 200, 20, 20, \"#8da5bb\")\nbox static6(440, 200, 20, 20, \"#8da5bb\")\nbox static7(20, 230, 20, 20, \"#8da5bb\")\nbox static8(90, 230, 20, 20, \"#8da5bb\")\nbox static9(160, 230, 20, 20, \"#8da5bb\")\nbox static10(230, 230, 20, 20, \"#8da5bb\")\nbox static11(300, 230, 20, 20, \"#8da5bb\")\nbox static12(370, 230, 20, 20, \"#8da5bb\")\nbox static13(440, 230, 20, 20, \"#8da5bb\")\nbox static14(20, 260, 20, 20, \"#8da5bb\")\nbox static15(90, 260, 20, 20, \"#8da5bb\")\nbox static16(160, 260, 20, 20, \"#8da5bb\")\nbox static17(230, 260, 20, 20, \"#8da5bb\")\nbox static18(300, 260, 20, 20, \"#8da5bb\")\nbox static19(370, 260, 20, 20, \"#8da5bb\")\nbox static20(440, 260, 20, 20, \"#8da5bb\")\nbox static21(20, 290, 20, 20, \"#8da5bb\")\non move.down { x = x + 0.25 }", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "scope": "100 synchronous synthetic keyboard taps after 20 warm-up taps. Timings include encoding/mutation instrumentation, exclude actual input arrival and presentation latency. CDP metrics may include browser work around the batch.", + "setup": "Only app.js is substituted from the verified archive for before runs. Other production modules are identical. No phase-timing wrappers in this comparison.", + "sourceSha256": { + "web/runtime.mjs": "d490c54fb2b422dfe89b90a594a8da9ebc95bb007910211cb4686ccb83ed75c7", + "web/app.js": "f91c076ff5242b10b390b57b4213d09b1ae74e61ea221845603428ecec5ea3f9", + "web/output.mjs": "1d22c44955e812d671f1a3c3ae4b2f0642a05ebe614017bb041bdddc8c363be3", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "qa-refresh.js": "afa6ac62f45a1d8e25a8667ecb23607c457964c410331587bbd2a2fe3bf5baaa", + "qa-refresh.py": "01da7aaba6148d0ad96d6e9686300674e6ba8779113aaf7928aa3e7ad1348960" + }, + "baselineManifestSha256": "bbf999305e08d48e94419491c20bd0efe95b36b337b91b2f2eba0f36065d0f91" +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/profile-before.json b/prototypes/workbench/results/2026-09-23-refresh/profile-before.json new file mode 100644 index 0000000..d36bfbe --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/profile-before.json @@ -0,0 +1,34 @@ +{ + "utc": "2026-09-23T05:49:37.571Z", + "taps": 100, + "elapsedMs": 375.8999999985099, + "phase": { + "present": { + "calls": 200, + "ms": 26.600000098347664 + }, + "costs": { + "calls": 200, + "ms": 56.59999991953373 + }, + "draft": { + "calls": 200, + "ms": 259.1000000089407 + }, + "refresh": { + "calls": 200, + "ms": 359.6000000536442 + } + }, + "metrics": { + "LayoutCount": 201, + "LayoutDuration": 0.177734, + "RecalcStyleCount": 201, + "RecalcStyleDuration": 0.03897, + "ScriptDuration": 0.15203 + }, + "source": "state x = 40\ninput move = KeyM\nbox moving(x, 100, 30, 30, \"#7ed9ae\")\ntext label(20, 40, x)\nbox static0(20, 200, 20, 20, \"#8da5bb\")\nbox static1(90, 200, 20, 20, \"#8da5bb\")\nbox static2(160, 200, 20, 20, \"#8da5bb\")\nbox static3(230, 200, 20, 20, \"#8da5bb\")\nbox static4(300, 200, 20, 20, \"#8da5bb\")\nbox static5(370, 200, 20, 20, \"#8da5bb\")\nbox static6(440, 200, 20, 20, \"#8da5bb\")\nbox static7(20, 230, 20, 20, \"#8da5bb\")\nbox static8(90, 230, 20, 20, \"#8da5bb\")\nbox static9(160, 230, 20, 20, \"#8da5bb\")\nbox static10(230, 230, 20, 20, \"#8da5bb\")\nbox static11(300, 230, 20, 20, \"#8da5bb\")\nbox static12(370, 230, 20, 20, \"#8da5bb\")\nbox static13(440, 230, 20, 20, \"#8da5bb\")\nbox static14(20, 260, 20, 20, \"#8da5bb\")\nbox static15(90, 260, 20, 20, \"#8da5bb\")\nbox static16(160, 260, 20, 20, \"#8da5bb\")\nbox static17(230, 260, 20, 20, \"#8da5bb\")\nbox static18(300, 260, 20, 20, \"#8da5bb\")\nbox static19(370, 260, 20, 20, \"#8da5bb\")\nbox static20(440, 260, 20, 20, \"#8da5bb\")\nbox static21(20, 290, 20, 20, \"#8da5bb\")\non move.down { x = x + 0.25 }", + "scope": "Phase timings overlap: draft/present/costs are inside refresh. CDP counters bracket the synchronous batch and may include surrounding browser work.", + "profileSourceSha256": "16773f1983261571a32c1e149e557c0fcb2436486437e950dd86fd3239c35d80", + "appSourceSha256": "a50e6d99306461f690530eedf5797b2c86c3367a704d24dc5cc86a1c1d2035ea" +} diff --git a/prototypes/workbench/results/2026-09-23-refresh/verification.json b/prototypes/workbench/results/2026-09-23-refresh/verification.json new file mode 100644 index 0000000..5b65f0d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-refresh/verification.json @@ -0,0 +1,41 @@ +{ + "utc": "2026-09-23T05:58:28.389318+00:00", + "host": { + "system": "Linux", + "machine": "aarch64", + "release": "6.18.39+rpt-rpi-2712" + }, + "browserChecks": 69, + "browserEvidenceSha256": { + "browser.json": "1ab4ee47a63fca29bf77507053bb672ac708b2dc0df55c1bbae5ac902917655d", + "browser-authoring.json": "8f2a7c13334c71b213d06fbfe90f524fb8bbf1857af9fa84c36b7566f995c1d3", + "browser-limit.json": "bfa2fb1bd385cdc2aa964ed54d465d69260d9b0a0966f9d0eb12d97cd1256a0b", + "profile-before.json": "716b7f1ad32c9f42c5ce3cda2f359eaccb3c81463b942372d20daa83b7616b3b" + }, + "sourceSha256": { + "web/app.js": "f91c076ff5242b10b390b57b4213d09b1ae74e61ea221845603428ecec5ea3f9", + "web/runtime.mjs": "d490c54fb2b422dfe89b90a594a8da9ebc95bb007910211cb4686ccb83ed75c7", + "web/output.mjs": "1d22c44955e812d671f1a3c3ae4b2f0642a05ebe614017bb041bdddc8c363be3", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "core.zig": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "profile-refresh.js": "16773f1983261571a32c1e149e557c0fcb2436486437e950dd86fd3239c35d80", + "qa-refresh.js": "afa6ac62f45a1d8e25a8667ecb23607c457964c410331587bbd2a2fe3bf5baaa", + "qa-refresh.py": "01da7aaba6148d0ad96d6e9686300674e6ba8779113aaf7928aa3e7ad1348960" + }, + "unchangedFromBaseline": [ + "web/runtime.mjs", + "web/output.mjs", + "web/compiler.mjs", + "web/core.wasm", + "core.zig", + "evaluation.zig" + ], + "unitTestNote": "No fresh JS/CLI/native unit test run: runtime/compiler/Zig/Wasm unchanged. Browser behavior is covered by 69 checks. Previous 52 JS/CLI/Wasm tests recorded in 2026-09-23-presentation/verification.json.", + "syntaxChecks": [ + "node --check web/app.js", + "node --check qa-refresh.js" + ], + "localLinksChecked": 46 +} diff --git a/prototypes/workbench/results/2026-09-23-release/README.md b/prototypes/workbench/results/2026-09-23-release/README.md new file mode 100644 index 0000000..2c04bc6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/README.md @@ -0,0 +1,19 @@ +# Drag release regression + +User reported rollback after mouse release in the Codex browser, with either up +or cancel displayed. The embedded browser event sequence was not directly observed. + +`before-browser.json` reproduces rollback by delivering pointerup on document, +then releasing SVG capture. This event injection tests a delivery-path hypothesis; +it is not a recording from the user's browser. +`after-browser.json` contains 14 release regression checks using real mouse drags +and explicit release/cancel event injections. The example now commits the last +rendered position on up; authors still receive the original up coordinates. +Actual root capture loss, pointercancel and window blur still cancel. + +Use `qa-release.js` with Playwright CLI run-code on the local workbench. +Implementation and test sources are under `after/`. Prior pointer results remain +unchanged in the sibling 2026-09-23-pointer directory. + +`pointer-browser.json`: 27 passing mouse/touch checks, including ignoring mouseup +while a touch owns a target. Pointer and CLI Node suites: 12 tests passed. diff --git a/prototypes/workbench/results/2026-09-23-release/after-browser.json b/prototypes/workbench/results/2026-09-23-release/after-browser.json new file mode 100644 index 0000000..0c7fd1d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after-browser.json @@ -0,0 +1,62 @@ +{ + "checks": [ + { + "label": "ordinary in-scene release retains the displayed position", + "passed": true + }, + { + "label": "ordinary release outside the scene retains the clamped position", + "passed": true + }, + { + "label": "document-pointerup: release commits once without a coordinate jump or later cancel", + "passed": true + }, + { + "label": "document-pointerup: duplicate release/capture loss produces no extra authored event", + "passed": true + }, + { + "label": "child-nonbubbling-up: release commits once without a coordinate jump or later cancel", + "passed": true + }, + { + "label": "child-nonbubbling-up: duplicate release/capture loss produces no extra authored event", + "passed": true + }, + { + "label": "mouse-only-up: release commits once without a coordinate jump or later cancel", + "passed": true + }, + { + "label": "mouse-only-up: duplicate release/capture loss produces no extra authored event", + "passed": true + }, + { + "label": "stale-scene-up: release commits once without a coordinate jump or later cancel", + "passed": true + }, + { + "label": "stale-scene-up: duplicate release/capture loss produces no extra authored event", + "passed": true + }, + { + "label": "descendant capture-loss notification cannot cancel the root owner", + "passed": true + }, + { + "label": "actual root capture loss while held still cancels", + "passed": true + }, + { + "label": "cancellation outside the scene still rolls back and ignores the late up", + "passed": true + }, + { + "label": "focus loss still cancels rather than committing", + "passed": true + } + ], + "passed": 14, + "failed": [] +} diff --git a/prototypes/workbench/results/2026-09-23-release/after/cli.test.mjs b/prototypes/workbench/results/2026-09-23-release/after/cli.test.mjs new file mode 100644 index 0000000..5a51536 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after/cli.test.mjs @@ -0,0 +1,97 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => { + const file = join(root, 'examples/direct.pure'); + const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]]) + runtime.point('drag', phase, x, y, 'terminal'); + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert.equal(result.snapshots.at(-1).states.x, 350); + assert.equal(result.snapshots.at(-1).states.y, 210); + assert.deepEqual(result.snapshots.at(-1).held, []); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); diff --git a/prototypes/workbench/results/2026-09-23-release/after/examples/direct.pure b/prototypes/workbench/results/2026-09-23-release/after/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after/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/workbench/results/2026-09-23-release/after/pointer.test.mjs b/prototypes/workbench/results/2026-09-23-release/after/pointer.test.mjs new file mode 100644 index 0000000..8bf530d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after/pointer.test.mjs @@ -0,0 +1,118 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = await readFile(new URL('./examples/direct.pure', import.meta.url), 'utf8'); +const fresh = async () => { const r = await createRuntime(wasm, {cacheEntries:32}); r.apply(source); return r; }; + +test('pointer declarations resolve forward targets and reject ambiguous or contextless inputs', () => { + const p = compile(source); + assert.equal(p.inputs[0].target, 'tile'); + assert(p.handlers.some(h => h.event === 17)); assert(p.handlers.some(h => h.event === 25)); + assert(p.instructions.some(i => i.op === 22)); assert(p.instructions.some(i => i.op === 23)); + for (const [code, message] of [ + ['input p = pointer(missing)', /Unknown pointer target/], + ['input p=pointer(tile)\ninput q=pointer(tile)\ntext tile(0,0,0)', /Duplicate pointer/], + ['input p=Space\non p.move {}', /require a pointer/], + ['input p=Space\non p.cancel {}', /require a pointer/], + ['state x=0\non frame { x=event_x }', /only available/], + ['text v(event_y,0,0)', /only available/], + ['state event_x=0', /Reserved name/], + ]) assert.throws(() => compile(code), message); + const full = compile(Array.from({length:8}, (_, i) => `input p${i}=pointer(v${i})\ntext v${i}(0,0,0)\non p${i}.move {}\non p${i}.cancel {}`).join('\n')); + assert.deepEqual(full.handlers.map(h => h.event), Array.from({length:8}, (_, i) => [17+i,25+i]).flat()); +}); + +test('authored drag keeps grab offset and commits the displayed position even if release coordinates differ', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'pen'); + assert.equal(r.snapshot().states.dragging, 1); + assert.throws(() => r.apply(''), /놓은 뒤/); + r.point('drag', 'move', 360, 220, 'pen'); + let s = r.snapshot(); + assert.equal(s.states.x, 350); assert.equal(s.states.y, 210); + assert.equal(s.views.find(v => v.id === 'region').value, 1); + r.point('drag', 'up', 410, 240, 'pen'); + s = r.snapshot(); + assert.equal(s.states.x, 350); assert.equal(s.states.y, 210); + assert.equal(s.states.dragging, 0); assert.deepEqual(s.held, []); + assert.equal(s.successfulEvents, 3); + assert.equal(s.lastEvent.x, 410); + r.down('left'); r.up('left'); + assert.equal(r.snapshot().states.x, 340); + r.point('drag', 'down', 350, 220, 'pen'); + r.point('drag', 'move', 450, 250, 'pen'); + r.point('drag', 'up', 350, 220, 'pen'); // A host may repeat the press coordinates. + assert.equal(r.snapshot().states.x, 440); + assert.equal(r.snapshot().states.y, 240); + r.apply('state x=0\nstate y=0\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.up { x=event_x\ny=event_y }'); + r.point('p','down',1,2); r.point('p','up',30,40); + assert.deepEqual(r.snapshot().states, {x:30,y:40}); // Payload remains available to authors. +}); + +test('ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input', async () => { + const r = await fresh(); + r.point('drag', 'down', 90, 170, 'a'); + r.point('drag', 'down', 500, 500, 'a'); + assert.throws(() => r.point('drag', 'down', 90, 170, 'b'), /다른 포인터/); + r.point('drag', 'move', 400, 200, 'b'); + assert.equal(r.snapshot().successfulEvents, 1); + r.point('drag', 'move', 400, 200, 'a'); + r.releaseAll(); + const canceled = r.snapshot(); + assert.equal(canceled.states.x, 80); assert.equal(canceled.states.y, 160); + assert.equal(canceled.states.dragging, 0); assert.equal(canceled.lastEvent.phase, 'cancel'); + assert.deepEqual(canceled.held, []); + r.point('drag', 'cancel', 1, 1, 'a'); r.point('drag', 'move', 500, 500, 'a'); r.releaseAll(); + assert.deepEqual(r.snapshot(), canceled); + r.point('drag', 'down', 90, 170, 'new'); r.reset(); + r.point('drag', 'up', 600, 500, 'new'); + assert.equal(r.snapshot().states.x, 80); assert.deepEqual(r.snapshot().held, []); +}); + +test('invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event', async () => { + const r = await fresh(); r.point('drag', 'down', 90, 170); + const before = r.snapshot(); + for (const invoke of [() => r.point('drag','move',NaN,1), () => r.point('drag','up',1,Infinity), + () => r.point('drag','unknown',1,2), () => r.point('left','down',1,2), + () => r.down('drag'), () => r.up('drag'), () => r.point('drag','move',1,2,'')]) assert.throws(invoke); + assert.deepEqual(r.snapshot(), before); + r.releaseAll(); + r.apply(source.replace('x = clamp(event_x - grab_x, 20, 564)', 'x = event_x\n y = 1 / 0')); + r.point('drag', 'down', 90, 170); + assert.equal(r.point('drag', 'move', 500, 220), 4); + let s = r.snapshot(); assert.equal(s.states.x, 80); assert.equal(s.states.y, 160); + assert.equal(s.lastFailure.phase, 'move'); + r.releaseAll(); s = r.snapshot(); assert.deepEqual(s.held, []); assert.equal(s.lastError, 4); + assert.equal(s.states.dragging, 0); +}); + +test('independent targets share the event pipeline and publication remains non-reentrant', async () => { + const r = await createRuntime(wasm); + r.apply('state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\ntext one(0,0,x)\ntext two(0,0,y)\non a.move { x=event_x }\non b.move { y=event_y }'); + r.point('a','down',1,2,'first'); r.point('b','down',3,4,'second'); + r.point('a','move',10,20,'first'); r.point('b','move',30,40,'second'); + assert.deepEqual(r.snapshot().states,{x:10,y:40}); + r.present({begin(){assert.throws(() => r.point('a','move',0,0,'first'), /출력 전달 중/);},state(){},view(){},value(){},end(){}}); + r.releaseAll(); assert.deepEqual(r.snapshot().held, []); +}); + +test('failing down, up and cancel never prevent terminal ownership cleanup', async () => { + for (const phase of ['down', 'up', 'cancel']) { + const r = await createRuntime(wasm); + r.apply(`state x=7\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.${phase} { x=event_x\nx=1/0 }`); + const code = r.point('p','down',10,20,'owner'); + assert.equal(code, phase === 'down' ? 4 : 0); + assert.deepEqual(r.snapshot().held, ['p']); + if (phase === 'up') assert.equal(r.point('p','up',30,40,'owner'), 4); + else assert.equal(r.releaseAll(), phase === 'cancel' ? 4 : 0); + const s = r.snapshot(); + assert.equal(s.states.x, 7); assert.equal(s.lastError, 4); + assert.equal(s.lastFailure.phase, phase); assert.deepEqual(s.held, []); + r.apply('state x=100'); // Cleanup also unblocks definition changes. + assert.equal(r.snapshot().states.x, 7); + } +}); diff --git a/prototypes/workbench/results/2026-09-23-release/after/qa-pointer.js b/prototypes/workbench/results/2026-09-23-release/after/qa-pointer.js new file mode 100644 index 0000000..0b0c591 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after/qa-pointer.js @@ -0,0 +1,122 @@ +// Playwright CLI run-code: real mouse / CDP touch input; no timing benchmark. +async (page) => { + const checks = [], errors = []; + const check = (condition, label) => { if (!condition) throw new Error(label); checks.push(label); }; + const close = (a, b) => Math.abs(a - b) < 0.1; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const client = (x, y) => page.evaluate(({x,y}) => { + const p = new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM()); + return {x:p.x,y:p.y}; + }, {x,y}); + const move = async (x,y) => { const p = await client(x,y); await page.mouse.move(p.x,p.y); }; + const start = async (x,y) => { await move(x,y); await page.mouse.down(); }; + const reset = async () => { await page.locator('#reset').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const apply = async source => { await page.locator('#source').fill(source); await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); }; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); page.on('dialog', onDialog); + let session; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('direct'); + await page.locator('#load-example').click(); + const source = await page.locator('#source').inputValue(); + check((await state()).views.length === 0, 'loading the pointer example leaves the running program unchanged until Apply'); + await page.locator('#apply').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); + check(await page.locator('[data-point-input="drag"]').count() === 1, 'authored pointer declaration binds the visible target'); + check(await page.locator('#input-controls button').count() === 2, 'keyboard alternatives remain available without a fake pointer button'); + await page.evaluate(() => { + window.__pointerTile = document.querySelector('[data-point-input="drag"]'); + document.querySelector('#scene').addEventListener('pointerdown', e => { window.__pointerID = e.pointerId; }); + }); + await start(90,170); + let s = await state(); + check(s.states.dragging === 1 && s.held.includes('drag'), 'real mouse down reaches the authored rule'); + await move(370,230); s = await state(); + check(close(s.states.x,360) && close(s.states.y,220), 'drag retains grab offset in scene coordinates'); + check(s.views.find(v => v.id === 'region').value === 1 && await page.locator('[data-view-id="region"]').textContent() === '오른쪽 영역: 1', 'linked output updates when the authored boundary is crossed'); + check(await page.evaluate(() => window.__pointerTile === document.querySelector('[data-point-input="drag"]')), 'drag keeps its bound DOM target'); + // Physical mouse outside the SVG still reaches the stable captured scene. + await move(750,400); s = await state(); + check(close(s.states.x,564) && close(s.states.y,276), 'capture delivers movement outside the scene and authored clamp applies'); + await page.mouse.up(); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'release outside the scene ends the gesture once'); + await reset(); + await start(90,170); await move(380,240); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + s = await state(); + check(close(s.states.x,80) && close(s.states.y,160) && s.states.dragging === 0 && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'focus loss invokes authored rollback and clears ownership'); + const ended = s.successfulEvents; + await page.mouse.up(); + check((await state()).successfulEvents === ended, 'late mouse release after cancel does not dispatch a second terminal event'); + await start(90,170); await move(350,220); + await page.evaluate(() => document.querySelector('#scene').releasePointerCapture(window.__pointerID)); + await move(360,230); s = await state(); + check(close(s.states.x,80) && s.held.length === 0 && s.lastEvent.phase === 'cancel', 'explicit capture loss cancels instead of committing a drop'); + await page.mouse.up(); + await start(90,170); await move(300,220); + await page.evaluate(() => document.querySelector('#scene').dispatchEvent(new PointerEvent('pointercancel', {pointerId:window.__pointerID,bubbles:true}))); + check((await state()).held.length === 0 && close((await state()).states.x,80), 'browser cancellation routes to the authored cancel handler'); + await page.mouse.up(); + // A source edit, not a renderer option, changes the movement constraint. + await apply(source.replaceAll('20, 564','20, 200')); + await start(90,170); await move(500,220); await page.mouse.up(); s = await state(); + check(close(s.states.x,200), 'editing the authored rule changes the drag limit'); + await apply(source + '\non drag.move { x = 1 / 0 }\n'); await reset(); + await start(90,170); await move(300,220); s = await state(); + check(s.lastError === 4 && close(s.states.x,80) && close(s.states.y,160), 'later handler failure rolls back all earlier movement writes'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); await page.mouse.up(); + s = await state(); + check(s.lastError === 4 && s.held.length === 0 && s.states.dragging === 0, 'cleanup releases a failed gesture while preserving its error'); + await apply(source); await reset(); + await page.screenshot({path:'output/playwright/pure-pointer-desktop.png',fullPage:true}); + + await page.setViewportSize({width:390,height:844}); + await page.locator('#scene').scrollIntoViewIfNeeded(); + session = await page.context().newCDPSession(page); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const touch = async (type, points) => session.send('Input.dispatchTouchEvent',{type,touchPoints:points}); + const a = await client(90,170), b = await client(270,210); + const scroll = await page.evaluate(() => window.scrollY); + await touch('touchStart',[{id:1,...a}]); + check((await state()).states.dragging === 1, 'touch contact starts the same authored interaction'); + await page.evaluate(() => window.dispatchEvent(new MouseEvent('mouseup', {button:0,buttons:0,bubbles:true}))); + check((await state()).held.includes('drag'), 'mouse release fallback cannot terminate an active touch contact'); + await touch('touchMove',[{id:1,...b}]); s = await state(); + check(close(s.states.x,260) && close(s.states.y,200), 'mobile scaled SVG maps touch coordinates back to world coordinates'); + check(await page.evaluate(() => window.scrollY) === scroll, 'touch drag manipulates the target without scrolling the page'); + await touch('touchEnd',[]); s = await state(); + check(s.held.length === 0 && s.states.dragging === 0 && s.lastEvent.phase === 'up', 'touch release clears capture ownership'); + await page.screenshot({path:'output/playwright/pure-pointer-mobile.png',fullPage:true}); + // Independent contacts are not collapsed into one global drag state. + const two = 'state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\nbox one(20,100,100,80,"#c9f08b")\nbox two(300,100,100,80,"#c9f08b")\non a.move { x=event_x }\non b.move { y=event_y }'; + await apply(two); + const p1 = {id:1,...await client(50,140)}, p2 = {id:2,...await client(330,140)}; + await touch('touchStart',[p1]); await touch('touchStart',[p1,p2]); + check((await state()).held.length === 2, 'two contacts on different authored targets retain separate owners'); + await touch('touchMove',[{id:1,...await client(180,160)},{id:2,...await client(430,200)}]); s = await state(); + check(close(s.states.x,180) && close(s.states.y,200), 'independent touch payloads reach their own rules'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'touch cancellation releases both targets'); + // A second contact on the same target cannot steal the first one. + await touch('touchStart',[p1]); + await touch('touchStart',[p1,{id:2,...await client(70,150)}]); + check((await state()).held.length === 1, 'second contact on an already owned target does not steal it'); + await touch('touchCancel',[]); + check((await state()).held.length === 0, 'ignored second contact cannot strand a logical input'); + check(errors.length === 0,'no browser page errors'); + await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); + await session.detach(); session = null; + await page.setViewportSize({width:1440,height:1000}); await apply(source); await reset(); + await page.evaluate(result => { window.__purePointerEvidence = result; }, {checks,errors,kind:'functional input/output checks; no performance benchmark'}); + return {checks:checks.length,errors}; + } catch (error) { + await page.evaluate(result => { window.__purePointerEvidence = result; }, {failed:String(error),checks,errors}); + throw error; + } finally { + if (session) { await session.send('Emulation.setTouchEmulationEnabled',{enabled:false}); await session.detach(); } + page.off('pageerror',onError); page.off('dialog',onDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-release/after/qa-release.js b/prototypes/workbench/results/2026-09-23-release/after/qa-release.js new file mode 100644 index 0000000..1674c5b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after/qa-release.js @@ -0,0 +1,59 @@ +// Playwright CLI run-code. Covers release routing and stale release coordinates. +async page => { + const checks = []; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const check = (ok, label) => checks.push({label, passed:Boolean(ok)}); + const near = (a,b) => Math.abs(a-b)<0.1; + const client = (x,y) => page.evaluate(({x,y}) => { + const p = new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM()); + return {x:p.x,y:p.y}; + },{x,y}); + const move = async(x,y) => { const p=await client(x,y); await page.mouse.move(p.x,p.y); }; + const start = async() => { + await page.locator('#reset').click(); await page.locator('#scene').scrollIntoViewIfNeeded(); + await move(90,170); await page.mouse.down(); await move(370,230); + }; + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await page.locator('#example').selectOption('direct'); + await page.locator('#load-example').click(); await page.locator('#apply').click(); + await page.evaluate(() => document.querySelector('#scene').addEventListener('pointerdown',e=>{ window.__releaseID=e.pointerId; })); + await start(); await page.mouse.up(); let s=await state(); + check(near(s.states.x,360)&&near(s.states.y,220)&&!s.held.length&&s.lastEvent.phase==='up','ordinary in-scene release retains the displayed position'); + await start(); await move(750,400); await page.mouse.up(); s=await state(); + check(near(s.states.x,564)&&near(s.states.y,276)&&!s.held.length&&s.lastEvent.phase==='up','ordinary release outside the scene retains the clamped position'); + for (const scenario of ['document-pointerup','child-nonbubbling-up','mouse-only-up','stale-scene-up']) { + await start(); + const position=await client(90,170); // Deliberately stale press coordinates. + await page.evaluate(({scenario,position})=>{ + const scene=document.querySelector('#scene'), tile=document.querySelector('[data-point-input="drag"]'); + const fields={pointerId:window.__releaseID,pointerType:'mouse',button:0,buttons:0,clientX:position.x,clientY:position.y,bubbles:scenario!=='child-nonbubbling-up'}; + if(scenario==='mouse-only-up') window.dispatchEvent(new MouseEvent('mouseup',fields)); + else (scenario==='document-pointerup'?document:scenario==='child-nonbubbling-up'?tile:scene).dispatchEvent(new PointerEvent('pointerup',fields)); + if(scene.hasPointerCapture(window.__releaseID)) scene.releasePointerCapture(window.__releaseID); + },{scenario,position}); + await page.mouse.up(); s=await state(); + check(near(s.states.x,360)&&near(s.states.y,220)&&!s.held.length&&s.lastEvent.phase==='up',`${scenario}: release commits once without a coordinate jump or later cancel`); + check(s.successfulEvents===3,`${scenario}: duplicate release/capture loss produces no extra authored event`); + } + await start(); + await page.evaluate(()=>document.querySelector('[data-point-input="drag"]').dispatchEvent(new PointerEvent('lostpointercapture',{pointerId:window.__releaseID,bubbles:true}))); + s=await state(); check(s.held.includes('drag')&&near(s.states.x,360),'descendant capture-loss notification cannot cancel the root owner'); + await page.mouse.up(); + await start(); + await page.evaluate(()=>document.querySelector('#scene').releasePointerCapture(window.__releaseID)); + await move(380,240); s=await state(); + check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','actual root capture loss while held still cancels'); + await page.mouse.up(); + await start(); + await page.evaluate(()=>document.dispatchEvent(new PointerEvent('pointercancel',{pointerId:window.__releaseID,bubbles:true}))); + await page.mouse.up(); s=await state(); + check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','cancellation outside the scene still rolls back and ignores the late up'); + await start(); + await page.evaluate(()=>window.dispatchEvent(new Event('blur'))); + await page.mouse.up(); s=await state(); + check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','focus loss still cancels rather than committing'); + const result={checks,passed:checks.filter(c=>c.passed).length,failed:checks.filter(c=>!c.passed)}; + await page.evaluate(result=>{window.__pureReleaseEvidence=result;},result); + return result; +} diff --git a/prototypes/workbench/results/2026-09-23-release/after/web/examples/direct.pure b/prototypes/workbench/results/2026-09-23-release/after/web/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after/web/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/workbench/results/2026-09-23-release/after/web/scene-input.mjs b/prototypes/workbench/results/2026-09-23-release/after/web/scene-input.mjs new file mode 100644 index 0000000..0f7cbef --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/after/web/scene-input.mjs @@ -0,0 +1,83 @@ +// This adapter owns browser capture only. Authored state changes stay in the VM. +export function bindSceneInput(scene, runtime, act) { + const captures = new Map(); + const events = scene.ownerDocument.defaultView; + const coordinates = event => { + const matrix = scene.getScreenCTM(); + if (!matrix) return null; + try { + const p = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse()); + return Number.isFinite(p.x) && Number.isFinite(p.y) ? p : null; + } catch { return null; } + }; + const release = id => { + if (scene.hasPointerCapture(id)) scene.releasePointerCapture(id); + }; + function finish(event, phase, pointerId = event.pointerId) { + const entry = captures.get(pointerId); + if (!entry) return; + event.preventDefault(); + // Capture loss/cancel may not carry a meaningful position. Use the last + // delivered coordinates. Up must use its own final position. + const p = phase === 'up' ? coordinates(event) : null; + if (p) { entry.x = p.x; entry.y = p.y; } + if (phase === 'up' && !p) phase = 'cancel'; + captures.delete(pointerId); + release(pointerId); + act(() => runtime().point(entry.name, phase, entry.x, entry.y, entry.source)); + } + scene.addEventListener('pointerdown', event => { + if (event.button !== 0 || !runtime() || captures.has(event.pointerId)) return; + const target = event.target.closest?.('[data-point-input]'); + if (!target || !scene.contains(target)) return; + const name = target.getAttribute('data-point-input'); + // A logical input has one coordinate owner. A second contact on it must not + // steal the gesture; other authored targets can be manipulated independently. + if ([...captures.values()].some(entry => entry.name === name)) return; + const p = coordinates(event); + if (!p) return; + event.preventDefault(); scene.focus(); + act(() => { + scene.setPointerCapture(event.pointerId); + const entry = {name, source:`scene:${event.pointerId}`, pointerType:event.pointerType, x:p.x, y:p.y}; + captures.set(event.pointerId, entry); + try { runtime().point(name, 'down', p.x, p.y, entry.source); } + catch (error) { captures.delete(event.pointerId); release(event.pointerId); throw error; } + }); + }); + scene.addEventListener('pointermove', event => { + const entry = captures.get(event.pointerId); + if (!entry) return; + const p = coordinates(event); + if (!p) { finish(event, 'cancel'); return; } + event.preventDefault(); + entry.x = p.x; entry.y = p.y; + act(() => runtime().point(entry.name, 'move', p.x, p.y, entry.source)); + }); + // A host may deliver release outside the SVG even while it owns capture. + // Observe termination before target handlers or capture cleanup can cancel it. + events.addEventListener('pointerup', event => finish(event, 'up'), true); + events.addEventListener('pointercancel', event => finish(event, 'cancel'), true); + events.addEventListener('mouseup', event => { + if (event.button !== 0) return; + // Mouse-only fallback when the host delivers mouseup without pointerup. + // A normal pointerup already removed the owner; touch compatibility events + // must never finish a different contact. + for (const [id, entry] of captures) { + if (entry.pointerType === 'mouse') finish(event, 'up', id); + } + }, true); + scene.addEventListener('lostpointercapture', event => { + // Descendant capture transitions bubble here but do not release our owner. + if (event.target === scene) finish(event, 'cancel'); + }); + return Object.freeze({ + releaseCaptures() { + const ids = [...captures.keys()]; + captures.clear(); + for (const id of ids) release(id); + // The caller subsequently invokes runtime.releaseAll(), which dispatches + // each pending cancel once. Clearing first makes lostcapture harmless. + }, + }); +} diff --git a/prototypes/workbench/results/2026-09-23-release/before-browser.json b/prototypes/workbench/results/2026-09-23-release/before-browser.json new file mode 100644 index 0000000..c019713 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/before-browser.json @@ -0,0 +1,21 @@ +{ + "states": { + "x": 80, + "y": 160, + "grab_x": 9.999986805438311, + "grab_y": 10.00000329864045, + "start_x": 80, + "start_y": 160, + "dragging": 0 + }, + "lastEvent": { + "name": "drag", + "phase": "cancel", + "source": "scene:1", + "status": 0, + "line": null, + "ops": 6, + "x": 370.0000131945618, + "y": 229.99999835067982 + } +} diff --git a/prototypes/workbench/results/2026-09-23-release/manifest.json b/prototypes/workbench/results/2026-09-23-release/manifest.json new file mode 100644 index 0000000..e6ab837 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/manifest.json @@ -0,0 +1,6 @@ +{ + "web/scene-input.mjs": "a7de0d0c26328fa6885b1d1633783f1550acfa4a2233db604b0ce4646e633a25", + "examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "qa-release.js": "09c4b5f9d4a13bd9b89ead072476b9019e906d921ae4ca7e6a10225750051d43", + "qa-pointer.js": "b51588231600d60677c89e8861984826d52fa2e0d6eb2f6bfffa826055033cf4" +} diff --git a/prototypes/workbench/results/2026-09-23-release/pointer-browser.json b/prototypes/workbench/results/2026-09-23-release/pointer-browser.json new file mode 100644 index 0000000..c7ba8ca --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-release/pointer-browser.json @@ -0,0 +1,33 @@ +{ + "checks": [ + "loading the pointer example leaves the running program unchanged until Apply", + "authored pointer declaration binds the visible target", + "keyboard alternatives remain available without a fake pointer button", + "real mouse down reaches the authored rule", + "drag retains grab offset in scene coordinates", + "linked output updates when the authored boundary is crossed", + "drag keeps its bound DOM target", + "capture delivers movement outside the scene and authored clamp applies", + "release outside the scene ends the gesture once", + "focus loss invokes authored rollback and clears ownership", + "late mouse release after cancel does not dispatch a second terminal event", + "explicit capture loss cancels instead of committing a drop", + "browser cancellation routes to the authored cancel handler", + "editing the authored rule changes the drag limit", + "later handler failure rolls back all earlier movement writes", + "cleanup releases a failed gesture while preserving its error", + "touch contact starts the same authored interaction", + "mouse release fallback cannot terminate an active touch contact", + "mobile scaled SVG maps touch coordinates back to world coordinates", + "touch drag manipulates the target without scrolling the page", + "touch release clears capture ownership", + "two contacts on different authored targets retain separate owners", + "independent touch payloads reach their own rules", + "touch cancellation releases both targets", + "second contact on an already owned target does not steal it", + "ignored second contact cannot strand a logical input", + "no browser page errors" + ], + "errors": [], + "kind": "functional input/output checks; no performance benchmark" +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/README.md b/prototypes/workbench/results/2026-09-23-shared-rules/README.md new file mode 100644 index 0000000..4d8b6d0 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/README.md @@ -0,0 +1,24 @@ +# Shared rule bodies — 2026-09-23 + +- build.json: final contiguous-range binding ABI, matching source/Wasm hashes, + 32 native tests passed. +- verification.json: 75 Node tests passed. Compiler/rules suites include the final + parameter-prefix shadowing guard and the generated-name document size boundary. +- shared-browser.json: 23 checks on the final contiguous-binding build; mouse, + two-touch ownership/cancel, editing one shared rule, reordered declarations, + file download and fresh-page restoration, async read conflicts. +- authoring-browser.json: 44 existing authoring checks passed. +- representation.json: compile/layout accounting on the same final core for + explicit and shared versions. Not a throughput, latency or process RAM claim. +- before/: previous implementation references. sources/ and manifest.json: + final implementation and identities. Screenshots and actual downloaded layout + are retained beside the JSON results. + +The browser scenarios precede the final compiler-only prefix-shadow rejection +check; that diagnostic change is covered by the final Node compiler/rules suites. +No browser performance benchmark was run. Browser is Linux Chromium; touches use +CDP emulation, not physical hardware. Codex embedded browser is not directly driven. + +Build with Zig 0.15.2 via manage.py build. Run *.test.mjs with Node child-process +support. Serve on port 8766 and run qa-shared-rules.js through Playwright CLI on a +fresh blank workbench page. The ordinary qa-browser.js changes only artifact names. diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/authoring-browser.json b/prototypes/workbench/results/2026-09-23-shared-rules/authoring-browser.json new file mode 100644 index 0000000..7e0927a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/authoring-browser.json @@ -0,0 +1,128 @@ +{ + "recordedUtc": "2026-09-23T07:08:12.678Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19904, + "programReservedBytes": 9648, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 480, + "activeStateBytes": 24, + "stagingReservedBytes": 9912, + "evalStackBytes": 512 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "output/playwright/pure-shared-workbench-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "output/playwright/pure-shared-workbench-blank.png", + "output/playwright/pure-shared-workbench-authored.png", + "output/playwright/pure-shared-workbench-mobile.png" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/before/core.zig b/prototypes/workbench/results/2026-09-23-shared-rules/before/core.zig new file mode 100644 index 0000000..1154297 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/before/core.zig @@ -0,0 +1,1136 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { event: u32 = 0, start: u32 = 0, length: u32 = 0 }; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +const Workspace = union { staging: Pending, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.program.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.program.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.program.initials[id] = initial; + engine.workspace.staging.program.old_ids[id] = old_id; + engine.workspace.staging.program.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 23) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) return .invalid; + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 23) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + break; + } + } + const h = owner orelse return .invalid; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + } + } + return .ok; +} + +export fn put_restored_value(id: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const pending = &engine.workspace.staging; + if (id >= pending.program.state_count) return stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return stagingFail(.bad_state); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; +} + +export fn commit_program() u32 { + return commit(false); +} + +export fn commit_restored_program() u32 { + return commit(true); +} + +fn commit(restoring: bool) u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const count = engine.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (engine.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return stagingFail(.bad_state); + const code = validate(&engine.workspace.staging.program); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.program.state_count) |i| { + const old_id = engine.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) engine.workspace.staging.values[i] else if (old_id < 0) engine.workspace.staging.program.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging.program; + engine.values = next; + finishStaging(); + if (restoring) { + engine.tick = 0; + engine.successful_events = 0; + } + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, point: bool = false }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23 => 0, + 7, 16, 20, 21 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20) { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[inst.arg] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + (context.point and event == 0) or (!context.point and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + const result = execute(&pending, h.start, h.length, context, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 12), layout(3)); + try std.testing.expectEqual(@as(u32, 76), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/before/examples/layout.pure b/prototypes/workbench/results/2026-09-23-shared-rules/before/examples/layout.pure new file mode 100644 index 0000000..41cf149 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/before/examples/layout.pure @@ -0,0 +1,92 @@ +# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요. +# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다. +state selected = 0 +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +state x1 = 80 +state y1 = 120 +state ox1 = 0 +state oy1 = 0 +state sx1 = 80 +state sy1 = 120 +state held1 = 0 +input drag1 = pointer(tile1) +state x2 = 280 +state y2 = 160 +state ox2 = 0 +state oy2 = 0 +state sx2 = 280 +state sy2 = 160 +state held2 = 0 +input drag2 = pointer(tile2) +state x3 = 450 +state y3 = 220 +state ox3 = 0 +state oy3 = 0 +state sx3 = 450 +state sy3 = 220 +state held3 = 0 +input drag3 = pointer(tile3) +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box tile1(x1,y1,56,44,"#c9f08b") +box tile2(x2,y2,56,44,"#74c4d4") +box tile3(x3,y3,56,44,"#dfb184") +on drag1.down { + selected = 1 + sx1 = x1; sy1 = y1 + ox1 = event_x - x1; oy1 = event_y - y1 + held1 = 1 +} +on drag1.move { + x1 = clamp(event_x - ox1, 20, 564) + y1 = clamp(event_y - oy1, 90, 286) +} +on drag1.up { held1 = 0 } +on drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 } +on drag2.down { + selected = 2 + sx2 = x2; sy2 = y2 + ox2 = event_x - x2; oy2 = event_y - y2 + held2 = 1 +} +on drag2.move { + x2 = clamp(event_x - ox2, 20, 564) + y2 = clamp(event_y - oy2, 90, 286) +} +on drag2.up { held2 = 0 } +on drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 } +on drag3.down { + selected = 3 + sx3 = x3; sy3 = y3 + ox3 = event_x - x3; oy3 = event_y - y3 + held3 = 1 +} +on drag3.move { + x3 = clamp(event_x - ox3, 20, 564) + y3 = clamp(event_y - oy3, 90, 286) +} +on drag3.up { held3 = 0 } +on drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 } +on left.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) } +} +on right.down { + if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) } + if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) } + if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) } +} +on up.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) } +} +on down.down { + if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) } + if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) } + if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) } +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/before/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/before/web/compiler.mjs new file mode 100644 index 0000000..1ec0c7c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/before/web/compiler.mjs @@ -0,0 +1,326 @@ +// 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', '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(); + } + 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: token.value }); + } 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 { + this.expect('='); + statements.push({ kind: 'assign', token, name: token.value, 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('='); + let sign = 1; + if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1; + const value = this.expect('number', 'numeric state initializer'); + declarations.push({ kind, token, name, initial: sign * value.value }); + } 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 === 'pointer') { + 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'].includes(edge.value)) fail(edge, 'Expected down or up, move or cancel'); + } + 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: [], metrics: {} }; + const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(), targets = new Set(); + 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 === '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.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}`); + } + let pointHandler = false; + 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_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 (!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 (!states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`); + expression(statement.expression); + emit(20, statement.token, states.get(statement.name), 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; + } + } + }; + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + pointHandler = 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); + if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input'); + event = edge === 'move' ? 17 + index : edge === 'cancel' ? 25 + index : 1 + index * 2 + (edge === 'up' ? 1 : 0); + } + 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); + } + } + 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/workbench/results/2026-09-23-shared-rules/before/web/core.wasm b/prototypes/workbench/results/2026-09-23-shared-rules/before/web/core.wasm new file mode 100644 index 0000000..f657ed7 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-shared-rules/before/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/before/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/before/web/runtime.mjs new file mode 100644 index 0000000..8a9fdce --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/before/web/runtime.mjs @@ -0,0 +1,324 @@ +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') + 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 = []; + // 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) { + const status = point ? core.dispatch_point(event, 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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), + }, + }; + } + + 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 handler of next.handlers) checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + checked(restored ? core.commit_restored_program() : core.commit_program()); + } catch (error) { + core.abort_program(); + throw error; + } + 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].target) 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].target) 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); + }, + point(name, phase, x, y, source = 'external') { + available(); + const index = inputIndex(name); + if (!program.inputs[index].target) throw new Error('포인터에 연결된 입력이 아닙니다.'); + 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(name); + if (phase === 'down') { + if (point?.source === source) return 0; + if (point) throw new Error('이 입력은 다른 포인터가 사용 중입니다.'); + point = {source, x, y}; + points.set(name, point); + held.set(name, new Set([source])); + } else { + if (!point || point.source !== source) return 0; + point.x = x; point.y = y; + if (phase === 'up' || phase === 'cancel') { points.delete(name); held.delete(name); } + } + 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 [name, point] of [...points]) { + const status = api.point(name, 'cancel', point.x, point.y, point.source); + 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/workbench/results/2026-09-23-shared-rules/build.json b/prototypes/workbench/results/2026-09-23-shared-rules/build.json new file mode 100644 index 0000000..b294d5d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/build.json @@ -0,0 +1,23 @@ +{ + "builtUtc": "2026-09-23T07:05:16.331700+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "630aee1cc20b5803657f75e541cb6b706e2b99dd00218ac71afd78b82259621b", + "wasmSha256": "2b1b6d2bbf9005f5c13275d6013cc4c21c155c7474d334c470fcb11094818b52", + "evaluationSourceSha256": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "wasmFileBytes": 58440, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/32 core.test.blank program, no hidden application behavior, and reset...OK\n2/32 core.test.arbitrary input counter and unused events...OK\n3/32 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/32 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/32 core.test.branch to program end is valid...OK\n6/32 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/32 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/32 core.test.stack underflow overflow and leftover values reject the event...OK\n9/32 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/32 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/32 core.test.capacity boundaries and sticky load errors...OK\n12/32 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/32 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/32 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/32 core.test.binding evaluation is pure on both success and failure...OK\n16/32 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/32 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/32 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/32 core.test.point coordinates are ephemeral event operands and unavailable to pure evaluation...OK\n20/32 core.test.point event ranges reject holes and invalid payloads without committing...OK\n21/32 core.test.point event failure rolls back writes from earlier matching handlers...OK\n22/32 core.test.loading shares storage with records while old events and bindings remain executable...OK\n23/32 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK\n24/32 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK\n25/32 core.test.restored definition and values commit together without changing reset initializers...OK\n26/32 core.test.missing duplicate nonfinite or invalid restored candidates leave active work untouched...OK\n27/32 core.test.empty and full restored programs validate all values and discard old display records...OK\n28/32 core.test.shared handler bodies access their own bound state slots in source order...OK\n29/32 core.test.failure in a later shared binding rolls back all earlier bindings...OK\n30/32 core.test.shared bindings reject missing arguments partial overlaps and invalid slot mappings...OK\n31/32 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n32/32 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 32 tests passed.\n" +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/manifest.json b/prototypes/workbench/results/2026-09-23-shared-rules/manifest.json new file mode 100644 index 0000000..9e8f06e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/manifest.json @@ -0,0 +1,21 @@ +{ + "recordedUtc": "2026-09-23T07:12:25.660651+00:00", + "purpose": "Shared-body representation and behavior validation; no performance benchmark", + "sha256": { + "core.zig": "630aee1cc20b5803657f75e541cb6b706e2b99dd00218ac71afd78b82259621b", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "web/compiler.mjs": "033227dcee1ecfe79aded2a3b5dd5f28b896377e27671347767ef99e2eb0cf56", + "web/runtime.mjs": "d4d8fa382f501e1cdb293a1079e5dcf3cf4e65ccfc860f800b51e32cc098cf51", + "web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "web/app.js": "d5e062f9eab74f7e2bd6ab9f44e1217b12f2c17537054f5f7e4ec55b96801a09", + "web/index.html": "729945ec20857df6fcb827d8b7ef247d21d427f22debf9018fbb99401e9c7e33", + "web/scene-input.mjs": "a7de0d0c26328fa6885b1d1633783f1550acfa4a2233db604b0ce4646e633a25", + "web/core.wasm": "2b1b6d2bbf9005f5c13275d6013cc4c21c155c7474d334c470fcb11094818b52", + "examples/shared-layout.pure": "be8296244b768f9adda99e7afc89249f34853fa27124a277aecb0841eb8176eb", + "rules.test.mjs": "caab0cb2618a39c7dcd547b402561d8f9edb11acc7cf389ae16761b70464db7b", + "runtime.test.mjs": "1b9bdbae7379fcdd106cebf1acc6454839fd0e989d7ce31bfd4924fb8501cb8a", + "workspace.test.mjs": "7289caf39283041271de39c6ac58424ba811402f1a12ae13f2c49a019e3ac2af", + "qa-shared-rules.js": "700ca81147fae8264bbdaf8aee83fc1070a75ab7bccc4ae54ad228a1dc06242e", + "CONTRACT.md": "8ab46e0e420822375ba01509fecc662c457cb2405e6b35b034f78167d6dc26b3" + } +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-desktop.png b/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-desktop.png new file mode 100644 index 0000000..227a270 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-desktop.png differ diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-mobile.png b/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-mobile.png new file mode 100644 index 0000000..ad8ff54 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-mobile.png differ diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-saved.json b/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-saved.json new file mode 100644 index 0000000..c9d066f --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/pure-shared-layout-saved.json @@ -0,0 +1,108 @@ +{ + "format": "pure-workbench", + "version": 2, + "source": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "draft": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n\nstate unfinished =", + "states": [ + { + "name": "selected", + "value": 3 + }, + { + "name": "green.x", + "value": 150.00000659728084 + }, + { + "name": "green.y", + "value": 169.9999983506798 + }, + { + "name": "green.ox", + "value": 9.999986805438311 + }, + { + "name": "green.oy", + "value": 10.00000164932024 + }, + { + "name": "green.sx", + "value": 80 + }, + { + "name": "green.sy", + "value": 120 + }, + { + "name": "green.held", + "value": 0 + }, + { + "name": "green.id", + "value": 1 + }, + { + "name": "blue.x", + "value": 350.00000659728073 + }, + { + "name": "blue.y", + "value": 209.9999983506798 + }, + { + "name": "blue.ox", + "value": 9.999980208157467 + }, + { + "name": "blue.oy", + "value": 10.00000329864045 + }, + { + "name": "blue.sx", + "value": 280 + }, + { + "name": "blue.sy", + "value": 160 + }, + { + "name": "blue.held", + "value": 0 + }, + { + "name": "blue.id", + "value": 2 + }, + { + "name": "amber.x", + "value": 509.999947221753 + }, + { + "name": "amber.y", + "value": 260.0000016493202 + }, + { + "name": "amber.ox", + "value": 10.000013194561802 + }, + { + "name": "amber.oy", + "value": 9.999998350679817 + }, + { + "name": "amber.sx", + "value": 450 + }, + { + "name": "amber.sy", + "value": 220 + }, + { + "name": "amber.held", + "value": 0 + }, + { + "name": "amber.id", + "value": 3 + } + ] +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/qa-browser.js b/prototypes/workbench/results/2026-09-23-shared-rules/qa-browser.js new file mode 100644 index 0000000..dea4a6e --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/qa-browser.js @@ -0,0 +1,228 @@ +// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-shared-workbench-blank.png', 'output/playwright/pure-shared-workbench-authored.png', 'output/playwright/pure-shared-workbench-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-shared-workbench-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/representation.json b/prototypes/workbench/results/2026-09-23-shared-rules/representation.json new file mode 100644 index 0000000..4719545 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/representation.json @@ -0,0 +1,403 @@ +{ + "purpose": "Compiled representation accounting, not a throughput/latency benchmark", + "examples": { + "layout": { + "metrics": { + "sourceBytes": 2530, + "sourceLines": 93, + "states": 22, + "inputs": 7, + "views": 5, + "handlers": 16, + "instructions": 319, + "maxStack": 3 + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19904, + "programReservedBytes": 9648, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 5360, + "activeStateBytes": 176, + "stagingReservedBytes": 9912, + "evalStackBytes": 512 + }, + "bodies": [ + { + "start": 19, + "length": 16 + }, + { + "start": 35, + "length": 14 + }, + { + "start": 49, + "length": 2 + }, + { + "start": 51, + "length": 6 + }, + { + "start": 57, + "length": 16 + }, + { + "start": 73, + "length": 14 + }, + { + "start": 87, + "length": 2 + }, + { + "start": 89, + "length": 6 + }, + { + "start": 95, + "length": 16 + }, + { + "start": 111, + "length": 14 + }, + { + "start": 125, + "length": 2 + }, + { + "start": 127, + "length": 6 + }, + { + "start": 133, + "length": 48 + }, + { + "start": 181, + "length": 45 + }, + { + "start": 226, + "length": 48 + }, + { + "start": 274, + "length": 45 + } + ], + "bindings": [ + { + "event": 9, + "start": 19, + "length": 16 + }, + { + "event": 21, + "start": 35, + "length": 14 + }, + { + "event": 10, + "start": 49, + "length": 2 + }, + { + "event": 29, + "start": 51, + "length": 6 + }, + { + "event": 11, + "start": 57, + "length": 16 + }, + { + "event": 22, + "start": 73, + "length": 14 + }, + { + "event": 12, + "start": 87, + "length": 2 + }, + { + "event": 30, + "start": 89, + "length": 6 + }, + { + "event": 13, + "start": 95, + "length": 16 + }, + { + "event": 23, + "start": 111, + "length": 14 + }, + { + "event": 14, + "start": 125, + "length": 2 + }, + { + "event": 31, + "start": 127, + "length": 6 + }, + { + "event": 1, + "start": 133, + "length": 48 + }, + { + "event": 3, + "start": 181, + "length": 45 + }, + { + "event": 5, + "start": 226, + "length": 48 + }, + { + "event": 7, + "start": 274, + "length": 45 + } + ] + }, + "shared-layout": { + "metrics": { + "sourceBytes": 2271, + "sourceLines": 76, + "states": 25, + "inputs": 7, + "views": 5, + "handlers": 24, + "instructions": 117, + "maxStack": 3 + }, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19904, + "programReservedBytes": 9648, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 2256, + "activeStateBytes": 200, + "stagingReservedBytes": 9912, + "evalStackBytes": 512 + }, + "bodies": [ + { + "start": 19, + "length": 16 + }, + { + "start": 35, + "length": 14 + }, + { + "start": 49, + "length": 2 + }, + { + "start": 51, + "length": 6 + }, + { + "start": 57, + "length": 15 + }, + { + "start": 72, + "length": 15 + }, + { + "start": 87, + "length": 15 + }, + { + "start": 102, + "length": 15 + } + ], + "bindings": [ + { + "event": 9, + "start": 19, + "length": 16, + "base": 1, + "count": 8 + }, + { + "event": 21, + "start": 35, + "length": 14, + "base": 1, + "count": 8 + }, + { + "event": 10, + "start": 49, + "length": 2, + "base": 1, + "count": 8 + }, + { + "event": 29, + "start": 51, + "length": 6, + "base": 1, + "count": 8 + }, + { + "event": 1, + "start": 57, + "length": 15, + "base": 1, + "count": 8 + }, + { + "event": 3, + "start": 72, + "length": 15, + "base": 1, + "count": 8 + }, + { + "event": 5, + "start": 87, + "length": 15, + "base": 1, + "count": 8 + }, + { + "event": 7, + "start": 102, + "length": 15, + "base": 1, + "count": 8 + }, + { + "event": 11, + "start": 19, + "length": 16, + "base": 9, + "count": 8 + }, + { + "event": 22, + "start": 35, + "length": 14, + "base": 9, + "count": 8 + }, + { + "event": 12, + "start": 49, + "length": 2, + "base": 9, + "count": 8 + }, + { + "event": 30, + "start": 51, + "length": 6, + "base": 9, + "count": 8 + }, + { + "event": 1, + "start": 57, + "length": 15, + "base": 9, + "count": 8 + }, + { + "event": 3, + "start": 72, + "length": 15, + "base": 9, + "count": 8 + }, + { + "event": 5, + "start": 87, + "length": 15, + "base": 9, + "count": 8 + }, + { + "event": 7, + "start": 102, + "length": 15, + "base": 9, + "count": 8 + }, + { + "event": 13, + "start": 19, + "length": 16, + "base": 17, + "count": 8 + }, + { + "event": 23, + "start": 35, + "length": 14, + "base": 17, + "count": 8 + }, + { + "event": 14, + "start": 49, + "length": 2, + "base": 17, + "count": 8 + }, + { + "event": 31, + "start": 51, + "length": 6, + "base": 17, + "count": 8 + }, + { + "event": 1, + "start": 57, + "length": 15, + "base": 17, + "count": 8 + }, + { + "event": 3, + "start": 72, + "length": 15, + "base": 17, + "count": 8 + }, + { + "event": 5, + "start": 87, + "length": 15, + "base": 17, + "count": 8 + }, + { + "event": 7, + "start": 102, + "length": 15, + "base": 17, + "count": 8 + } + ] + } + } +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/shared-browser.json b/prototypes/workbench/results/2026-09-23-shared-rules/shared-browser.json new file mode 100644 index 0000000..49c7b15 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/shared-browser.json @@ -0,0 +1,106 @@ +{ + "checks": [ + "placement save requires an applied definition", + "three independently authored targets move and selection directs keyboard adjustment", + "placement download is a separate document file", + "saving neither applies nor discards the unfinished draft", + "reset returns to authored initial positions", + "downloaded file restores all states and visible positions", + "the draft remains separate from the restored executable definition", + "restored work starts stopped with no replayed inputs or clock", + "restored selection and bindings remain editable", + "a fresh browser page reconstructs the saved work without old runtime state", + "invalid saved state cannot partially replace code, draft or scene", + "a delayed file read cannot overwrite a newer draft", + "a delayed restore cannot overwrite placement changes made while reading", + "saving while time advances requests a stopped document", + "saving with a held input is refused", + "editing the shared rule changes the selected amber target", + "the same single rule edit also changes the blue target", + "reordering instance declarations preserves every field by qualified name", + "rebuilt parameter maps still send selection input to blue after reorder", + "simultaneous touches share code but retain independent field bindings", + "shared cancel restores each contact to its own start position", + "extra persistence control fits a mobile viewport", + "no uncaught page errors" + ], + "errors": [], + "saved": "output/playwright/pure-shared-layout-saved.json", + "expected": { + "states": { + "selected": 3, + "green.x": 150.00000659728084, + "green.y": 169.9999983506798, + "green.ox": 9.999986805438311, + "green.oy": 10.00000164932024, + "green.sx": 80, + "green.sy": 120, + "green.held": 0, + "green.id": 1, + "blue.x": 350.00000659728073, + "blue.y": 209.9999983506798, + "blue.ox": 9.999980208157467, + "blue.oy": 10.00000329864045, + "blue.sx": 280, + "blue.sy": 160, + "blue.held": 0, + "blue.id": 2, + "amber.x": 509.999947221753, + "amber.y": 260.0000016493202, + "amber.ox": 10.000013194561802, + "amber.oy": 9.999998350679817, + "amber.sx": 450, + "amber.sy": 220, + "amber.held": 0, + "amber.id": 3 + }, + "views": [ + { + "id": "board", + "type": "box", + "color": "#233a31", + "x": 20, + "y": 90, + "width": 600, + "height": 240 + }, + { + "id": "help", + "type": "text", + "prefix": "선택한 도형: ", + "x": 25, + "y": 40, + "value": 3 + }, + { + "id": "green_box", + "type": "box", + "color": "#c9f08b", + "x": 150.00000659728084, + "y": 169.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "blue_box", + "type": "box", + "color": "#74c4d4", + "x": 350.00000659728073, + "y": 209.9999983506798, + "width": 56, + "height": 44 + }, + { + "id": "amber_box", + "type": "box", + "color": "#dfb184", + "x": 509.999947221753, + "y": 260.0000016493202, + "width": 56, + "height": 44 + } + ] + }, + "source": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "draft": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n\nstate unfinished =" +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/CONTRACT.md b/prototypes/workbench/results/2026-09-23-shared-rules/sources/CONTRACT.md new file mode 100644 index 0000000..cfbe597 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/CONTRACT.md @@ -0,0 +1,437 @@ +# Workbench experiment contract + +An author starts with empty source. There is no built-in player, physics, jump, +score, or timer behavior. All behavior is declared by the author. This is a +bounded scalar interaction language experiment, not a general language claim. + +## Source language + +```text +state count = 0 +input tap = Space +box tile(80, 150, 60, 60, "#356548") +circle dot(200, 180, 20, "#c78638") +text label(80, 100, count, "Count: ") +on tap.down { + count = count + 1 +} +on frame { + if count > 20 { + count = 0 + } +} +``` + +Declarations and statements end at a newline or semicolon; `#` comments outside +strings. ASCII identifiers, finite numeric literals, state initializers must be +numeric constants (optional unary sign). Input key codes accepted: +Space, Enter, ArrowLeft/Right/Up/Down, KeyA..KeyZ, Digit0..Digit9. Input names +must be unique, key codes unique, names cannot shadow `frame`, `dt`, `min`, `max`, +`clamp`, `event_x`, `event_y`. State and input names use separate namespaces, entity IDs unique. + +Expressions: parentheses, state references, dt (seconds on frame, zero on input), +unary - and !, + - * /, < <= > >= == !=, && ||, min(a,b), max(a,b), clamp(x,lo,hi). +Logical operators evaluate both sides. Division by zero/non-finite results fail +the whole event without committing state changes. No loops, recursion or eval. +An if without else executes its condition once; assignment statements are ordered. +Multiple handlers for the same event execute in source order within one atomic event. +`dt` is allowed in handlers, not view bindings. A frame always uses 1/60 seconds. + +box: x,y,width,height expressions + literal hex color; circle: x,y,radius + color; +text: x,y,value expression + optional literal prefix. Values are world units in a +640x360 view. No implicit clipping/collision/physics. Render expressions are pure. +Unknown identifiers and invalid syntax include 1-based line+column diagnostics. + +Initial scene is empty. Optional examples are explicit insertions, never auto-run. +Successful apply preserves scalar states by exact name; new names use initializers, +removed names disappear. Apply/reset are separate. Failed parse/load changes nothing. +Apply is refused while any input is held; release first. This avoids inventing +release events across changed mappings. Focus loss sends .up to ordinary buttons +and .cancel to pointer inputs. + +## Compiler interface (web/compiler.mjs, browser and Node) + +`compile(source)` returns: +``` +{ + states:[{name,initial,line}], inputs:[{name,key,line} | {name,target,line}], + instructions:[{op,arg,value,line}], handlers:[{event,start,length,line}], + views:[{id,type,line,color?,prefix?,bindings:{x:{start,length},...}}], + metrics:{...} +} +``` +Bindings: box x/y/width/height; circle x/y/radius; text x/y/value. +Compiler throws an Error with .line and .column. Empty source is a valid empty program. +Handlers and binding expressions all occupy instructions (maximum 512 total), but +only handlers execute on events; bindings are evaluated on demand by VM eval_at. +Limits: 32 states, 8 inputs, 24 views, 64 handlers, 512 instructions, stack 64, +source 32768 UTF-8 bytes. State IDs follow declaration order. Input event IDs: +frame=0; input i down=1+2*i, up=2+2*i, move=17+i, cancel=25+i. +Move/cancel handlers require a pointer input in source. States may be referenced before declaration; +symbols must be resolved after gathering top-level declarations. `metrics` can +record source bytes/lines and instruction/handler/view/state counts. + +## VM opcodes + +| op | operation | +|---:|---| +|0| push constant `value` | +|1| push state `arg` | +|2| push dt | +|3..6| add, subtract, multiply, divide | +|7| negate | +|8..13| less, less-equal, greater, greater-equal, equal, not-equal | +|14..16| AND, OR, NOT (nonzero true) | +|17..19| min, max, clamp (push x,lo,hi; invalid lo>hi fails) | +|20| store state `arg` (pop) | +|21| jump-if-zero to absolute instruction index `arg` (pop), forward only | +|22,23| push current pointer event x,y; fail without point context | +|24,25| load/store field at handler parameter_base + arg; handler-bound only | + +Rules start with an empty stack and must end with empty stack. Bindings must finish +with exactly one value and cannot STORE, branch, or read dt/event coordinates. Branch targets may be handler end. + +## Zig exported ABI + +`reset_all()` clears programs and state. `begin_program(state_count,input_count)` +starts staging, returns0 success; `put_state(id,initial,old_id)` old_id=-1 means init; +`put_instruction(op,arg,value)` append; `put_handler(event,start,length)` append; +`commit_program()` all0 success otherwise error code, atomic; candidate is separate +from the active program, but shares its workspace with optional evaluation records. +Preserved IDs read active values only at successful commit. Validate limits, +indices, finite values and forward branch ranges; runtime stack errors are checked. +`reset_state()` restores active initializers, resets tick/event diagnostics. +`dispatch(event,dt)` accepts0..16 and finite dt; event0 dt must equal1/60, input dt0; +returns0 success, other code failure with no state mutation. Tick increments for +successful frame; one event runs all matching handlers in source order. +`dispatch_point(event,x,y)` accepts existing input down/up IDs plus move/cancel IDs +within input_count, never frame0. Coordinates must be finite; dt is implicitly0. +The core only knows input count, not host device kinds. The compiler and host enforce +pointer ownership and device mapping. Calling legacy dispatch on a coordinate-reading +handler fails with bad_event9. Point operands are ephemeral execution context, not +extra persistent state slots. Both dispatch paths use the same atomic commit/history. +The host requires this new export; older Wasm must be rebuilt. +`eval_at(start,length)` returns f64 or NaN on invalid/pure-expression failure; +must not mutate state or event diagnostics. `get_state(id)` returns f64 orNaN. +`info(id)` u32: 0 tick,1 last_error,2 error_pc (0xffffffff none),3 last_executed_ops, +4 revision,5 state_count,6 instruction_count,7 handler_count,8 input_count, +9 successful_event_count. `layout(id)` u32: 0 Engine reserved @sizeOf, +1 Program reserved @sizeOf,2 Instruction @sizeOf,3 Handler @sizeOf, +4 active used IR bytes (instructions+handlers),5 active state-value bytes, +6 staging reserved bytes (subset of Engine),7 evaluation stack capacity bytes. +`error_message` belongs to host map; core error codes documented in core.zig. +No allocation or imported runtime in Wasm. Fixed64KiB linear memory target with +16KiB stack; increase only if actual linker evidence requires and document it. + +## Host interface (web/runtime.mjs) + +`await createRuntime(wasmBytes?, evaluationOptions?)` fetches core.wasm in browser if omitted. +Returns object with apply(source), reset(), frame(count=1), down(name,source), +up(name,source), point(name,phase,x,y,source), releaseAll(), snapshot(), exportProject(), currentProgram. +Multiple sources per input share one logical held state; down emits only first +source, up only last. Source strings identify keyboard vs onscreen pointer. +`apply` returns compiled program or throws with diagnostic. It must fully validate +in host and stage+commit in core. It preserves by name. `reset` keeps active program. +`snapshot` returns {states:{name:value}, views:[{id,type,x,y,width?,height?,radius?, +value?,prefix?,color?}], tick,revision,lastError,errorLine,lastOps,memory:{...},held:[]} +`currentProgram` is the latest successfully applied compiled program or null. +The program and all nested metadata are frozen so readers cannot corrupt state +name mappings or binding ranges. `lastFailure` records the last failed event; +its `lastError`/line remain visible through successful release or focus cleanup +until successful apply or explicit reset. `lastEvent` still describes the latest +event. A view evaluation failure uses host error100 and does not roll back events. +UI draft and applied code are distinct. Browser/CLI share compiler/runtime/wasm; +instances are separate, not a live collaborative session. No arbitrary JS evaluation. + +## Bounded display evaluation (2026-09-22) + +The existing event and float semantics above are unchanged. The forgetful C +prototype's contract is adapted into `evaluation.zig`; its uint64 DAG is not +used as a substitute for this VM's float instructions. + +Each snapshot starts one display budget, shared by all bindings. Default options +are `{cacheEntries:0, historyEntries:8, workLimit:2048}`. A unit is a binding +request, a candidate-cache probe, a change-record probe, or an executed VM +instruction. State extraction, host object creation, event dispatch and DOM work +are outside this budget. It is not a time limit or a bound on whole-app work. +For the current compiler limits, 96 bindings * 10 units plus 512 instructions +is at most 1,472 units; the default admits even an uncached valid scene with +cache lookup/validation enabled. Malformed bindings may fail sooner. + +`configureEvaluation({cacheEntries?,historyEntries?,workLimit?})` accepts integers +0..32, 0..8, and 0..4294967295 respectively. Missing fields retain prior settings. +Valid configuration clears reuse records; invalid configuration changes nothing. +`forgetEvaluation('values'|'history'|'all')` drops optional records without +changing authored state, event diagnostics or program identity. +`evaluationStats()` returns the options plus work, instructions, cacheHits for +the last snapshot, and reservedBytes for the complete fixed evaluation structure. +Statistics are separate from snapshots so repeated observations remain equivalent. + +Memo entries contain the value, instruction range, input dependency bit mask +and validated revision. Direct mapping uses one candidate probe, with collisions +causing recomputation. Only successful computations are cached. Constant results +need no input-change history. All other reuse requires a complete retained +history since validation and no intersection with changed states. Dependencies +are collected from actual VM LOADs, not trusted compiler-supplied annotations. +History loss causes recomputation. All history and memo storage is fixed size; +setting capacities to zero does not return reserved linear memory. + +Only bitwise-different committed states advance change history (including signed +zero). Failed events and net-zero updates do not. Successful apply and reset +discard all reuse records. Parse/host validation failure before staging preserves +records. Beginning core staging discards them, including if that load is later +rejected or aborted. Epoch wrap also clears records. No inputs or effectful event +handlers are replayed to rebuild a binding. + +Budget exhaustion returns an empty views array for the entire snapshot, with +displayError `{code:101,line,message}`. Successfully committed event state remains. +There is no automatic retry or silent return of an old scene. Raising the budget +allows a fresh attempt. Arithmetic/size errors retain the earlier per-view error100 +behavior. `displayError` is null on display success; a latched execution error +still takes precedence in lastError/errorMessage, while displayError independently +reports display failure. Partial successful binding memos may survive a failed +snapshot but no partial scene is published. + +New core exports: `configure_evaluation(cache,history)` returns 0 or capacity1; +`forget_evaluation(flags)` bits 1=values, 2=history; `begin_evaluation(limit)`; +`eval_bounded(start,length)` returns f64 or NaN and consumes that shared budget. +`evaluation_info(id)` returns 0 status (10=work limit), 1 work, 2 instructions, +3 cache hits, 4 cache capacity, 5 history capacity, 6 reserved bytes. These +diagnostics do not replace event info. The unbounded pure `eval_at` remains for +reference checks and is not the host snapshot path. + +## Shared load/reuse workspace (2026-09-23) + +The pending definition/restore values and evaluation Tables occupy one untagged union. The active +Program, current state and evaluation control fields remain separate. All core +calls are single-threaded and synchronous; no callback can reenter a phase change. +No pointer to the reused tables is retained between calls. + +`begin_program` switches the workspace to staging and drops optional records. +Until commit/abort, old events and bindings still run against active. Binding +evaluation bypasses reuse and recomputes within its budget; cache configuration, +forget and reset must not write the staging payload. Old-id state preservation +reads the current values at successful commit, including events/reset during load. + +Successful commit publishes the new Program and then initializes empty Tables in +the shared workspace. `abort_program()` discards a pending definition and restores +empty Tables without changing active program/state/event diagnostics. Inactive +abort is a no-op. A failed commit leaves staging open for explicit abort or the +next begin. An invalid begin closes any previous pending load. Host apply aborts +after a core loading failure. Lost reuse may make a previously warm tiny budget +insufficient; neither a preserved cache nor completion at that budget is promised. + +`evaluation_info` additionally reports 7 dedicated control bytes, 8 shared Tables +bytes, 9 reuse available (false during load), and 10 union safety-tag overhead. +The host exposes these as dedicatedBytes, sharedBytes, reuseAvailable and +workspaceTagBytes. reservedBytes is control + Tables capacity, not incremental +Engine storage. The Tables capacity overlaps stagingReservedBytes and must not +be added to it. Before document restoration was added, the Wasm control was 48 B, shared Tables +1,152 B, union overhead 8 B. That Engine was 19,128 B, only 56 B larger than the +19,072 B pre-evaluation core; linear memory remains 65,536 B. No OS pages are +returned and this is not a measurement of browser/process memory. + +## Direct output publication (2026-09-23) + +`present(target)` evaluates the same bindings, in the same order and shared +budget as `snapshot()`, then calls a synchronous trusted output adapter: + +- `begin(program)`: the frozen current definition, or null at startup. +- `state(index, value)`: each current authored state, without a host state map. +- `view(index, valid)`: every declared view's publication status. +- `value(viewIndex, bindingName, value)`: each binding of a valid view only. +- `end(visibleCount)`: number of valid views. + +It returns the snapshot's diagnostics, held inputs, event information and memory +report, without `states` or `views`. Evaluation finishes before the first adapter +callback. On budget error101 every view is invalid and no binding values are +published; committed state values are still published. Error100 invalidates only +the affected views, as before. No stale value is silently substituted. + +Runtime mutations, `snapshot()` and nested `present()` are rejected during +callbacks. Read-only program/export/statistics access remains available. Adapters +must be synchronous and must not throw. If one does throw, the exception +propagates and the runtime unlocks in a finally block; arbitrary external adapter +effects cannot be rolled back. A later call republishes every value. This is not +a transaction across arbitrary output devices or an untrusted plugin boundary. + +Evaluation uses a private reusable Float64Array(96) and Uint8Array(24), allocated +on first snapshot/presentation. Their payload totals 792 host bytes, outside the +64 KiB Wasm memory. Typed-array object overhead, binding-entry arrays built at +apply, and DOM destination references are additional host memory. The numeric +buffer is a publication barrier, not another authoritative state or public +mutable scene. `snapshot()` remains an independent object representation for +CLI/export/debugging, built only when explicitly requested. + +The browser calls `present()` for normal refreshes. Its output adapter binds DOM +destinations once per successful definition change and compares values with the +existing DOM before writing. Input refreshes reuse scene, state and cost elements. +Invalid views remain allocated but are hidden using SVG `display="none"`; recovery +reveals them. A definition change can replace all scene/state destinations. This +does not skip unchanged binding evaluation, eliminate all host allocations, or +provide measured process-memory/paint-latency savings. + +## Authored direct manipulation (2026-09-23) + +`input drag = pointer(tile)` binds a logical input to a declared view, including a +forward declaration. One input per target; it shares the existing 8-input limit +with keyboard inputs. `on drag.down/move/up/cancel` handlers may read `event_x` +and `event_y` in the same 640x360 world coordinates as output. They are unavailable +in keyboard/frame handlers and display bindings; copy to authored state to retain +them. There is no built-in drag constraint, snap, grab offset, or gesture rollback. +The direct.pure example authors these using ordinary assignments and expressions. + +Host `point` requires a pointer input, valid phase, finite coordinates and nonempty +source string. Each logical input has one owning source; different targets may +have independent active contacts. Same-owner repeated down is a no-op; another +owner's down throws. Move/up/cancel without that owner are no-ops. Down establishes +ownership before dispatch; up/cancel release it before dispatch, even if authored +arithmetic fails. An event failure rolls back that event's state writes, not the +whole gesture. `releaseAll` cancels owned pointers at their last delivered position, +then releases ordinary buttons. `reset` directly clears ownership and resets state. +Last point events include x,y. A failed cancel can leave authored state unchanged, +but cannot strand host ownership; the failure remains visible until reset/apply. + +Browser hit testing selects the declared target on primary-button pointerdown. +The stable SVG root captures that pointer, including movement/release outside the +scene. Client coordinates are transformed through the inverse SVG screen matrix. +Pointerup exposes its own final coordinates to authored handlers. The direct.pure +example updates position only on move and ends dragging on up, preserving the +last displayed position even if a host repeats stale coordinates on release. Termination is observed in the window capture phase, including releases outside +the SVG. A primary mouseup can finish an owned mouse gesture if pointerup was +not delivered; it cannot finish a touch gesture. Duplicate termination is ignored. +Only capture loss of the SVG root ends its owner, not a bubbled descendant event. +Pointercancel, lostpointercapture and +focus loss use the last delivered position and dispatch cancel, never fake up. +Missing/non-invertible coordinate transforms cancel an active gesture. A second +contact on the same target is ignored; contacts on different targets are independent. +Touch scenes disable native panning on the SVG. Mouse/touch share this adapter; +pen-specific pressure, tilt and button meanings are not implemented or validated. + +Every delivered pointermove executes synchronously; this layer adds no event queue, +coalescing, frame gating or newest-only drop policy. This is not a guarantee that +browsers expose every hardware sample. Keyboard commands and pointer coordinates +enter the same authored state/commit/output path. CLI `point NAME PHASE X Y` uses +that host API and core with source `terminal`; it does not replay browser hit testing. + + +## Placement documents (2026-09-23) + +Code-only version1 JSON and .pure files keep their earlier behavior. Version2 +stores a restorable authored document, not a suspended execution image: + +```json +{"format":"pure-workbench","version":2,"source":"state x=80","draft":"state x=80", + "states":[{"name":"x","value":350}]} +``` + +`source` is the applied definition; optional `draft` is separate unapplied editor +text (default source). Each is at most 32768 UTF-8 bytes. Draft syntax may be +unfinished. State records must contain exactly one finite value for every declared +state name, with no extra, missing or duplicate names. Record order is irrelevant. +JSON numbers preserve finite f64 values, except negative zero which is encoded as +the exact string `"-0"`. No other numeric string is accepted. Input latches, running +clock, diagnostics, caches, DOM, compiled IR and external resources are not saved. +The serialized input file limit is 1449984 bytes, including escaped source/draft +and up to 32 generated item.field names (which can repeat a long schema name). + +`exportDocument(draft?)` returns an independent version2 object. It refuses an +unapplied runtime, held input or latched execution failure. It reads core state, +without constructing an output snapshot. `restoreDocument(document)` validates +and compiles before staging, refuses held input, then commits the definition and +all saved values together. It returns `{source,draft}`. Failed validation leaves +active program/values/diagnostics intact. A failed core load is aborted; optional +reuse records may be lost as with ordinary apply. Successful restore discards old +reuse, clears diagnostics, resets tick/event count and starts with no held inputs. +The original source initializers remain intact: explicit reset restores those, +not saved values. Existing `apply` continues to preserve current values by name. +Both new document APIs reject reentry during output publication. + +Core exports `put_restored_value(id,value)` and `commit_restored_program()` extend +the existing begin/put/commit loader. Every declared state needs exactly one finite +restored value; duplicate/out-of-range/nonfinite input poisons staging. Restored +commit checks completeness and program validity before touching active state. +Ordinary commit rejects staged restored values. Pending values live alongside the +pending Program in the same union as optional evaluation Tables; active Program +layout does not grow. `layout(6)` now includes Pending, rather than Program only. +Before shared rule bindings, the Wasm Pending was 9656 B (264 B added for 32 values, mask and alignment), +Engine was 19392 B; linear memory remains fixed at 65536 B. These are build-layout +facts, not a browser RAM or performance improvement claim. `evaluation_info(10)` +continues to report only union tag overhead, excluding restoration payload. + +Browser **배치 저장** stores applied source, all current values and editor draft. +It requires a stopped clock. **파일 열기** restores version2 after replacement +confirmation when existing work is present; it starts stopped. Version1/plain +source still only replaces the editor draft. Async reading checks draft generation, +program identity and successful event count so intervening edits/inputs cannot be +overwritten by a stale read. Invalid files do not alter the working document. +**.pure 저장 / 코드 JSON 저장** remain draft-only exports. + +CLI `save-project PATH` writes version2 with exclusive creation; `restore PATH` +restores it. Loading a .json path (including at startup) distinguishes versions. +A browser draft is retained through CLI save-project, but never applied implicitly. +Existing `save PATH` remains applied-source-only. No filesystem overwrite is added. + + +## Shared rules and named records (2026-09-23) + +```text +record Position { x=0; y=0 } +item green = Position(x=80,y=120) +item blue = Position(x=280,y=160) +rule move(t: Position) { t.x=event_x; t.y=event_y } +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +on drag_green.move use move(green) +on drag_blue.move use move(blue) +``` + +A record defines 1..8 named finite numeric initializers. An item has its own values; +optional literal field overrides replace defaults. Its fields occupy contiguous +slots in the existing scalar state array, with public names `item.field`. They +count toward the same 32-state limit. Schemas and items may be forward-referenced. +Unknown/duplicate fields, types or items fail compilation. Scalar and item names +cannot collide; type and rule names have separate namespaces. There are no runtime +record objects, nested records, record copying, dynamic item creation or deletion. + +A rule takes exactly one named record parameter with an exact schema name match. +Its body uses the existing expressions, ordered assignments and conditional blocks. +Parameter fields read/write the connected item's current slots; globals and other +qualified state names remain accessible. The parameter shadows the same qualified +prefix only inside its body. No rule calls from expressions or rule bodies, recursion, +return values, stack frames, or implicit event loop are introduced. Frame handlers +may use rules; coordinate-reading rules may only bind to pointer events. +Unused rules are name/type checked without emitting a body into the VM program. + +`on input.edge use rule(item)` is one event handler. Each used rule's instructions +are emitted once at its first use. Every use adds handler metadata referencing that +same exact instruction range, with parameterBase/parameterCount. Bindings run in +source order, including repeated uses of one item. All matching bindings and +ordinary handlers participate in the same atomic event: a failure in any rolls +back every state write in that event. Error PCs refer to the shared source body. + +Core `bind_handler(handler_index,base,count)` sets a previously unbound staged +handler's contiguous state range. Counts are 1..8; the entire range must fit current +staged state_count. A duplicate binding poisons staging. All owners of shared code +must have the same nonzero field count and exactly the same range. Partial overlap +remains invalid. Parameter opcodes must belong to a bound handler and use an index +below every owner's field count. Pure display evaluation rejects parameter ops; +view bindings use ordinary resolved state loads and their normal cache dependencies. + +Handler metadata grows from 12 to 16 B (base/count plus alignment). There is no +per-field indirection list or per-event parameter-value copy. Current Wasm Program +is 9648 B, Pending 9912 B, Engine 19904 B: 512 B more fixed reservation across active +and pending handler arrays. The linear-memory limit stays 64 KiB. The required +bind_handler export makes the host reject older incompatible Wasm. + +Apply preserves each qualified field by name. Reordering schema fields or item +instances rebuilds physical bindings without changing their named values. Removing +an item removes its fields; adding it again initializes new fields. An active +input still blocks definition changes. Version2 documents save qualified field +names, not physical offsets; restoration recompiles all bindings and validates all +values before commit. This is static named-field identity, not a general lifetime +handle for dynamically deleted/reused objects. Example `id` and `selected` are +ordinary author-defined numbers; uniqueness/selection policy is not enforced by +an implicit object system. diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/core.zig b/prototypes/workbench/results/2026-09-23-shared-rules/sources/core.zig new file mode 100644 index 0000000..eedeeb5 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/core.zig @@ -0,0 +1,1230 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { + event: u32 = 0, start: u32 = 0, length: u32 = 0, + parameter_base: u8 = 0, parameter_count: u8 = 0, +}; +const Program = struct { + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +const Workspace = union { staging: Pending, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.program.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.program.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.program.initials[id] = initial; + engine.workspace.staging.program.old_ids[id] = old_id; + engine.workspace.staging.program.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 25) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +export fn bind_handler(handler: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 25) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; +} + +export fn put_restored_value(id: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const pending = &engine.workspace.staging; + if (id >= pending.program.state_count) return stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return stagingFail(.bad_state); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; +} + +export fn commit_program() u32 { + return commit(false); +} + +export fn commit_restored_program() u32 { + return commit(true); +} + +fn commit(restoring: bool) u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const count = engine.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (engine.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return stagingFail(.bad_state); + const code = validate(&engine.workspace.staging.program); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.program.state_count) |i| { + const old_id = engine.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) engine.workspace.staging.values[i] else if (old_id < 0) engine.workspace.staging.program.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging.program; + engine.values = next; + finishStaging(); + if (restoring) { + engine.tick = 0; + engine.successful_events = 0; + } + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + (context.point and event == 0) or (!context.point and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = execute(&pending, h.start, h.length, bound, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 16), layout(3)); + try std.testing.expectEqual(@as(u32, 80), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "shared handler bodies access their own bound state slots in source order" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try emit(24, 0, 0); try emit(0, 0, 1); try emit(3, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(1, 1, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(2, 0, 1)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(f64, 21), get_state(1)); + try std.testing.expectEqual(@as(u32, 4), info(6)); // Three bindings, one body. + try std.testing.expectEqual(@as(u32, 12), info(3)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); +} + +test "failure in a later shared binding rolls back all earlier bindings" { + reset_all(); + try ok(begin_program(4, 1)); + for ([_]f64{ 8, 2, 12, 0 }, 0..) |value, i| try ok(put_state(@intCast(i), value, -1)); + try emit(24, 0, 0); try emit(24, 1, 0); try emit(6, 0, 0); try emit(25, 0, 0); + for (0..2) |i| { + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(@intCast(i), @intCast(i * 2), 2)); + } + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(2)); + try std.testing.expectEqual(@as(u32, 0), info(9)); +} + +test "shared bindings reject missing arguments partial overlaps and invalid slot mappings" { + reset_all(); + try counterProgram(); + const revision = info(4); + for (0..5) |mode| { + try ok(begin_program(1, 1)); try ok(put_state(0, 9, -1)); + try emit(24, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 2)); + switch (mode) { + 0 => {}, // Parameter op without a binding. + 1 => { try ok(bind_handler(0, 0, 1)); try std.testing.expectEqual(public(.invalid), bind_handler(0, 0, 1)); }, + 2 => { try std.testing.expectEqual(public(.bad_state), bind_handler(0, 0, 2)); }, + 3 => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 1, 1)); try ok(bind_handler(1, 0, 1)); + }, + else => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 2)); // Another owner missing its mapping. + }, + } + try std.testing.expect(commit_program() != 0); abort_program(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + } +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/evaluation.zig b/prototypes/workbench/results/2026-09-23-shared-rules/sources/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/examples/shared-layout.pure b/prototypes/workbench/results/2026-09-23-shared-rules/sources/examples/shared-layout.pure new file mode 100644 index 0000000..38c001d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/examples/shared-layout.pure @@ -0,0 +1,75 @@ +# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다. +state selected = 0 +record Tile { + x = 0; y = 0 + ox = 0; oy = 0 + sx = 0; sy = 0 + held = 0; id = 0 +} +item green = Tile(x=80, y=120, sx=80, sy=120, id=1) +item blue = Tile(x=280, y=160, sx=280, sy=160, id=2) +item amber = Tile(x=450, y=220, sx=450, sy=220, id=3) +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +input drag_amber = pointer(amber_box) +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +box amber_box(amber.x,amber.y,56,44,"#dfb184") + +rule grab(t: Tile) { + selected = t.id + t.sx = t.x; t.sy = t.y + t.ox = event_x - t.x; t.oy = event_y - t.y + t.held = 1 +} +rule move(t: Tile) { + t.x = clamp(event_x - t.ox, 20, 564) + t.y = clamp(event_y - t.oy, 90, 286) +} +rule drop(t: Tile) { t.held = 0 } +rule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 } +rule go_left(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) } +} +rule go_right(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) } +} +rule go_up(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) } +} +rule go_down(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) } +} + +on drag_green.down use grab(green) +on drag_green.move use move(green) +on drag_green.up use drop(green) +on drag_green.cancel use cancel(green) +on left.down use go_left(green) +on right.down use go_right(green) +on up.down use go_up(green) +on down.down use go_down(green) + +on drag_blue.down use grab(blue) +on drag_blue.move use move(blue) +on drag_blue.up use drop(blue) +on drag_blue.cancel use cancel(blue) +on left.down use go_left(blue) +on right.down use go_right(blue) +on up.down use go_up(blue) +on down.down use go_down(blue) + +on drag_amber.down use grab(amber) +on drag_amber.move use move(amber) +on drag_amber.up use drop(amber) +on drag_amber.cancel use cancel(amber) +on left.down use go_left(amber) +on right.down use go_right(amber) +on up.down use go_up(amber) +on down.down use go_down(amber) diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/qa-shared-rules.js b/prototypes/workbench/results/2026-09-23-shared-rules/sources/qa-shared-rules.js new file mode 100644 index 0000000..6dad3dc --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/qa-shared-rules.js @@ -0,0 +1,137 @@ +// Functional end-to-end persistence checks. Run with Playwright CLI run-code. +async page => { + const checks=[],errors=[]; + const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot()); + const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message); + page.on('dialog',onDialog);page.on('pageerror',onError); + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y}); + const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);}; + const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();}; + const upload=async(text,name='layout.json')=>{ + await page.locator('#file-input').evaluate((input,{text,name})=>{ + const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'})); + input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + },{text,name}); + }; + try { + await page.setViewportSize({width:1440,height:1000}); + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition'); + await page.locator('#example').selectOption('shared-layout');await page.locator('#load-example').click();await page.locator('#apply').click(); + const source=await page.locator('#source').inputValue(); + await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270); + await page.keyboard.press('ArrowLeft'); + let s=await state(); + check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment'); + const expected={states:s.states,views:s.views}; + const draft=source+'\nstate unfinished ='; + await page.locator('#source').fill(draft); + const pending=page.waitForEvent('download');await page.locator('#download-layout').click(); + const download=await pending;const saved='output/playwright/pure-shared-layout-saved.json';await download.saveAs(saved); + check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file'); + check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft'); + await page.locator('#reset').click(); + check((await state()).states['green.x']===80,'reset returns to authored initial positions'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + s=await state(); + check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions'); + check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition'); + check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable'); + // Use a fresh page/runtime, not just preservation inside the original instance. + const fresh=await page.context().newPage();await fresh.goto(page.url()); + await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(saved); + await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot()); + check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state'); + await fresh.close();await page.bringToFront(); + + const before=await state(); + await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]})); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록')); + s=await state(); + check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene'); + // Delay File.text, then edit before it completes. Restoring must not overwrite the edit. + await page.locator('#file-input').evaluate(input=>{ + const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]}; + const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'}); + const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#source').fill(draft+'\n# edit during file read'); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft'); + // Repeat with an input event while file content is pending. + await page.locator('#file-input').evaluate(input=>{ + const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]}); + const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer(); + file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);}); + transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true})); + }); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state(); + await page.evaluate(()=>window.__finishLayoutRead()); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌')); + check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading'); + // UI-only run state is deliberately not persisted. + await page.locator('#play').click();await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document'); + await page.locator('#play').click(); + // Refuse while a logical input remains held, with no lost source or partial save. + await page.locator('#scene').focus();await page.keyboard.down('ArrowRight'); + await page.locator('#download-layout').click(); + check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused'); + await page.keyboard.up('ArrowRight'); + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + // One source edit affects every bound target, without editing input adapters. + const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,'); + await page.locator('#source').fill(edited);await page.locator('#apply').click(); + const beforeEdit=await state(); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target'); + await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10); + const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target'); + const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse(); + const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n'); + const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click(); + const afterReorder=await state(); + check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name'); + await page.locator('#scene').focus();await page.keyboard.press('ArrowRight'); + check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder'); + // Two contacts execute the same body with different bound fields. + await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click(); + await page.locator('#scene').scrollIntoViewIfNeeded(); + const session=await page.context().newCDPSession(page); + try { + await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2}); + const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)}; + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]}); + await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]}); + await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]}); + const touch=await state(); + check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings'); + await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]}); + const canceled=await state(); + check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position'); + } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();} + await page.locator('#file-input').setInputFiles(saved); + await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원')); + await page.screenshot({path:'output/playwright/pure-shared-layout-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844}); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport'); + await page.screenshot({path:'output/playwright/pure-shared-layout-mobile.png',fullPage:true}); + check(errors.length===0,'no uncaught page errors'); + const result={checks,errors,saved,expected,source,draft}; + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},result); + return {checks:checks.length,errors}; + } catch(error) { + await page.evaluate(result=>{window.__pureSharedLayoutEvidence=result;},{failed:String(error),checks,errors});throw error; + } finally {page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/rules.test.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/sources/rules.test.mjs new file mode 100644 index 0000000..b4c4969 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/rules.test.mjs @@ -0,0 +1,94 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +import {parseProject} from './web/project.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const shared=await readFile(new URL('./examples/shared-layout.pure',import.meta.url),'utf8'); +const explicit=await readFile(new URL('./examples/layout.pure',import.meta.url),'utf8'); +const fresh=()=>createRuntime(wasm,{cacheEntries:32}); +const named=['green','blue','amber']; + +function simple(n){return 'record Cell { value=1 }\ninput tap=Space\nrule inc(t: Cell) { t.value=t.value+1 }\n'+Array.from({length:n},(_,i)=>`item cell${i}=Cell()\non tap.down use inc(cell${i})`).join('\n');} + +test('binding more objects shares one VM body instead of expanding its instructions',()=>{ + for(const n of [1,2,4,8]){ + const p=compile(simple(n)); + assert.equal(p.instructions.length,4);assert.equal(p.handlers.length,n); + assert.deepEqual(p.handlers.map(h=>h.start),Array(n).fill(0)); + assert.deepEqual(p.handlers.map(h=>[h.parameterBase,h.parameterCount]),Array.from({length:n},(_,i)=>[i,1])); + } + assert.equal(compile('record R { value=0 }\nrule unused(t:R) { t.value=t.value+1 }').instructions.length,0); +}); + +test('records and rules resolve forward names and reject invalid types fields and bindings',()=>{ + const p=compile('on tap.down use inc(a)\nitem a=R(v=4)\ninput tap=Space\nrule inc(t:R) { t.v=t.v+1 }\nrecord R { v=0 }'); + assert.equal(p.states[0].name,'a.v');assert.deepEqual([p.handlers[0].parameterBase,p.handlers[0].parameterCount],[0,1]); + for(const code of [ + 'record R {}','record R { x=0; x=1 }','record R { event_x=0 }', + 'item a=Missing()','record R {x=0}\nitem a=R(y=1)', + 'record R {x=0}\nitem a=R(x=1,x=2)','record R {x=0}\nstate a=0\nitem a=R()', + 'record R {x=0}\nrule bad(t:R) {t.missing=0}', + 'record R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {t.y=1}', + 'state value=0\nrecord R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {value=t.y}', + 'rule bad(t:Missing) {}','record R {x=0}\nitem a=R()\ninput k=Space\non k.down use missing(a)', + 'record R {x=0}\nrule r(t:R) {}\ninput k=Space\non k.down use r(missing)', + 'record R {x=0}\nrecord S {x=0}\nitem a=S()\nrule r(t:R) {}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_x}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_y}\non frame use r(a)', + 'record R {x=0}\nitem a=R()\ntext label(0,0,t.x)', + `record R { ${Array.from({length:9},(_,i)=>`f${i}=0`).join(';')} }`, + ]) assert.throws(()=>compile(code),code); + assert.throws(()=>compile(simple(33)),/32 states/); +}); + +test('shared layout matches the existing authored layout across independent and simultaneous gestures',async()=>{ + const a=await fresh(),b=await fresh();a.apply(explicit);b.apply(shared); + const compare=()=>{ + const old=a.snapshot(),next=b.snapshot();assert.equal(next.lastError,old.lastError); + assert.equal(next.states.selected,old.states.selected); + for(let i=1;i<=3;i++)for(const field of ['x','y','ox','oy','sx','sy','held'])assert.equal(next.states[`${named[i-1]}.${field}`],old.states[`${field}${i}`]); + for(let i=0;i{a.point(`drag${i}`,phase,x,y,`p${i}`);b.point(`drag_${named[i-1]}`,phase,x,y,`p${i}`);compare();}; + compare();point(1,'down',90,130);point(2,'down',290,170);point(1,'move',350,200);point(2,'move',500,300);point(2,'up',290,170);point(1,'cancel',350,200); + for(const key of ['left','right','up','down']){a.down(key);b.down(key);a.up(key);b.up(key);compare();} + point(3,'down',460,230);point(3,'move',900,500);point(3,'up',460,230); + const saved=JSON.parse(JSON.stringify(b.exportDocument()));const c=await fresh();c.restoreDocument(saved); + assert.deepEqual(c.snapshot().states,b.snapshot().states);assert.deepEqual(c.snapshot().views,b.snapshot().views); + c.down('left');c.up('left');assert.equal(c.snapshot().states['amber.x'],b.snapshot().states['amber.x']-10); +}); + +test('field names retain identity across schema and instance reorder, removal and restoration',async()=>{ + const r=await fresh(); + const source='record R { x=1;y=2 }\nitem a=R(x=10)\nitem b=R(x=20)\ninput k=Space\nrule step(t:R) {t.x=t.x+1}\non k.down use step(b)'; + r.apply(source);r.down('k');r.up('k'); + r.apply(source.replace('x=1;y=2','y=2;x=1').replace('item a=R(x=10)\nitem b=R(x=20)','item b=R(x=20)\nitem a=R(x=10)')); + r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],22);assert.equal(r.snapshot().states['a.x'],10); + r.apply(source.replace('item a=R(x=10)\n',''));r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); + const saved=r.exportDocument();r.apply(source);assert.equal(r.snapshot().states['a.x'],10); + r.restoreDocument(saved);assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); +}); + +test('bindings share one event transaction including global effects and multiple uses of one item',async()=>{ + const r=await fresh();r.apply('state total=0\nrecord R {x=8;divisor=2}\nitem a=R()\nitem b=R(divisor=0)\ninput k=Space\nrule divide(t:R) {total=total+1\nt.x=t.x/t.divisor}\non k.down use divide(a)\non k.down use divide(b)'); + r.down('k');assert.equal(r.snapshot().lastError,4);assert.equal(r.snapshot().states.total,0);assert.equal(r.snapshot().states['a.x'],8); + r.up('k');r.apply(simple(1)+'\non tap.down use inc(cell0)');r.down('tap');r.up('tap');assert.equal(r.snapshot().states['cell0.value'],3); +}); + +test('bound writes invalidate affected display caches and failed edits preserve active bindings',async()=>{ + const r=await fresh();const source=simple(2)+'\ntext first(0,0,cell0.value)\ntext second(0,0,cell1.value)';r.apply(source);r.snapshot();r.snapshot(); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[2,2]); + const before=r.snapshot();assert.throws(()=>r.apply(source.replace('inc(cell1)','inc(missing)')));assert.deepEqual(r.snapshot(),before); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[3,3]); +}); + +test('generated qualified names cannot make a valid exported document unreadable by its own loader',async()=>{ + const field='f'.repeat(18000); + const source=`record R { ${field}=1 }\n`+Array.from({length:32},(_,i)=>`item i${i}=R()`).join('\n'); + const r=await fresh();r.apply(source); + const saved=JSON.stringify(r.exportDocument('\u0001'.repeat(32768))); + r.restoreDocument(parseProject(saved)); + assert.equal(r.snapshot().states[`i31.${field}`],1); +}); diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/runtime.test.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/sources/runtime.test.mjs new file mode 100644 index 0000000..86bc9ba --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/runtime.test.mjs @@ -0,0 +1,197 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const fresh = () => createRuntime(wasm); +const countSource = `state total = 3 +input add = KeyA +box bar(20, 80, total * 10, 30, "#abcdef") +text label(20, 60, total, "Total: ") +on add.down { total = total + 2 } +on add.up { total = total + 1 } +`; + +test('blank startup and applied empty source contain no application objects or behavior', async () => { + const runtime = await fresh(); + assert.equal(runtime.currentProgram, null); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + runtime.apply(''); + runtime.frame(60); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + assert.equal(runtime.snapshot().tick, 60); + assert.throws(() => runtime.down('jump'), /jump/); +}); + +test('author can add unrelated states inputs views and behavior entirely through source', async () => { + const runtime = await fresh(); + runtime.apply(countSource); + runtime.down('add'); runtime.up('add'); + assert.equal(runtime.snapshot().states.total, 6); + const added = `${countSource}\nstate radius = 12 +input inflate = KeyI +circle bubble(240, 160, radius, "#123456") +on inflate.down { radius = radius + total } +on frame { radius = radius + 60 * dt } +`; + runtime.apply(added); + runtime.down('inflate'); runtime.up('inflate'); runtime.frame(2); + assert.deepEqual(runtime.snapshot().states, { total: 6, radius: 20 }); + assert.equal(runtime.snapshot().views.find(view => view.id === 'bubble').radius, 20); + assert.equal(runtime.snapshot().views.length, 3); +}); + +test('keyboard and pointer sources aggregate first down and last up; repeats are ignored', async () => { + const runtime = await fresh(); runtime.apply(countSource); + runtime.down('add', 'keyboard'); runtime.down('add', 'keyboard'); runtime.down('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 5); + runtime.up('add', 'unknown'); runtime.up('add', 'keyboard'); + assert.equal(runtime.snapshot().states.total, 5); + assert.deepEqual(runtime.snapshot().held, ['add']); + runtime.releaseAll(); runtime.up('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 6); + assert.deepEqual(runtime.snapshot().held, []); + assert.equal(runtime.snapshot().successfulEvents, 2); +}); + +test('held input rejects apply without changing applied source, metadata, or values', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); + const before = runtime.snapshot(); + assert.throws(() => runtime.apply('state changed = 9'), /놓은 뒤/); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + runtime.up('add'); runtime.apply('state changed = 9'); + assert.deepEqual(runtime.snapshot().states, { changed: 9 }); +}); + +test('invalid syntax, unknown references and capacity overflow preserve the previous program', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (const source of ['state broken =', 'state n = 0\non frame { n = missing }', + Array.from({ length: 33 }, (_, i) => `state v${i} = ${i}`).join('\n'), '#'.repeat(32769)]) { + assert.throws(() => runtime.apply(source)); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + } +}); + +test('state preservation follows names across reorder/add/remove; reset uses new initializers', async () => { + const runtime = await fresh(); + runtime.apply('state x = 11\nstate y = 22'); + runtime.apply('state z = 30\nstate y = 2\nstate x = 1'); + assert.deepEqual(runtime.snapshot().states, { z: 30, y: 22, x: 11 }); + runtime.apply('state y = 200\nstate renamed = 9'); + assert.deepEqual(runtime.snapshot().states, { y: 22, renamed: 9 }); + runtime.reset(); + assert.deepEqual(runtime.snapshot().states, { y: 200, renamed: 9 }); + assert.equal(runtime.snapshot().revision, 3); +}); + +test('callers cannot mutate active metadata and corrupt name-based state preservation', async () => { + const runtime = await fresh(); const program = runtime.apply('state x = 11\nstate y = 22'); + assert.throws(() => program.states.reverse(), TypeError); + assert.throws(() => { runtime.currentProgram.states[0].name = 'y'; }, TypeError); + runtime.apply('state x = 0\nstate y = 0'); + assert.deepEqual(runtime.snapshot().states, { x: 11, y: 22 }); +}); + +test('conditions execute once, and assignments and multiple handlers follow source order', async () => { + const runtime = await fresh(); + runtime.apply(`state x = 0 +state seen = 0 +on frame { + if x == 0 { + x = 1 + seen = x + 1 + } + x = x + 3 +} +on frame { seen = seen + x } +`); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 4, seen: 6 }); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 7, seen: 13 }); +}); + +test('a failed event rolls back all handlers and keeps its source diagnostic through release', async () => { + const runtime = await fresh(); + const source = `state x = 7 +state y = 9 +input breakit = Space +on breakit.down { x = 99 } +on breakit.down { + y = 2 + x = 1 / 0 +} +`; + runtime.apply(source); + assert.equal(runtime.down('breakit'), 4); + assert.deepEqual(runtime.snapshot().states, { x: 7, y: 9 }); + assert.equal(runtime.snapshot().successfulEvents, 0); + assert.equal(runtime.snapshot().errorLine, 7); + runtime.up('breakit'); runtime.frame(); + assert.equal(runtime.snapshot().lastEvent.status, 0); + assert.equal(runtime.snapshot().lastFailure.phase, 'down'); + assert.equal(runtime.snapshot().lastError, 4); + assert.equal(runtime.snapshot().errorLine, 7); + assert.throws(() => runtime.apply('bad')); + assert.equal(runtime.snapshot().lastError, 4); + runtime.apply(source.replace('1 / 0', '10')); + assert.equal(runtime.snapshot().lastError, 0); + runtime.down('breakit'); runtime.up('breakit'); + assert.deepEqual(runtime.snapshot().states, { x: 10, y: 2 }); +}); + +test('reset clears held inputs and latched errors; invalid frame counts have no effects', async () => { + const runtime = await fresh(); + runtime.apply('state x = 5\ninput tap = Space\non tap.down { x = 1 / 0 }'); + runtime.down('tap'); runtime.reset(); + const before = runtime.snapshot(); + assert.equal(before.lastError, 0); assert.equal(before.lastFailure, null); + assert.deepEqual(before.held, []); + for (const count of [-1, 601, 0.5, NaN, Infinity]) assert.throws(() => runtime.frame(count)); + assert.deepEqual(runtime.snapshot(), before); +}); + +test('render evaluation is pure and invalid binding is distinct from an event rollback', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (let i = 0; i < 100; i++) assert.deepEqual(runtime.snapshot(), before); + runtime.apply('state n = 2\ncircle bad(20, 20, -n, "#123456")\ntext fine(0, 0, n)'); + const result = runtime.snapshot(); + assert.equal(result.lastError, 100); assert.equal(result.errorLine, 2); + assert.deepEqual(result.states, { n: 2 }); assert.equal(result.views.length, 1); + assert.equal(result.lastFailure, null); + assert.deepEqual(runtime.snapshot(), result); +}); + +test('independent adapters replay the same author program without sharing live state', async () => { + const left = await fresh(), right = await fresh(); + left.apply(countSource); right.apply(countSource); + for (const runtime of [left, right]) { + runtime.down('add', 'keyboard'); runtime.frame(3); runtime.up('add', 'keyboard'); + } + assert.deepEqual(left.snapshot(), right.snapshot()); + left.down('add'); + assert.equal(right.snapshot().states.total, 6); + assert.equal(left.snapshot().states.total, 8); +}); + +test('Wasm has no imports and reports fixed memory and exact active IR/state storage', async () => { + const module = await WebAssembly.compile(wasm); + assert.deepEqual(WebAssembly.Module.imports(module), []); + const runtime = await fresh(); const program = runtime.apply(countSource); + const before = runtime.snapshot().memory; + assert.equal(before.linearBytes, 65536); + assert.equal(before.instructionBytes, 16); assert.equal(before.handlerBytes, 16); + assert.equal(before.activeIRBytes, program.instructions.length * 16 + program.handlers.length * 16); + assert.equal(before.activeStateBytes, program.states.length * 8); + assert(before.stagingReservedBytes < before.reservedEngineBytes); + assert(before.reservedEngineBytes < before.linearBytes); + for (let i = 0; i < 20; i++) runtime.frame(600); + assert.deepEqual(runtime.snapshot().memory, before); +}); diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/app.js b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/app.js new file mode 100644 index 0000000..e163ebc --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/app.js @@ -0,0 +1,403 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; +import { bindSceneInput } from './scene-input.mjs'; +import { parseProject, MAX_PROJECT_BYTES } from './project.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +let draftGeneration = 0; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); +const sceneInput = bindSceneInput($('scene'), () => runtime, safeAction); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +// DOM is the published value. Avoid rewriting unchanged diagnostics/controls. +function text(id, value) { + const node = $(id); + if (node.textContent !== value) node.textContent = value; +} +function pressed(node, value) { + const next = String(value); + if (node.getAttribute('aria-pressed') !== next) node.setAttribute('aria-pressed', next); +} +function draftStatus() { + const changed = dirty(); + text('draft-status', changed ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'); + $('draft-status').classList.toggle('dirty', changed); +} + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + text('cursor-position', `${lines.length}행 · ${lines.at(-1).length + 1}열`); +} + +function draftChanged() { + draftGeneration++; + // Read scroll position before writes; this path runs only for source edits. + const scrollTop = source.scrollTop; + const count = source.value.split('\n').length; + const size = bytes(source.value); + text('line-numbers', Array.from({ length: count }, (_, i) => i + 1).join('\n')); + $('line-numbers').scrollTop = scrollTop; + text('source-size', `${size.toLocaleString('en-US')} / 32,768 bytes`); + $('source-size').style.color = size > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + draftStatus(); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + text('play', '▶ 실행'); + pressed($('play'), false); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + sceneInput.releaseCaptures(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + pressed(button, held.has(name)); + } + text('scene-status', runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'); + text('clock-status', `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`); + text('revision', `revision ${snapshot.revision || 0}`); + for (const id of ['play', 'step', 'reset', 'download-layout']) { + if ($(id).disabled !== !runtime.currentProgram) $(id).disabled = !runtime.currentProgram; + } + const event = snapshot.lastEvent; + text('last-event', event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'); + if ($('runtime-error').hidden !== !snapshot.lastError) $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + let message = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + message += ` · ${snapshot.displayError.message}`; + text('runtime-error', message); + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + if (input.target) { + const note = document.createElement('p'); + note.className = 'empty-note'; + note.textContent = `${input.name} · 장면의 ${input.target}을 잡아 조작하세요.`; + children.push(note); + continue; + } + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + draftStatus(); + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime, message = '편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.') { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic(message); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('download-layout').addEventListener('click', () => { + try { + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 저장하세요.'); + const project = runtime.exportDocument(source.value); + download('my-layout.pure.json', JSON.stringify(project, null, 2) + '\n', 'application/json', + '적용된 코드·현재 배치·미적용 초안을 함께 저장했습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_PROJECT_BYTES) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = draftGeneration; + const active = runtime?.currentProgram; + const events = runtime?.snapshot().successfulEvents; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + const project = parseProject(content); + if (project.version === 2) { + if (!runtime) throw new Error('실행기가 준비되지 않았습니다.'); + if (draftGeneration !== before || runtime.currentProgram !== active || runtime.snapshot().successfulEvents !== events) + throw new Error('파일을 읽는 동안 작업이 바뀌었습니다. 정지한 뒤 다시 불러오세요.'); + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 복원하세요.'); + if ((source.value || appliedSource) && !window.confirm('현재 코드와 배치를 파일의 내용으로 복원할까요? 저장하지 않은 작업은 사라집니다.')) return; + const restored = runtime.restoreDocument(project); + pause(); droppedTicks = 0; + appliedSource = restored.source; + rebuildInputControls(); + replaceDraft(restored.draft, '배치를 복원했습니다. 미적용 초안도 보존했고, 시간은 정지 상태입니다.'); + refresh(); + return; + } + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (draftGeneration !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/compiler.mjs new file mode 100644 index 0000000..7e8bff3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/compiler.mjs @@ -0,0 +1,447 @@ +// 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', '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 === 'pointer') { + 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'].includes(edge.value)) fail(edge, 'Expected down or up, move or cancel'); + } + if (this.is('name', 'use')) { + this.take(); const rule = this.expect('name', 'rule name'); this.expect('('); + const item = this.expect('name', 'item name'); this.expect(')'); + declarations.push({kind:'handler',token,event,edge,rule,item}); + } 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: [], 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.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}`); + } + 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; + 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_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; 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 body = {start,length,point}; bodies.set(rule.name.value,body); return body; + } + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + pointHandler = 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); + if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input'); + event = 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), item = 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; + const body = ruleBody(rule); + 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: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/workbench/results/2026-09-23-shared-rules/sources/web/core.wasm b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/core.wasm new file mode 100644 index 0000000..8c7b1c5 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/index.html b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/index.html new file mode 100644 index 0000000..5520163 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/index.html @@ -0,0 +1,94 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

직접 조작
input drag = pointer(tile)로 도형에 입력을 연결합니다. on drag.down / .move / .up / .cancel에서 event_x, event_y는 장면 좌표입니다. 이동 방식과 취소 시 동작도 코드로 정합니다. 초점이나 포인터 캡처를 잃으면 .cancel이 전달됩니다.

값과 규칙 재사용
record Tile { x=0; y=0 }로 필드를 정하고 item green = Tile(x=80)으로 대상을 만듭니다. rule move(t: Tile) { t.x=event_x }를 on drag.move use move(green)으로 연결합니다. 화면에서는 green.x를 읽습니다. 규칙은 연결한 대상의 값을 직접 바꿉니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/project.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/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/workbench/results/2026-09-23-shared-rules/sources/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/runtime.mjs new file mode 100644 index 0000000..4a9cd14 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/runtime.mjs @@ -0,0 +1,328 @@ +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') + 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 = []; + // 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) { + const status = point ? core.dispatch_point(event, 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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), + }, + }; + } + + 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 [index, handler] of next.handlers.entries()) { + checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + if (handler.parameterCount) + checked(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; + } + 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].target) 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].target) 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); + }, + point(name, phase, x, y, source = 'external') { + available(); + const index = inputIndex(name); + if (!program.inputs[index].target) throw new Error('포인터에 연결된 입력이 아닙니다.'); + 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(name); + if (phase === 'down') { + if (point?.source === source) return 0; + if (point) throw new Error('이 입력은 다른 포인터가 사용 중입니다.'); + point = {source, x, y}; + points.set(name, point); + held.set(name, new Set([source])); + } else { + if (!point || point.source !== source) return 0; + point.x = x; point.y = y; + if (phase === 'up' || phase === 'cancel') { points.delete(name); held.delete(name); } + } + 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 [name, point] of [...points]) { + const status = api.point(name, 'cancel', point.x, point.y, point.source); + 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/workbench/results/2026-09-23-shared-rules/sources/web/scene-input.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/scene-input.mjs new file mode 100644 index 0000000..0f7cbef --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/web/scene-input.mjs @@ -0,0 +1,83 @@ +// This adapter owns browser capture only. Authored state changes stay in the VM. +export function bindSceneInput(scene, runtime, act) { + const captures = new Map(); + const events = scene.ownerDocument.defaultView; + const coordinates = event => { + const matrix = scene.getScreenCTM(); + if (!matrix) return null; + try { + const p = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse()); + return Number.isFinite(p.x) && Number.isFinite(p.y) ? p : null; + } catch { return null; } + }; + const release = id => { + if (scene.hasPointerCapture(id)) scene.releasePointerCapture(id); + }; + function finish(event, phase, pointerId = event.pointerId) { + const entry = captures.get(pointerId); + if (!entry) return; + event.preventDefault(); + // Capture loss/cancel may not carry a meaningful position. Use the last + // delivered coordinates. Up must use its own final position. + const p = phase === 'up' ? coordinates(event) : null; + if (p) { entry.x = p.x; entry.y = p.y; } + if (phase === 'up' && !p) phase = 'cancel'; + captures.delete(pointerId); + release(pointerId); + act(() => runtime().point(entry.name, phase, entry.x, entry.y, entry.source)); + } + scene.addEventListener('pointerdown', event => { + if (event.button !== 0 || !runtime() || captures.has(event.pointerId)) return; + const target = event.target.closest?.('[data-point-input]'); + if (!target || !scene.contains(target)) return; + const name = target.getAttribute('data-point-input'); + // A logical input has one coordinate owner. A second contact on it must not + // steal the gesture; other authored targets can be manipulated independently. + if ([...captures.values()].some(entry => entry.name === name)) return; + const p = coordinates(event); + if (!p) return; + event.preventDefault(); scene.focus(); + act(() => { + scene.setPointerCapture(event.pointerId); + const entry = {name, source:`scene:${event.pointerId}`, pointerType:event.pointerType, x:p.x, y:p.y}; + captures.set(event.pointerId, entry); + try { runtime().point(name, 'down', p.x, p.y, entry.source); } + catch (error) { captures.delete(event.pointerId); release(event.pointerId); throw error; } + }); + }); + scene.addEventListener('pointermove', event => { + const entry = captures.get(event.pointerId); + if (!entry) return; + const p = coordinates(event); + if (!p) { finish(event, 'cancel'); return; } + event.preventDefault(); + entry.x = p.x; entry.y = p.y; + act(() => runtime().point(entry.name, 'move', p.x, p.y, entry.source)); + }); + // A host may deliver release outside the SVG even while it owns capture. + // Observe termination before target handlers or capture cleanup can cancel it. + events.addEventListener('pointerup', event => finish(event, 'up'), true); + events.addEventListener('pointercancel', event => finish(event, 'cancel'), true); + events.addEventListener('mouseup', event => { + if (event.button !== 0) return; + // Mouse-only fallback when the host delivers mouseup without pointerup. + // A normal pointerup already removed the owner; touch compatibility events + // must never finish a different contact. + for (const [id, entry] of captures) { + if (entry.pointerType === 'mouse') finish(event, 'up', id); + } + }, true); + scene.addEventListener('lostpointercapture', event => { + // Descendant capture transitions bubble here but do not release our owner. + if (event.target === scene) finish(event, 'cancel'); + }); + return Object.freeze({ + releaseCaptures() { + const ids = [...captures.keys()]; + captures.clear(); + for (const id of ids) release(id); + // The caller subsequently invokes runtime.releaseAll(), which dispatches + // each pending cancel once. Clearing first makes lostcapture harmless. + }, + }); +} diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/sources/workspace.test.mjs b/prototypes/workbench/results/2026-09-23-shared-rules/sources/workspace.test.mjs new file mode 100644 index 0000000..0fcbeae --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/sources/workspace.test.mjs @@ -0,0 +1,89 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = factor => `state x = 10 +input next = KeyN +text label(0, 0, x * ${factor}) +on next.down { x = x + 0.25 } +`; + +function begin(core, program, preserve) { + assert.equal(core.begin_program(program.states.length, program.inputs.length), 0); + program.states.forEach((state, i) => assert.equal(core.put_state(i, state.initial, preserve ? i : -1), 0)); +} + +function finish(core, program, during = () => {}) { + for (const op of program.instructions) { + assert.equal(core.put_instruction(op.op, op.arg, op.value), 0); + during(); + } + for (const handler of program.handlers) + assert.equal(core.put_handler(handler.event, handler.start, handler.length), 0); + assert.equal(core.commit_program(), 0); +} + +function observe(core, binding) { + core.begin_evaluation(100); + const value = core.eval_bounded(binding.start, binding.length); + assert.equal(core.evaluation_info(0), 0); + return value; +} + +test('Wasm executes old events and output during staged replacement without corrupting either phase', async () => { + const {instance:{exports:core}} = await WebAssembly.instantiate(wasm); + core.reset_all(); + const old = compile(source(2)), next = compile(source(3)); + begin(core, old, false); finish(core, old); + assert.equal(core.configure_evaluation(32, 8), 0); + const binding = old.views[0].bindings.value; + assert.equal(observe(core, binding), 20); + assert.equal(observe(core, binding), 20); + assert.equal(core.evaluation_info(3), 1); + begin(core, next, true); + let value = 10; + finish(core, next, () => { + assert.equal(core.evaluation_info(9), 0); + assert.equal(core.dispatch(1, 0), 0); value += 0.25; + assert.equal(core.configure_evaluation(32, 8), 0); + core.forget_evaluation(3); + assert.equal(observe(core, binding), value * 2); + assert.equal(core.evaluation_info(3), 0); + }); + assert.equal(core.evaluation_info(9), 1); + assert.equal(core.get_state(0), value); + const replaced = next.views[0].bindings.value; + assert.equal(observe(core, replaced), value * 3); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.evaluation_info(3), 1); + + const revision = core.info(4); + begin(core, old, true); + assert.equal(core.put_instruction(0, 0, Infinity), 5); + assert.equal(core.commit_program(), 5); + assert.equal(observe(core, replaced), value * 3); + core.abort_program(); + assert.equal(core.info(4), revision); + assert.equal(core.get_state(0), value); + assert.equal(core.evaluation_info(9), 1); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.memory.buffer.byteLength, 65536); +}); + +test('workspace accounting reports shared payload and safety tag without double-counting', async () => { + const runtime = await createRuntime(wasm, {cacheEntries:32}); + runtime.apply(source(2)); runtime.snapshot(); + const stats = runtime.evaluationStats(), memory = runtime.snapshot().memory; + assert.equal(stats.dedicatedBytes, 48); + assert.equal(stats.sharedBytes, 1152); + assert.equal(stats.workspaceTagBytes, 8); + assert.equal(stats.reservedBytes, stats.dedicatedBytes + stats.sharedBytes); + const restoreStorage = memory.stagingReservedBytes - memory.programReservedBytes; + assert.equal(restoreStorage, 264); // 32 values + mask/alignment, only in pending storage. + assert.equal(memory.reservedEngineBytes, 19072 + stats.dedicatedBytes + stats.workspaceTagBytes + restoreStorage + 2 * 64 * (memory.handlerBytes - 12)); + assert(stats.sharedBytes < memory.stagingReservedBytes); + assert.equal(stats.reuseAvailable, true); +}); diff --git a/prototypes/workbench/results/2026-09-23-shared-rules/verification.json b/prototypes/workbench/results/2026-09-23-shared-rules/verification.json new file mode 100644 index 0000000..0768cc3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-shared-rules/verification.json @@ -0,0 +1,62 @@ +[ + { + "file": "prototypes/workbench/baseline.test.mjs", + "exitCode": 0, + "stdout": "\u2714 reference begins with the authored states and matching scene (8.045605ms)\n\u2714 press, hold, release, and frame order produce exact expected values (0.643315ms)\n\u2714 charge clamps at one and explicit floor restores rest (1.415832ms)\n\u2714 repeat and concurrent physical sources produce only logical edges (0.440981ms)\n\u2714 airborne presses cannot charge or cause another jump (0.296037ms)\n\u2714 releaseAll emits release and reset clears latches without a jump (0.460666ms)\n\u2714 unknown inputs and invalid frame counts reject without mutation (0.833592ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 104.998545\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/cli.test.mjs", + "exitCode": 0, + "stdout": "\u2714 terminal starts empty and produces the same charge trace as the shared runtime (132.23208ms)\n\u2714 invalid terminal commands do not alter state and fail piped execution (55.4384ms)\n\u2714 terminal replays authored pointer movement and cancellation through the shared runtime (72.766738ms)\n\u2714 file load preserves by name, rejected load preserves active work, and save never overwrites (60.705193ms)\n\u2714 terminal exposes arithmetic rollback and retains failure through matching release (57.574139ms)\n\u2714 terminal configures and discards evaluation records and recovers from a display limit (51.583977ms)\n\u2714 terminal saves and restores placement independently of reset and preserves a browser draft (214.094236ms)\n\u2714 invalid restoration leaves terminal code and state unchanged (64.081708ms)\n\u2139 tests 8\n\u2139 suites 0\n\u2139 pass 8\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 791.095627\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/compiler.test.mjs", + "exitCode": 0, + "stdout": "\u2714 a blank document is an empty program with no implied behavior (4.437219ms)\n\u2714 forward references and separate state/input namespaces preserve declaration IDs (2.043572ms)\n\u2714 ordered assignments, repeated handlers, and condition evaluation once (0.933296ms)\n\u2714 nested branches use absolute forward targets and skip correct bodies (1.152407ms)\n\u2714 operator precedence, associativity, unary and builtin function lowering (2.408572ms)\n\u2714 multiline parentheses, scientific literals, comments, and string escapes (0.938277ms)\n\u2714 all instruction fields are numeric and carry original source line (0.394222ms)\n\u2714 contextual declaration words remain valid names outside reserved builtins (0.313389ms)\n\u2714 unknown and reserved symbols have useful one-based locations (1.743147ms)\n\u2714 duplicates, unsupported keys, initializers, and invalid syntax are rejected (1.464017ms)\n\u2714 document, declaration, instruction, and nesting limits reject overflow (6.69505ms)\n\u2714 every supported input maps to a bounded ordered event ID (1.028536ms)\n\u2714 optional examples compile and charge behavior is entirely authored (20.773687ms)\n\u2139 tests 13\n\u2139 suites 0\n\u2139 pass 13\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 120.580868\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/evaluation.test.mjs", + "exitCode": 0, + "stdout": "\u2714 cache reuse, unrelated changes and discarded records preserve authored values (20.981335ms)\n\u2714 committed changes only: failed events and failed edits preserve reusable output (4.493144ms)\n\u2714 program edits, state reordering and reset cannot reuse old definitions (3.96033ms)\n\u2714 finite float semantics include signed zero, high state bit and arithmetic errors (4.212293ms)\n\u2714 one budget covers the whole scene; failures publish no partial scene or state rollback (2.07635ms)\n\u2714 small caches, no history and every small work limit give exact results or explicit failure (28.417737ms)\n\u2714 invalid settings and forget names preserve policy and state (2.441665ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 151.059416\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/pointer.test.mjs", + "exitCode": 0, + "stdout": "\u2714 pointer declarations resolve forward targets and reject ambiguous or contextless inputs (9.304659ms)\n\u2714 authored drag keeps grab offset and commits the displayed position even if release coordinates differ (11.38525ms)\n\u2714 ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input (3.902664ms)\n\u2714 invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event (3.549423ms)\n\u2714 independent targets share the event pipeline and publication remains non-reentrant (3.036349ms)\n\u2714 failing down, up and cancel never prevent terminal ownership cleanup (3.723978ms)\n\u2139 tests 6\n\u2139 suites 0\n\u2139 pass 6\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 118.292424\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/presentation.test.mjs", + "exitCode": 0, + "stdout": "\u2714 direct publication matches archived snapshots and work counts through authored changes (58.271083ms)\n\u2714 whole-scene budget failure never publishes partial bindings and recovers exactly (8.306901ms)\n\u2714 latched event rollback, nonfinite bindings, reset and signed zero keep their semantics (3.417886ms)\n\u2714 publication rejects reentrant runtime changes and reads before they affect the scene (4.134738ms)\n\u2714 invalid or throwing output adapters do not strand the runtime or alter authored state (2.927572ms)\n\u2139 tests 5\n\u2139 suites 0\n\u2139 pass 5\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 170.584957\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/project.test.mjs", + "exitCode": 0, + "stdout": "\u2714 multiple authored placements and a separate invalid draft survive a fresh runtime restore (35.665731ms)\n\u2714 malformed files reject before changing active code, values, diagnostics or warm display records (12.678175ms)\n\u2714 restoration matches names rather than ordering and never reuses stale cached values (4.795756ms)\n\u2714 finite float values including negative zero round trip without changing authored reset values (3.274775ms)\n\u2714 held and failed interactions cannot be saved and restoration cannot steal an active input (12.313916ms)\n\u2714 empty and full documents restore without imposing a new source size or state count (4.861367ms)\n\u2714 document APIs respect the output callback boundary and export independent values (9.588011ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 197.019695\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/rules.test.mjs", + "exitCode": 0, + "stdout": "\u2714 binding more objects shares one VM body instead of expanding its instructions (8.572475ms)\n\u2714 records and rules resolve forward names and reject invalid types fields and bindings (8.254919ms)\n\u2714 shared layout matches the existing authored layout across independent and simultaneous gestures (32.486364ms)\n\u2714 field names retain identity across schema and instance reorder, removal and restoration (6.508144ms)\n\u2714 bindings share one event transaction including global effects and multiple uses of one item (2.478183ms)\n\u2714 bound writes invalidate affected display caches and failed edits preserve active bindings (2.754313ms)\n\u2714 generated qualified names cannot make a valid exported document unreadable by its own loader (10.14301ms)\n\u2139 tests 7\n\u2139 suites 0\n\u2139 pass 7\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 159.341189\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/runtime.test.mjs", + "exitCode": 0, + "stdout": "\u2714 blank startup and applied empty source contain no application objects or behavior (12.167268ms)\n\u2714 author can add unrelated states inputs views and behavior entirely through source (5.777903ms)\n\u2714 keyboard and pointer sources aggregate first down and last up; repeats are ignored (2.04235ms)\n\u2714 held input rejects apply without changing applied source, metadata, or values (2.22159ms)\n\u2714 invalid syntax, unknown references and capacity overflow preserve the previous program (2.604239ms)\n\u2714 state preservation follows names across reorder/add/remove; reset uses new initializers (2.888331ms)\n\u2714 callers cannot mutate active metadata and corrupt name-based state preservation (1.489535ms)\n\u2714 conditions execute once, and assignments and multiple handlers follow source order (1.677591ms)\n\u2714 a failed event rolls back all handlers and keeps its source diagnostic through release (1.809258ms)\n\u2714 reset clears held inputs and latched errors; invalid frame counts have no effects (2.023887ms)\n\u2714 render evaluation is pure and invalid binding is distinct from an event rollback (5.592051ms)\n\u2714 independent adapters replay the same author program without sharing live state (3.838849ms)\n\u2714 Wasm has no imports and reports fixed memory and exact active IR/state storage (7.566272ms)\n\u2139 tests 13\n\u2139 suites 0\n\u2139 pass 13\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 129.686452\n", + "stderr": "" + }, + { + "file": "prototypes/workbench/workspace.test.mjs", + "exitCode": 0, + "stdout": "\u2714 Wasm executes old events and output during staged replacement without corrupting either phase (10.164529ms)\n\u2714 workspace accounting reports shared payload and safety tag without double-counting (3.272442ms)\n\u2139 tests 2\n\u2139 suites 0\n\u2139 pass 2\n\u2139 fail 0\n\u2139 cancelled 0\n\u2139 skipped 0\n\u2139 todo 0\n\u2139 duration_ms 92.159223\n", + "stderr": "" + } +] diff --git a/prototypes/workbench/results/2026-09-23-web-layout/README.md b/prototypes/workbench/results/2026-09-23-web-layout/README.md new file mode 100644 index 0000000..a64f2f1 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/README.md @@ -0,0 +1,47 @@ +# Native web layout boundary — 2026-09-23 + +Functional and ownership evidence, not a performance or total-memory benchmark. + +- `build.json` / `native-build.log`: pinned Zig 0.15.2, 37 native tests, source/Wasm hashes. +- `node-tests.log`: final 87 tests (compiler, runtime, numeric/pointer routes, CLI, + persistence, display evaluation/publication and previous examples). +- `browser.json`: 32 document-workspace checks, with source/preview text and counters. +- `authoring.json`: 44 legacy authoring-flow checks. +- `value-control.json`: 3 numeric-control checks in the original workbench UI. +- `guards.json`: 6 focused checks including fixture validation, rejected out-of-range + output, arithmetic rollback/control reconciliation, active-gesture apply refusal, + and oversized-file rejection. +- `pure-document-saved.txt`: actual browser download, byte/text checked against the + UTF-8 LF fixture in `browser.json`, also reopened in a fresh page. +- `pure-document-{desktop,mobile}.png`: final screenshots, visually inspected. +- `sources/`: implementation and test snapshots; `before/`: previous core/host files. +- `verification.json`, `manifest.json`: bounded result counts and artifact hashes. + +The 32-case document run precedes the final range-control reconciliation on rejected +host output. The focused guard run tests that final correction and records the final +screenshots (also incorporating a small heading-wrap adjustment). The core/runtime +used by the final snapshots have matching build hashes. No timing comparison with +before/ is made. + +Reproduce from repository root: + +```sh +python3 prototypes/workbench/manage.py build --zig /path/to/zig-0.15.2 +node --test prototypes/workbench/*.test.mjs +python3 prototypes/workbench/manage.py serve --port 8766 +``` + +Open `/document.html` with Playwright CLI, and pass `qa-document.js` to `run-code`. +After completion read `window.__pureDocumentEvidence`. `qa-document-guards.js` +exposes `__pureDocumentGuards`. The original workbench uses `qa-authoring.js` and +`qa-value-control.js` here, with `__pureWorkbenchQAEvidence` / `__pureValueEvidence`. +Dialogs/downloads can make the CLI return before the async function finishes; +read completion evidence rather than treating a CLI return as success. + +Counters prove which application callbacks run, not whether a browser internally +performs layout or allocates memory. Native editor and preview both retain text; +there is no additional persistent JS/VM document model. Per-edit whole-preview text +publication remains. No physical mobile, OS IME, screen-reader, macOS/Metal or +large-document performance result is claimed. Composition checks use synthetic +composition events, touch uses Chromium CDP, other mouse/keyboard flows use actual +browser input. File exchange is UTF-8 text with native textarea newline normalization. diff --git a/prototypes/workbench/results/2026-09-23-web-layout/authoring-result.log b/prototypes/workbench/results/2026-09-23-web-layout/authoring-result.log new file mode 100644 index 0000000..efc3322 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/authoring-result.log @@ -0,0 +1,13 @@ +### Result +{"recordedUtc":"2026-09-23T09:00:17.827Z","browser":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36","checks":["initial editor has literally empty source","initial scene has no built-in entities, states, inputs, or running clock","controls require an authored program","authoring a new program is a draft until Apply","new custom program constructs states and multiple views from source","input buttons are generated from the authored declarations","view bindings render the authored state value","Space and arrows in the editor never reach authored inputs","pointer down dispatches once while the clock is paused","pointer up releases without double dispatch from click","keyboard repeat is ignored","keyboard activation on a generated button is one complete tap","assistive click activation is one complete tap","keyboard and pointer sources share one logical input","releasing one source does not release the other source","last source releases the logical input","authored down/frame behavior uses the declared dt without built-in motion","keyup releases even after focus moves into the editor","focus loss sends release and pauses the clock","Play advances and Pause freezes the clock","invalid code preserves the previous executable program and states","compile error exposes a source-line diagnostic","diagnostic location moves the editor caret to the problem","apply preserves matching state names and initializes newly authored states","new input, entity, binding, and behavior work with source changes alone","changed behavior takes effect without resetting existing values","explicit Reset restores active initializers and keeps the draft","Apply is refused while an input is held","clear editor is explicit and does not clear the running scene before Apply","downloaded project restores the exact un-applied draft without execution","invalid JSON project schema cannot replace the draft","failed assistive down event rolls back and its error survives the following successful up","latched runtime failure is visible and labeled separately from the latest input","explicit Reset clears the failure record","applying empty source removes all authored states, inputs, and scene contents","explicit example loading only fills the editor and never auto-applies","authored scene executes in the actual fixed 64 KiB Wasm memory","cost inspector shows measured Wasm memory without claiming browser heap totals","390px mobile viewport has no horizontal overflow","generated mobile input buttons remain usable","ordinary-JavaScript comparison and equivalent Pure example are linked and served","missing Wasm shows an actionable load failure and disables execution controls","fresh load always returns to a blank authoring environment","no uncaught page errors during the browser scenarios"],"authoredSource":"# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n","authoredSnapshot":{"states":{"x":114,"moving":0,"count":2},"views":[{"id":"marker","type":"box","color":"#7ed9ae","x":114,"y":170,"width":48,"height":48},{"id":"reading","type":"text","prefix":"Signals: ","x":40,"y":70,"value":2}],"tick":12,"revision":5,"displayError":null,"lastError":0,"errorMessage":"","errorLine":null,"lastOps":2,"successfulEvents":18,"held":[],"lastEvent":{"name":"advance","phase":"up","source":"keyboard:ArrowRight","status":0,"line":null,"ops":2},"lastFailure":null,"memory":{"linearBytes":65536,"reservedEngineBytes":20064,"programReservedBytes":9728,"instructionBytes":16,"handlerBytes":16,"activeIRBytes":480,"activeStateBytes":24,"stagingReservedBytes":9992,"evalStackBytes":512,"pointerTargetBytes":3,"pointerTargets":0},"ready":true,"source":"# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n","appliedSource":"# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n","dirty":false,"running":false,"droppedTicks":0,"diagnostic":"코드를 적용했습니다. 같은 이름의 상태는 유지됩니다."},"mobile":{"viewport":390,"content":390},"downloaded":{"filename":"my-work.pure.json","path":"output/playwright/pure-document-authoring-draft.json","expectedSource":"# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n"},"errors":[],"screenshots":["output/playwright/pure-document-authoring-blank.png","output/playwright/pure-document-authoring-authored.png","output/playwright/pure-document-authoring-mobile.png"]} +### Ran Playwright code +```js +await (async page => await page.evaluate(() => window.__pureWorkbenchQAEvidence ?? {pending:true}))(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +- Page Title: Pure · 빈 작업물에서 시작하기 +### Events +- New console entries: .playwright-cli/console-2026-09-23T08-54-35-115Z.log#L2 +- Downloading file my-work.pure.json ... +- Downloaded file my-work.pure.json to ".playwright-cli/my-work-pure.json" diff --git a/prototypes/workbench/results/2026-09-23-web-layout/authoring-run.log b/prototypes/workbench/results/2026-09-23-web-layout/authoring-run.log new file mode 100644 index 0000000..cca9455 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/authoring-run.log @@ -0,0 +1,237 @@ +### Ran Playwright code +```js +await (// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-document-authoring-blank.png', 'output/playwright/pure-document-authoring-authored.png', 'output/playwright/pure-document-authoring-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.goto('http://127.0.0.1:8766/'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-document-authoring-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/document.html +### Modal state +- ["beforeunload" dialog with message ""]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-web-layout/authoring.json b/prototypes/workbench/results/2026-09-23-web-layout/authoring.json new file mode 100644 index 0000000..96d5c0a --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/authoring.json @@ -0,0 +1,130 @@ +{ + "recordedUtc": "2026-09-23T09:00:17.827Z", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "checks": [ + "initial editor has literally empty source", + "initial scene has no built-in entities, states, inputs, or running clock", + "controls require an authored program", + "authoring a new program is a draft until Apply", + "new custom program constructs states and multiple views from source", + "input buttons are generated from the authored declarations", + "view bindings render the authored state value", + "Space and arrows in the editor never reach authored inputs", + "pointer down dispatches once while the clock is paused", + "pointer up releases without double dispatch from click", + "keyboard repeat is ignored", + "keyboard activation on a generated button is one complete tap", + "assistive click activation is one complete tap", + "keyboard and pointer sources share one logical input", + "releasing one source does not release the other source", + "last source releases the logical input", + "authored down/frame behavior uses the declared dt without built-in motion", + "keyup releases even after focus moves into the editor", + "focus loss sends release and pauses the clock", + "Play advances and Pause freezes the clock", + "invalid code preserves the previous executable program and states", + "compile error exposes a source-line diagnostic", + "diagnostic location moves the editor caret to the problem", + "apply preserves matching state names and initializes newly authored states", + "new input, entity, binding, and behavior work with source changes alone", + "changed behavior takes effect without resetting existing values", + "explicit Reset restores active initializers and keeps the draft", + "Apply is refused while an input is held", + "clear editor is explicit and does not clear the running scene before Apply", + "downloaded project restores the exact un-applied draft without execution", + "invalid JSON project schema cannot replace the draft", + "failed assistive down event rolls back and its error survives the following successful up", + "latched runtime failure is visible and labeled separately from the latest input", + "explicit Reset clears the failure record", + "applying empty source removes all authored states, inputs, and scene contents", + "explicit example loading only fills the editor and never auto-applies", + "authored scene executes in the actual fixed 64 KiB Wasm memory", + "cost inspector shows measured Wasm memory without claiming browser heap totals", + "390px mobile viewport has no horizontal overflow", + "generated mobile input buttons remain usable", + "ordinary-JavaScript comparison and equivalent Pure example are linked and served", + "missing Wasm shows an actionable load failure and disables execution controls", + "fresh load always returns to a blank authoring environment", + "no uncaught page errors during the browser scenarios" + ], + "authoredSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "authoredSnapshot": { + "states": { + "x": 114, + "moving": 0, + "count": 2 + }, + "views": [ + { + "id": "marker", + "type": "box", + "color": "#7ed9ae", + "x": 114, + "y": 170, + "width": 48, + "height": 48 + }, + { + "id": "reading", + "type": "text", + "prefix": "Signals: ", + "x": 40, + "y": 70, + "value": 2 + } + ], + "tick": 12, + "revision": 5, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 2, + "successfulEvents": 18, + "held": [], + "lastEvent": { + "name": "advance", + "phase": "up", + "source": "keyboard:ArrowRight", + "status": 0, + "line": null, + "ops": 2 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20064, + "programReservedBytes": 9728, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 480, + "activeStateBytes": 24, + "stagingReservedBytes": 9992, + "evalStackBytes": 512, + "pointerTargetBytes": 3, + "pointerTargets": 0 + }, + "ready": true, + "source": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "appliedSource": "# A new authored interaction, not a bundled example.\nstate x = 90\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 1 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "mobile": { + "viewport": 390, + "content": 390 + }, + "downloaded": { + "filename": "my-work.pure.json", + "path": "output/playwright/pure-document-authoring-draft.json", + "expectedSource": "# A new authored interaction, not a bundled example.\nstate x = 500\nstate moving = 0\nstate count = 0\ninput advance = ArrowRight\ninput pulse = Space\nbox marker(x, 170, 48, 48, \"#7ed9ae\")\ntext reading(40, 70, count, \"Signals: \")\non advance.down { moving = 1 }\non advance.up { moving = 0 }\non pulse.down { count = count + 5 }\non frame { x = clamp(x + moving * 120 * dt, 0, 580) }\nstate bonus = 7\ninput reverse = KeyA\ncircle beacon(300, 190, 15 + bonus, \"#c9f08b\")\non reverse.down { bonus = bonus + 2 }\n# This un-applied draft must survive Reset.\n" + }, + "errors": [], + "screenshots": [ + "output/playwright/pure-document-authoring-blank.png", + "output/playwright/pure-document-authoring-authored.png", + "output/playwright/pure-document-authoring-mobile.png" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/before/compiler.mjs b/prototypes/workbench/results/2026-09-23-web-layout/before/compiler.mjs new file mode 100644 index 0000000..bfe671b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/before/compiler.mjs @@ -0,0 +1,484 @@ +// 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', '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 === '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'].includes(edge.value)) fail(edge, 'Expected down or up, move or cancel'); + } + 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.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; + 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_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; 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 body = {start,length,point}; bodies.set(rule.name.value,body); return body; + } + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + pointHandler = 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); + if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input'); + event = 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; + const body = ruleBody(rule); + 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/workbench/results/2026-09-23-web-layout/before/core.wasm b/prototypes/workbench/results/2026-09-23-web-layout/before/core.wasm new file mode 100755 index 0000000..117f25d Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-web-layout/before/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-web-layout/before/core.zig b/prototypes/workbench/results/2026-09-23-web-layout/before/core.zig new file mode 100644 index 0000000..a68f483 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/before/core.zig @@ -0,0 +1,1398 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const max_pointer_targets = 24; +const event_target = 255; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { + event: u32 = 0, start: u32 = 0, length: u32 = 0, + parameter_base: u8 = 0, parameter_count: u8 = 0, +}; +const PointerTarget = struct { input: u8, base: u8, count: u8 }; +const Program = struct { + pointer_targets: [max_pointer_targets]PointerTarget = undefined, + pointer_target_count: u8 = 0, + routed_inputs: u8 = 0, + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +const Workspace = union { staging: Pending, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.program.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.program.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.program.initials[id] = initial; + engine.workspace.staging.program.old_ids[id] = old_id; + engine.workspace.staging.program.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 25) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +export fn bind_handler(handler: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; +} + +// A route is a static program-local index, never a persistent object identity. +export fn put_pointer_target(input: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.pointer_target_count == max_pointer_targets) return stagingFail(.capacity); + if (input >= p.input_count) return stagingFail(.bad_event); + if (base >= p.state_count or count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (base < @as(u32, t.base) + t.count and t.base < base + count) return stagingFail(.invalid); + if (t.input == input and t.count != count) return stagingFail(.bad_state); + } + p.pointer_targets[p.pointer_target_count] = .{ .input = @intCast(input), .base = @intCast(base), .count = @intCast(count) }; + p.pointer_target_count += 1; + p.routed_inputs |= @as(u8, 1) << @as(u3, @intCast(input)); + return 0; +} + +export fn bind_target_handler(handler: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or count == 0 or count > 8) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = event_target; + h.parameter_count = @intCast(count); + return 0; +} + +fn eventInput(event: u32) u32 { + return if (event >= 25) event - 25 else if (event >= 17) event - 17 else (event - 1) / 2; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_base == event_target) { + if (h.event == 0 or h.parameter_count == 0 or h.parameter_count > 8) return .bad_event; + var found = false; + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (t.input != eventInput(h.event)) continue; + if (t.count != h.parameter_count) return .bad_state; + found = true; + } + if (!found) return .bad_event; + } else if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 25) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; +} + +export fn put_restored_value(id: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const pending = &engine.workspace.staging; + if (id >= pending.program.state_count) return stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return stagingFail(.bad_state); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; +} + +export fn commit_program() u32 { + return commit(false); +} + +export fn commit_restored_program() u32 { + return commit(true); +} + +fn commit(restoring: bool) u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const count = engine.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (engine.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return stagingFail(.bad_state); + const code = validate(&engine.workspace.staging.program); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.program.state_count) |i| { + const old_id = engine.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) engine.workspace.staging.values[i] else if (old_id < 0) engine.workspace.staging.program.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging.program; + engine.values = next; + finishStaging(); + if (restoring) { + engine.tick = 0; + engine.successful_events = 0; + } + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false, target: ?u32 = null }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +export fn dispatch_target(event: u32, target: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true, .target = target }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + (context.point and event == 0) or (!context.point and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var route: ?PointerTarget = null; + const routed = event != 0 and engine.active.routed_inputs & (@as(u8, 1) << @as(u3, @intCast(eventInput(event)))) != 0; + if (context.target) |target| { + if (!routed or target >= engine.active.pointer_target_count or + engine.active.pointer_targets[target].input != eventInput(event)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + route = engine.active.pointer_targets[target]; + } else if (routed) { + engine.last_error = .bad_event; + return public(.bad_event); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = if (h.parameter_base == event_target) route.?.base else h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = execute(&pending, h.start, h.length, bound, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler) + @as(u32, engine.active.pointer_target_count) * @sizeOf(PointerTarget), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + 8 => @sizeOf(PointerTarget), + 9 => engine.active.pointer_target_count, + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 16), layout(3)); + try std.testing.expectEqual(@as(u32, 80), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "shared handler bodies access their own bound state slots in source order" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try emit(24, 0, 0); try emit(0, 0, 1); try emit(3, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(1, 1, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(2, 0, 1)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(f64, 21), get_state(1)); + try std.testing.expectEqual(@as(u32, 4), info(6)); // Three bindings, one body. + try std.testing.expectEqual(@as(u32, 12), info(3)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); +} + +test "failure in a later shared binding rolls back all earlier bindings" { + reset_all(); + try ok(begin_program(4, 1)); + for ([_]f64{ 8, 2, 12, 0 }, 0..) |value, i| try ok(put_state(@intCast(i), value, -1)); + try emit(24, 0, 0); try emit(24, 1, 0); try emit(6, 0, 0); try emit(25, 0, 0); + for (0..2) |i| { + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(@intCast(i), @intCast(i * 2), 2)); + } + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(2)); + try std.testing.expectEqual(@as(u32, 0), info(9)); +} + +test "shared bindings reject missing arguments partial overlaps and invalid slot mappings" { + reset_all(); + try counterProgram(); + const revision = info(4); + for (0..5) |mode| { + try ok(begin_program(1, 1)); try ok(put_state(0, 9, -1)); + try emit(24, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 2)); + switch (mode) { + 0 => {}, // Parameter op without a binding. + 1 => { try ok(bind_handler(0, 0, 1)); try std.testing.expectEqual(public(.invalid), bind_handler(0, 0, 1)); }, + 2 => { try std.testing.expectEqual(public(.bad_state), bind_handler(0, 0, 2)); }, + 3 => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 1, 1)); try ok(bind_handler(1, 0, 1)); + }, + else => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 2)); // Another owner missing its mapping. + }, + } + try std.testing.expect(commit_program() != 0); abort_program(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + } +} + +test "one routed handler changes only its event target and rejects missing or foreign routes" { + reset_all(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try ok(put_pointer_target(0, 0, 1)); + try ok(put_pointer_target(0, 1, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try ok(commit_program()); + try ok(dispatch_target(17, 1, 77, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(3)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(17, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, 2, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, no_pc, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(18, 0, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(0, 0, 99, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_target(17, 0, std.math.inf(f64), 0)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 3), layout(8)); + try std.testing.expectEqual(@as(u32, 2), layout(9)); + try std.testing.expectEqual(@as(u32, 54), layout(4)); +} + +test "routes reject invalid mappings and bindings without replacing active work" { + for (0..7) |mode| { + reset_all(); + try counterProgram(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + switch (mode) { + 0 => try std.testing.expectEqual(public(.bad_state), put_pointer_target(0, 1, 2)), + 1 => try std.testing.expectEqual(public(.bad_event), put_pointer_target(2, 0, 1)), + 2 => { + try ok(put_pointer_target(0, 0, 2)); + try std.testing.expectEqual(public(.invalid), put_pointer_target(1, 1, 1)); + }, + else => { + if (mode != 3) try ok(put_pointer_target(0, 0, 1)); + try emit(0, 0, 2); + try emit(25, 0, 0); + try ok(put_handler(if (mode == 4) 0 else 1, 0, 2)); + try ok(bind_target_handler(0, if (mode == 5) 2 else 1)); + if (mode == 6) try std.testing.expectEqual(public(.invalid), bind_target_handler(0, 1)); + }, + } + try std.testing.expect(commit_program() != 0); + // Active program still executes while the failed candidate occupies staging. + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 0), layout(9)); + } + reset_all(); + try ok(begin_program(32, 1)); + for (0..32) |i| try ok(put_state(@intCast(i), 0, -1)); + for (0..24) |i| try ok(put_pointer_target(0, @intCast(i), 1)); + try std.testing.expectEqual(public(.capacity), put_pointer_target(0, 24, 1)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "routed event rollback includes globals and earlier handlers and survives staged route replacement" { + reset_all(); + try ok(begin_program(3, 1)); + for (0..3) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + try ok(put_pointer_target(0, 1, 1)); + try ok(put_pointer_target(0, 2, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try emit(0, 0, 1); + try emit(23, 0, 0); + try emit(6, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 2, 6)); + try ok(bind_target_handler(1, 1)); + try ok(commit_program()); + try ok(begin_program(1, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_pointer_target(0, 0, 1)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_target(17, 1, 88, 0)); + try std.testing.expectEqual(@as(f64, 0), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 2), get_state(2)); + abort_program(); + try ok(dispatch_target(17, 1, 88, 2)); + try std.testing.expectEqual(@as(f64, 99), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(2)); +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/before/runtime.mjs b/prototypes/workbench/results/2026-09-23-web-layout/before/runtime.mjs new file mode 100644 index 0000000..eb36839 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/before/runtime.mjs @@ -0,0 +1,339 @@ +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') + 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) { + const status = 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 (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + core.begin_evaluation(evaluationPolicy.workLimit); + 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].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].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); + }, + 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/workbench/results/2026-09-23-web-layout/browser-result.log b/prototypes/workbench/results/2026-09-23-web-layout/browser-result.log new file mode 100644 index 0000000..fbb03ff --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/browser-result.log @@ -0,0 +1,12 @@ +### Result +{"checks":["only the authored ratio occupies VM state; no document or geometry mirror","native editor and reading pane show the same Unicode document","document markup is displayed as text","resize reflows through CSS without VM events, document publication or application geometry reads","reflow preserves the native editing selection","long wrapped content uses the native reading scroll area","reading scroll does not move the editing pane","native scrolling does not dispatch layout data to the VM","a native range input uses the authored numeric-input policy","keyboard separator control publishes the same value and accessibility state","mouse release commits the last delivered split position","resizing panes never republishes unchanged document text","Escape restores the ratio captured at gesture start","a resize during capture invalidates geometry before the next move","a compatibility mouseup cannot release a touch owner","touch cancellation restores the original panel ratio","narrow layout cancels the active split gesture and stacks the panes","the narrow document workspace has no page-level horizontal overflow","editing Unicode updates the text projection","text editing neither sends document copies to the VM nor reads geometry","native editing undo survives the Pure integration","in-progress composition does not replace the reading projection","composition completion publishes the final text (synthetic event sequence)","a standard UTF-8 text file opens without a custom container","export keeps the standard text filename","the exported file reopens in a fresh page with exact text","a delayed file read cannot overwrite newer editing","invalid UTF-8 leaves the current document intact","changing the authored rule changes native control behavior without touching the HTML adapter","invalid policy edits preserve the executable policy and document","missing Wasm disables split controls while standard text editing and saving remain available","no uncaught page errors"],"errors":[],"fileText":"표준 UTF-8 파일\n외부 편집기에서 만든 내용\n😀
문자
\n","savedPath":"output/playwright/pure-document-saved.txt","final":{"ready":true,"stats":{"geometryReads":12,"previewWrites":8,"ratioWrites":14},"runtime":{"states":{"ratio":50},"views":[],"tick":0,"revision":3,"displayError":null,"lastError":0,"errorMessage":"","errorLine":null,"lastOps":0,"successfulEvents":16,"held":[],"lastEvent":{"name":"reset","phase":"up","source":"control","status":0,"line":null,"ops":0},"lastFailure":null,"memory":{"linearBytes":65536,"reservedEngineBytes":20064,"programReservedBytes":9728,"instructionBytes":16,"handlerBytes":16,"activeIRBytes":144,"activeStateBytes":8,"stagingReservedBytes":9992,"evalStackBytes":512,"pointerTargetBytes":3,"pointerTargets":0}},"dragging":false,"composing":false,"dirty":true,"generation":10},"browser":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36"} +### Ran Playwright code +```js +await (async page => await page.evaluate(() => window.__pureDocumentEvidence ?? {pending:true}))(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/document.html +- Page Title: Pure · 문서 작업 +### Events +- Downloading file external.txt ... +- Downloaded file external.txt to ".playwright-cli/external.txt" diff --git a/prototypes/workbench/results/2026-09-23-web-layout/browser-run.log b/prototypes/workbench/results/2026-09-23-web-layout/browser-run.log new file mode 100644 index 0000000..24e6494 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/browser-run.log @@ -0,0 +1,111 @@ +### Ran Playwright code +```js +await (// Playwright CLI run-code. Functional and ownership checks, not a timing benchmark. +async page => { + const checks=[],errors=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const onDialog=d=>d.accept(),onError=e=>errors.push(e.message);page.on('dialog',onDialog);page.on('pageerror',onError); + const state=()=>page.evaluate(()=>window.pureDocument.snapshot()); + const settled=()=>page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); + const ratio=async expected=>{await page.waitForFunction(n=>Math.abs(window.pureDocument.snapshot().runtime.states.ratio-n)<0.2,expected);}; + const geometry=()=>page.evaluate(()=>{const p=document.querySelector('#panes').getBoundingClientRect(),h=document.querySelector('#divider').getBoundingClientRect();return{left:p.left,width:p.width,handle:h.width,x:h.left+h.width/2,y:h.top+h.height/2};}); + const target=(g,r)=>g.left+g.handle/2+(g.width-g.handle)*r/100; + const dragStart=async()=>{await page.locator('#divider').scrollIntoViewIfNeeded();const g=await geometry();await page.mouse.move(g.x,g.y);await page.mouse.down();return g;}; + const upload=async(content,name='notes.txt')=>page.locator('#file-input').evaluate((input,{content,name})=>{const d=new DataTransfer();d.items.add(new File([content],name,{type:'text/plain'}));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));},{content,name}); + try { + await page.setViewportSize({width:1440,height:1000});await page.reload(); + await page.waitForFunction(()=>window.pureDocument?.snapshot().ready); + check((await state()).runtime.memory.activeStateBytes===8,'only the authored ratio occupies VM state; no document or geometry mirror'); + await page.locator('#sample').click();await settled(); + const original=await page.locator('#document-text').inputValue(); + check(await page.locator('#reading-text').textContent()===original,'native editor and reading pane show the same Unicode document'); + check(await page.locator('#reading-text div').count()===0,'document markup is displayed as text'); + const beforeResize=await state(); + await page.locator('#document-text').evaluate(t=>{t.focus();t.setSelectionRange(20,25);t.scrollTop=80;}); + await page.setViewportSize({width:1040,height:900});await settled(); + const afterResize=await state(); + check(afterResize.runtime.successfulEvents===beforeResize.runtime.successfulEvents&&afterResize.stats.previewWrites===beforeResize.stats.previewWrites&&afterResize.stats.geometryReads===beforeResize.stats.geometryReads,'resize reflows through CSS without VM events, document publication or application geometry reads'); + check(await page.locator('#document-text').evaluate(t=>t.selectionStart===20&&t.selectionEnd===25),'reflow preserves the native editing selection'); + const scrolling=await page.evaluate(()=>{const t=document.querySelector('#document-text'),r=document.querySelector('#reading-scroll');r.scrollTop=120;return{editor:t.scrollTop,reading:r.scrollTop,scrollable:r.scrollHeight>r.clientHeight};}); + check(scrolling.scrollable&&scrolling.reading>0,'long wrapped content uses the native reading scroll area'); + await page.locator('#reading-scroll').evaluate(r=>r.scrollTop+=80);await settled(); + check(await page.locator('#document-text').evaluate((t,y)=>t.scrollTop===y,scrolling.editor),'reading scroll does not move the editing pane'); + check((await state()).runtime.successfulEvents===afterResize.runtime.successfulEvents,'native scrolling does not dispatch layout data to the VM'); + await page.locator('#split-range').focus();await page.keyboard.press('End');await ratio(80); + check((await state()).runtime.states.ratio===80,'a native range input uses the authored numeric-input policy'); + await page.locator('#divider').focus();await page.keyboard.press('Home');await ratio(20);await page.keyboard.press('Shift+ArrowRight');await ratio(30); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='30','keyboard separator control publishes the same value and accessibility state'); + await page.locator('#reset-split').click();await ratio(50); + const textWrites=(await state()).stats.previewWrites; + let g=await dragStart();await page.mouse.move(target(g,65),g.y);await ratio(65);await page.mouse.up(); + check(Math.abs((await state()).runtime.states.ratio-65)<0.2&&!(await state()).dragging,'mouse release commits the last delivered split position'); + check((await state()).stats.previewWrites===textWrites,'resizing panes never republishes unchanged document text'); + g=await dragStart();await page.mouse.move(target(g,35),g.y);await ratio(35);await page.keyboard.press('Escape');await page.mouse.up();await ratio(65); + check(!(await state()).dragging,'Escape restores the ratio captured at gesture start'); + g=await dragStart();await page.setViewportSize({width:1240,height:1000});await settled();g=await geometry(); + await page.mouse.move(target(g,45),g.y);await ratio(45);await page.mouse.up(); + check(Math.abs((await state()).runtime.states.ratio-45)<0.2,'a resize during capture invalidates geometry before the next move'); + // Capture a real touch; mouse compatibility termination must not release it. + const cdp=await page.context().newCDPSession(page); + try { + await cdp.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2});g=await geometry(); + await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{id:1,x:g.x,y:g.y}]}); + await cdp.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,x:target(g,70),y:g.y}]});await ratio(70); + await page.evaluate(()=>window.dispatchEvent(new MouseEvent('mouseup',{button:0,bubbles:true}))); + check((await state()).dragging,'a compatibility mouseup cannot release a touch owner'); + await cdp.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]});await ratio(45); + check(!(await state()).dragging,'touch cancellation restores the original panel ratio'); + } finally {await cdp.send('Emulation.setTouchEmulationEnabled',{enabled:false});await cdp.detach();} + g=await dragStart();await page.mouse.move(target(g,60),g.y);await ratio(60); + await page.setViewportSize({width:390,height:844});await settled();await page.mouse.up();await ratio(45); + check(await page.locator('#divider').isHidden()&&!(await state()).dragging,'narrow layout cancels the active split gesture and stacks the panes'); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'the narrow document workspace has no page-level horizontal overflow'); + const oldStats=await state(); + await page.locator('#document-text').fill('한글 문서 📝\n새 줄과 <태그>도 그대로.');await settled(); + check(await page.locator('#reading-text').textContent()==='한글 문서 📝\n새 줄과 <태그>도 그대로.','editing Unicode updates the text projection'); + check((await state()).runtime.successfulEvents===oldStats.runtime.successfulEvents&& (await state()).stats.geometryReads===oldStats.stats.geometryReads,'text editing neither sends document copies to the VM nor reads geometry'); + await page.locator('#document-text').focus();await page.keyboard.press('Control+End');await page.keyboard.insertText(' 추가');await settled();await page.keyboard.press('Control+z');await settled(); + check(await page.locator('#document-text').inputValue()==='한글 문서 📝\n새 줄과 <태그>도 그대로.','native editing undo survives the Pure integration'); + const compositionBefore=(await state()).stats.previewWrites; + await page.locator('#document-text').evaluate(t=>{t.dispatchEvent(new CompositionEvent('compositionstart'));t.value+=' 조합';t.dispatchEvent(new InputEvent('input',{bubbles:true,isComposing:true}));});await settled(); + check((await state()).stats.previewWrites===compositionBefore,'in-progress composition does not replace the reading projection'); + await page.locator('#document-text').evaluate(t=>t.dispatchEvent(new CompositionEvent('compositionend')));await settled(); + check((await page.locator('#reading-text').textContent()).endsWith(' 조합'),'composition completion publishes the final text (synthetic event sequence)'); + const fileText='표준 UTF-8 파일\n외부 편집기에서 만든 내용\n😀
문자
\n'; + await upload(fileText,'external.txt');await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('파일을 열었습니다'));await settled(); + check(await page.locator('#document-text').inputValue()===fileText,'a standard UTF-8 text file opens without a custom container'); + const download=page.waitForEvent('download');await page.locator('#save').click();const saved=await download;const savedPath='output/playwright/pure-document-saved.txt';await saved.saveAs(savedPath); + check(saved.suggestedFilename()==='external.txt','export keeps the standard text filename'); + const fresh=await page.context().newPage();await fresh.goto(page.url());await fresh.waitForFunction(()=>window.pureDocument?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(savedPath);await fresh.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('파일을 열었습니다')); + check(await fresh.locator('#document-text').inputValue()===fileText,'the exported file reopens in a fresh page with exact text');await fresh.close();await page.bringToFront(); + await page.locator('#file-input').evaluate(input=>{const f=new File(['late'],'late.txt');f.arrayBuffer=()=>new Promise(resolve=>window.finishDocumentRead=()=>resolve(new TextEncoder().encode('late').buffer));const d=new DataTransfer();d.items.add(f);Object.defineProperty(input,'files',{configurable:true,value:d.files});input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.locator('#document-text').fill('읽는 동안 새로 편집한 문서');await page.evaluate(()=>window.finishDocumentRead());await settled(); + check(await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서'&&(await page.locator('#document-status').textContent()).includes('읽는 동안'),'a delayed file read cannot overwrite newer editing'); + await page.locator('#file-input').evaluate(input=>{delete input.files;const d=new DataTransfer();d.items.add(new File([new Uint8Array([255])],'invalid.txt'));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('UTF-8 텍스트로 읽을 수'));check(await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서','invalid UTF-8 leaves the current document intact'); + await page.setViewportSize({width:1440,height:1000});await settled(); + await page.locator('.policy summary').click();const source=await page.locator('#policy-source').inputValue(); + await page.locator('#policy-source').fill(source.replace('20, 80','30, 70'));await page.locator('#apply-policy').click(); + await page.locator('#split-range').focus();await page.keyboard.press('End');await ratio(70); + check((await state()).runtime.states.ratio===70,'changing the authored rule changes native control behavior without touching the HTML adapter'); + const revision=(await state()).runtime.revision;await page.locator('#policy-source').fill('state broken =');await page.locator('#apply-policy').click(); + check((await state()).runtime.revision===revision&&await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서','invalid policy edits preserve the executable policy and document'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click();await page.locator('.policy summary').click(); + await page.locator('#sample').click();await settled();await page.locator('#reset-split').click(); + await page.screenshot({path:'output/playwright/pure-document-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844});await page.screenshot({path:'output/playwright/pure-document-mobile.png',fullPage:true}); + const fallback=await page.context().newPage();await fallback.route('**/core.wasm',r=>r.abort());await fallback.goto(page.url());await fallback.waitForFunction(()=>window.pureDocument); + await fallback.locator('#document-text').fill('실행기 없이도 보존되는 텍스트');await fallback.waitForFunction(()=>document.querySelector('#reading-text').textContent.includes('실행기 없이도')); + check(!await fallback.evaluate(()=>window.pureDocument.snapshot().ready)&&await fallback.locator('#split-range').isDisabled()&&await fallback.locator('#save').isEnabled(),'missing Wasm disables split controls while standard text editing and saving remain available');await fallback.close(); + check(errors.length===0,'no uncaught page errors'); + const evidence={checks,errors,fileText,savedPath,final:await state(),browser:await page.evaluate(()=>navigator.userAgent)}; + await page.evaluate(e=>window.__pureDocumentEvidence=e,evidence);return{checks:checks.length,errors}; + } catch(error){await page.evaluate(e=>window.__pureDocumentEvidence=e,{failed:String(error),checks,errors,state:await state()});throw error;} + finally{page.off('dialog',onDialog);page.off('pageerror',onError);} +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/document.html +### Modal state +- ["confirm" dialog with message "저장하지 않은 내용을 파일의 내용으로 바꿀까요?"]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-web-layout/browser.json b/prototypes/workbench/results/2026-09-23-web-layout/browser.json new file mode 100644 index 0000000..bb99277 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/browser.json @@ -0,0 +1,89 @@ +{ + "checks": [ + "only the authored ratio occupies VM state; no document or geometry mirror", + "native editor and reading pane show the same Unicode document", + "document markup is displayed as text", + "resize reflows through CSS without VM events, document publication or application geometry reads", + "reflow preserves the native editing selection", + "long wrapped content uses the native reading scroll area", + "reading scroll does not move the editing pane", + "native scrolling does not dispatch layout data to the VM", + "a native range input uses the authored numeric-input policy", + "keyboard separator control publishes the same value and accessibility state", + "mouse release commits the last delivered split position", + "resizing panes never republishes unchanged document text", + "Escape restores the ratio captured at gesture start", + "a resize during capture invalidates geometry before the next move", + "a compatibility mouseup cannot release a touch owner", + "touch cancellation restores the original panel ratio", + "narrow layout cancels the active split gesture and stacks the panes", + "the narrow document workspace has no page-level horizontal overflow", + "editing Unicode updates the text projection", + "text editing neither sends document copies to the VM nor reads geometry", + "native editing undo survives the Pure integration", + "in-progress composition does not replace the reading projection", + "composition completion publishes the final text (synthetic event sequence)", + "a standard UTF-8 text file opens without a custom container", + "export keeps the standard text filename", + "the exported file reopens in a fresh page with exact text", + "a delayed file read cannot overwrite newer editing", + "invalid UTF-8 leaves the current document intact", + "changing the authored rule changes native control behavior without touching the HTML adapter", + "invalid policy edits preserve the executable policy and document", + "missing Wasm disables split controls while standard text editing and saving remain available", + "no uncaught page errors" + ], + "errors": [], + "fileText": "표준 UTF-8 파일\n외부 편집기에서 만든 내용\n😀
문자
\n", + "savedPath": "output/playwright/pure-document-saved.txt", + "final": { + "ready": true, + "stats": { + "geometryReads": 12, + "previewWrites": 8, + "ratioWrites": 14 + }, + "runtime": { + "states": { + "ratio": 50 + }, + "views": [], + "tick": 0, + "revision": 3, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 16, + "held": [], + "lastEvent": { + "name": "reset", + "phase": "up", + "source": "control", + "status": 0, + "line": null, + "ops": 0 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 20064, + "programReservedBytes": 9728, + "instructionBytes": 16, + "handlerBytes": 16, + "activeIRBytes": 144, + "activeStateBytes": 8, + "stagingReservedBytes": 9992, + "evalStackBytes": 512, + "pointerTargetBytes": 3, + "pointerTargets": 0 + } + }, + "dragging": false, + "composing": false, + "dirty": true, + "generation": 10 + }, + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36" +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/build.json b/prototypes/workbench/results/2026-09-23-web-layout/build.json new file mode 100644 index 0000000..4167481 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/build.json @@ -0,0 +1,23 @@ +{ + "builtUtc": "2026-09-23T08:54:16.810753+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "60307704dbb5e11a75d14108fb7d87a5a4480359fdd771ec73beebf5f5c48b81", + "wasmSha256": "9f760e4a2ce6ea11f2be07693fceca74088884885cc545a70ceef5003d0b2479", + "evaluationSourceSha256": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "wasmFileBytes": 61072, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/37 core.test.blank program, no hidden application behavior, and reset...OK\n2/37 core.test.arbitrary input counter and unused events...OK\n3/37 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/37 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/37 core.test.branch to program end is valid...OK\n6/37 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/37 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/37 core.test.stack underflow overflow and leftover values reject the event...OK\n9/37 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/37 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/37 core.test.capacity boundaries and sticky load errors...OK\n12/37 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/37 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/37 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/37 core.test.binding evaluation is pure on both success and failure...OK\n16/37 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/37 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/37 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/37 core.test.point coordinates are ephemeral event operands and unavailable to pure evaluation...OK\n20/37 core.test.point event ranges reject holes and invalid payloads without committing...OK\n21/37 core.test.point event failure rolls back writes from earlier matching handlers...OK\n22/37 core.test.loading shares storage with records while old events and bindings remain executable...OK\n23/37 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK\n24/37 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK\n25/37 core.test.restored definition and values commit together without changing reset initializers...OK\n26/37 core.test.missing duplicate nonfinite or invalid restored candidates leave active work untouched...OK\n27/37 core.test.empty and full restored programs validate all values and discard old display records...OK\n28/37 core.test.shared handler bodies access their own bound state slots in source order...OK\n29/37 core.test.failure in a later shared binding rolls back all earlier bindings...OK\n30/37 core.test.shared bindings reject missing arguments partial overlaps and invalid slot mappings...OK\n31/37 core.test.one routed handler changes only its event target and rejects missing or foreign routes...OK\n32/37 core.test.routes reject invalid mappings and bindings without replacing active work...OK\n33/37 core.test.routed event rollback includes globals and earlier handlers and survives staged route replacement...OK\n34/37 core.test.scalar inputs carry a finite ephemeral value without adding state or pointer ownership...OK\n35/37 core.test.scalar input failure rolls back earlier writes and old scalar events survive staging...OK\n36/37 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n37/37 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 37 tests passed.\n" +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/compiler-check.log b/prototypes/workbench/results/2026-09-23-web-layout/compiler-check.log new file mode 100644 index 0000000..22d5929 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/compiler-check.log @@ -0,0 +1,21 @@ +✔ a blank document is an empty program with no implied behavior (4.977136ms) +✔ forward references and separate state/input namespaces preserve declaration IDs (2.107257ms) +✔ ordered assignments, repeated handlers, and condition evaluation once (0.987638ms) +✔ nested branches use absolute forward targets and skip correct bodies (0.87564ms) +✔ operator precedence, associativity, unary and builtin function lowering (2.617456ms) +✔ multiline parentheses, scientific literals, comments, and string escapes (0.840566ms) +✔ all instruction fields are numeric and carry original source line (0.411107ms) +✔ contextual declaration words remain valid names outside reserved builtins (0.322923ms) +✔ unknown and reserved symbols have useful one-based locations (2.02148ms) +✔ duplicates, unsupported keys, initializers, and invalid syntax are rejected (1.536577ms) +✔ document, declaration, instruction, and nesting limits reject overflow (6.955487ms) +✔ every supported input maps to a bounded ordered event ID (0.7934ms) +✔ optional examples compile and charge behavior is entirely authored (17.558808ms) +ℹ tests 13 +ℹ suites 0 +ℹ pass 13 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 113.13035 diff --git a/prototypes/workbench/results/2026-09-23-web-layout/guards-result.log b/prototypes/workbench/results/2026-09-23-web-layout/guards-result.log new file mode 100644 index 0000000..909606d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/guards-result.log @@ -0,0 +1,9 @@ +### Result +{"checks":["invalid-output fixture changes the numeric rule","out-of-range authored output preserves both the layout and its native control projection","a corrected policy and valid request recover from invalid host output","arithmetic rollback also restores the native range projection","policy replacement is refused during an active native gesture","oversize files cannot replace the editable text"]} +### Ran Playwright code +```js +await (async page => await page.evaluate(() => window.__pureDocumentGuards ?? {pending:true}))(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/document.html +- Page Title: Pure · 문서 작업 diff --git a/prototypes/workbench/results/2026-09-23-web-layout/guards-run.log b/prototypes/workbench/results/2026-09-23-web-layout/guards-run.log new file mode 100644 index 0000000..75ca6b1 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/guards-run.log @@ -0,0 +1,38 @@ +### Ran Playwright code +```js +await (async page => { + const checks=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const onDialog=d=>d.accept();page.on('dialog',onDialog); + try { + await page.goto('http://127.0.0.1:8766/document.html');await page.waitForFunction(()=>window.pureDocument?.snapshot().ready); + await page.setViewportSize({width:1440,height:1000});await page.locator('#document-text').fill('보존할 문서'); + await page.locator('.policy summary').click();const source=await page.locator('#policy-source').inputValue(); + const bad=source.replace('clamp(event_value, 20, 80)','999');check(bad!==source,'invalid-output fixture changes the numeric rule'); + await page.locator('#policy-source').fill(bad);await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('End'); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='50'&&await page.locator('#split-range').inputValue()==='50'&&(await page.locator('#policy-status').textContent()).includes('20~80'),'out-of-range authored output preserves both the layout and its native control projection'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('Home'); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='20','a corrected policy and valid request recover from invalid host output'); + await page.locator('#policy-source').fill(source.replace('clamp(event_value, 20, 80)','1 / 0'));await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('End'); + const s=await page.evaluate(()=>window.pureDocument.snapshot());check(s.runtime.lastError===4&&s.runtime.states.ratio===20&&await page.locator('#split-range').inputValue()==='20','arithmetic rollback also restores the native range projection'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click(); + await page.locator('#divider').scrollIntoViewIfNeeded();const box=await page.locator('#divider').boundingBox();await page.mouse.move(box.x+box.width/2,box.y+box.height/2);await page.mouse.down(); + const revision=await page.evaluate(()=>window.pureDocument.snapshot().runtime.revision); + await page.evaluate(()=>document.querySelector('#apply-policy').click()); + check(await page.evaluate(r=>window.pureDocument.snapshot().dragging&&window.pureDocument.snapshot().runtime.revision===r,revision)&&(await page.locator('#policy-status').textContent()).includes('경계를 놓거나'),'policy replacement is refused during an active native gesture');await page.mouse.up(); + await page.locator('#file-input').evaluate(input=>{const d=new DataTransfer();d.items.add(new File([new Uint8Array(2*1024*1024+1)],'oversize.txt'));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('2 MiB')); + check(await page.locator('#document-text').inputValue()==='보존할 문서','oversize files cannot replace the editable text'); + await page.locator('.policy summary').click();await page.locator('#sample').click();await page.locator('#reset-split').click(); + await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); + await page.screenshot({path:'output/playwright/pure-document-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844});await page.screenshot({path:'output/playwright/pure-document-mobile.png',fullPage:true}); + await page.evaluate(e=>window.__pureDocumentGuards=e,{checks});return{checks}; + }catch(error){await page.evaluate(e=>window.__pureDocumentGuards=e,{failed:String(error),checks});throw error;} + finally{page.off('dialog',onDialog);} +} +)(page); +``` +### Page +- Page URL: http://127.0.0.1:8766/ +### Modal state +- ["beforeunload" dialog with message ""]: can be handled by dialog-accept or dialog-dismiss diff --git a/prototypes/workbench/results/2026-09-23-web-layout/guards.json b/prototypes/workbench/results/2026-09-23-web-layout/guards.json new file mode 100644 index 0000000..2082a78 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/guards.json @@ -0,0 +1,10 @@ +{ + "checks": [ + "invalid-output fixture changes the numeric rule", + "out-of-range authored output preserves both the layout and its native control projection", + "a corrected policy and valid request recover from invalid host output", + "arithmetic rollback also restores the native range projection", + "policy replacement is refused during an active native gesture", + "oversize files cannot replace the editable text" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/manifest.json b/prototypes/workbench/results/2026-09-23-web-layout/manifest.json new file mode 100644 index 0000000..e2952a3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/manifest.json @@ -0,0 +1,45 @@ +{ + "README.md": "f3c303c8e824ff1dd1692f6bc55493efbf36337325f9a4a0b960123de94cfbe3", + "authoring-result.log": "c788e72ebcafef9e46a41c2d2fb554347e60f2059f5296305c2385366387e897", + "authoring-run.log": "42a7655af1bf01316f5cbc2785cffe3ede11c6e2fd2e9d17724bba74aaa04a2c", + "authoring.json": "7c748a1f6624a9a82804bdafd4c4ba59685cc3b28411452ecf0d15243ec3a71c", + "before/compiler.mjs": "a9d5d1a863519e86c37df24385ae9cf605384bea5e6b4298f6e67177e6a2a811", + "before/core.wasm": "b494269c1332f71de9725a4c7f0b58c074b669403d5d9dc95ddfeb7b16967c0d", + "before/core.zig": "56b8db87bcf7e49ab2b2e8d4166ade6ed04c77571fbb69c60633e76376b46976", + "before/runtime.mjs": "cb22adac57452453fe3add6cd885b5636d49fc946b1cd54e9e8ee502d167e7e2", + "browser-result.log": "67a99bb012002df426c481aef11f6449f667fd28a09987ae95b76d3f0d76cfa8", + "browser-run.log": "4e4cc8d0a889f0dc866337fbe9beca1eff8740512bbeef4e1dfae9054a0ef391", + "browser.json": "cc1b01fa1ff37636a41820b809e885aee4954f5e72d5e6e605931a3e2a8f6b9b", + "build.json": "cbaa131d63fb64a03f080bcd97d797a66754628192ac264df8f7228ffc6d9ed4", + "compiler-check.log": "9beca80a174458581337435093b818670bd60376b77fc5bebe0ea0c38a3da7df", + "guards-result.log": "b8ed60c6502111159045c95dafe9821620a102ede5a774ae944b8fb1ec1a1f0d", + "guards-run.log": "5030edd96dbf77c43efedaefc491fb25ffa6d1a168317c9df7a01f7e192c918d", + "guards.json": "552a8943d0fda2f4a417eecab7dce67825ae354580687b571e2c8d972413947b", + "native-build.log": "1d94c8ee4d8661903235bddc387bfae7c24509c63cb0b1dc6abe5849eb3f0124", + "node-tests.log": "67458943179cdb726afe9d8368ed96624e9eb94ff8bb7767e62c75a448aa8de2", + "pure-document-desktop.png": "81be2cf443fbf4f3b2fd32699b5c90d7d09ee1977ae3b99f0fc829143acdd362", + "pure-document-mobile.png": "21dc43b6f9b838acaf139d0fa14be4b262f5d6d654d432721280c58fab6bc5c9", + "pure-document-saved.txt": "377ee4ce8ed66005074d8b7e6a683a04fbe24f556218ec7d49cd6613d500704d", + "qa-authoring.js": "b6e2998652a020a65f3210b1d7790a95fee021cbd7016bb0e1cdb258fd06b295", + "qa-value-control.js": "f7fc9f93fb4873e22645fc6e8199544d0786aeb4b6458c7979ce56505922943d", + "sources/CONTRACT.md": "70cf2bb14f2e12bdc7f72ea1ca13fea70dbf8b91362cd6d2b818aeb962f3e930", + "sources/cli.mjs": "c02bb3775bc193f2505c413cd97c39a29fdd4a7447635972a6d335a047eb8f60", + "sources/core.zig": "60307704dbb5e11a75d14108fb7d87a5a4480359fdd771ec73beebf5f5c48b81", + "sources/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "sources/examples/document.pure": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "sources/qa-document-guards.js": "a8617e51d8a8de044a880771aa16a09994f930b9c0cc914bcea5db60ab793fff", + "sources/qa-document.js": "d9aa61a376167e31f10aebccf91f016a41bfa419e602c4bac14971a53854b3c2", + "sources/value-input.test.mjs": "8b08813ed64949cb0d26b9b4e02a5fbf28e84dcccf8641c0ec7c8b6e686c1267", + "sources/web/app.js": "82006b868cd6325b8cbb6392e766fddc4805499d0abbeb33c56293cd49d73b5b", + "sources/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "sources/web/core.wasm": "9f760e4a2ce6ea11f2be07693fceca74088884885cc545a70ceef5003d0b2479", + "sources/web/document.css": "7f549cc2cf85019a220f5bd11c4477afae066c8d7d993a8706cf4a0515fb5d4a", + "sources/web/document.html": "77116a14e0e85c95ded98c8e3df76970221968ae06278ada37350eb3f88d18e1", + "sources/web/document.mjs": "3115841429e3c78add3ec04f2a5f197901a11302e449b29acefffcf9e617fb9d", + "sources/web/index.html": "4241bd1a2d45f6ac1e8166abe0daac3230d9f080c4ba3b9653565530dc4e8cc7", + "sources/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "sources/web/split-input.mjs": "cfc76b56dbc7386ff1d024c13807afe1ae4c260bb0bbee51fe1b14549143381e", + "value-control.json": "e9d69bbcd4cb1136239cf43f59620a080097b2100a542bbd531d2b1937a6d1e7", + "value-control.log": "57353ddce606d43574558712ff46ef80daac5794220f187d8ce09eb5594310b1", + "verification.json": "c81b2adceb496e733bb51244d1c79258727f9f093894e4fdf3b395233dc54f7f" +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/native-build.log b/prototypes/workbench/results/2026-09-23-web-layout/native-build.log new file mode 100644 index 0000000..5f13011 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/native-build.log @@ -0,0 +1,39 @@ +1/37 core.test.blank program, no hidden application behavior, and reset...OK +2/37 core.test.arbitrary input counter and unused events...OK +3/37 core.test.charge and release behavior exists only in the authored instruction program...OK +4/37 core.test.forward condition executes once and assignments and handlers are ordered...OK +5/37 core.test.branch to program end is valid...OK +6/37 core.test.one event rolls back writes from all handlers on arithmetic failure...OK +7/37 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK +8/37 core.test.stack underflow overflow and leftover values reject the event...OK +9/37 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK +10/37 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK +11/37 core.test.capacity boundaries and sticky load errors...OK +12/37 core.test.invalid declaration values state mappings event ids and handler ranges...OK +13/37 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK +14/37 core.test.event inputs and dt enforce deterministic frame contract...OK +15/37 core.test.binding evaluation is pure on both success and failure...OK +16/37 core.test.arithmetic comparison logical min max and clamp expression semantics...OK +17/37 core.test.diagnostic counters wrap without affecting scalar values...OK +18/37 core.test.memory accounting separates reserved storage used IR and stack capacity...OK +19/37 core.test.point coordinates are ephemeral event operands and unavailable to pure evaluation...OK +20/37 core.test.point event ranges reject holes and invalid payloads without committing...OK +21/37 core.test.point event failure rolls back writes from earlier matching handlers...OK +22/37 core.test.loading shares storage with records while old events and bindings remain executable...OK +23/37 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK +24/37 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK +25/37 core.test.restored definition and values commit together without changing reset initializers...OK +26/37 core.test.missing duplicate nonfinite or invalid restored candidates leave active work untouched...OK +27/37 core.test.empty and full restored programs validate all values and discard old display records...OK +28/37 core.test.shared handler bodies access their own bound state slots in source order...OK +29/37 core.test.failure in a later shared binding rolls back all earlier bindings...OK +30/37 core.test.shared bindings reject missing arguments partial overlaps and invalid slot mappings...OK +31/37 core.test.one routed handler changes only its event target and rejects missing or foreign routes...OK +32/37 core.test.routes reject invalid mappings and bindings without replacing active work...OK +33/37 core.test.routed event rollback includes globals and earlier handlers and survives staged route replacement...OK +34/37 core.test.scalar inputs carry a finite ephemeral value without adding state or pointer ownership...OK +35/37 core.test.scalar input failure rolls back earlier writes and old scalar events survive staging...OK +36/37 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK +37/37 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK +All 37 tests passed. +Built /home/eunhhu/work/pure/prototypes/workbench/web/core.wasm (61,072 bytes) diff --git a/prototypes/workbench/results/2026-09-23-web-layout/node-tests.log b/prototypes/workbench/results/2026-09-23-web-layout/node-tests.log new file mode 100644 index 0000000..5a5df79 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/node-tests.log @@ -0,0 +1,95 @@ +✔ reference begins with the authored states and matching scene (12.66232ms) +✔ press, hold, release, and frame order produce exact expected values (1.077864ms) +✔ charge clamps at one and explicit floor restores rest (4.575512ms) +✔ repeat and concurrent physical sources produce only logical edges (4.137847ms) +✔ airborne presses cannot charge or cause another jump (0.449738ms) +✔ releaseAll emits release and reset clears latches without a jump (1.114142ms) +✔ unknown inputs and invalid frame counts reject without mutation (1.183864ms) +✔ terminal starts empty and produces the same charge trace as the shared runtime (390.652434ms) +✔ invalid terminal commands do not alter state and fail piped execution (233.630018ms) +✔ terminal replays authored pointer movement and cancellation through the shared runtime (162.430096ms) +✔ file load preserves by name, rejected load preserves active work, and save never overwrites (245.025646ms) +✔ terminal exposes arithmetic rollback and retains failure through matching release (155.756539ms) +✔ terminal configures and discards evaluation records and recovers from a display limit (113.106013ms) +✔ terminal saves and restores placement independently of reset and preserves a browser draft (525.765513ms) +✔ invalid restoration leaves terminal code and state unchanged (127.509012ms) +✔ a blank document is an empty program with no implied behavior (8.64436ms) +✔ forward references and separate state/input namespaces preserve declaration IDs (3.976626ms) +✔ ordered assignments, repeated handlers, and condition evaluation once (1.714565ms) +✔ nested branches use absolute forward targets and skip correct bodies (1.537677ms) +✔ operator precedence, associativity, unary and builtin function lowering (4.025015ms) +✔ multiline parentheses, scientific literals, comments, and string escapes (3.961885ms) +✔ all instruction fields are numeric and carry original source line (1.301363ms) +✔ contextual declaration words remain valid names outside reserved builtins (0.999902ms) +✔ unknown and reserved symbols have useful one-based locations (3.873349ms) +✔ duplicates, unsupported keys, initializers, and invalid syntax are rejected (3.797849ms) +✔ document, declaration, instruction, and nesting limits reject overflow (21.576327ms) +✔ every supported input maps to a bounded ordered event ID (4.487642ms) +✔ optional examples compile and charge behavior is entirely authored (63.171839ms) +✔ cache reuse, unrelated changes and discarded records preserve authored values (145.592017ms) +✔ committed changes only: failed events and failed edits preserve reusable output (16.185689ms) +✔ program edits, state reordering and reset cannot reuse old definitions (13.364686ms) +✔ finite float semantics include signed zero, high state bit and arithmetic errors (11.375975ms) +✔ one budget covers the whole scene; failures publish no partial scene or state rollback (6.433502ms) +✔ small caches, no history and every small work limit give exact results or explicit failure (142.930942ms) +✔ invalid settings and forget names preserve policy and state (18.528843ms) +✔ pointer declarations resolve forward targets and reject ambiguous or contextless inputs (63.575855ms) +✔ authored drag keeps grab offset and commits the displayed position even if release coordinates differ (47.043964ms) +✔ ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input (20.144926ms) +✔ invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event (24.907531ms) +✔ independent targets share the event pipeline and publication remains non-reentrant (20.336407ms) +✔ failing down, up and cancel never prevent terminal ownership cleanup (17.199536ms) +✔ direct publication matches archived snapshots and work counts through authored changes (123.09694ms) +✔ whole-scene budget failure never publishes partial bindings and recovers exactly (14.010757ms) +✔ latched event rollback, nonfinite bindings, reset and signed zero keep their semantics (10.119575ms) +✔ publication rejects reentrant runtime changes and reads before they affect the scene (11.072124ms) +✔ invalid or throwing output adapters do not strand the runtime or alter authored state (5.967394ms) +✔ multiple authored placements and a separate invalid draft survive a fresh runtime restore (83.580766ms) +✔ malformed files reject before changing active code, values, diagnostics or warm display records (29.663338ms) +✔ restoration matches names rather than ordering and never reuses stale cached values (7.112109ms) +✔ finite float values including negative zero round trip without changing authored reset values (6.838574ms) +✔ held and failed interactions cannot be saved and restoration cannot steal an active input (12.077971ms) +✔ empty and full documents restore without imposing a new source size or state count (7.015221ms) +✔ document APIs respect the output callback boundary and export independent values (5.109731ms) +✔ one input and one handler serve up to 24 static targets without instruction or handler expansion (63.170859ms) +✔ routes validate forward references, exact types, unique views/items and current input target (5.637618ms) +✔ grouped layout matches explicit bindings through concurrent capture, partial cancel, keyboard and restore (62.651066ms) +✔ capture remains bound to its target and releaseAll cancels each target exactly once (13.395149ms) +✔ reorder and remove/readd rebuild routes by names without persisting physical indices (16.455874ms) +✔ routed writes invalidate cached displays and failed handlers rollback without losing another target owner (7.482478ms) +✔ CLI sends grouped pointer events and rejects an unknown target (355.285569ms) +✔ a route from a different input group cannot be delivered through the named group (12.613062ms) +✔ binding more objects shares one VM body instead of expanding its instructions (15.696248ms) +✔ records and rules resolve forward names and reject invalid types fields and bindings (12.425414ms) +✔ shared layout matches the existing authored layout across independent and simultaneous gestures (63.591654ms) +✔ field names retain identity across schema and instance reorder, removal and restoration (15.784988ms) +✔ bindings share one event transaction including global effects and multiple uses of one item (12.381266ms) +✔ bound writes invalidate affected display caches and failed edits preserve active bindings (13.678055ms) +✔ generated qualified names cannot make a valid exported document unreadable by its own loader (32.819469ms) +✔ blank startup and applied empty source contain no application objects or behavior (40.046003ms) +✔ author can add unrelated states inputs views and behavior entirely through source (21.605345ms) +✔ keyboard and pointer sources aggregate first down and last up; repeats are ignored (9.64304ms) +✔ held input rejects apply without changing applied source, metadata, or values (9.352079ms) +✔ invalid syntax, unknown references and capacity overflow preserve the previous program (16.480058ms) +✔ state preservation follows names across reorder/add/remove; reset uses new initializers (4.060756ms) +✔ callers cannot mutate active metadata and corrupt name-based state preservation (17.230926ms) +✔ conditions execute once, and assignments and multiple handlers follow source order (19.697116ms) +✔ a failed event rolls back all handlers and keeps its source diagnostic through release (4.942696ms) +✔ reset clears held inputs and latched errors; invalid frame counts have no effects (8.38588ms) +✔ render evaluation is pure and invalid binding is distinct from an event rollback (26.878928ms) +✔ independent adapters replay the same author program without sharing live state (17.047112ms) +✔ Wasm has no imports and reports fixed memory and exact active IR/state storage (37.971164ms) +✔ numeric controls use explicit change context and cannot read pointer or frame payloads (14.06335ms) +✔ numeric input handles scalar rules, rollback, cache invalidation and signed zero (32.805007ms) +✔ the document policy receives range, keyboard and pointer requests without layouts or text in VM state (12.342451ms) +✔ CLI supplies the same numeric control value and rejects nonfinite input (206.351585ms) +✔ Wasm executes old events and output during staged replacement without corrupting either phase (17.789348ms) +✔ workspace accounting reports shared payload and safety tag without double-counting (9.700743ms) +ℹ tests 87 +ℹ suites 0 +ℹ pass 87 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2402.223363 diff --git a/prototypes/workbench/results/2026-09-23-web-layout/pure-document-desktop.png b/prototypes/workbench/results/2026-09-23-web-layout/pure-document-desktop.png new file mode 100644 index 0000000..a2e1092 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-web-layout/pure-document-desktop.png differ diff --git a/prototypes/workbench/results/2026-09-23-web-layout/pure-document-mobile.png b/prototypes/workbench/results/2026-09-23-web-layout/pure-document-mobile.png new file mode 100644 index 0000000..1565cd2 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-web-layout/pure-document-mobile.png differ diff --git a/prototypes/workbench/results/2026-09-23-web-layout/pure-document-saved.txt b/prototypes/workbench/results/2026-09-23-web-layout/pure-document-saved.txt new file mode 100644 index 0000000..8681227 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/pure-document-saved.txt @@ -0,0 +1,3 @@ +표준 UTF-8 파일 +외부 편집기에서 만든 내용 +😀
문자
diff --git a/prototypes/workbench/results/2026-09-23-web-layout/qa-authoring.js b/prototypes/workbench/results/2026-09-23-web-layout/qa-authoring.js new file mode 100644 index 0000000..37f56a0 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/qa-authoring.js @@ -0,0 +1,229 @@ +// Run this expression with Playwright CLI run-code after opening the local host. +// All behavior is authored through the editor; snapshot() only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = error => errors.push(error.message); + const acceptDialog = dialog => dialog.accept(); + const check = (condition, label) => { + if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); }; + const authorSource = [ + '# A new authored interaction, not a bundled example.', + 'state x = 90', 'state moving = 0', 'state count = 0', + 'input advance = ArrowRight', 'input pulse = Space', + 'box marker(x, 170, 48, 48, "#7ed9ae")', + 'text reading(40, 70, count, "Signals: ")', + 'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }', + 'on pulse.down { count = count + 1 }', + 'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '', + ].join('\n'); + let lastAuthored = null; + let mobile = null; + let downloaded = null; + const shots = ['output/playwright/pure-document-authoring-blank.png', 'output/playwright/pure-document-authoring-authored.png', 'output/playwright/pure-document-authoring-mobile.png']; + page.on('pageerror', onError); + page.on('dialog', acceptDialog); + try { + await page.goto('http://127.0.0.1:8766/'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + let s = await state(); + check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source'); + check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock'); + check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program'); + await page.screenshot({ path: shots[0], fullPage: true }); + + await page.locator('#source').fill(authorSource); + s = await state(); + check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply'); + await page.locator('#apply').click(); + s = await state(); + check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source'); + check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations'); + check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value'); + + await page.locator('#source').focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Space'); + await page.keyboard.press('ArrowRight'); + check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs'); + await page.locator('#source').fill(authorSource); + + const pulse = page.locator('[data-input="pulse"]'); + let box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + s = await state(); + check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused'); + await page.mouse.up(); + check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).states.count === 2, 'keyboard repeat is ignored'); + await pulse.focus(); + await page.keyboard.press('Enter'); + check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap'); + await pulse.dispatchEvent('click', { detail: 0 }); + check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap'); + + await page.locator('#scene').focus(); + await page.keyboard.down('Space'); + box = await pulse.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input'); + await page.mouse.up(); + check((await state()).held.includes('pulse'), 'releasing one source does not release the other source'); + await page.keyboard.up('Space'); + check((await state()).held.length === 0, 'last source releases the logical input'); + + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.locator('#step').click(); + await page.locator('#step').click(); + s = await state(); + check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion'); + await page.locator('#source').focus(); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.keyboard.up('ArrowRight'); + check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock'); + + await page.locator('#play').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5); + await page.locator('#play').click(); + const paused = await state(); + await page.waitForTimeout(100); + check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock'); + + const beforeFailure = await state(); + await apply(authorSource + 'state broken =\n'); + s = await state(); + check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states'); + check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic'); + await page.locator('#error-location').click(); + check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem'); + + const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [ + 'state bonus = 7', 'input reverse = KeyA', + 'circle beacon(300, 190, 15 + bonus, "#c9f08b")', + 'on reverse.down { bonus = bonus + 2 }', '', + ].join('\n'); + await apply(expanded); + s = await state(); + check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states'); + await page.locator('[data-input="reverse"]').click(); + check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values'); + + const withDraft = expanded + '# This un-applied draft must survive Reset.\n'; + await page.locator('#source').fill(withDraft); + await page.locator('#reset').click(); + s = await state(); + check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft'); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + const heldRevision = (await state()).revision; + await page.locator('#apply').click(); + check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held'); + await page.keyboard.up('ArrowRight'); + + const downloadPromise = page.waitForEvent('download'); + await page.locator('#download-project').click(); + const projectDownload = await downloadPromise; + const downloadPath = 'output/playwright/pure-document-authoring-draft.json'; + await projectDownload.saveAs(downloadPath); + downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft }; + await page.locator('#new').click(); + s = await state(); + check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply'); + await page.locator('#file-input').setInputFiles(downloadPath); + await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft); + check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution'); + await page.locator('#file-input').evaluate(input => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식')); + check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft'); + + await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n'); + await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 }); + s = await state(); + check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up'); + check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record'); + + await page.locator('#new').click(); + await page.locator('#apply').click(); + s = await state(); + check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents'); + await page.locator('#example').selectOption('counter'); + await page.locator('#load-example').click(); + await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count')); + check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies'); + + await apply(authorSource); + await page.locator('[data-input="pulse"]').click(); + await page.locator('[data-input="pulse"]').click(); + await page.locator('#scene').focus(); + await page.keyboard.down('ArrowRight'); + for (let i = 0; i < 12; i++) await page.locator('#step').click(); + await page.keyboard.up('ArrowRight'); + lastAuthored = await state(); + check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory'); + await page.locator('.cost-details summary').click(); + check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals'); + await page.locator('.cost-details summary').click(); + await page.screenshot({ path: shots[1], fullPage: true }); + + await page.setViewportSize({ width: 390, height: 844 }); + mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow'); + await page.locator('[data-input="pulse"]').click(); + check((await state()).states.count === 3, 'generated mobile input buttons remain usable'); + await page.screenshot({ path: shots[2], fullPage: true }); + const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html'); + const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure'); + check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served'); + + await page.route('**/core.wasm', route => route.abort()); + await page.reload(); + await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패')); + check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls'); + await page.unroute('**/core.wasm'); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment'); + check(errors.length === 0, 'no uncaught page errors during the browser scenarios'); + const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots }; + // CLI can yield early when native dialogs/download events occur. Retain the + // observation report so a follow-up CLI call can retrieve the complete run. + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors }; + await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence); + return evidence; + } finally { + page.off('pageerror', onError); + page.off('dialog', acceptDialog); + } +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/qa-value-control.js b/prototypes/workbench/results/2026-09-23-web-layout/qa-value-control.js new file mode 100644 index 0000000..a5a3eb1 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/qa-value-control.js @@ -0,0 +1,12 @@ +async page => { + const checks=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await page.locator('#source').fill('state amount=0\ninput level=value\ntext label(20,40,amount)\non level.change {amount=clamp(event_value,0,100)}'); + await page.locator('#apply').click(); + await page.getByLabel('level',{exact:true}).fill('123');await page.getByRole('button',{name:'값 전달',exact:true}).click(); + let s=await page.evaluate(()=>window.pureWorkbench.snapshot());check(s.states.amount===100&&s.lastEvent.phase==='change','numeric control dispatches change and publishes the authored result'); + check(s.held.length===0,'numeric controls do not create a held pointer owner'); + await page.getByLabel('level',{exact:true}).fill('-5');await page.getByRole('button',{name:'값 전달',exact:true}).click(); + s=await page.evaluate(()=>window.pureWorkbench.snapshot());check(s.states.amount===0,'the same authored clamp handles negative requests'); + await page.evaluate(e=>window.__pureValueEvidence=e,{checks});return{checks}; +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/CONTRACT.md b/prototypes/workbench/results/2026-09-23-web-layout/sources/CONTRACT.md new file mode 100644 index 0000000..e4873be --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/CONTRACT.md @@ -0,0 +1,568 @@ +# Workbench experiment contract + +An author starts with empty source. There is no built-in player, physics, jump, +score, or timer behavior. All behavior is declared by the author. This is a +bounded scalar interaction language experiment, not a general language claim. + +## Source language + +```text +state count = 0 +input tap = Space +box tile(80, 150, 60, 60, "#356548") +circle dot(200, 180, 20, "#c78638") +text label(80, 100, count, "Count: ") +on tap.down { + count = count + 1 +} +on frame { + if count > 20 { + count = 0 + } +} +``` + +Declarations and statements end at a newline or semicolon; `#` comments outside +strings. ASCII identifiers, finite numeric literals, state initializers must be +numeric constants (optional unary sign). Input key codes accepted: +Space, Enter, ArrowLeft/Right/Up/Down, KeyA..KeyZ, Digit0..Digit9. Input names +must be unique, key codes unique, names cannot shadow `frame`, `dt`, `min`, `max`, +`clamp`, `event_x`, `event_y`. State and input names use separate namespaces, entity IDs unique. + +Expressions: parentheses, state references, dt (seconds on frame, zero on input), +unary - and !, + - * /, < <= > >= == !=, && ||, min(a,b), max(a,b), clamp(x,lo,hi). +Logical operators evaluate both sides. Division by zero/non-finite results fail +the whole event without committing state changes. No loops, recursion or eval. +An if without else executes its condition once; assignment statements are ordered. +Multiple handlers for the same event execute in source order within one atomic event. +`dt` is allowed in handlers, not view bindings. A frame always uses 1/60 seconds. + +box: x,y,width,height expressions + literal hex color; circle: x,y,radius + color; +text: x,y,value expression + optional literal prefix. Values are world units in a +640x360 view. No implicit clipping/collision/physics. Render expressions are pure. +Unknown identifiers and invalid syntax include 1-based line+column diagnostics. + +Initial scene is empty. Optional examples are explicit insertions, never auto-run. +Successful apply preserves scalar states by exact name; new names use initializers, +removed names disappear. Apply/reset are separate. Failed parse/load changes nothing. +Apply is refused while any input is held; release first. This avoids inventing +release events across changed mappings. Focus loss sends .up to ordinary buttons +and .cancel to pointer inputs. + +## Compiler interface (web/compiler.mjs, browser and Node) + +`compile(source)` returns: +``` +{ + states:[{name,initial,line}], inputs:[{name,key,line} | {name,target,line} | {name,type,routes,line}], + instructions:[{op,arg,value,line}], handlers:[{event,start,length,line}], + views:[{id,type,line,color?,prefix?,bindings:{x:{start,length},...}}], + pointerTargets:[{input,base,count}], metrics:{...} +} +``` +Bindings: box x/y/width/height; circle x/y/radius; text x/y/value. +Compiler throws an Error with .line and .column. Empty source is a valid empty program. +Handlers and binding expressions all occupy instructions (maximum 512 total), but +only handlers execute on events; bindings are evaluated on demand by VM eval_at. +Limits: 32 states, 8 inputs, 24 views, 64 handlers, 512 instructions, stack 64, +source 32768 UTF-8 bytes. State IDs follow declaration order. Input event IDs: +frame=0; input i down=1+2*i, up=2+2*i, move=17+i, cancel=25+i. +Move/cancel handlers require a pointer input in source. States may be referenced before declaration; +symbols must be resolved after gathering top-level declarations. `metrics` can +record source bytes/lines and instruction/handler/view/state counts. + +## VM opcodes + +| op | operation | +|---:|---| +|0| push constant `value` | +|1| push state `arg` | +|2| push dt | +|3..6| add, subtract, multiply, divide | +|7| negate | +|8..13| less, less-equal, greater, greater-equal, equal, not-equal | +|14..16| AND, OR, NOT (nonzero true) | +|17..19| min, max, clamp (push x,lo,hi; invalid lo>hi fails) | +|20| store state `arg` (pop) | +|21| jump-if-zero to absolute instruction index `arg` (pop), forward only | +|22,23| push current pointer event x,y; fail without point context | +|24,25| load/store field at handler parameter_base + arg; handler-bound only | + +Rules start with an empty stack and must end with empty stack. Bindings must finish +with exactly one value and cannot STORE, branch, or read dt/event coordinates. Branch targets may be handler end. + +## Zig exported ABI + +`reset_all()` clears programs and state. `begin_program(state_count,input_count)` +starts staging, returns0 success; `put_state(id,initial,old_id)` old_id=-1 means init; +`put_instruction(op,arg,value)` append; `put_handler(event,start,length)` append; +`commit_program()` all0 success otherwise error code, atomic; candidate is separate +from the active program, but shares its workspace with optional evaluation records. +Preserved IDs read active values only at successful commit. Validate limits, +indices, finite values and forward branch ranges; runtime stack errors are checked. +`reset_state()` restores active initializers, resets tick/event diagnostics. +`dispatch(event,dt)` accepts0..16 and finite dt; event0 dt must equal1/60, input dt0; +returns0 success, other code failure with no state mutation. Tick increments for +successful frame; one event runs all matching handlers in source order. +`dispatch_point(event,x,y)` accepts existing input down/up IDs plus move/cancel IDs +within input_count, never frame0. Coordinates must be finite; dt is implicitly0. +The core only knows input count, not host device kinds. The compiler and host enforce +pointer ownership and device mapping. Calling legacy dispatch on a coordinate-reading +handler fails with bad_event9. Point operands are ephemeral execution context, not +extra persistent state slots. Both dispatch paths use the same atomic commit/history. +The host requires this new export; older Wasm must be rebuilt. +`eval_at(start,length)` returns f64 or NaN on invalid/pure-expression failure; +must not mutate state or event diagnostics. `get_state(id)` returns f64 orNaN. +`info(id)` u32: 0 tick,1 last_error,2 error_pc (0xffffffff none),3 last_executed_ops, +4 revision,5 state_count,6 instruction_count,7 handler_count,8 input_count, +9 successful_event_count. `layout(id)` u32: 0 Engine reserved @sizeOf, +1 Program reserved @sizeOf,2 Instruction @sizeOf,3 Handler @sizeOf, +4 active used IR bytes (instructions+handlers+pointer targets),5 active state-value bytes, +6 staging reserved bytes (subset of Engine),7 evaluation stack capacity bytes,8 pointer target entry bytes,9 active pointer target count. +`error_message` belongs to host map; core error codes documented in core.zig. +No allocation or imported runtime in Wasm. Fixed64KiB linear memory target with +16KiB stack; increase only if actual linker evidence requires and document it. + +## Host interface (web/runtime.mjs) + +`await createRuntime(wasmBytes?, evaluationOptions?)` fetches core.wasm in browser if omitted. +Returns object with apply(source), reset(), frame(count=1), down(name,source), +up(name,source), point(name,phase,x,y,source,target?), releaseAll(), snapshot(), exportProject(), currentProgram. +Multiple sources per input share one logical held state; down emits only first +source, up only last. Source strings identify keyboard vs onscreen pointer. +`apply` returns compiled program or throws with diagnostic. It must fully validate +in host and stage+commit in core. It preserves by name. `reset` keeps active program. +`snapshot` returns {states:{name:value}, views:[{id,type,x,y,width?,height?,radius?, +value?,prefix?,color?}], tick,revision,lastError,errorLine,lastOps,memory:{...},held:[]} +`currentProgram` is the latest successfully applied compiled program or null. +The program and all nested metadata are frozen so readers cannot corrupt state +name mappings or binding ranges. `lastFailure` records the last failed event; +its `lastError`/line remain visible through successful release or focus cleanup +until successful apply or explicit reset. `lastEvent` still describes the latest +event. A view evaluation failure uses host error100 and does not roll back events. +UI draft and applied code are distinct. Browser/CLI share compiler/runtime/wasm; +instances are separate, not a live collaborative session. No arbitrary JS evaluation. + +## Bounded display evaluation (2026-09-22) + +The existing event and float semantics above are unchanged. The forgetful C +prototype's contract is adapted into `evaluation.zig`; its uint64 DAG is not +used as a substitute for this VM's float instructions. + +Each snapshot starts one display budget, shared by all bindings. Default options +are `{cacheEntries:0, historyEntries:8, workLimit:2048}`. A unit is a binding +request, a candidate-cache probe, a change-record probe, or an executed VM +instruction. State extraction, host object creation, event dispatch and DOM work +are outside this budget. It is not a time limit or a bound on whole-app work. +For the current compiler limits, 96 bindings * 10 units plus 512 instructions +is at most 1,472 units; the default admits even an uncached valid scene with +cache lookup/validation enabled. Malformed bindings may fail sooner. + +`configureEvaluation({cacheEntries?,historyEntries?,workLimit?})` accepts integers +0..32, 0..8, and 0..4294967295 respectively. Missing fields retain prior settings. +Valid configuration clears reuse records; invalid configuration changes nothing. +`forgetEvaluation('values'|'history'|'all')` drops optional records without +changing authored state, event diagnostics or program identity. +`evaluationStats()` returns the options plus work, instructions, cacheHits for +the last snapshot, and reservedBytes for the complete fixed evaluation structure. +Statistics are separate from snapshots so repeated observations remain equivalent. + +Memo entries contain the value, instruction range, input dependency bit mask +and validated revision. Direct mapping uses one candidate probe, with collisions +causing recomputation. Only successful computations are cached. Constant results +need no input-change history. All other reuse requires a complete retained +history since validation and no intersection with changed states. Dependencies +are collected from actual VM LOADs, not trusted compiler-supplied annotations. +History loss causes recomputation. All history and memo storage is fixed size; +setting capacities to zero does not return reserved linear memory. + +Only bitwise-different committed states advance change history (including signed +zero). Failed events and net-zero updates do not. Successful apply and reset +discard all reuse records. Parse/host validation failure before staging preserves +records. Beginning core staging discards them, including if that load is later +rejected or aborted. Epoch wrap also clears records. No inputs or effectful event +handlers are replayed to rebuild a binding. + +Budget exhaustion returns an empty views array for the entire snapshot, with +displayError `{code:101,line,message}`. Successfully committed event state remains. +There is no automatic retry or silent return of an old scene. Raising the budget +allows a fresh attempt. Arithmetic/size errors retain the earlier per-view error100 +behavior. `displayError` is null on display success; a latched execution error +still takes precedence in lastError/errorMessage, while displayError independently +reports display failure. Partial successful binding memos may survive a failed +snapshot but no partial scene is published. + +New core exports: `configure_evaluation(cache,history)` returns 0 or capacity1; +`forget_evaluation(flags)` bits 1=values, 2=history; `begin_evaluation(limit)`; +`eval_bounded(start,length)` returns f64 or NaN and consumes that shared budget. +`evaluation_info(id)` returns 0 status (10=work limit), 1 work, 2 instructions, +3 cache hits, 4 cache capacity, 5 history capacity, 6 reserved bytes. These +diagnostics do not replace event info. The unbounded pure `eval_at` remains for +reference checks and is not the host snapshot path. + +## Shared load/reuse workspace (2026-09-23) + +The pending definition/restore values and evaluation Tables occupy one untagged union. The active +Program, current state and evaluation control fields remain separate. All core +calls are single-threaded and synchronous; no callback can reenter a phase change. +No pointer to the reused tables is retained between calls. + +`begin_program` switches the workspace to staging and drops optional records. +Until commit/abort, old events and bindings still run against active. Binding +evaluation bypasses reuse and recomputes within its budget; cache configuration, +forget and reset must not write the staging payload. Old-id state preservation +reads the current values at successful commit, including events/reset during load. + +Successful commit publishes the new Program and then initializes empty Tables in +the shared workspace. `abort_program()` discards a pending definition and restores +empty Tables without changing active program/state/event diagnostics. Inactive +abort is a no-op. A failed commit leaves staging open for explicit abort or the +next begin. An invalid begin closes any previous pending load. Host apply aborts +after a core loading failure. Lost reuse may make a previously warm tiny budget +insufficient; neither a preserved cache nor completion at that budget is promised. + +`evaluation_info` additionally reports 7 dedicated control bytes, 8 shared Tables +bytes, 9 reuse available (false during load), and 10 union safety-tag overhead. +The host exposes these as dedicatedBytes, sharedBytes, reuseAvailable and +workspaceTagBytes. reservedBytes is control + Tables capacity, not incremental +Engine storage. The Tables capacity overlaps stagingReservedBytes and must not +be added to it. Before document restoration was added, the Wasm control was 48 B, shared Tables +1,152 B, union overhead 8 B. That Engine was 19,128 B, only 56 B larger than the +19,072 B pre-evaluation core; linear memory remains 65,536 B. No OS pages are +returned and this is not a measurement of browser/process memory. + +## Direct output publication (2026-09-23) + +`present(target)` evaluates the same bindings, in the same order and shared +budget as `snapshot()`, then calls a synchronous trusted output adapter: + +- `begin(program)`: the frozen current definition, or null at startup. +- `state(index, value)`: each current authored state, without a host state map. +- `view(index, valid)`: every declared view's publication status. +- `value(viewIndex, bindingName, value)`: each binding of a valid view only. +- `end(visibleCount)`: number of valid views. + +It returns the snapshot's diagnostics, held inputs, event information and memory +report, without `states` or `views`. Evaluation finishes before the first adapter +callback. On budget error101 every view is invalid and no binding values are +published; committed state values are still published. Error100 invalidates only +the affected views, as before. No stale value is silently substituted. + +Runtime mutations, `snapshot()` and nested `present()` are rejected during +callbacks. Read-only program/export/statistics access remains available. Adapters +must be synchronous and must not throw. If one does throw, the exception +propagates and the runtime unlocks in a finally block; arbitrary external adapter +effects cannot be rolled back. A later call republishes every value. This is not +a transaction across arbitrary output devices or an untrusted plugin boundary. + +Evaluation uses a private reusable Float64Array(96) and Uint8Array(24), allocated +on first snapshot/presentation. Their payload totals 792 host bytes, outside the +64 KiB Wasm memory. Typed-array object overhead, binding-entry arrays built at +apply, and DOM destination references are additional host memory. The numeric +buffer is a publication barrier, not another authoritative state or public +mutable scene. `snapshot()` remains an independent object representation for +CLI/export/debugging, built only when explicitly requested. + +The browser calls `present()` for normal refreshes. Its output adapter binds DOM +destinations once per successful definition change and compares values with the +existing DOM before writing. Input refreshes reuse scene, state and cost elements. +Invalid views remain allocated but are hidden using SVG `display="none"`; recovery +reveals them. A definition change can replace all scene/state destinations. This +does not skip unchanged binding evaluation, eliminate all host allocations, or +provide measured process-memory/paint-latency savings. + +## Authored direct manipulation (2026-09-23) + +`input drag = pointer(tile)` binds a logical input to a declared view, including a +forward declaration. One input per target; it shares the existing 8-input limit +with keyboard inputs. `on drag.down/move/up/cancel` handlers may read `event_x` +and `event_y` in the same 640x360 world coordinates as output. They are unavailable +in keyboard/frame handlers and display bindings; copy to authored state to retain +them. There is no built-in drag constraint, snap, grab offset, or gesture rollback. +The direct.pure example authors these using ordinary assignments and expressions. + +Host `point` requires a pointer input, valid phase, finite coordinates and nonempty +source string. Each logical input has one owning source; different targets may +have independent active contacts. Same-owner repeated down is a no-op; another +owner's down throws. Move/up/cancel without that owner are no-ops. Down establishes +ownership before dispatch; up/cancel release it before dispatch, even if authored +arithmetic fails. An event failure rolls back that event's state writes, not the +whole gesture. `releaseAll` cancels owned pointers at their last delivered position, +then releases ordinary buttons. `reset` directly clears ownership and resets state. +Last point events include x,y. A failed cancel can leave authored state unchanged, +but cannot strand host ownership; the failure remains visible until reset/apply. + +Browser hit testing selects the declared target on primary-button pointerdown. +The stable SVG root captures that pointer, including movement/release outside the +scene. Client coordinates are transformed through the inverse SVG screen matrix. +Pointerup exposes its own final coordinates to authored handlers. The direct.pure +example updates position only on move and ends dragging on up, preserving the +last displayed position even if a host repeats stale coordinates on release. Termination is observed in the window capture phase, including releases outside +the SVG. A primary mouseup can finish an owned mouse gesture if pointerup was +not delivered; it cannot finish a touch gesture. Duplicate termination is ignored. +Only capture loss of the SVG root ends its owner, not a bubbled descendant event. +Pointercancel, lostpointercapture and +focus loss use the last delivered position and dispatch cancel, never fake up. +Missing/non-invertible coordinate transforms cancel an active gesture. A second +contact on the same target is ignored; contacts on different targets are independent. +Touch scenes disable native panning on the SVG. Mouse/touch share this adapter; +pen-specific pressure, tilt and button meanings are not implemented or validated. + +Every delivered pointermove executes synchronously; this layer adds no event queue, +coalescing, frame gating or newest-only drop policy. This is not a guarantee that +browsers expose every hardware sample. Keyboard commands and pointer coordinates +enter the same authored state/commit/output path. CLI `point NAME PHASE X Y` uses +that host API and core with source `terminal`; it does not replay browser hit testing. + + +## Placement documents (2026-09-23) + +Code-only version1 JSON and .pure files keep their earlier behavior. Version2 +stores a restorable authored document, not a suspended execution image: + +```json +{"format":"pure-workbench","version":2,"source":"state x=80","draft":"state x=80", + "states":[{"name":"x","value":350}]} +``` + +`source` is the applied definition; optional `draft` is separate unapplied editor +text (default source). Each is at most 32768 UTF-8 bytes. Draft syntax may be +unfinished. State records must contain exactly one finite value for every declared +state name, with no extra, missing or duplicate names. Record order is irrelevant. +JSON numbers preserve finite f64 values, except negative zero which is encoded as +the exact string `"-0"`. No other numeric string is accepted. Input latches, running +clock, diagnostics, caches, DOM, compiled IR and external resources are not saved. +The serialized input file limit is 1449984 bytes, including escaped source/draft +and up to 32 generated item.field names (which can repeat a long schema name). + +`exportDocument(draft?)` returns an independent version2 object. It refuses an +unapplied runtime, held input or latched execution failure. It reads core state, +without constructing an output snapshot. `restoreDocument(document)` validates +and compiles before staging, refuses held input, then commits the definition and +all saved values together. It returns `{source,draft}`. Failed validation leaves +active program/values/diagnostics intact. A failed core load is aborted; optional +reuse records may be lost as with ordinary apply. Successful restore discards old +reuse, clears diagnostics, resets tick/event count and starts with no held inputs. +The original source initializers remain intact: explicit reset restores those, +not saved values. Existing `apply` continues to preserve current values by name. +Both new document APIs reject reentry during output publication. + +Core exports `put_restored_value(id,value)` and `commit_restored_program()` extend +the existing begin/put/commit loader. Every declared state needs exactly one finite +restored value; duplicate/out-of-range/nonfinite input poisons staging. Restored +commit checks completeness and program validity before touching active state. +Ordinary commit rejects staged restored values. Pending values live alongside the +pending Program in the same union as optional evaluation Tables; active Program +layout does not grow. `layout(6)` now includes Pending, rather than Program only. +Before shared rule bindings, the Wasm Pending was 9656 B (264 B added for 32 values, mask and alignment), +Engine was 19392 B; linear memory remains fixed at 65536 B. These are build-layout +facts, not a browser RAM or performance improvement claim. `evaluation_info(10)` +continues to report only union tag overhead, excluding restoration payload. + +Browser **배치 저장** stores applied source, all current values and editor draft. +It requires a stopped clock. **파일 열기** restores version2 after replacement +confirmation when existing work is present; it starts stopped. Version1/plain +source still only replaces the editor draft. Async reading checks draft generation, +program identity and successful event count so intervening edits/inputs cannot be +overwritten by a stale read. Invalid files do not alter the working document. +**.pure 저장 / 코드 JSON 저장** remain draft-only exports. + +CLI `save-project PATH` writes version2 with exclusive creation; `restore PATH` +restores it. Loading a .json path (including at startup) distinguishes versions. +A browser draft is retained through CLI save-project, but never applied implicitly. +Existing `save PATH` remains applied-source-only. No filesystem overwrite is added. + + +## Shared rules and named records (2026-09-23) + +```text +record Position { x=0; y=0 } +item green = Position(x=80,y=120) +item blue = Position(x=280,y=160) +rule move(t: Position) { t.x=event_x; t.y=event_y } +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +on drag_green.move use move(green) +on drag_blue.move use move(blue) +``` + +A record defines 1..8 named finite numeric initializers. An item has its own values; +optional literal field overrides replace defaults. Its fields occupy contiguous +slots in the existing scalar state array, with public names `item.field`. They +count toward the same 32-state limit. Schemas and items may be forward-referenced. +Unknown/duplicate fields, types or items fail compilation. Scalar and item names +cannot collide; type and rule names have separate namespaces. There are no runtime +record objects, nested records, record copying, dynamic item creation or deletion. + +A rule takes exactly one named record parameter with an exact schema name match. +Its body uses the existing expressions, ordered assignments and conditional blocks. +Parameter fields read/write the connected item's current slots; globals and other +qualified state names remain accessible. The parameter shadows the same qualified +prefix only inside its body. No rule calls from expressions or rule bodies, recursion, +return values, stack frames, or implicit event loop are introduced. Frame handlers +may use rules; coordinate-reading rules may only bind to pointer events. +Unused rules are name/type checked without emitting a body into the VM program. + +`on input.edge use rule(item)` is one event handler. Each used rule's instructions +are emitted once at its first use. Every use adds handler metadata referencing that +same exact instruction range, with parameterBase/parameterCount. Bindings run in +source order, including repeated uses of one item. All matching bindings and +ordinary handlers participate in the same atomic event: a failure in any rolls +back every state write in that event. Error PCs refer to the shared source body. + +Core `bind_handler(handler_index,base,count)` sets a previously unbound staged +handler's contiguous state range. Counts are 1..8; the entire range must fit current +staged state_count. A duplicate binding poisons staging. All owners of shared code +must have the same nonzero field count and exactly the same range. Partial overlap +remains invalid. Parameter opcodes must belong to a bound handler and use an index +below every owner's field count. Pure display evaluation rejects parameter ops; +view bindings use ordinary resolved state loads and their normal cache dependencies. + +Handler metadata grows from 12 to 16 B (base/count plus alignment). There is no +per-field indirection list or per-event parameter-value copy. At the shared-rules step, Wasm Program +was 9648 B, Pending 9912 B, Engine 19904 B: 512 B more fixed reservation across active +and pending handler arrays. The linear-memory limit stays 64 KiB. The required +bind_handler export makes the host reject older incompatible Wasm. + +Apply preserves each qualified field by name. Reordering schema fields or item +instances rebuilds physical bindings without changing their named values. Removing +an item removes its fields; adding it again initializes new fields. An active +input still blocks definition changes. Version2 documents save qualified field +names, not physical offsets; restoration recompiles all bindings and validates all +values before commit. This is static named-field identity, not a general lifetime +handle for dynamically deleted/reused objects. Example `id` and `selected` are +ordinary author-defined numbers; uniqueness/selection policy is not enforced by +an implicit object system. + + +## Target-routed pointer groups (2026-09-23) + +```text +input drag = pointer { green_box: green; blue_box: blue } +on drag.move use move(drag.target) +``` + +A group resolves named views to named items of exactly one record type. Forward +references work. Every view may be connected only once across single-target inputs +and groups. Every item may occur in only one group route, even across groups; +this prevents two group captures from writing aliased field ranges. Explicit +ordinary handlers and statically bound rules may still write other states as authored. +Groups must be nonempty. There are at most 24 routes in the program, independently +of the existing eight logical inputs. State/view/instruction limits are unchanged. +`input.target` is a contextual rule argument for the current group's event only, +not a scalar expression or persistent object handle. Wrong groups/types are rejected. + +A group handler is emitted once per authored `on`, independent of target count. +It can share its exact body with statically bound handlers. One route consists of +three bytes: logical input index, field base, field count. The compiler's routes +also retain source view/item names for the host. The host builds one bounded view +lookup Map per successful load; target-name lookup does not scan all group targets +on each event. JS Map/metadata allocations are outside the core memory accounting. + +`put_pointer_target(input,base,count)` appends a staged route, with count 1..8 and +an in-bounds, nonoverlapping state range. Same-input routes must have equal counts. +`bind_target_handler(handler,count)` binds a previously unbound staged handler to +the event target, encoded as parameter_base=255. Commit requires a non-frame event +with matching routes/counts. Existing `bind_handler` accepts only concrete bases. +`dispatch_target(event,route,x,y)` validates the active route's membership in the +event's logical input, then supplies its base to the matching handlers. Missing, +foreign, and out-of-range targets fail before any authored writes. Ordinary dispatch +and dispatch_point reject inputs that require a route. All matching static, routed, +and inline handlers still run in source order within one atomic event. +The VM indexes the route directly; it still scans the bounded handler list to match +event IDs. No claim of constant-time complete dispatch is made. + +Host `point(name,phase,x,y,source,target)` requires a group view name in the final +argument. Legacy single-target calls keep the old form and reject an extra target. +Capture/held ownership is `(input,view)`, so two views in a group can be manipulated +concurrently; a second source cannot steal the same view. Continuations from a +non-owner are ignored. Browser capture records retain the initial view across +movement, release outside the view, and cancellation. `releaseAll` cancels every +owned view once. `held` uses `input:view` for groups and the old input names for +legacy inputs; lastEvent/lastFailure add `target` for group events. + +Route indices live only in the active definition. Apply/restore remain blocked +while inputs are held. Reset clears owners; load rebuilds routes by source names. +Documents still store source and named scalar fields, never physical route indices. +Deletion/recreation at runtime and generation-checked dynamic identity are not added. +CLI uses `point-target NAME VIEW PHASE X Y`; existing `point` is unchanged. + +Each Program adds 80 B (24 three-byte routes, mask/count and alignment). Current +Program=9728 B, Pending=9992 B, Engine=20064 B, an additional 160 B of fixed engine +reservation over the shared-rules step. Handler remains 16 B, Instruction 16 B. +The active IR byte count now includes used routes. Linear memory remains 65536 B. +The host requires put_pointer_target, bind_target_handler, and dispatch_target. + +## Numeric host inputs and native document layout (2026-09-23) + +```text +state ratio = 50 +input split = value +on split.change { ratio = clamp(event_value,20,80) } +``` + +`value` is a numeric input kind, not a state initializer. Its only edge is +`.change`, event ID `33 + input_index` (33..40). `event_value` is reserved and +available only in these handlers; view expressions and pointer/keyboard/frame +bindings cannot read it. Shared rules may use it only when bound to a value input. +A rule requiring both pointer coordinates and a scalar value has no compatible +binding. Values are ephemeral event operands, not hidden state slots. + +Opcode26 pushes the event's numeric value. Core `dispatch_value(event,value)` +requires the scalar event range and a finite value. Ordinary dispatch/dispatch_point +reject scalar event IDs; scalar dispatch rejects other event IDs. Pure evaluation +rejects opcode26. The same ordered, atomic state commit and display-cache invalidation +apply. A later handler failure rolls back all earlier writes in this event. +No new persistent core tables are added: Engine remains 20064 B; linear memory is +65536 B. The existing event context's x payload holds the scalar under an explicit +scalar tag, separate from pointer context. Host requires dispatch_value export. + +Host `change(name,value,source='external')` validates the input kind and value, +returns the dispatch status, and records `.change` and `value` in the last event. +It does not acquire a held-input owner. Repeated equal requests still dispatch; +no authored side effects are dropped. Reentrant output callbacks cannot invoke it. +`down/up/point` reject numeric inputs. Source forms can submit finite numbers through +native number controls. CLI command is `change NAME VALUE`. + +`document.html` is a concrete host, not a generic UI declaration language. It uses +CSS Grid/media queries and native textarea editing, scrolling and text rendering. +The authored policy contains one scalar ratio and no output views. With no declared +views, display evaluation resets its budget then returns without allocating the +96-number/24-flag display publication buffers. No browser layout tree, text buffer, +scroll offset or viewport dimensions are copied into VM state. + +The document host projects ratio to CSS and accessible controls. Values outside +20..80 are rejected at publication, leaving the last valid visual ratio in place; +this host-output error is separate from VM arithmetic rollback. A later valid value +or corrected rule can restore valid output. Policy apply during its own splitter +capture is refused. The generic numeric port itself has no capture semantics. + +The splitter owns one pointer and an original ratio for cancel. Mouse, touch and +keyboard feed the same policy. Moves are coalesced to the latest coordinate once +per animation frame; release flushes a pending move and does not replace it with a +possibly stale release coordinate. Cancel/Escape/blur/capture loss restore the +original ratio. Crossing into narrow stacked layout cancels. Geometry is read at +gesture start and invalidated by resize observation, window resize and scroll; +observer callbacks do not write layout. No geometry reads occur in inactive resize, +scroll or text-edit handlers. A single geometry cache remains in the host between +gestures and is remeasured on every new gesture; it is not a layout-tree mirror. + +The textarea is the editable document authority. The reading DOM contains a second +text projection. There is no additional persistent JS string model, AST or document +copy in Wasm. This does NOT mean a zero-copy editor: native undo/layout storage, +DOM text and transient reads/decoding/encoding are outside core memory accounting. +Text edits schedule one reading publication per frame, deferred through composition. +Publication currently replaces the entire reading text; no incremental text format +or delta editor is claimed. Resize/split/scroll do not trigger text publication. + +Open/save use plain UTF-8 .txt, up to 2 MiB per imported/exported file. Invalid UTF-8 +and oversize files are refused without changing the current document. File reads +use generation/request guards against stale replacement. Native textarea normalizes +line endings; saved text uses LF and no BOM, not byte-identical preservation of +all original files. Files contain text only, not panel ratios or Pure rules. An +unavailable Wasm disables split policy controls while text editing/preview/save stay +available. Native undo is retained for ordinary editing; file/sample replacement +starts a new document value. No complete accessibility certification or physical +mobile/IME test is claimed by the browser automation. diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/cli.mjs b/prototypes/workbench/results/2026-09-23-web-layout/sources/cli.mjs new file mode 100644 index 0000000..4282b52 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/cli.mjs @@ -0,0 +1,103 @@ +#!/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'; + +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 저장한 배치를 복원 (초안이 있으면 파일에 보존) + 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; + 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 === '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/workbench/results/2026-09-23-web-layout/sources/core.zig b/prototypes/workbench/results/2026-09-23-web-layout/sources/core.zig new file mode 100644 index 0000000..b9a0177 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/core.zig @@ -0,0 +1,1443 @@ +//! Bounded scalar interaction VM experiment. No application behavior is built in. +//! Same source for native tests and freestanding Wasm; no allocation or imports. +//! Error ABI: 0 ok, 1 capacity, 2 invalid, 3 stack, 4 division by zero, +//! 5 non-finite, 6 bad branch, 7 bad state, 8 bad frame, 9 bad event. +//! Counters wrap modulo 2^32. Failed loads preserve the active program, authored +//! state and event diagnostics; beginning a load discards optional reuse records. +//! Failed events preserve values and counters but +//! report their error and attempted instruction count. Binding evaluation is pure. + +const std = @import("std"); +const Evaluation = @import("evaluation.zig").Evaluation; +const Tables = @import("evaluation.zig").Tables; + +const max_states = 32; +const max_inputs = 8; +const max_instructions = 512; +const max_handlers = 64; +const max_stack = 64; +const max_pointer_targets = 24; +const event_target = 255; +const no_pc = std.math.maxInt(u32); +const frame_dt: f64 = 1.0 / 60.0; + +const Code = enum(u32) { + ok = 0, + capacity = 1, + invalid = 2, + stack = 3, + division_by_zero = 4, + non_finite = 5, + bad_branch = 6, + bad_state = 7, + bad_frame = 8, + bad_event = 9, + work_limit = 10, +}; + +const Instruction = struct { value: f64 = 0, op: u32 = 0, arg: u32 = 0 }; +const Handler = struct { + event: u32 = 0, start: u32 = 0, length: u32 = 0, + parameter_base: u8 = 0, parameter_count: u8 = 0, +}; +const PointerTarget = struct { input: u8, base: u8, count: u8 }; +const Program = struct { + pointer_targets: [max_pointer_targets]PointerTarget = undefined, + pointer_target_count: u8 = 0, + routed_inputs: u8 = 0, + instructions: [max_instructions]Instruction = @splat(.{}), + handlers: [max_handlers]Handler = @splat(.{}), + initials: [max_states]f64 = @splat(0), + old_ids: [max_states]i32 = @splat(-1), + declared: [max_states]bool = @splat(false), + state_count: u32 = 0, + input_count: u32 = 0, + instruction_count: u32 = 0, + handler_count: u32 = 0, +}; +// Loading a definition/optional restored values and result reuse are mutually exclusive phases. Current execution +// always reads active, and can recompute bindings while this workspace loads. +const Pending = struct { + program: Program = .{}, + values: [max_states]f64 = @splat(0), + value_mask: u32 = 0, +}; +const Workspace = union { staging: Pending, reuse: Tables }; +const Engine = struct { + active: Program = .{}, + workspace: Workspace = .{ .reuse = .{} }, + values: [max_states]f64 = @splat(0), + staging_open: bool = false, + staging_error: Code = .ok, + tick: u32 = 0, + last_error: Code = .ok, + error_pc: u32 = no_pc, + last_ops: u32 = 0, + revision: u32 = 0, + successful_events: u32 = 0, + evaluation: Evaluation = .{}, +}; +var engine: Engine = .{}; + +fn reuseTables() ?*Tables { + return if (engine.staging_open) null else &engine.workspace.reuse; +} + +fn finishStaging() void { + engine.workspace = .{ .reuse = .{} }; + engine.staging_open = false; + engine.evaluation.forget(null, 3); +} + +fn public(code: Code) u32 { + return @intFromEnum(code); +} + +fn stagingFail(code: Code) u32 { + if (engine.staging_error == .ok) engine.staging_error = code; + return public(code); +} + +export fn reset_all() void { + engine = .{}; +} + +export fn begin_program(state_count: u32, input_count: u32) u32 { + engine.staging_error = .ok; + if (state_count > max_states or input_count > max_inputs) { + if (engine.staging_open) finishStaging(); + return stagingFail(.capacity); + } + engine.workspace = .{ .staging = .{ .program = .{ .state_count = state_count, .input_count = input_count } } }; + engine.staging_open = true; + engine.evaluation.forget(null, 3); + return 0; +} + +export fn abort_program() void { + if (engine.staging_open) finishStaging(); +} + +export fn put_state(id: u32, initial: f64, old_id: i32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + if (id >= engine.workspace.staging.program.state_count or old_id < -1) return stagingFail(.bad_state); + if (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count) return stagingFail(.bad_state); + if (engine.workspace.staging.program.declared[id]) return stagingFail(.bad_state); + if (!std.math.isFinite(initial)) return stagingFail(.non_finite); + engine.workspace.staging.program.initials[id] = initial; + engine.workspace.staging.program.old_ids[id] = old_id; + engine.workspace.staging.program.declared[id] = true; + return 0; +} + +export fn put_instruction(op: u32, arg: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.instruction_count == max_instructions) return stagingFail(.capacity); + if (op > 26) return stagingFail(.invalid); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + p.instructions[p.instruction_count] = .{ .op = op, .arg = arg, .value = value }; + p.instruction_count += 1; + return 0; +} + +fn validEvent(event: u32, inputs: u32) bool { + return event <= inputs * 2 or + (event >= 17 and event - 17 < inputs) or + (event >= 25 and event - 25 < inputs) or + (event >= 33 and event - 33 < inputs); +} + +export fn put_handler(event: u32, start: u32, length: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.handler_count == max_handlers) return stagingFail(.capacity); + if (!validEvent(event, p.input_count)) return stagingFail(.bad_event); + // Handlers may be appended before their instructions, so range validation + // is deferred until commit. Arithmetic remains guarded there. + p.handlers[p.handler_count] = .{ .event = event, .start = start, .length = length }; + p.handler_count += 1; + return 0; +} + +export fn bind_handler(handler: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or base >= p.state_count) return stagingFail(.bad_state); + if (count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = @intCast(base); + h.parameter_count = @intCast(count); + return 0; +} + +// A route is a static program-local index, never a persistent object identity. +export fn put_pointer_target(input: u32, base: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (p.pointer_target_count == max_pointer_targets) return stagingFail(.capacity); + if (input >= p.input_count) return stagingFail(.bad_event); + if (base >= p.state_count or count == 0 or count > 8 or count > p.state_count - base) return stagingFail(.bad_state); + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (base < @as(u32, t.base) + t.count and t.base < base + count) return stagingFail(.invalid); + if (t.input == input and t.count != count) return stagingFail(.bad_state); + } + p.pointer_targets[p.pointer_target_count] = .{ .input = @intCast(input), .base = @intCast(base), .count = @intCast(count) }; + p.pointer_target_count += 1; + p.routed_inputs |= @as(u8, 1) << @as(u3, @intCast(input)); + return 0; +} + +export fn bind_target_handler(handler: u32, count: u32) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const p = &engine.workspace.staging.program; + if (handler >= p.handler_count or count == 0 or count > 8) return stagingFail(.bad_state); + const h = &p.handlers[handler]; + if (h.parameter_count != 0) return stagingFail(.invalid); + h.parameter_base = event_target; + h.parameter_count = @intCast(count); + return 0; +} + +fn eventInput(event: u32) u32 { + return if (event >= 33) event - 33 else if (event >= 25) event - 25 else if (event >= 17) event - 17 else (event - 1) / 2; +} + +fn validate(p: *const Program) Code { + for (0..p.state_count) |id| { + if (!p.declared[id]) return .bad_state; + if (!std.math.isFinite(p.initials[id])) return .non_finite; + const old_id = p.old_ids[id]; + if (old_id < -1 or (old_id >= 0 and @as(u32, @intCast(old_id)) >= engine.active.state_count)) return .bad_state; + } + for (p.handlers[0..p.handler_count], 0..) |h, i| { + if (!validEvent(h.event, p.input_count)) return .bad_event; + if (h.start > p.instruction_count or h.length > p.instruction_count - h.start) return .invalid; + if (h.parameter_base == event_target) { + if (h.event == 0 or h.event >= 33 or h.parameter_count == 0 or h.parameter_count > 8) return .bad_event; + var found = false; + for (p.pointer_targets[0..p.pointer_target_count]) |t| { + if (t.input != eventInput(h.event)) continue; + if (t.count != h.parameter_count) return .bad_state; + found = true; + } + if (!found) return .bad_event; + } else if (h.parameter_count > 8 or @as(u32, h.parameter_base) + h.parameter_count > p.state_count) return .bad_state; + if (h.length == 0) continue; + for (p.handlers[0..i]) |other| { + if (other.length != 0 and h.start < other.start + other.length and other.start < h.start + h.length) { + // Bound rules may share exactly one body, never a partial range. + if (h.parameter_count == 0 or h.parameter_count != other.parameter_count or + h.start != other.start or h.length != other.length) return .invalid; + } + } + } + for (p.instructions[0..p.instruction_count], 0..) |inst, pc| { + if (inst.op > 26) return .invalid; + if (!std.math.isFinite(inst.value)) return .non_finite; + if ((inst.op == 1 or inst.op == 20) and inst.arg >= p.state_count) return .bad_state; + if (inst.op >= 20) { + var owner: ?Handler = null; + for (p.handlers[0..p.handler_count]) |h| { + if (pc >= h.start and pc < h.start + h.length) { + owner = h; + if (inst.op == 21 and (inst.arg <= pc or inst.arg > h.start + h.length)) return .bad_branch; + if ((inst.op == 24 or inst.op == 25) and inst.arg >= h.parameter_count) return .bad_state; + } + } + if (owner == null) return .invalid; + } + } + return .ok; +} + +export fn put_restored_value(id: u32, value: f64) u32 { + if (!engine.staging_open) return stagingFail(.invalid); + const pending = &engine.workspace.staging; + if (id >= pending.program.state_count) return stagingFail(.bad_state); + const bit = @as(u32, 1) << @as(u5, @intCast(id)); + if (pending.value_mask & bit != 0) return stagingFail(.bad_state); + if (!std.math.isFinite(value)) return stagingFail(.non_finite); + pending.values[id] = value; + pending.value_mask |= bit; + return 0; +} + +export fn commit_program() u32 { + return commit(false); +} + +export fn commit_restored_program() u32 { + return commit(true); +} + +fn commit(restoring: bool) u32 { + if (!engine.staging_open) return public(.invalid); + if (engine.staging_error != .ok) return public(engine.staging_error); + const count = engine.workspace.staging.program.state_count; + const expected: u32 = if (count == 32) 0xffffffff else (@as(u32, 1) << @as(u5, @intCast(count))) - 1; + if (engine.workspace.staging.value_mask != (if (restoring) expected else @as(u32, 0))) + return stagingFail(.bad_state); + const code = validate(&engine.workspace.staging.program); + if (code != .ok) return stagingFail(code); + var next: [max_states]f64 = @splat(0); + for (0..engine.workspace.staging.program.state_count) |i| { + const old_id = engine.workspace.staging.program.old_ids[i]; + next[i] = if (restoring) engine.workspace.staging.values[i] else if (old_id < 0) engine.workspace.staging.program.initials[i] else engine.values[@intCast(old_id)]; + } + engine.active = engine.workspace.staging.program; + engine.values = next; + finishStaging(); + if (restoring) { + engine.tick = 0; + engine.successful_events = 0; + } + engine.revision +%= 1; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + return 0; +} + +export fn reset_state() void { + engine.values = engine.active.initials; + engine.evaluation.forget(reuseTables(), 3); + engine.tick = 0; + engine.successful_events = 0; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; +} + +const Execution = struct { + code: Code = .ok, + pc: u32 = no_pc, + ops: u32 = 0, + value: f64 = 0, + dependencies: u32 = 0, +}; + +fn executionFail(result: Execution, code: Code, pc: u32) Execution { + var failed = result; + failed.code = code; + failed.pc = pc; + return failed; +} + +const EventContext = struct { dt: f64 = 0, x: f64 = 0, y: f64 = 0, parameter_base: u32 = 0, parameter_count: u32 = 0, point: bool = false, scalar: bool = false, target: ?u32 = null }; + +fn execute(values: *[max_states]f64, start: u32, length: u32, context: EventContext, pure: bool, budget: ?*Evaluation) Execution { + const p = &engine.active; + var result: Execution = .{}; + if (start > p.instruction_count or length > p.instruction_count - start) return executionFail(result, .invalid, no_pc); + const end = start + length; + var stack: [max_stack]f64 = undefined; + var sp: usize = 0; + var pc = start; + while (pc < end) { + if (budget) |b| { + if (!b.spend()) return executionFail(result, .work_limit, pc); + b.instructions += 1; + } + const inst = p.instructions[pc]; + result.ops += 1; + if (pure and (inst.op == 2 or inst.op >= 20)) return executionFail(result, .invalid, pc); + const need: usize = switch (inst.op) { + 0...2, 22, 23, 24, 26 => 0, + 7, 16, 20, 21, 25 => 1, + 3...6, 8...15, 17, 18 => 2, + 19 => 3, + else => return executionFail(result, .invalid, pc), + }; + if (sp < need) return executionFail(result, .stack, pc); + if (inst.op <= 2 or inst.op == 22 or inst.op == 23 or inst.op == 24 or inst.op == 26) { + if (sp == max_stack) return executionFail(result, .stack, pc); + const value = switch (inst.op) { + 0 => inst.value, + 1 => blk: { + if (inst.arg >= p.state_count) return executionFail(result, .bad_state, pc); + if (budget != null and budget.?.cache_count != 0) + result.dependencies |= @as(u32, 1) << @as(u5, @intCast(inst.arg)); + break :blk values[inst.arg]; + }, + 24 => blk: { + if (inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = context.parameter_base + inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + break :blk values[state]; + }, + 26 => if (context.scalar) context.x else return executionFail(result, .bad_event, pc), + 2 => context.dt, + 22, 23 => if (!context.point) return executionFail(result, .bad_event, pc) else if (inst.op == 22) context.x else context.y, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + stack[sp] = value; + sp += 1; + } else if (inst.op == 20 or inst.op == 25) { + if (inst.op == 25 and inst.arg >= context.parameter_count) return executionFail(result, .bad_state, pc); + const state = if (inst.op == 25) context.parameter_base + inst.arg else inst.arg; + if (state >= p.state_count) return executionFail(result, .bad_state, pc); + sp -= 1; + values[state] = stack[sp]; + } else if (inst.op == 21) { + if (inst.arg <= pc or inst.arg > end) return executionFail(result, .bad_branch, pc); + sp -= 1; + if (stack[sp] == 0) { + pc = inst.arg; + continue; + } + } else { + const a = stack[sp - need]; + const b = if (need >= 2) stack[sp - need + 1] else 0; + const value: f64 = switch (inst.op) { + 3 => a + b, + 4 => a - b, + 5 => a * b, + 6 => blk: { + if (b == 0) return executionFail(result, .division_by_zero, pc); + break :blk a / b; + }, + 7 => -a, + 8 => @floatFromInt(@intFromBool(a < b)), + 9 => @floatFromInt(@intFromBool(a <= b)), + 10 => @floatFromInt(@intFromBool(a > b)), + 11 => @floatFromInt(@intFromBool(a >= b)), + 12 => @floatFromInt(@intFromBool(a == b)), + 13 => @floatFromInt(@intFromBool(a != b)), + 14 => @floatFromInt(@intFromBool(a != 0 and b != 0)), + 15 => @floatFromInt(@intFromBool(a != 0 or b != 0)), + 16 => @floatFromInt(@intFromBool(a == 0)), + 17 => @min(a, b), + 18 => @max(a, b), + 19 => blk: { + const hi = stack[sp - 1]; + if (b > hi) return executionFail(result, .invalid, pc); + break :blk @max(b, @min(a, hi)); + }, + else => unreachable, + }; + if (!std.math.isFinite(value)) return executionFail(result, .non_finite, pc); + sp = sp - need + 1; + stack[sp - 1] = value; + } + pc += 1; + } + if (sp != @as(usize, if (pure) 1 else 0)) return executionFail(result, .stack, end); + if (pure) result.value = stack[0]; + return result; +} + +export fn dispatch(event: u32, dt: f64) u32 { + return dispatchContext(event, .{ .dt = dt }); +} + +export fn dispatch_point(event: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true }); +} + +export fn dispatch_target(event: u32, target: u32, x: f64, y: f64) u32 { + return dispatchContext(event, .{ .x = x, .y = y, .point = true, .target = target }); +} + +export fn dispatch_value(event: u32, value: f64) u32 { + return dispatchContext(event, .{ .x = value, .scalar = true }); +} + +fn dispatchContext(event: u32, context: EventContext) u32 { + const dt = context.dt; + engine.last_error = .ok; + engine.error_pc = no_pc; + engine.last_ops = 0; + if (!validEvent(event, engine.active.input_count) or + ((context.point or context.scalar) and event == 0) or + (context.scalar != (event >= 33)) or + (!context.point and !context.scalar and event > engine.active.input_count * 2)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + if (!std.math.isFinite(dt) or !std.math.isFinite(context.x) or !std.math.isFinite(context.y)) { + engine.last_error = .non_finite; + return public(.non_finite); + } + if ((event == 0 and dt != frame_dt) or (event != 0 and dt != 0)) { + engine.last_error = .bad_frame; + return public(.bad_frame); + } + var route: ?PointerTarget = null; + const routed = event != 0 and engine.active.routed_inputs & (@as(u8, 1) << @as(u3, @intCast(eventInput(event)))) != 0; + if (context.target) |target| { + if (!routed or target >= engine.active.pointer_target_count or + engine.active.pointer_targets[target].input != eventInput(event)) { + engine.last_error = .bad_event; + return public(.bad_event); + } + route = engine.active.pointer_targets[target]; + } else if (routed) { + engine.last_error = .bad_event; + return public(.bad_event); + } + var pending = engine.values; + for (engine.active.handlers[0..engine.active.handler_count]) |h| { + if (h.event != event) continue; + var bound = context; + bound.parameter_base = if (h.parameter_base == event_target) route.?.base else h.parameter_base; + bound.parameter_count = h.parameter_count; + const result = execute(&pending, h.start, h.length, bound, false, null); + engine.last_ops += result.ops; + if (result.code != .ok) { + engine.last_error = result.code; + engine.error_pc = result.pc; + return public(result.code); + } + } + if (engine.evaluation.cache_count != 0 and !engine.staging_open) { + var changed: u32 = 0; + for (0..engine.active.state_count) |i| { + // Signed zero is observable, so compare representation, not ==. + if (@as(u64, @bitCast(pending[i])) != @as(u64, @bitCast(engine.values[i]))) + changed |= @as(u32, 1) << @as(u5, @intCast(i)); + } + engine.evaluation.changed(reuseTables(), changed); + } + engine.values = pending; + if (event == 0) engine.tick +%= 1; + engine.successful_events +%= 1; + return 0; +} + +export fn eval_at(start: u32, length: u32) f64 { + // A local copy makes the no-side-effects boundary structural as well as + // checked by the pure instruction filter. Diagnostics are never touched. + var values = engine.values; + const result = execute(&values, start, length, .{}, true, null); + return if (result.code == .ok) result.value else std.math.nan(f64); +} + +export fn configure_evaluation(cache: u32, history: u32) u32 { + return if (engine.evaluation.configure(reuseTables(), cache, history)) 0 else public(.capacity); +} + +export fn forget_evaluation(flags: u32) void { + engine.evaluation.forget(reuseTables(), flags); +} + +export fn begin_evaluation(limit: u32) void { + engine.evaluation.begin(limit); +} + +export fn eval_bounded(start: u32, length: u32) f64 { + const e = &engine.evaluation; + e.status = 0; + // Charge per request as well as per instruction and validation probe. + if (!e.spend()) return std.math.nan(f64); + if (e.lookup(reuseTables(), start, length)) |value| return value; + if (e.status != 0) return std.math.nan(f64); + var values = engine.values; + const result = execute(&values, start, length, .{}, true, e); + e.status = public(result.code); + if (result.code != .ok) return std.math.nan(f64); + e.remember(reuseTables(), start, length, result.value, result.dependencies); + return result.value; +} + +export fn evaluation_info(id: u32) u32 { + const e = &engine.evaluation; + return switch (id) { + 0 => e.status, + 1 => e.work, + 2 => e.instructions, + 3 => e.hits, + 4 => e.cache_count, + 5 => e.history_count, + 6 => @sizeOf(Evaluation) + @sizeOf(Tables), + 7 => @sizeOf(Evaluation), + 8 => @sizeOf(Tables), + 9 => @intFromBool(!engine.staging_open), + 10 => @sizeOf(Workspace) - @sizeOf(Pending), + else => 0, + }; +} + +export fn get_state(id: u32) f64 { + return if (id < engine.active.state_count) engine.values[id] else std.math.nan(f64); +} + +export fn info(id: u32) u32 { + return switch (id) { + 0 => engine.tick, + 1 => public(engine.last_error), + 2 => engine.error_pc, + 3 => engine.last_ops, + 4 => engine.revision, + 5 => engine.active.state_count, + 6 => engine.active.instruction_count, + 7 => engine.active.handler_count, + 8 => engine.active.input_count, + 9 => engine.successful_events, + else => 0, + }; +} + +export fn layout(id: u32) u32 { + return switch (id) { + 0 => @sizeOf(Engine), + 1 => @sizeOf(Program), + 2 => @sizeOf(Instruction), + 3 => @sizeOf(Handler), + 4 => engine.active.instruction_count * @sizeOf(Instruction) + engine.active.handler_count * @sizeOf(Handler) + @as(u32, engine.active.pointer_target_count) * @sizeOf(PointerTarget), + 5 => engine.active.state_count * @sizeOf(f64), + 6 => @sizeOf(Pending), + 7 => max_stack * @sizeOf(f64), + 8 => @sizeOf(PointerTarget), + 9 => engine.active.pointer_target_count, + else => 0, + }; +} + +// Test helpers describe programs directly; the product compiler is separately +// tested against this ABI. These tests exercise semantics and failure boundaries. +fn ok(code: u32) !void { + try std.testing.expectEqual(@as(u32, 0), code); +} + +fn emit(op: u32, arg: u32, value: f64) !void { + try ok(put_instruction(op, arg, value)); +} + +fn counterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try ok(commit_program()); +} + +test "blank program, no hidden application behavior, and reset" { + reset_all(); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 1), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(3)); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); + try std.testing.expect(std.math.isNan(eval_at(0, 0))); + reset_state(); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 1), info(4)); +} + +test "arbitrary input counter and unused events" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(2, 0)); + try ok(dispatch(0, frame_dt)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(3)); + reset_state(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 4), info(6)); +} + +test "charge and release behavior exists only in the authored instruction program" { + reset_all(); + try ok(begin_program(3, 1)); // held, charge, released_value + for (0..3) |i| try ok(put_state(@intCast(i), 0, -1)); + try emit(0, 0, 1); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 2)); + try emit(1, 1, 0); + try emit(1, 0, 0); + try emit(2, 0, 0); + try emit(5, 0, 0); + try emit(3, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(0, 2, 6)); + try emit(1, 1, 0); + try emit(20, 2, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(2, 8, 6)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + try ok(dispatch(1, 0)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try ok(dispatch(2, 0)); + try std.testing.expectApproxEqAbs(@as(f64, 0.5), get_state(2), 1e-12); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); + for (0..30) |_| try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 0), get_state(1)); +} + +test "forward condition executes once and assignments and handlers are ordered" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 1, -1)); + try ok(put_state(1, 0, -1)); + try emit(1, 0, 0); + try emit(21, 6, 0); + try emit(0, 0, 0); + try emit(20, 0, 0); + try emit(0, 0, 7); + try emit(20, 1, 0); + try ok(put_handler(1, 0, 6)); + try emit(1, 1, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(1, 6, 4)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 14), get_state(1)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 28), get_state(1)); + try std.testing.expectEqual(@as(u32, 6), info(3)); +} + +test "branch to program end is valid" { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, 2, 0); + try ok(put_handler(0, 0, 2)); + try ok(commit_program()); + try ok(dispatch(0, frame_dt)); +} + +test "one event rolls back writes from all handlers on arithmetic failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 8, -1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 2)); + try emit(0, 0, 1); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); + try std.testing.expectEqual(@as(u32, 5), info(3)); +} + +test "nonfinite arithmetic and invalid clamp fail atomically" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, -1)); + try emit(0, 0, 1e308); + try emit(0, 0, 1e308); + try emit(5, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.non_finite), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 5, 0)); + try emit(0, 0, 1); + try emit(0, 0, 10); + try emit(0, 0, 2); + try emit(19, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(0, 0, 5)); + try ok(commit_program()); + try std.testing.expectEqual(public(.invalid), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(f64, 5), get_state(0)); +} + +test "stack underflow overflow and leftover values reject the event" { + for (0..3) |mode| { + reset_all(); + try ok(begin_program(0, 0)); + if (mode == 0) { + try emit(3, 0, 0); + } else { + for (0..@as(usize, if (mode == 1) 65 else 1)) |_| try emit(0, 0, 7); + } + try ok(put_handler(0, 0, engine.workspace.staging.program.instruction_count)); + try ok(commit_program()); + try std.testing.expectEqual(public(.stack), dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + } +} + +test "failed candidates leave active values program revision and diagnostics unchanged" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const old_revision = info(4); + const old_ops = info(3); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(1, 5, 0); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try std.testing.expectEqual(old_revision, info(4)); + try std.testing.expectEqual(old_ops, info(3)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.invalid), put_instruction(100, 0, 0)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "state preservation mapping reads active values at commit and reset uses new initials" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(2, 0)); + try ok(put_state(0, 42, -1)); // new symbol first + try ok(put_state(1, 100, 0)); // old symbol moves from id 0 to 1 + try ok(dispatch(1, 0)); // changed after staging began + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 42), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(4)); + try ok(begin_program(0, 0)); + try ok(commit_program()); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "capacity boundaries and sticky load errors" { + reset_all(); + try std.testing.expectEqual(public(.capacity), begin_program(33, 0)); + try std.testing.expectEqual(public(.capacity), begin_program(0, 9)); + try ok(begin_program(32, 8)); + for (0..32) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + for (0..512) |_| try emit(0, 0, 0); + for (0..64) |_| try ok(put_handler(16, 0, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 512), info(6)); + try std.testing.expectEqual(@as(u32, 64), info(7)); + try std.testing.expectEqual(@as(f64, 31), get_state(31)); + try ok(begin_program(0, 0)); + for (0..512) |_| try emit(0, 0, 0); + try std.testing.expectEqual(public(.capacity), put_instruction(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); + try ok(begin_program(0, 0)); + for (0..64) |_| try ok(put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), put_handler(0, 0, 0)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "invalid declaration values state mappings event ids and handler ranges" { + reset_all(); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 0, 0)); + try ok(begin_program(1, 0)); + try std.testing.expectEqual(public(.non_finite), put_state(0, std.math.inf(f64), -1)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try std.testing.expectEqual(public(.bad_state), put_state(0, 1, -1)); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.non_finite), put_instruction(0, 0, std.math.nan(f64))); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.bad_event), put_handler(1, 0, 0)); + try ok(begin_program(0, 0)); + try ok(put_handler(0, no_pc, 5)); + try std.testing.expectEqual(public(.invalid), commit_program()); + try ok(begin_program(0, 0)); + try emit(0, 0, 1); + try ok(put_handler(0, 0, 1)); + try ok(put_handler(0, 0, 1)); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "backward self out-of-handler branches and stores without handlers are rejected" { + for ([_]u32{ 0, 1, 3 }) |target| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, 0); + try emit(21, target, 0); + try emit(0, 0, 7); + try ok(put_handler(0, 0, 2)); + try std.testing.expectEqual(public(.bad_branch), commit_program()); + } + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 0, -1)); + try emit(0, 0, 5); + try emit(20, 0, 0); + try std.testing.expectEqual(public(.invalid), commit_program()); +} + +test "event inputs and dt enforce deterministic frame contract" { + reset_all(); + try counterProgram(); + try std.testing.expectEqual(public(.bad_event), dispatch(3, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(no_pc, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + try std.testing.expectEqual(public(.bad_frame), dispatch(1, frame_dt)); + try std.testing.expectEqual(public(.non_finite), dispatch(0, std.math.nan(f64))); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(u32, 0), info(1)); + try std.testing.expectEqual(no_pc, info(2)); +} + +test "binding evaluation is pure on both success and failure" { + reset_all(); + try ok(begin_program(1, 0)); + try ok(put_state(0, 4, -1)); + try emit(1, 0, 0); + try emit(0, 0, 3); + try emit(5, 0, 0); + try emit(20, 0, 0); + try emit(2, 0, 0); + try emit(0, 0, 0); + try emit(21, 7, 0); + try ok(put_handler(0, 3, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.bad_frame), dispatch(0, 0)); + const before = engine; + try std.testing.expectEqual(@as(f64, 12), eval_at(0, 3)); + try std.testing.expect(std.math.isNan(eval_at(0, 4))); // attempted store + try std.testing.expect(std.math.isNan(eval_at(4, 1))); // dt forbidden in views + try std.testing.expect(std.math.isNan(eval_at(5, 2))); // branch forbidden + try std.testing.expect(std.math.isNan(eval_at(no_pc, 7))); + try std.testing.expect(std.math.isNan(eval_at(0, no_pc))); + try std.testing.expect(std.math.isNan(eval_at(0, 2))); // two results + // Compare only the live member of the untagged workspace, never padding + // or the inactive member's indeterminate storage. + try std.testing.expectEqual(before.staging_open, engine.staging_open); + inline for (std.meta.fields(Engine)) |field| { + if (comptime std.mem.eql(u8, field.name, "workspace")) { + if (before.staging_open) { + try std.testing.expectEqualDeep(before.workspace.staging, engine.workspace.staging); + } else { + try std.testing.expectEqualDeep(before.workspace.reuse, engine.workspace.reuse); + } + } else try std.testing.expectEqualDeep(@field(before, field.name), @field(engine, field.name)); + } +} + +test "arithmetic comparison logical min max and clamp expression semantics" { + const cases = [_]struct { op: u32, a: f64, b: f64, want: f64 }{ + .{ .op = 3, .a = 7, .b = 2, .want = 9 }, + .{ .op = 4, .a = 7, .b = 2, .want = 5 }, + .{ .op = 5, .a = 7, .b = 2, .want = 14 }, + .{ .op = 6, .a = 7, .b = 2, .want = 3.5 }, + .{ .op = 8, .a = 2, .b = 7, .want = 1 }, + .{ .op = 9, .a = 7, .b = 7, .want = 1 }, + .{ .op = 10, .a = 2, .b = 7, .want = 0 }, + .{ .op = 11, .a = 7, .b = 7, .want = 1 }, + .{ .op = 12, .a = 7, .b = 2, .want = 0 }, + .{ .op = 13, .a = 7, .b = 2, .want = 1 }, + .{ .op = 14, .a = -7, .b = 2, .want = 1 }, + .{ .op = 15, .a = 0, .b = 2, .want = 1 }, + .{ .op = 17, .a = 7, .b = 2, .want = 2 }, + .{ .op = 18, .a = 7, .b = 2, .want = 7 }, + }; + for (cases) |case| { + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, case.a); + try emit(0, 0, case.b); + try emit(case.op, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(case.want, eval_at(0, 3)); + } + reset_all(); + try ok(begin_program(0, 0)); + try emit(0, 0, -8); + try emit(7, 0, 0); + try emit(0, 0, 0); + try emit(0, 0, 5); + try emit(19, 0, 0); + try emit(16, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 5), eval_at(0, 5)); + try std.testing.expectEqual(@as(f64, 0), eval_at(0, 6)); +} + +test "diagnostic counters wrap without affecting scalar values" { + reset_all(); + try counterProgram(); + engine.tick = no_pc; + engine.successful_events = no_pc; + engine.revision = no_pc; + try ok(dispatch(0, frame_dt)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(1, 0)); + try ok(put_state(0, 99, 0)); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, 0), info(4)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); +} + +test "memory accounting separates reserved storage used IR and stack capacity" { + reset_all(); + try std.testing.expectEqual(@as(u32, 0), layout(4)); + try std.testing.expectEqual(@as(u32, 0), layout(5)); + try counterProgram(); + try std.testing.expectEqual(@as(u32, @sizeOf(Engine)), layout(0)); + try std.testing.expectEqual(@as(u32, 16), layout(2)); + try std.testing.expectEqual(@as(u32, 16), layout(3)); + try std.testing.expectEqual(@as(u32, 80), layout(4)); + try std.testing.expectEqual(@as(u32, 8), layout(5)); + try std.testing.expectEqual(@as(u32, @sizeOf(Pending)), layout(6)); + try std.testing.expect(layout(6) >= layout(1) + max_states * @sizeOf(f64)); + try std.testing.expect(layout(0) >= layout(1) * 2); + try std.testing.expectEqual(@as(u32, 512), layout(7)); + try std.testing.expect(layout(0) + 16 * 1024 < 64 * 1024); +} + +fn displayCounterProgram() !void { + try ok(begin_program(1, 1)); + try ok(put_state(0, 10, -1)); + try emit(1, 0, 0); + try emit(0, 0, 1); + try emit(3, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(1, 0, 4)); + try emit(1, 0, 0); + try emit(0, 0, 2); + try emit(5, 0, 0); + try ok(commit_program()); + try ok(configure_evaluation(32, 8)); +} + +fn boundedEquals(want: f64, start: u32, length: u32) !void { + begin_evaluation(100); + try std.testing.expectEqual(want, eval_bounded(start, length)); + try ok(evaluation_info(0)); +} + +test "point coordinates are ephemeral event operands and unavailable to pure evaluation" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + const events = [_]u32{ 1, 17, 2, 25 }; + for (events, 0..) |event, i| { + try emit(22, 0, 0); + try emit(20, 0, 0); + try emit(23, 0, 0); + try emit(20, 1, 0); + try ok(put_handler(event, @intCast(i * 4), 4)); + } + try ok(commit_program()); + for (events, 0..) |event, i| { + const x: f64 = @floatFromInt(i + 10); + try ok(dispatch_point(event, x, -x)); + try std.testing.expectEqual(x, get_state(0)); + try std.testing.expectEqual(-x, get_state(1)); + } + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 13), get_state(0)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); + try std.testing.expectEqual(@as(u32, 4), info(9)); +} + +test "point event ranges reject holes and invalid payloads without committing" { + reset_all(); + try counterProgram(); + for ([_]u32{ 0, 3, 16, 18, 24, 26, 32, 33, no_pc }) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_point(event, 1, 2)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, std.math.nan(f64), 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_point(1, 0, std.math.inf(f64))); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try ok(begin_program(0, 8)); + try ok(put_handler(24, 0, 0)); + try ok(put_handler(32, 0, 0)); + try ok(commit_program()); + try ok(dispatch_point(24, 1, 2)); + try ok(dispatch_point(32, 1, 2)); + try std.testing.expectEqual(public(.bad_event), dispatch(24, 0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "point event failure rolls back writes from earlier matching handlers" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 7, -1)); + try emit(22, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 0, 2)); + try emit(23, 0, 0); + try emit(0, 0, 0); + try emit(6, 0, 0); + try emit(20, 0, 0); + try ok(put_handler(17, 2, 4)); + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch_point(17, 400, 200)); + try std.testing.expectEqual(@as(f64, 7), get_state(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + try std.testing.expectEqual(@as(u32, 4), info(2)); +} + +test "loading shares storage with records while old events and bindings remain executable" { + reset_all(); + try displayCounterProgram(); + try boundedEquals(20, 4, 3); + try boundedEquals(20, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(9)); + try emit(1, 0, 0); + try ok(dispatch(1, 0)); // Active state advances after staging begins. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 0), evaluation_info(3)); + try ok(configure_evaluation(32, 1)); + forget_evaluation(3); // Must not overwrite the pending program. + try emit(0, 0, 3); + try emit(5, 0, 0); + try ok(commit_program()); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(33, 0, 3); + try boundedEquals(33, 0, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); +} + +test "failed and abandoned candidates restore empty reuse storage without changing active work" { + reset_all(); + try displayCounterProgram(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + try emit(20, 0, 0); // Store without a handler fails validation. + try std.testing.expectEqual(public(.invalid), commit_program()); + try boundedEquals(20, 4, 3); + try ok(dispatch(1, 0)); + abort_program(); + try boundedEquals(22, 4, 3); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + abort_program(); // Inactive abort is a no-op, including for warmed records. + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(3)); + try ok(begin_program(1, 1)); + try std.testing.expectEqual(public(.capacity), begin_program(max_states + 1, 0)); + try std.testing.expectEqual(@as(u32, 1), evaluation_info(9)); + try boundedEquals(22, 4, 3); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); +} + +test "full pending program survives repeated evaluation configuration record loss and reset" { + reset_all(); + try displayCounterProgram(); + try ok(dispatch(1, 0)); + try ok(begin_program(1, 1)); + try ok(put_state(0, 900, 0)); + for (0..max_instructions) |i| { + try emit(0, 0, @floatFromInt(i)); + try ok(configure_evaluation(32, @intCast(i % 9))); + forget_evaluation(3); + try boundedEquals(22, 4, 3); + } + reset_state(); // The pending definition and old-id mapping survive reset. + try boundedEquals(20, 4, 3); + try ok(commit_program()); + try std.testing.expectEqual(@as(u32, max_instructions), info(6)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try boundedEquals(511, 511, 1); + // Safe Zig builds may reserve a hidden union tag in addition to the payload. + try std.testing.expect(@sizeOf(Workspace) >= @sizeOf(Pending)); + try std.testing.expect(@sizeOf(Workspace) <= @sizeOf(Pending) + @alignOf(Pending)); + try std.testing.expectEqual(@as(u32, @sizeOf(Tables)), evaluation_info(8)); +} + +test "restored definition and values commit together without changing reset initializers" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + try ok(dispatch(0, frame_dt)); + const revision = info(4); + try ok(begin_program(2, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_state(1, 200, -1)); + try ok(put_restored_value(1, -0.0)); + try ok(put_restored_value(0, 37)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try ok(dispatch(1, 0)); // Old execution remains independent of pending values. + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(f64, 37), get_state(0)); + try std.testing.expectEqual(@as(u64, @bitCast(@as(f64, -0.0))), @as(u64, @bitCast(get_state(1)))); + try std.testing.expectEqual(revision + 1, info(4)); + try std.testing.expectEqual(@as(u32, 0), info(0)); + try std.testing.expectEqual(@as(u32, 0), info(9)); + reset_state(); + try std.testing.expectEqual(@as(f64, 100), get_state(0)); + try std.testing.expectEqual(@as(f64, 200), get_state(1)); +} + +test "missing duplicate nonfinite or invalid restored candidates leave active work untouched" { + reset_all(); + try counterProgram(); + try ok(dispatch(1, 0)); + const revision = info(4); + for (0..4) |mode| { + try ok(begin_program(1, 0)); + try ok(put_state(0, 999, -1)); + if (mode == 1) { + try ok(put_restored_value(0, 30)); + try std.testing.expectEqual(public(.bad_state), put_restored_value(0, 40)); + } else if (mode == 2) { + try std.testing.expectEqual(public(.non_finite), put_restored_value(0, std.math.inf(f64))); + } else if (mode == 3) { + try ok(put_restored_value(0, 30)); + try emit(20, 0, 0); // Invalid store outside a handler. + } + try std.testing.expect(commit_restored_program() != 0); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + abort_program(); + } + try ok(begin_program(1, 0)); + try ok(put_state(0, 100, -1)); + try ok(put_restored_value(0, 42)); + try std.testing.expectEqual(public(.bad_state), commit_program()); + abort_program(); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); +} + +test "empty and full restored programs validate all values and discard old display records" { + reset_all(); + try displayCounterProgram(); + try ok(configure_evaluation(32, 8)); + try boundedEquals(20, 4, 3); + try ok(begin_program(32, 0)); + for (0..32) |i| { + try ok(put_state(@intCast(i), 0, -1)); + try ok(put_restored_value(@intCast(i), @floatFromInt(i + 1))); + } + try emit(1, 31, 0); + try ok(commit_restored_program()); + try boundedEquals(32, 0, 1); + try ok(begin_program(0, 0)); + try ok(commit_restored_program()); + try std.testing.expectEqual(@as(u32, 0), info(5)); + try std.testing.expect(std.math.isNan(get_state(0))); +} + +test "shared handler bodies access their own bound state slots in source order" { + reset_all(); + try ok(begin_program(2, 1)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try emit(24, 0, 0); try emit(0, 0, 1); try emit(3, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(1, 1, 1)); + try ok(put_handler(1, 0, 4)); try ok(bind_handler(2, 0, 1)); + try ok(commit_program()); + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 12), get_state(0)); + try std.testing.expectEqual(@as(f64, 21), get_state(1)); + try std.testing.expectEqual(@as(u32, 4), info(6)); // Three bindings, one body. + try std.testing.expectEqual(@as(u32, 12), info(3)); + const diagnostics = [_]u32{ info(1), info(2), info(3), info(9) }; + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqualSlices(u32, &diagnostics, &.{ info(1), info(2), info(3), info(9) }); +} + +test "failure in a later shared binding rolls back all earlier bindings" { + reset_all(); + try ok(begin_program(4, 1)); + for ([_]f64{ 8, 2, 12, 0 }, 0..) |value, i| try ok(put_state(@intCast(i), value, -1)); + try emit(24, 0, 0); try emit(24, 1, 0); try emit(6, 0, 0); try emit(25, 0, 0); + for (0..2) |i| { + try ok(put_handler(1, 0, 4)); + try ok(bind_handler(@intCast(i), @intCast(i * 2), 2)); + } + try ok(commit_program()); + try std.testing.expectEqual(public(.division_by_zero), dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 8), get_state(0)); + try std.testing.expectEqual(@as(f64, 12), get_state(2)); + try std.testing.expectEqual(@as(u32, 0), info(9)); +} + +test "shared bindings reject missing arguments partial overlaps and invalid slot mappings" { + reset_all(); + try counterProgram(); + const revision = info(4); + for (0..5) |mode| { + try ok(begin_program(1, 1)); try ok(put_state(0, 9, -1)); + try emit(24, 0, 0); try emit(25, 0, 0); + try ok(put_handler(1, 0, 2)); + switch (mode) { + 0 => {}, // Parameter op without a binding. + 1 => { try ok(bind_handler(0, 0, 1)); try std.testing.expectEqual(public(.invalid), bind_handler(0, 0, 1)); }, + 2 => { try std.testing.expectEqual(public(.bad_state), bind_handler(0, 0, 2)); }, + 3 => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 1, 1)); try ok(bind_handler(1, 0, 1)); + }, + else => { + try ok(bind_handler(0, 0, 1)); + try ok(put_handler(1, 0, 2)); // Another owner missing its mapping. + }, + } + try std.testing.expect(commit_program() != 0); abort_program(); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(revision, info(4)); + } +} + +test "one routed handler changes only its event target and rejects missing or foreign routes" { + reset_all(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 10, -1)); + try ok(put_state(1, 20, -1)); + try ok(put_pointer_target(0, 0, 1)); + try ok(put_pointer_target(0, 1, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try ok(commit_program()); + try ok(dispatch_target(17, 1, 77, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 2), info(3)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(17, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch(1, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, 2, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(17, no_pc, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(18, 0, 99, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_target(0, 0, 99, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_target(17, 0, std.math.inf(f64), 0)); + try std.testing.expectEqual(@as(u32, 1), info(9)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try std.testing.expectEqual(@as(f64, 77), get_state(1)); + try std.testing.expectEqual(@as(u32, 3), layout(8)); + try std.testing.expectEqual(@as(u32, 2), layout(9)); + try std.testing.expectEqual(@as(u32, 54), layout(4)); +} + +test "routes reject invalid mappings and bindings without replacing active work" { + for (0..7) |mode| { + reset_all(); + try counterProgram(); + try ok(begin_program(2, 2)); + try ok(put_state(0, 0, -1)); + try ok(put_state(1, 0, -1)); + switch (mode) { + 0 => try std.testing.expectEqual(public(.bad_state), put_pointer_target(0, 1, 2)), + 1 => try std.testing.expectEqual(public(.bad_event), put_pointer_target(2, 0, 1)), + 2 => { + try ok(put_pointer_target(0, 0, 2)); + try std.testing.expectEqual(public(.invalid), put_pointer_target(1, 1, 1)); + }, + else => { + if (mode != 3) try ok(put_pointer_target(0, 0, 1)); + try emit(0, 0, 2); + try emit(25, 0, 0); + try ok(put_handler(if (mode == 4) 0 else 1, 0, 2)); + try ok(bind_target_handler(0, if (mode == 5) 2 else 1)); + if (mode == 6) try std.testing.expectEqual(public(.invalid), bind_target_handler(0, 1)); + }, + } + try std.testing.expect(commit_program() != 0); + // Active program still executes while the failed candidate occupies staging. + try ok(dispatch(1, 0)); + try std.testing.expectEqual(@as(f64, 11), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 0), layout(9)); + } + reset_all(); + try ok(begin_program(32, 1)); + for (0..32) |i| try ok(put_state(@intCast(i), 0, -1)); + for (0..24) |i| try ok(put_pointer_target(0, @intCast(i), 1)); + try std.testing.expectEqual(public(.capacity), put_pointer_target(0, 24, 1)); + try std.testing.expectEqual(public(.capacity), commit_program()); +} + +test "routed event rollback includes globals and earlier handlers and survives staged route replacement" { + reset_all(); + try ok(begin_program(3, 1)); + for (0..3) |i| try ok(put_state(@intCast(i), @floatFromInt(i), -1)); + try ok(put_pointer_target(0, 1, 1)); + try ok(put_pointer_target(0, 2, 1)); + try emit(22, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 0, 2)); + try ok(bind_target_handler(0, 1)); + try emit(0, 0, 99); + try emit(20, 0, 0); + try emit(0, 0, 1); + try emit(23, 0, 0); + try emit(6, 0, 0); + try emit(25, 0, 0); + try ok(put_handler(17, 2, 6)); + try ok(bind_target_handler(1, 1)); + try ok(commit_program()); + try ok(begin_program(1, 1)); + try ok(put_state(0, 0, -1)); + try ok(put_pointer_target(0, 0, 1)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_target(17, 1, 88, 0)); + try std.testing.expectEqual(@as(f64, 0), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 2), get_state(2)); + abort_program(); + try ok(dispatch_target(17, 1, 88, 2)); + try std.testing.expectEqual(@as(f64, 99), get_state(0)); + try std.testing.expectEqual(@as(f64, 1), get_state(1)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(2)); +} + +test "scalar inputs carry a finite ephemeral value without adding state or pointer ownership" { + reset_all(); + try ok(begin_program(1, 1)); + try ok(put_state(0, 50, -1)); + try emit(26, 0, 0); try emit(0, 0, 20); try emit(0, 0, 80); try emit(19, 0, 0); try emit(20, 0, 0); + try ok(put_handler(33, 0, 5)); try ok(commit_program()); + try ok(dispatch_value(33, 200)); + try std.testing.expectEqual(@as(f64, 80), get_state(0)); + try ok(dispatch_value(33, -200)); + try std.testing.expectEqual(@as(f64, 20), get_state(0)); + try std.testing.expectEqual(@as(u32, 1), info(5)); + try std.testing.expect(std.math.isNan(eval_at(0, 1))); + try std.testing.expectEqual(@as(u32, 2), info(9)); + for ([_]u32{0, 1, 17, 25, 34, no_pc}) |event| + try std.testing.expectEqual(public(.bad_event), dispatch_value(event, 60)); + try std.testing.expectEqual(public(.bad_event), dispatch(33, 0)); + try std.testing.expectEqual(public(.bad_event), dispatch_point(33, 60, 0)); + try std.testing.expectEqual(public(.non_finite), dispatch_value(33, std.math.nan(f64))); + try std.testing.expectEqual(@as(f64, 20), get_state(0)); + try std.testing.expectEqual(@as(u32, 2), info(9)); +} + +test "scalar input failure rolls back earlier writes and old scalar events survive staging" { + reset_all(); + try ok(begin_program(1, 1)); try ok(put_state(0, 10, -1)); + try emit(26, 0, 0); try emit(20, 0, 0); try ok(put_handler(33, 0, 2)); + try emit(0, 0, 1); try emit(26, 0, 0); try emit(6, 0, 0); try emit(20, 0, 0); + try ok(put_handler(33, 2, 4)); try ok(commit_program()); + try ok(begin_program(0, 0)); + try std.testing.expectEqual(public(.division_by_zero), dispatch_value(33, 0)); + try std.testing.expectEqual(@as(f64, 10), get_state(0)); + try ok(dispatch_value(33, 2)); + try std.testing.expectEqual(@as(f64, 0.5), get_state(0)); + abort_program(); + try std.testing.expectEqual(@as(u32, 1), info(9)); +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/evaluation.zig b/prototypes/workbench/results/2026-09-23-web-layout/sources/evaluation.zig new file mode 100644 index 0000000..56081d9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/evaluation.zig @@ -0,0 +1,164 @@ +//! Disposable binding results and change history. No event state lives here. +const std = @import("std"); + +const Memo = struct { + value: f64 = 0, + revision: u64 = 0, + dependencies: u32 = 0, + start: u32 = 0, + length: u32 = 0, +}; +const Change = struct { revision: u64 = 0, mask: u32 = 0 }; + +pub const Tables = struct { + memo: [32]Memo = @splat(.{}), + history: [8]Change = @splat(.{}), +}; + +pub const Evaluation = struct { + pub const max_cache = 32; + pub const max_history = 8; + revision: u64 = 1, + floor: u64 = 1, + cache_count: u32 = 0, + history_count: u32 = 8, + next_change: u32 = 0, + limit: u32 = 0, + work: u32 = 0, + instructions: u32 = 0, + hits: u32 = 0, + status: u32 = 0, + + pub fn configure(self: *Evaluation, tables: ?*Tables, cache: u32, history: u32) bool { + if (cache > max_cache or history > max_history) return false; + self.cache_count = cache; + self.history_count = history; + self.forget(tables, 3); + return true; + } + + pub fn forget(self: *Evaluation, tables: ?*Tables, flags: u32) void { + if (flags & 1 != 0) if (tables) |t| { t.memo = @splat(.{}); }; + if (flags & 2 != 0) { + if (tables) |t| t.history = @splat(.{}); + self.floor = self.revision; + self.next_change = 0; + } + } + + pub fn changed(self: *Evaluation, tables: ?*Tables, mask: u32) void { + const t = tables orelse return; + if (mask == 0 or self.cache_count == 0) return; + if (self.revision == std.math.maxInt(u64)) { + self.revision = 1; + self.forget(t, 3); + } + self.revision += 1; + if (self.history_count == 0) { + self.floor = self.revision; + return; + } + const change = &t.history[self.next_change]; + self.floor = @max(self.floor, change.revision); + change.* = .{ .revision = self.revision, .mask = mask }; + self.next_change = (self.next_change + 1) % self.history_count; + } + + pub fn begin(self: *Evaluation, limit: u32) void { + self.limit = limit; + self.work = 0; + self.instructions = 0; + self.hits = 0; + self.status = 0; + } + + pub fn spend(self: *Evaluation) bool { + if (self.work == self.limit) { + self.status = 10; + return false; + } + self.work += 1; + return true; + } + + fn slot(self: *const Evaluation, start: u32, length: u32) u32 { + // One candidate, including on misses; no scan proportional to cache size. + return (start ^ (start >> 5) ^ (length *% 0x9e3779b9)) % self.cache_count; + } + + pub fn lookup(self: *Evaluation, tables: ?*Tables, start: u32, length: u32) ?f64 { + const t = tables orelse return null; + if (self.cache_count == 0) return null; + if (!self.spend()) return null; + const memo = &t.memo[self.slot(start, length)]; + if (memo.revision == 0 or memo.start != start or memo.length != length) return null; + // Successful pure constants need no input-change evidence. A program + // replacement still clears them, just like every other binding memo. + if (memo.dependencies != 0 and memo.revision < self.floor) return null; + if (memo.dependencies != 0 and memo.revision != self.revision) { + for (t.history[0..self.history_count]) |change| { + if (!self.spend()) return null; + if (change.revision > memo.revision and change.mask & memo.dependencies != 0) return null; + } + } + memo.revision = self.revision; + self.hits += 1; + return memo.value; + } + + pub fn remember(self: *Evaluation, tables: ?*Tables, start: u32, length: u32, value: f64, dependencies: u32) void { + const t = tables orelse return; + if (self.cache_count == 0) return; + t.memo[self.slot(start, length)] = .{ .start = start, .length = length, + .value = value, .dependencies = dependencies, .revision = self.revision }; + } +}; + +test "record loss cannot authorize an old memo; unrelated changes can" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 1)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, 12), e.lookup(&tables, 0, 3)); + e.changed(&tables, 2); + e.changed(&tables, 2); // The certificate now predates the surviving history. + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 12, 1); + e.changed(&tables, 1); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.changed(&tables, 2); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + e.remember(&tables, 0, 3, 20, 1); + e.revision = std.math.maxInt(u64); + e.changed(&tables, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); +} + +test "budget, collisions, invalid settings, and disabled history are conservative" { + var tables: Tables = .{}; + var e: Evaluation = .{}; + try std.testing.expect(e.configure(&tables, 1, 0)); + e.begin(100); + e.remember(&tables, 0, 3, 12, 1); + e.remember(&tables, 1, 3, 15, 2); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 0, 3)); + try std.testing.expectEqual(@as(?f64, 15), e.lookup(&tables, 1, 3)); + e.changed(&tables, 0x80000000); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expect(!e.configure(&tables, 33, 0)); + try std.testing.expectEqual(@as(u32, 1), e.cache_count); + e.begin(0); + try std.testing.expectEqual(@as(?f64, null), e.lookup(&tables, 1, 3)); + try std.testing.expectEqual(@as(u32, 10), e.status); + try std.testing.expectEqual(@as(u32, 0), e.work); + e.begin(100); + e.remember(&tables, 2, 1, 99, 0); + e.changed(&tables, 1); + e.forget(&tables, 2); + try std.testing.expectEqual(@as(?f64, 99), e.lookup(&tables, 2, 1)); + try std.testing.expectEqual(@as(u32, 1), e.work); +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/examples/document.pure b/prototypes/workbench/results/2026-09-23-web-layout/sources/examples/document.pure new file mode 100644 index 0000000..6d78f8c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/examples/document.pure @@ -0,0 +1,6 @@ +# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다. +state ratio = 50 +input split = value +input reset = KeyR +on split.change { ratio = clamp(event_value, 20, 80) } +on reset.down { ratio = 50 } diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document-guards.js b/prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document-guards.js new file mode 100644 index 0000000..c938ec3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document-guards.js @@ -0,0 +1,30 @@ +async page => { + const checks=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const onDialog=d=>d.accept();page.on('dialog',onDialog); + try { + await page.goto('http://127.0.0.1:8766/document.html');await page.waitForFunction(()=>window.pureDocument?.snapshot().ready); + await page.setViewportSize({width:1440,height:1000});await page.locator('#document-text').fill('보존할 문서'); + await page.locator('.policy summary').click();const source=await page.locator('#policy-source').inputValue(); + const bad=source.replace('clamp(event_value, 20, 80)','999');check(bad!==source,'invalid-output fixture changes the numeric rule'); + await page.locator('#policy-source').fill(bad);await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('End'); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='50'&&await page.locator('#split-range').inputValue()==='50'&&(await page.locator('#policy-status').textContent()).includes('20~80'),'out-of-range authored output preserves both the layout and its native control projection'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('Home'); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='20','a corrected policy and valid request recover from invalid host output'); + await page.locator('#policy-source').fill(source.replace('clamp(event_value, 20, 80)','1 / 0'));await page.locator('#apply-policy').click();await page.locator('#split-range').focus();await page.keyboard.press('End'); + const s=await page.evaluate(()=>window.pureDocument.snapshot());check(s.runtime.lastError===4&&s.runtime.states.ratio===20&&await page.locator('#split-range').inputValue()==='20','arithmetic rollback also restores the native range projection'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click(); + await page.locator('#divider').scrollIntoViewIfNeeded();const box=await page.locator('#divider').boundingBox();await page.mouse.move(box.x+box.width/2,box.y+box.height/2);await page.mouse.down(); + const revision=await page.evaluate(()=>window.pureDocument.snapshot().runtime.revision); + await page.evaluate(()=>document.querySelector('#apply-policy').click()); + check(await page.evaluate(r=>window.pureDocument.snapshot().dragging&&window.pureDocument.snapshot().runtime.revision===r,revision)&&(await page.locator('#policy-status').textContent()).includes('경계를 놓거나'),'policy replacement is refused during an active native gesture');await page.mouse.up(); + await page.locator('#file-input').evaluate(input=>{const d=new DataTransfer();d.items.add(new File([new Uint8Array(2*1024*1024+1)],'oversize.txt'));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('2 MiB')); + check(await page.locator('#document-text').inputValue()==='보존할 문서','oversize files cannot replace the editable text'); + await page.locator('.policy summary').click();await page.locator('#sample').click();await page.locator('#reset-split').click(); + await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); + await page.screenshot({path:'output/playwright/pure-document-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844});await page.screenshot({path:'output/playwright/pure-document-mobile.png',fullPage:true}); + await page.evaluate(e=>window.__pureDocumentGuards=e,{checks});return{checks}; + }catch(error){await page.evaluate(e=>window.__pureDocumentGuards=e,{failed:String(error),checks});throw error;} + finally{page.off('dialog',onDialog);} +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document.js b/prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document.js new file mode 100644 index 0000000..46b9b8c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/qa-document.js @@ -0,0 +1,103 @@ +// Playwright CLI run-code. Functional and ownership checks, not a timing benchmark. +async page => { + const checks=[],errors=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + const onDialog=d=>d.accept(),onError=e=>errors.push(e.message);page.on('dialog',onDialog);page.on('pageerror',onError); + const state=()=>page.evaluate(()=>window.pureDocument.snapshot()); + const settled=()=>page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); + const ratio=async expected=>{await page.waitForFunction(n=>Math.abs(window.pureDocument.snapshot().runtime.states.ratio-n)<0.2,expected);}; + const geometry=()=>page.evaluate(()=>{const p=document.querySelector('#panes').getBoundingClientRect(),h=document.querySelector('#divider').getBoundingClientRect();return{left:p.left,width:p.width,handle:h.width,x:h.left+h.width/2,y:h.top+h.height/2};}); + const target=(g,r)=>g.left+g.handle/2+(g.width-g.handle)*r/100; + const dragStart=async()=>{await page.locator('#divider').scrollIntoViewIfNeeded();const g=await geometry();await page.mouse.move(g.x,g.y);await page.mouse.down();return g;}; + const upload=async(content,name='notes.txt')=>page.locator('#file-input').evaluate((input,{content,name})=>{const d=new DataTransfer();d.items.add(new File([content],name,{type:'text/plain'}));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));},{content,name}); + try { + await page.setViewportSize({width:1440,height:1000});await page.reload(); + await page.waitForFunction(()=>window.pureDocument?.snapshot().ready); + check((await state()).runtime.memory.activeStateBytes===8,'only the authored ratio occupies VM state; no document or geometry mirror'); + await page.locator('#sample').click();await settled(); + const original=await page.locator('#document-text').inputValue(); + check(await page.locator('#reading-text').textContent()===original,'native editor and reading pane show the same Unicode document'); + check(await page.locator('#reading-text div').count()===0,'document markup is displayed as text'); + const beforeResize=await state(); + await page.locator('#document-text').evaluate(t=>{t.focus();t.setSelectionRange(20,25);t.scrollTop=80;}); + await page.setViewportSize({width:1040,height:900});await settled(); + const afterResize=await state(); + check(afterResize.runtime.successfulEvents===beforeResize.runtime.successfulEvents&&afterResize.stats.previewWrites===beforeResize.stats.previewWrites&&afterResize.stats.geometryReads===beforeResize.stats.geometryReads,'resize reflows through CSS without VM events, document publication or application geometry reads'); + check(await page.locator('#document-text').evaluate(t=>t.selectionStart===20&&t.selectionEnd===25),'reflow preserves the native editing selection'); + const scrolling=await page.evaluate(()=>{const t=document.querySelector('#document-text'),r=document.querySelector('#reading-scroll');r.scrollTop=120;return{editor:t.scrollTop,reading:r.scrollTop,scrollable:r.scrollHeight>r.clientHeight};}); + check(scrolling.scrollable&&scrolling.reading>0,'long wrapped content uses the native reading scroll area'); + await page.locator('#reading-scroll').evaluate(r=>r.scrollTop+=80);await settled(); + check(await page.locator('#document-text').evaluate((t,y)=>t.scrollTop===y,scrolling.editor),'reading scroll does not move the editing pane'); + check((await state()).runtime.successfulEvents===afterResize.runtime.successfulEvents,'native scrolling does not dispatch layout data to the VM'); + await page.locator('#split-range').focus();await page.keyboard.press('End');await ratio(80); + check((await state()).runtime.states.ratio===80,'a native range input uses the authored numeric-input policy'); + await page.locator('#divider').focus();await page.keyboard.press('Home');await ratio(20);await page.keyboard.press('Shift+ArrowRight');await ratio(30); + check(await page.locator('#divider').getAttribute('aria-valuenow')==='30','keyboard separator control publishes the same value and accessibility state'); + await page.locator('#reset-split').click();await ratio(50); + const textWrites=(await state()).stats.previewWrites; + let g=await dragStart();await page.mouse.move(target(g,65),g.y);await ratio(65);await page.mouse.up(); + check(Math.abs((await state()).runtime.states.ratio-65)<0.2&&!(await state()).dragging,'mouse release commits the last delivered split position'); + check((await state()).stats.previewWrites===textWrites,'resizing panes never republishes unchanged document text'); + g=await dragStart();await page.mouse.move(target(g,35),g.y);await ratio(35);await page.keyboard.press('Escape');await page.mouse.up();await ratio(65); + check(!(await state()).dragging,'Escape restores the ratio captured at gesture start'); + g=await dragStart();await page.setViewportSize({width:1240,height:1000});await settled();g=await geometry(); + await page.mouse.move(target(g,45),g.y);await ratio(45);await page.mouse.up(); + check(Math.abs((await state()).runtime.states.ratio-45)<0.2,'a resize during capture invalidates geometry before the next move'); + // Capture a real touch; mouse compatibility termination must not release it. + const cdp=await page.context().newCDPSession(page); + try { + await cdp.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2});g=await geometry(); + await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{id:1,x:g.x,y:g.y}]}); + await cdp.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,x:target(g,70),y:g.y}]});await ratio(70); + await page.evaluate(()=>window.dispatchEvent(new MouseEvent('mouseup',{button:0,bubbles:true}))); + check((await state()).dragging,'a compatibility mouseup cannot release a touch owner'); + await cdp.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]});await ratio(45); + check(!(await state()).dragging,'touch cancellation restores the original panel ratio'); + } finally {await cdp.send('Emulation.setTouchEmulationEnabled',{enabled:false});await cdp.detach();} + g=await dragStart();await page.mouse.move(target(g,60),g.y);await ratio(60); + await page.setViewportSize({width:390,height:844});await settled();await page.mouse.up();await ratio(45); + check(await page.locator('#divider').isHidden()&&!(await state()).dragging,'narrow layout cancels the active split gesture and stacks the panes'); + check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'the narrow document workspace has no page-level horizontal overflow'); + const oldStats=await state(); + await page.locator('#document-text').fill('한글 문서 📝\n새 줄과 <태그>도 그대로.');await settled(); + check(await page.locator('#reading-text').textContent()==='한글 문서 📝\n새 줄과 <태그>도 그대로.','editing Unicode updates the text projection'); + check((await state()).runtime.successfulEvents===oldStats.runtime.successfulEvents&& (await state()).stats.geometryReads===oldStats.stats.geometryReads,'text editing neither sends document copies to the VM nor reads geometry'); + await page.locator('#document-text').focus();await page.keyboard.press('Control+End');await page.keyboard.insertText(' 추가');await settled();await page.keyboard.press('Control+z');await settled(); + check(await page.locator('#document-text').inputValue()==='한글 문서 📝\n새 줄과 <태그>도 그대로.','native editing undo survives the Pure integration'); + const compositionBefore=(await state()).stats.previewWrites; + await page.locator('#document-text').evaluate(t=>{t.dispatchEvent(new CompositionEvent('compositionstart'));t.value+=' 조합';t.dispatchEvent(new InputEvent('input',{bubbles:true,isComposing:true}));});await settled(); + check((await state()).stats.previewWrites===compositionBefore,'in-progress composition does not replace the reading projection'); + await page.locator('#document-text').evaluate(t=>t.dispatchEvent(new CompositionEvent('compositionend')));await settled(); + check((await page.locator('#reading-text').textContent()).endsWith(' 조합'),'composition completion publishes the final text (synthetic event sequence)'); + const fileText='표준 UTF-8 파일\n외부 편집기에서 만든 내용\n😀
문자
\n'; + await upload(fileText,'external.txt');await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('파일을 열었습니다'));await settled(); + check(await page.locator('#document-text').inputValue()===fileText,'a standard UTF-8 text file opens without a custom container'); + const download=page.waitForEvent('download');await page.locator('#save').click();const saved=await download;const savedPath='output/playwright/pure-document-saved.txt';await saved.saveAs(savedPath); + check(saved.suggestedFilename()==='external.txt','export keeps the standard text filename'); + const fresh=await page.context().newPage();await fresh.goto(page.url());await fresh.waitForFunction(()=>window.pureDocument?.snapshot().ready); + await fresh.locator('#file-input').setInputFiles(savedPath);await fresh.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('파일을 열었습니다')); + check(await fresh.locator('#document-text').inputValue()===fileText,'the exported file reopens in a fresh page with exact text');await fresh.close();await page.bringToFront(); + await page.locator('#file-input').evaluate(input=>{const f=new File(['late'],'late.txt');f.arrayBuffer=()=>new Promise(resolve=>window.finishDocumentRead=()=>resolve(new TextEncoder().encode('late').buffer));const d=new DataTransfer();d.items.add(f);Object.defineProperty(input,'files',{configurable:true,value:d.files});input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.locator('#document-text').fill('읽는 동안 새로 편집한 문서');await page.evaluate(()=>window.finishDocumentRead());await settled(); + check(await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서'&&(await page.locator('#document-status').textContent()).includes('읽는 동안'),'a delayed file read cannot overwrite newer editing'); + await page.locator('#file-input').evaluate(input=>{delete input.files;const d=new DataTransfer();d.items.add(new File([new Uint8Array([255])],'invalid.txt'));input.files=d.files;input.dispatchEvent(new Event('change',{bubbles:true}));}); + await page.waitForFunction(()=>document.querySelector('#document-status').textContent.includes('UTF-8 텍스트로 읽을 수'));check(await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서','invalid UTF-8 leaves the current document intact'); + await page.setViewportSize({width:1440,height:1000});await settled(); + await page.locator('.policy summary').click();const source=await page.locator('#policy-source').inputValue(); + await page.locator('#policy-source').fill(source.replace('20, 80','30, 70'));await page.locator('#apply-policy').click(); + await page.locator('#split-range').focus();await page.keyboard.press('End');await ratio(70); + check((await state()).runtime.states.ratio===70,'changing the authored rule changes native control behavior without touching the HTML adapter'); + const revision=(await state()).runtime.revision;await page.locator('#policy-source').fill('state broken =');await page.locator('#apply-policy').click(); + check((await state()).runtime.revision===revision&&await page.locator('#document-text').inputValue()==='읽는 동안 새로 편집한 문서','invalid policy edits preserve the executable policy and document'); + await page.locator('#policy-source').fill(source);await page.locator('#apply-policy').click();await page.locator('.policy summary').click(); + await page.locator('#sample').click();await settled();await page.locator('#reset-split').click(); + await page.screenshot({path:'output/playwright/pure-document-desktop.png',fullPage:true}); + await page.setViewportSize({width:390,height:844});await page.screenshot({path:'output/playwright/pure-document-mobile.png',fullPage:true}); + const fallback=await page.context().newPage();await fallback.route('**/core.wasm',r=>r.abort());await fallback.goto(page.url());await fallback.waitForFunction(()=>window.pureDocument); + await fallback.locator('#document-text').fill('실행기 없이도 보존되는 텍스트');await fallback.waitForFunction(()=>document.querySelector('#reading-text').textContent.includes('실행기 없이도')); + check(!await fallback.evaluate(()=>window.pureDocument.snapshot().ready)&&await fallback.locator('#split-range').isDisabled()&&await fallback.locator('#save').isEnabled(),'missing Wasm disables split controls while standard text editing and saving remain available');await fallback.close(); + check(errors.length===0,'no uncaught page errors'); + const evidence={checks,errors,fileText,savedPath,final:await state(),browser:await page.evaluate(()=>navigator.userAgent)}; + await page.evaluate(e=>window.__pureDocumentEvidence=e,evidence);return{checks:checks.length,errors}; + } catch(error){await page.evaluate(e=>window.__pureDocumentEvidence=e,{failed:String(error),checks,errors,state:await state()});throw error;} + finally{page.off('dialog',onDialog);page.off('pageerror',onError);} +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/value-input.test.mjs b/prototypes/workbench/results/2026-09-23-web-layout/sources/value-input.test.mjs new file mode 100644 index 0000000..3044048 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/value-input.test.mjs @@ -0,0 +1,50 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {spawnSync} from 'node:child_process'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const source=await readFile(new URL('./examples/document.pure',import.meta.url),'utf8'); + +test('numeric controls use explicit change context and cannot read pointer or frame payloads',()=>{ + const p=compile(source);assert.equal(p.states.length,1);assert.equal(p.views.length,0);assert.equal(p.handlers[0].event,33); + for(const s of [ + 'state x=0\ninput n=value\non n.down {x=1}', + 'state x=0\ninput k=Space\non k.change {x=1}', + 'state x=0\ninput n=value\non n.change {x=event_x}', + 'state x=0\non frame {x=event_value}', + 'input n=value\ntext v(0,0,event_value)', + 'record R {x=0}\nitem a=R()\ninput k=Space\nrule r(t:R){t.x=event_value}\non k.down use r(a)', + 'state event_value=0', + ])assert.throws(()=>compile(s),s); +}); + +test('numeric input handles scalar rules, rollback, cache invalidation and signed zero',async()=>{ + const r=await createRuntime(wasm,{cacheEntries:32}); + const code='record R {x=10}\nitem a=R()\ninput n=value\nrule r(t:R){t.x=event_value}\non n.change use r(a)\ntext v(0,0,a.x)'; + r.apply(code);r.snapshot();r.snapshot();r.change('n',25);assert.equal(r.snapshot().views[0].value,25); + r.change('n',-0);assert(Object.is(r.snapshot().states['a.x'],-0));assert.deepEqual(r.snapshot().held,[]); + const before=r.snapshot();assert.throws(()=>r.change('n',Infinity));assert.throws(()=>r.down('n'));assert.throws(()=>r.point('n','down',1,2)); + assert.equal(r.snapshot().successfulEvents,before.successfulEvents); + r.apply(code+'\non n.change {a.x=1/0}');assert.equal(r.change('n',80),4);assert(Object.is(r.snapshot().states['a.x'],-0)); + assert.equal(r.snapshot().lastFailure.value,80);assert.throws(()=>r.exportDocument(),/오류/); + r.apply(code);const saved=r.exportDocument();const next=await createRuntime(wasm);next.restoreDocument(saved);next.change('n',32);assert.equal(next.snapshot().states['a.x'],32); +}); + +test('the document policy receives range, keyboard and pointer requests without layouts or text in VM state',async()=>{ + const r=await createRuntime(wasm);r.apply(source); + for(const [value,expected] of [[60,60],[1000,80],[-1000,20],[45.5,45.5]]){assert.equal(r.change('split',value,'control'),0);assert.deepEqual(r.snapshot().states,{ratio:expected});} + r.down('reset');r.up('reset');assert.deepEqual(r.snapshot().states,{ratio:50}); + assert.equal(r.snapshot().views.length,0);assert.equal(r.snapshot().memory.activeStateBytes,8); + const published=[];const target={begin(){},view(){assert.fail('no declared views');},value(){assert.fail('no view values');},state(i,v){published.push(v);},end(n){assert.equal(n,0);}}; + r.present(target);assert.deepEqual(published,[50]); + r.apply(source.replace('20, 80','30, 70'));r.change('split',10);assert.equal(r.snapshot().states.ratio,30); + const current=r.currentProgram;assert.throws(()=>r.apply(source+'\non split.down {ratio=0}'));assert.equal(r.currentProgram,current); +}); + +test('CLI supplies the same numeric control value and rejects nonfinite input',()=>{ + const run=input=>spawnSync(process.execPath,[new URL('./cli.mjs',import.meta.url).pathname,new URL('./examples/document.pure',import.meta.url).pathname],{input,encoding:'utf8'}); + const ok=run('change split 95\nquit\n');assert.equal(ok.status,0,ok.stderr);assert.equal(JSON.parse(ok.stdout.trim()).states.ratio,80); + const bad=run('change split NaN\nquit\n');assert.equal(bad.status,1);assert.match(bad.stderr,/유한/); +}); diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/web/app.js b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/app.js new file mode 100644 index 0000000..9e53ee3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/app.js @@ -0,0 +1,411 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; +import { bindSceneInput } from './scene-input.mjs'; +import { parseProject, MAX_PROJECT_BYTES } from './project.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +let draftGeneration = 0; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); +const sceneInput = bindSceneInput($('scene'), () => runtime, safeAction); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +// DOM is the published value. Avoid rewriting unchanged diagnostics/controls. +function text(id, value) { + const node = $(id); + if (node.textContent !== value) node.textContent = value; +} +function pressed(node, value) { + const next = String(value); + if (node.getAttribute('aria-pressed') !== next) node.setAttribute('aria-pressed', next); +} +function draftStatus() { + const changed = dirty(); + text('draft-status', changed ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'); + $('draft-status').classList.toggle('dirty', changed); +} + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + text('cursor-position', `${lines.length}행 · ${lines.at(-1).length + 1}열`); +} + +function draftChanged() { + draftGeneration++; + // Read scroll position before writes; this path runs only for source edits. + const scrollTop = source.scrollTop; + const count = source.value.split('\n').length; + const size = bytes(source.value); + text('line-numbers', Array.from({ length: count }, (_, i) => i + 1).join('\n')); + $('line-numbers').scrollTop = scrollTop; + text('source-size', `${size.toLocaleString('en-US')} / 32,768 bytes`); + $('source-size').style.color = size > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + draftStatus(); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + text('play', '▶ 실행'); + pressed($('play'), false); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + sceneInput.releaseCaptures(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + pressed(button, held.has(name)); + } + text('scene-status', runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'); + text('clock-status', `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`); + text('revision', `revision ${snapshot.revision || 0}`); + for (const id of ['play', 'step', 'reset', 'download-layout']) { + if ($(id).disabled !== !runtime.currentProgram) $(id).disabled = !runtime.currentProgram; + } + const event = snapshot.lastEvent; + text('last-event', event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''}${event.target ? ` → ${event.target}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'); + if ($('runtime-error').hidden !== !snapshot.lastError) $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + let message = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + message += ` · ${snapshot.displayError.message}`; + text('runtime-error', message); + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + if (input.kind === 'value') { + const form = document.createElement('form'), label = document.createElement('label'); + const field = document.createElement('input'), button = document.createElement('button'); + label.textContent = input.name + ' '; field.type = 'number'; field.step = 'any'; field.value = '0'; field.required = true; + button.type = 'submit'; button.textContent = '값 전달'; label.append(field); form.append(label, button); + form.addEventListener('submit', event => { event.preventDefault(); safeAction(() => runtime.change(input.name, field.valueAsNumber, 'control')); }); + children.push(form); continue; + } + if (input.target || input.routes) { + const note = document.createElement('p'); + note.className = 'empty-note'; + note.textContent = `${input.name} · 장면의 ${input.target || input.routes.map(route => route.target).join(', ')}을 잡아 조작하세요.`; + children.push(note); + continue; + } + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + draftStatus(); + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime, message = '편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.') { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic(message); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('download-layout').addEventListener('click', () => { + try { + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 저장하세요.'); + const project = runtime.exportDocument(source.value); + download('my-layout.pure.json', JSON.stringify(project, null, 2) + '\n', 'application/json', + '적용된 코드·현재 배치·미적용 초안을 함께 저장했습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_PROJECT_BYTES) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = draftGeneration; + const active = runtime?.currentProgram; + const events = runtime?.snapshot().successfulEvents; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + const project = parseProject(content); + if (project.version === 2) { + if (!runtime) throw new Error('실행기가 준비되지 않았습니다.'); + if (draftGeneration !== before || runtime.currentProgram !== active || runtime.snapshot().successfulEvents !== events) + throw new Error('파일을 읽는 동안 작업이 바뀌었습니다. 정지한 뒤 다시 불러오세요.'); + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 복원하세요.'); + if ((source.value || appliedSource) && !window.confirm('현재 코드와 배치를 파일의 내용으로 복원할까요? 저장하지 않은 작업은 사라집니다.')) return; + const restored = runtime.restoreDocument(project); + pause(); droppedTicks = 0; + appliedSource = restored.source; + rebuildInputControls(); + replaceDraft(restored.draft, '배치를 복원했습니다. 미적용 초안도 보존했고, 시간은 정지 상태입니다.'); + refresh(); + return; + } + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (draftGeneration !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/web/compiler.mjs b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/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/workbench/results/2026-09-23-web-layout/sources/web/core.wasm b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/core.wasm new file mode 100644 index 0000000..79c50a8 Binary files /dev/null and b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/core.wasm differ diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.css b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.css new file mode 100644 index 0000000..7e7ed97 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.css @@ -0,0 +1,3 @@ +:root{color-scheme:dark;--bg:#101815;--panel:#19241e;--line:#35483c;--text:#e5eade;--muted:#a6b3a7;--accent:#c9f08b;font-family:system-ui,-apple-system,sans-serif;color:var(--text);background:var(--bg)} +*{box-sizing:border-box}body{margin:0}header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding:22px max(24px,calc((100vw - 1500px)/2))}.brand{font-size:22px;font-weight:700;letter-spacing:-1px;text-decoration:none;color:var(--accent)}.brand span{font-size:14px;color:var(--muted);font-weight:400;letter-spacing:0}.badge,.eyebrow{color:var(--muted);font-size:12px;letter-spacing:.1em}main{max-width:1550px;margin:auto;padding:28px 24px}.intro{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:25px}h1{font-size:clamp(23px,3vw,34px);font-weight:550;letter-spacing:-.04em;word-break:keep-all;margin:8px 0}.intro p:not(.eyebrow){color:var(--muted);font-size:14px;line-height:1.65;margin-bottom:0}button,input,textarea{font:inherit;color:inherit}button{background:#273a2e;border:1px solid #456049;padding:9px 14px;border-radius:7px;cursor:pointer;white-space:nowrap}button:hover{background:#334d3d}button:disabled{opacity:.45;cursor:default}input:not([type=range]){background:var(--bg);border:1px solid var(--line);border-radius:5px;padding:8px;max-width:180px}button:focus-visible,input:focus-visible,textarea:focus-visible,[tabindex]:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;background:var(--panel);padding:14px;border:1px solid var(--line);border-radius:9px 9px 0 0}.toolbar label{font-size:13px;color:var(--muted)}#document-status{font-size:12px;color:var(--muted);margin-left:auto;max-width:100%;overflow-wrap:anywhere}.layout-tools{display:flex;align-items:center;gap:12px;padding:15px 0;font-size:13px}.layout-tools input{width:145px;accent-color:var(--accent)}.layout-tools output{min-width:42px;font-variant-numeric:tabular-nums}.layout-tools button{padding:5px 10px}.hint{color:var(--muted);font-size:12px;margin-left:auto}.panes{display:grid;grid-template-columns:minmax(0,calc((100% - 18px)*var(--split))) 18px minmax(0,1fr);height:clamp(400px,65vh,850px)}.pane{min-width:0;min-height:0;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:8px;overflow:hidden}.pane-heading{display:flex;align-items:center;justify-content:space-between;padding:13px 18px;border-bottom:1px solid var(--line);flex:0 0 auto}.pane-heading h2{font-size:14px;font-weight:550;margin:0}.pane-heading span{font-size:10px;letter-spacing:.12em;color:var(--muted)}#document-text{width:100%;flex:1;min-height:0;resize:none;background:transparent;border:0;padding:22px;font:15px/1.8 ui-monospace,monospace;outline-offset:-3px;tab-size:4}#document-text::placeholder{color:#84998b}#reading-scroll{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;outline-offset:-3px;padding:26px clamp(18px,3vw,42px);font-size:17px;line-height:1.95;background:#e5e8dc;color:#26352d}#reading-text{white-space:pre-wrap;overflow-wrap:anywhere}#reading-empty{color:#637566;margin:0}#divider{cursor:col-resize;touch-action:none;display:flex;align-items:center;justify-content:center;outline-offset:-2px}#divider span{height:44px;width:4px;background:#617768;border-radius:4px}#divider:hover span,#divider.dragging span,#divider:focus-visible span{background:var(--accent)}#divider[aria-disabled=true]{pointer-events:none}.policy{margin-top:22px;border:1px solid var(--line);padding:14px 18px;border-radius:8px}.policy summary{cursor:pointer;color:var(--muted);font-size:13px}.policy p{font-size:13px;color:var(--muted);line-height:1.7}.policy textarea{display:block;width:100%;height:160px;resize:vertical;background:#121b16;border:1px solid var(--line);border-radius:5px;font:13px/1.65 ui-monospace,monospace;padding:12px;margin:12px 0}.policy span{font-size:12px;margin-left:12px;color:var(--muted)}footer{font-size:12px;color:var(--muted);line-height:1.7;padding:22px 0}a{color:var(--accent)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important} +@media(max-width:720px){header{padding:18px}.badge{display:none}main{padding:20px 16px}.intro{align-items:flex-start}.intro button{font-size:12px;padding:8px}.toolbar input:not([type=range]){max-width:155px}.toolbar label{width:100%}#document-status{width:100%;margin:2px 0}.layout-tools{display:none}.panes{display:flex;flex-direction:column;height:auto;gap:18px}.pane{height:430px}.read-pane{height:500px}#divider{display:none}.policy span{display:block;margin:10px 0 0}.hint{display:none}#reading-scroll{padding:22px}} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.html b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.html new file mode 100644 index 0000000..7e96674 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.html @@ -0,0 +1,32 @@ + + + + + + Pure · 문서 작업 + + +
pure / 문서 작업텍스트 워크스페이스
+
+

WRITE / READ

쓰고, 읽고, 공간을 조절하세요.

가운데 경계를 움직여 편한 폭을 만드세요. 좁은 화면에서는 위아래로 이어집니다.

+
+ + + + UTF-8 텍스트 파일을 열거나 바로 작성하세요. +
+
+ 50% + 경계에서 ← → · Shift로 크게 · Esc로 취소 +
+
+

작성

TEXT
+ +

읽기

PREVIEW

작성한 내용이 여기에 나타납니다.

+
+
분할 규칙 수정

비율을 받는 규칙을 수정할 수 있습니다. 문서 내용은 유지됩니다. 숫자 입력 이름은 split, 결과 상태는 ratio입니다.

실행기를 준비하고 있습니다.
+
텍스트는 일반 .txt 파일로 저장됩니다. 줄바꿈과 글자 배치는 브라우저가 처리합니다. 기존 작업 공간 ↗
+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.mjs b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.mjs new file mode 100644 index 0000000..1f19322 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/document.mjs @@ -0,0 +1,132 @@ +import {createRuntime} from './runtime.mjs'; +import {compile} from './compiler.mjs'; +import {bindSplit} from './split-input.mjs'; +const $=id=>document.getElementById(id), text=$('document-text'), reading=$('reading-text'); +const MAX_BYTES=2*1024*1024, stats={geometryReads:0,previewWrites:0,ratioWrites:0}; +let runtime=null, splitter=null, previewFrame=0, composing=false, generation=0, request=0, dirty=false; +const message=(id,value)=>{$(id).textContent=value;}; +const narrow=matchMedia('(max-width:720px)'); +const sample=`작업 노트 — 인터페이스의 경계 + +1. 이 문서에서 해볼 일 + +창의 폭을 줄여보세요. 읽기 화면의 글자는 새 폭에 맞춰 줄바꿈됩니다. 화면이 좁아지면 작성과 읽기가 위아래로 놓입니다. 문서 자체를 다른 형식으로 바꿀 필요는 없습니다. + +넓은 화면에서는 두 영역 사이의 경계를 잡아 움직일 수 있습니다. 경계에 키보드 초점을 두고 방향키를 눌러도 됩니다. 손을 놓으면 현재 비율을 유지하고, 움직이는 도중 Esc를 누르면 시작한 비율로 돌아갑니다. + +2. 작성과 읽기 + +왼쪽은 브라우저의 텍스트 입력입니다. 이 문장을 고치고, 몇 줄을 더 적어보세요. 한글을 조합하고 복사하거나 붙여넣고, 실행 취소를 해볼 수 있습니다. 읽기 화면에서는 같은 글을 다른 폭으로 살펴봅니다. + +양쪽의 스크롤은 독립적입니다. 읽던 곳을 옮긴다고 작성 커서가 움직이지 않습니다. 분할 비율을 바꿀 때도 글자나 커서, 문서를 새로 만들지 않습니다. + +3. 연결을 유지하기 + +일반 UTF-8 텍스트 파일을 열 수 있습니다. 작성한 결과도 .txt로 내려받습니다. 다른 편집기에서 그대로 읽을 수 있는 경로를 유지하는 것이 이 작업의 조건입니다. + +아래 문자열은 코드가 아니라 문서의 일부입니다. +
이 글은 화면의 HTML 요소로 해석되지 않습니다.
+ +4. 작업 메모 + +여기에 실제 프로젝트의 메모를 적어도 됩니다. + +하려던 일: + +실제로 일어난 일: + +기대했던 일: + +다음 수정: + +5. 남겨둘 질문 + +입력이 바뀌었을 때 꼭 다시 계산해야 하는 것은 무엇일까요? 문서가 바뀌지 않았는데 문서를 다시 전달할 이유가 있을까요? 화면의 폭과 문서의 의미를 같은 상태로 보관할 필요가 있을까요? + +이 화면이 답하는 범위는 작습니다. 실제 브라우저의 편집과 배치를 사용하면서, 조작 규칙을 어디에 둘 수 있는지를 확인합니다. +`; +function publishText() { + previewFrame=0; + if(composing)return; + reading.textContent=text.value;stats.previewWrites++; + $('reading-empty').hidden=text.value.length!==0; +} +function scheduleText(){if(!previewFrame)previewFrame=requestAnimationFrame(publishText);} +text.addEventListener('compositionstart',()=>{composing=true;}); +text.addEventListener('compositionend',()=>{composing=false;scheduleText();}); +text.addEventListener('input',()=>{generation++;dirty=true;scheduleText();message('document-status','편집 중 · 저장하지 않은 변경이 있습니다.');}); +function replaceText(value,name) { + text.value=value;text.scrollTop=0;$('reading-scroll').scrollTop=0; + if(name)$('filename').value=name; + generation++;dirty=false;scheduleText(); +} +$('sample').addEventListener('click',()=>{ + if(dirty&&!confirm('편집한 내용을 예제 문서로 바꿀까요?'))return; + request++;replaceText(sample,'interface-notes.txt');dirty=true;message('document-status','예제 문서를 불러왔습니다. 자유롭게 고쳐보세요.'); +}); +$('open').addEventListener('click',()=>$('file-input').click()); +$('file-input').addEventListener('change',async event=>{ + const file=event.target.files[0];event.target.value='';if(!file)return; + if(dirty&&!confirm('저장하지 않은 내용을 파일의 내용으로 바꿀까요?'))return; + const ticket=++request, version=generation; + try { + if(file.size>MAX_BYTES)throw new Error('2 MiB 이하의 텍스트 파일을 선택하세요.'); + const bytes=await file.arrayBuffer(); + if(ticket!==request||version!==generation)throw new Error('파일을 읽는 동안 문서가 바뀌어 열기를 취소했습니다.'); + if(bytes.byteLength>MAX_BYTES)throw new Error('파일 크기 한도를 넘었습니다.'); + const value=new TextDecoder('utf-8',{fatal:true}).decode(bytes); + replaceText(value,file.name);message('document-status',`${file.name} 파일을 열었습니다.`); + } catch(error){if(ticket===request)message('document-status',error instanceof TypeError?'UTF-8 텍스트로 읽을 수 없습니다. 현재 문서는 유지됩니다.':error.message);} +}); +$('save').addEventListener('click',()=>{ + const bytes=new TextEncoder().encode(text.value); + if(bytes.byteLength>MAX_BYTES){message('document-status','저장은 UTF-8 기준 2 MiB까지 지원합니다. 문서는 그대로 유지됩니다.');return;} + const name=$('filename').value.trim()||'notes.txt'; + const url=URL.createObjectURL(new Blob([bytes],{type:'text/plain;charset=utf-8'})); + const link=document.createElement('a');link.href=url;link.download=name.endsWith('.txt')?name:`${name}.txt`; + link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);dirty=false;message('document-status','텍스트 파일을 내려받았습니다.'); +}); +window.addEventListener('beforeunload',event=>{if(dirty){event.preventDefault();event.returnValue='';}}); +const output={ + begin(){},view(){},value(){},end(){}, + state(index,value){ + if(runtime.currentProgram.states[index].name!=='ratio')return; + // Invalid authored output never produces a broken grid or invalid ARIA value. + if(value<20||value>80)throw new Error('이 화면의 분할 비율은 20~80% 범위여야 합니다. 규칙을 수정하세요.'); + const ratio=String(value); + if($('divider').getAttribute('aria-valuenow')===ratio)return; + $('divider').setAttribute('aria-valuenow',ratio); + $('panes').style.setProperty('--split',String(value/100)); + $('split-range').value=ratio;$('split-output').value=`${Math.round(value)}%`;stats.ratioWrites++; + }, +}; +function publish(){const result=runtime.present(output);if(result.lastError)throw new Error(result.errorMessage);} +function writeRatio(value,source) { + if(!runtime)return; + try {runtime.change('split',value,source);publish();message('policy-status','적용된 규칙으로 분할 비율을 조절합니다.');} + catch(error){$('split-range').value=$('divider').getAttribute('aria-valuenow');message('policy-status',error.message);} +} +function applyPolicy() { + if(splitter?.isDragging())throw new Error('경계를 놓거나 취소한 뒤 규칙을 적용하세요.'); + const source=$('policy-source').value, p=compile(source), ratio=p.states.find(state=>state.name==='ratio'); + if(!ratio||ratio.initial<20||ratio.initial>80||!p.inputs.some(input=>input.name==='split'&&input.kind==='value')||!p.inputs.some(input=>input.name==='reset'&&input.key==='KeyR')||p.views.length) + throw new Error('ratio 상태(초깃값 20~80), split 숫자 입력, reset = KeyR 입력을 유지하세요. 이 화면은 HTML로 표시합니다.'); + runtime.apply(source);publish();message('policy-status','규칙을 적용했습니다. 문서와 편집 커서는 유지됩니다.'); +} +$('apply-policy').addEventListener('click',()=>{try{applyPolicy();}catch(error){message('policy-status',error.message);}}); +$('split-range').addEventListener('input',event=>{splitter?.cancel();writeRatio(event.target.valueAsNumber,'range');}); +$('reset-split').addEventListener('click',()=>{ + splitter?.cancel();if(!runtime)return; + try{runtime.down('reset','control');runtime.up('reset','control');publish();message('policy-status','분할 비율을 초기화했습니다.');}catch(error){message('policy-status',error.message);} +}); +try { + const [instance,response]=await Promise.all([createRuntime(),fetch('./examples/document.pure',{cache:'no-store'})]); + if(!response.ok)throw new Error('분할 규칙을 불러올 수 없습니다.'); + runtime=instance;$('policy-source').value=await response.text();applyPolicy(); + splitter=bindSplit({container:$('panes'),handle:$('divider'),narrow,stats, + readRatio:()=>Number($('divider').getAttribute('aria-valuenow')),writeRatio}); + for(const id of ['split-range','reset-split','apply-policy'])$(id).disabled=false; + $('divider').setAttribute('aria-disabled','false'); +} catch(error){message('policy-status',`분할 실행기를 열지 못했습니다: ${error.message} 문서 작성과 파일 저장은 계속 사용할 수 있습니다.`);} +window.pureDocument=Object.freeze({snapshot:()=>({ready:Boolean(splitter),stats:{...stats}, + runtime:runtime?.snapshot()??null,dragging:splitter?.isDragging()??false,composing,dirty,generation})}); diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/web/index.html b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/index.html new file mode 100644 index 0000000..089302c --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/index.html @@ -0,0 +1,95 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

숫자 입력
input level = value와 on level.change { amount = event_value }로 숫자 컨트롤을 연결합니다. 웹 호스트와 CLI에서도 같은 값을 전달할 수 있습니다.

직접 조작
input drag = pointer(tile)로 도형에 입력을 연결합니다. on drag.down / .move / .up / .cancel에서 event_x, event_y는 장면 좌표입니다. 이동 방식과 취소 시 동작도 코드로 정합니다. 초점이나 포인터 캡처를 잃으면 .cancel이 전달됩니다.

값과 규칙 재사용
record Tile { x=0; y=0 }로 필드를 정하고 item green = Tile(x=80)으로 대상을 만듭니다. rule move(t: Tile) { t.x=event_x }를 on drag.move use move(green)으로 연결합니다. 화면에서는 green.x를 읽습니다. 규칙은 연결한 대상의 값을 직접 바꿉니다.

여러 도형의 입력 묶기
input drag = pointer { green_box: green; blue_box: blue }로 연결하고 on drag.move use move(drag.target)으로 현재 조작 대상을 전달합니다. 같은 레코드 타입의 대상끼리 묶으며, 각 도형은 따로 잡고 놓을 수 있습니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

문서 편집 · 반응형 레이아웃 작업 화면 ↗

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/results/2026-09-23-web-layout/sources/web/runtime.mjs b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/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/workbench/results/2026-09-23-web-layout/sources/web/split-input.mjs b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/split-input.mjs new file mode 100644 index 0000000..294e9a3 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/sources/web/split-input.mjs @@ -0,0 +1,57 @@ +// Only gesture ownership and transient geometry live here. Layout belongs to CSS. +export function bindSplit({container, handle, readRatio, writeRatio, narrow, stats}) { + const events=container.ownerDocument.defaultView, abort=new AbortController(); + const listen=(node,type,callback,options={})=>node.addEventListener(type,callback,{...options,signal:abort.signal}); + let drag=null, frame=0, dirty=true, rect=null, handleWidth=0; + const invalidate=()=>{dirty=true;}; + const observer=new ResizeObserver(invalidate);observer.observe(container);observer.observe(handle); + function measure() { + rect=container.getBoundingClientRect();const h=handle.getBoundingClientRect(); + handleWidth=h.width;stats.geometryReads+=2;dirty=false;return h; + } + function flush() { + if(frame){cancelAnimationFrame(frame);frame=0;} + if(!drag || drag.x===null)return; + if(dirty)measure(); + const width=rect.width-handleWidth; + if(width>0)writeRatio(100*(drag.x-rect.left-handleWidth/2-drag.offset)/width,'pointer'); + drag.x=null; + } + function finish(cancel) { + if(!drag)return; + if(cancel){if(frame)cancelAnimationFrame(frame);frame=0;}else flush(); + const previous=drag;drag=null; + if(handle.hasPointerCapture(previous.id))handle.releasePointerCapture(previous.id); + handle.classList.remove('dragging'); + if(cancel)writeRatio(previous.before,'cancel'); + } + listen(handle,'pointerdown',event=>{ + if(event.button!==0 || drag || narrow.matches)return; + const h=measure();if(rect.width<=handleWidth)return; + event.preventDefault();handle.focus(); + handle.setPointerCapture(event.pointerId); + drag={id:event.pointerId,type:event.pointerType,before:readRatio(),offset:event.clientX-(h.left+h.width/2),x:null}; + handle.classList.add('dragging'); + }); + listen(handle,'pointermove',event=>{ + if(!drag || drag.id!==event.pointerId)return; + event.preventDefault();drag.x=event.clientX; + if(!frame)frame=requestAnimationFrame(()=>{frame=0;flush();}); + }); + listen(events,'pointerup',event=>{if(drag?.id===event.pointerId)finish(false);},{capture:true}); + listen(events,'pointercancel',event=>{if(drag?.id===event.pointerId)finish(true);},{capture:true}); + listen(events,'mouseup',event=>{if(event.button===0&&drag?.type==='mouse')finish(false);},{capture:true}); + listen(handle,'lostpointercapture',event=>{if(event.target===handle&&drag?.id===event.pointerId)finish(true);}); + listen(events,'blur',()=>finish(true)); + listen(container.ownerDocument,'visibilitychange',()=>{if(container.ownerDocument.hidden)finish(true);}); + listen(events,'resize',invalidate);listen(events,'scroll',invalidate,{capture:true,passive:true}); + listen(narrow,'change',()=>{invalidate();if(narrow.matches)finish(true);}); + listen(handle,'keydown',event=>{ + if(event.key==='Escape'){event.preventDefault();finish(true);return;} + if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return; + event.preventDefault();finish(true); + writeRatio(event.key==='Home'?20:event.key==='End'?80:readRatio()+(event.key==='ArrowLeft'?-1:1)*(event.shiftKey?10:1),'keyboard'); + }); + return Object.freeze({cancel:()=>finish(true),isDragging:()=>drag!==null, + dispose(){finish(true);observer.disconnect();abort.abort();}}); +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/value-control.json b/prototypes/workbench/results/2026-09-23-web-layout/value-control.json new file mode 100644 index 0000000..36ac5dc --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/value-control.json @@ -0,0 +1,7 @@ +{ + "checks": [ + "numeric control dispatches change and publishes the authored result", + "numeric controls do not create a held pointer owner", + "the same authored clamp handles negative requests" + ] +} diff --git a/prototypes/workbench/results/2026-09-23-web-layout/value-control.log b/prototypes/workbench/results/2026-09-23-web-layout/value-control.log new file mode 100644 index 0000000..346a5a6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/value-control.log @@ -0,0 +1,18 @@ +### Result +{"checks":["numeric control dispatches change and publishes the authored result","numeric controls do not create a held pointer owner","the same authored clamp handles negative requests"]} +### Ran Playwright code +```js +await (async page => { + const checks=[],check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);}; + await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready); + await page.locator('#source').fill('state amount=0\ninput level=value\ntext label(20,40,amount)\non level.change {amount=clamp(event_value,0,100)}'); + await page.locator('#apply').click(); + await page.getByLabel('level',{exact:true}).fill('123');await page.getByRole('button',{name:'값 전달',exact:true}).click(); + let s=await page.evaluate(()=>window.pureWorkbench.snapshot());check(s.states.amount===100&&s.lastEvent.phase==='change','numeric control dispatches change and publishes the authored result'); + check(s.held.length===0,'numeric controls do not create a held pointer owner'); + await page.getByLabel('level',{exact:true}).fill('-5');await page.getByRole('button',{name:'값 전달',exact:true}).click(); + s=await page.evaluate(()=>window.pureWorkbench.snapshot());check(s.states.amount===0,'the same authored clamp handles negative requests'); + await page.evaluate(e=>window.__pureValueEvidence=e,{checks});return{checks}; +} +)(page); +``` diff --git a/prototypes/workbench/results/2026-09-23-web-layout/verification.json b/prototypes/workbench/results/2026-09-23-web-layout/verification.json new file mode 100644 index 0000000..a29556b --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-web-layout/verification.json @@ -0,0 +1,14 @@ +{ + "nativeTests": 37, + "nodeTests": 87, + "browserChecks": { + "document": 32, + "authoring": 44, + "numericControl": 3, + "guards": 6 + }, + "sourceSha256": "60307704dbb5e11a75d14108fb7d87a5a4480359fdd771ec73beebf5f5c48b81", + "wasmSha256": "9f760e4a2ce6ea11f2be07693fceca74088884885cc545a70ceef5003d0b2479", + "wasmFileBytes": 61072, + "scope": "Functional and ownership checks, not a throughput/latency/RAM benchmark" +} diff --git a/prototypes/workbench/results/2026-09-23-workspace/browser.json b/prototypes/workbench/results/2026-09-23-workspace/browser.json new file mode 100644 index 0000000..5c40c3d --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-workspace/browser.json @@ -0,0 +1,240 @@ +{ + "utc": "2026-09-23T02:06:08.428Z", + "checks": [ + "authored browser input commits fractional state", + "bounded evaluation renders the exact authored expression", + "code replacement preserves state and changes binding result", + "browser Wasm reuses completed bindings", + "browser runs the shared-workspace build with only 56 additional dedicated bytes", + "discarding records preserves the browser snapshot", + "browser API publishes no partial scene after budget exhaustion", + "raising the budget recovers the browser snapshot", + "configured UI keeps committed state while withholding incomplete display", + "UI distinguishes display limit from event rollback", + "normal configuration reloads to a clean blank work", + "no uncaught browser errors" + ], + "api": { + "before": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "warm": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 6, + "instructions": 0, + "cacheHits": 3, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + }, + "forgotten": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limited": { + "states": { + "x": 3 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + }, + "limitStats": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 1, + "work": 1, + "instructions": 0, + "cacheHits": 0, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + }, + "recovered": { + "states": { + "x": 3 + }, + "views": [ + { + "id": "label", + "type": "text", + "prefix": "", + "x": 40, + "y": 60, + "value": 10 + } + ], + "tick": 0, + "revision": 1, + "displayError": null, + "lastError": 0, + "errorMessage": "", + "errorLine": null, + "lastOps": 0, + "successfulEvents": 0, + "held": [], + "lastEvent": null, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + } + } + }, + "limitedUi": { + "states": { + "x": 3.25 + }, + "views": [], + "tick": 0, + "revision": 1, + "displayError": { + "code": 101, + "line": 3, + "message": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요." + }, + "lastError": 101, + "errorMessage": "표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.", + "errorLine": 3, + "lastOps": 0, + "successfulEvents": 2, + "held": [], + "lastEvent": { + "name": "next", + "phase": "up", + "source": "pointer:1", + "status": 0, + "line": null, + "ops": 0 + }, + "lastFailure": null, + "memory": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 252, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "ready": true, + "source": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "appliedSource": "state x = 3\ninput next = KeyN\ntext label(40, 60, (x + 1) * (x + 2) / 2)\non next.down { x = x + 0.25 }\n", + "dirty": false, + "running": false, + "droppedTicks": 0, + "diagnostic": "코드를 적용했습니다. 같은 이름의 상태는 유지됩니다." + }, + "message": "표시 오류 · 3행 · 표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요. — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.", + "errors": [], + "uiLimitSetup": "Test-only module response overrides default workLimit to 1; production files are unchanged.", + "screenshot": "output/playwright/pure-workspace-limit.png", + "browser": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36", + "qaSourceSha256": "d83577c0851b465c3bd43c256b53659aef9df96f3c3dffe7e59f5d7c98db80c6" +} diff --git a/prototypes/workbench/results/2026-09-23-workspace/comparison.json b/prototypes/workbench/results/2026-09-23-workspace/comparison.json new file mode 100644 index 0000000..c53cc60 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-workspace/comparison.json @@ -0,0 +1,142 @@ +{ + "utc": "2026-09-23T01:58:27.939Z", + "kind": "workspace-reuse-correctness-and-storage", + "baselineManifestSha256": "f3e4eabf8a5a2a085a6cce62336b254e960d40892902653aba387dcbe4e775a8", + "sourceSha256": { + "core.zig": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "web/runtime.mjs": "8516b0d7d89f5fd483b8c48cfc33bbb2bcbe8c27db3aa35b5f6cf892e89faa6e", + "web/compiler.mjs": "a34f3beb069c811a9bd5be73dde9341907ba77756066030b8b95fc6d66a5cc02", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "workspace-check.mjs": "a841167a2be597a56c1adc1cf31e4b1e8841579a42d853e260059531009015f0", + "examples/counter.pure": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "examples/toggle.pure": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "examples/charge.pure": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56" + }, + "cases": [ + { + "name": "counter", + "snapshots": 257, + "before": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 284, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "after": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 284, + "activeStateBytes": 8, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "evaluation": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 14, + "instructions": 0, + "cacheHits": 7, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + } + }, + { + "name": "toggle", + "snapshots": 257, + "before": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 456, + "activeStateBytes": 16, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "after": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 456, + "activeStateBytes": 16, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "evaluation": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 20, + "instructions": 0, + "cacheHits": 10, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + } + }, + { + "name": "charge", + "snapshots": 257, + "before": { + "linearBytes": 65536, + "reservedEngineBytes": 20272, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 1092, + "activeStateBytes": 32, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "after": { + "linearBytes": 65536, + "reservedEngineBytes": 19128, + "programReservedBytes": 9392, + "instructionBytes": 16, + "handlerBytes": 12, + "activeIRBytes": 1092, + "activeStateBytes": 32, + "stagingReservedBytes": 9392, + "evalStackBytes": 512 + }, + "evaluation": { + "cacheEntries": 32, + "historyEntries": 8, + "workLimit": 2048, + "work": 28, + "instructions": 0, + "cacheHits": 14, + "reservedBytes": 1200, + "dedicatedBytes": 48, + "sharedBytes": 1152, + "reuseAvailable": true, + "workspaceTagBytes": 8 + } + } + ], + "node": "v24.20.0", + "platform": "linux", + "arch": "arm64", + "wasmFileBytes": { + "before": 53125, + "after": 53952 + } +} diff --git a/prototypes/workbench/results/2026-09-23-workspace/verification.json b/prototypes/workbench/results/2026-09-23-workspace/verification.json new file mode 100644 index 0000000..204e308 --- /dev/null +++ b/prototypes/workbench/results/2026-09-23-workspace/verification.json @@ -0,0 +1,75 @@ +{ + "utc": "2026-09-23T01:59:46.692314+00:00", + "build": { + "builtUtc": "2026-09-23T01:55:46.672638+00:00", + "zigVersion": "0.15.2", + "sourceSha256": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "wasmSha256": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "evaluationSourceSha256": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "wasmFileBytes": 53952, + "flags": [ + "-target", + "wasm32-freestanding", + "-O", + "ReleaseSafe", + "-fstrip", + "-fno-entry", + "-rdynamic", + "--export-memory", + "--stack", + "16384", + "--initial-memory=65536", + "--max-memory=65536" + ], + "nativeTests": "1/23 core.test.blank program, no hidden application behavior, and reset...OK\n2/23 core.test.arbitrary input counter and unused events...OK\n3/23 core.test.charge and release behavior exists only in the authored instruction program...OK\n4/23 core.test.forward condition executes once and assignments and handlers are ordered...OK\n5/23 core.test.branch to program end is valid...OK\n6/23 core.test.one event rolls back writes from all handlers on arithmetic failure...OK\n7/23 core.test.nonfinite arithmetic and invalid clamp fail atomically...OK\n8/23 core.test.stack underflow overflow and leftover values reject the event...OK\n9/23 core.test.failed candidates leave active values program revision and diagnostics unchanged...OK\n10/23 core.test.state preservation mapping reads active values at commit and reset uses new initials...OK\n11/23 core.test.capacity boundaries and sticky load errors...OK\n12/23 core.test.invalid declaration values state mappings event ids and handler ranges...OK\n13/23 core.test.backward self out-of-handler branches and stores without handlers are rejected...OK\n14/23 core.test.event inputs and dt enforce deterministic frame contract...OK\n15/23 core.test.binding evaluation is pure on both success and failure...OK\n16/23 core.test.arithmetic comparison logical min max and clamp expression semantics...OK\n17/23 core.test.diagnostic counters wrap without affecting scalar values...OK\n18/23 core.test.memory accounting separates reserved storage used IR and stack capacity...OK\n19/23 core.test.loading shares storage with records while old events and bindings remain executable...OK\n20/23 core.test.failed and abandoned candidates restore empty reuse storage without changing active work...OK\n21/23 core.test.full pending program survives repeated evaluation configuration record loss and reset...OK\n22/23 evaluation.test.record loss cannot authorize an old memo; unrelated changes can...OK\n23/23 evaluation.test.budget, collisions, invalid settings, and disabled history are conservative...OK\nAll 23 tests passed.\n" + }, + "javascript": [ + { + "file": "compiler.test.mjs", + "tests": 13, + "output": "✔ a blank document is an empty program with no implied behavior (4.084845ms)\n✔ forward references and separate state/input namespaces preserve declaration IDs (1.916867ms)\n✔ ordered assignments, repeated handlers, and condition evaluation once (1.004795ms)\n✔ nested branches use absolute forward targets and skip correct bodies (0.627499ms)\n✔ operator precedence, associativity, unary and builtin function lowering (1.465349ms)\n✔ multiline parentheses, scientific literals, comments, and string escapes (0.665517ms)\n✔ all instruction fields are numeric and carry original source line (1.468127ms)\n✔ contextual declaration words remain valid names outside reserved builtins (0.597314ms)\n✔ unknown and reserved symbols have useful one-based locations (1.610405ms)\n✔ duplicates, unsupported keys, initializers, and invalid syntax are rejected (1.42372ms)\n✔ document, declaration, instruction, and nesting limits reject overflow (6.016712ms)\n✔ every supported input maps to a bounded ordered event ID (0.619481ms)\n✔ optional examples compile and charge behavior is entirely authored (9.912298ms)\nℹ tests 13\nℹ suites 0\nℹ pass 13\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 38.96273\n", + "sha256": "6aa442d9d1eeb69453082f1780ff4597e46b1fc09d8ba3cf955545bc94d81431" + }, + { + "file": "runtime.test.mjs", + "tests": 13, + "output": "✔ blank startup and applied empty source contain no application objects or behavior (6.367748ms)\n✔ author can add unrelated states inputs views and behavior entirely through source (4.274419ms)\n✔ keyboard and pointer sources aggregate first down and last up; repeats are ignored (2.158996ms)\n✔ held input rejects apply without changing applied source, metadata, or values (1.463775ms)\n✔ invalid syntax, unknown references and capacity overflow preserve the previous program (1.888256ms)\n✔ state preservation follows names across reorder/add/remove; reset uses new initializers (1.034961ms)\n✔ callers cannot mutate active metadata and corrupt name-based state preservation (0.856887ms)\n✔ conditions execute once, and assignments and multiple handlers follow source order (0.784572ms)\n✔ a failed event rolls back all handlers and keeps its source diagnostic through release (2.938199ms)\n✔ reset clears held inputs and latched errors; invalid frame counts have no effects (1.303664ms)\n✔ render evaluation is pure and invalid binding is distinct from an event rollback (3.887456ms)\n✔ independent adapters replay the same author program without sharing live state (1.536738ms)\n✔ Wasm has no imports and reports fixed memory and exact active IR/state storage (6.509674ms)\nℹ tests 13\nℹ suites 0\nℹ pass 13\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 45.605312\n", + "sha256": "5fe66115aab0c178a283dcb592376a2e7209308368004865e6c0a1cc58fa7f62" + }, + { + "file": "baseline.test.mjs", + "tests": 7, + "output": "✔ reference begins with the authored states and matching scene (2.33507ms)\n✔ press, hold, release, and frame order produce exact expected values (0.559814ms)\n✔ charge clamps at one and explicit floor restores rest (1.323109ms)\n✔ repeat and concurrent physical sources produce only logical edges (0.477092ms)\n✔ airborne presses cannot charge or cause another jump (0.26063ms)\n✔ releaseAll emits release and reset clears latches without a jump (0.432721ms)\n✔ unknown inputs and invalid frame counts reject without mutation (0.798832ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 14.77855\n", + "sha256": "3ea469a29d301b95701bc6f6ffafa2471ac0f334c5bd462c0cbc7c320bdc0764" + }, + { + "file": "cli.test.mjs", + "tests": 5, + "output": "✔ terminal starts empty and produces the same charge trace as the shared runtime (103.338011ms)\n✔ invalid terminal commands do not alter state and fail piped execution (46.394366ms)\n✔ file load preserves by name, rejected load preserves active work, and save never overwrites (51.618413ms)\n✔ terminal exposes arithmetic rollback and retains failure through matching release (51.527709ms)\n✔ terminal configures and discards evaluation records and recovers from a display limit (44.876573ms)\nℹ tests 5\nℹ suites 0\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 305.313987\n", + "sha256": "655c6d80b51af818fa7cdeb27cb5462962a0e1aef70f609ef5e2ddc85f4a6bc7" + }, + { + "file": "evaluation.test.mjs", + "tests": 7, + "output": "✔ cache reuse, unrelated changes and discarded records preserve authored values (14.743642ms)\n✔ committed changes only: failed events and failed edits preserve reusable output (2.431237ms)\n✔ program edits, state reordering and reset cannot reuse old definitions (2.475107ms)\n✔ finite float semantics include signed zero, high state bit and arithmetic errors (2.389774ms)\n✔ one budget covers the whole scene; failures publish no partial scene or state rollback (1.167016ms)\n✔ small caches, no history and every small work limit give exact results or explicit failure (23.256183ms)\n✔ invalid settings and forget names preserve policy and state (1.602997ms)\nℹ tests 7\nℹ suites 0\nℹ pass 7\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 56.583738\n", + "sha256": "d3026ab5c6bffce56c0bde3155d7dddd6fa442a5688656ee710626c762258ff1" + }, + { + "file": "workspace.test.mjs", + "tests": 2, + "output": "✔ Wasm executes old events and output during staged replacement without corrupting either phase (6.216471ms)\n✔ workspace accounting reports shared payload and safety tag without double-counting (1.978441ms)\nℹ tests 2\nℹ suites 0\nℹ pass 2\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 14.652234\n", + "sha256": "7ea664736e4c9d791d72cf64de0fbc230b8d849910cc2a456d4d0e959acb9857" + } + ], + "javascriptTestCount": 47, + "sourceSha256": { + "core.zig": "dc84bcb75e4417fb1a804529c78ffa8cd1ddd079c00df4ca97a35da0b8fdbb8a", + "evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "web/core.wasm": "76823b024fa5f4a79cbf1ed722ab757614a81a9f1c81d1d6268781a38bed7cc9", + "web/runtime.mjs": "8516b0d7d89f5fd483b8c48cfc33bbb2bcbe8c27db3aa35b5f6cf892e89faa6e", + "web/app.js": "6d63f3bfdfe212146894dbb1772509eb4ecc2f3be3b106675f0fc10cdeb5bfd3", + "cli.mjs": "2e0627ef84a45c8a9e91fb28c0010673212d459d0b0d09a5342856a049060030", + "manage.py": "e04199a0b5addc73bea7748420eee21ccf0aaa1a5b8c83082e5588187d6ed669", + "qa-evaluation.js": "d83577c0851b465c3bd43c256b53659aef9df96f3c3dffe7e59f5d7c98db80c6" + } +} diff --git a/prototypes/workbench/results/2026-09-24-compact-runtime/app-memory-three-way-final.json b/prototypes/workbench/results/2026-09-24-compact-runtime/app-memory-three-way-final.json new file mode 100644 index 0000000..3367cec --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-compact-runtime/app-memory-three-way-final.json @@ -0,0 +1,160 @@ +{ + "profile": "whole-native-app-idle-work-storage", + "prepared_sha256": "b7524e53a5d90d1ce16d2e2fbaac67ceb242f1cd27f280afc3a80857ad8c21ba", + "scalar_archive_sha256": "30a015c55d385b337639b54b2f31aea34b40bc37d1fe6c9e3c884660d26e4cbc", + "harness_sha256": "f0e3eedda1bb38bf4d2be3d1b43ca8c30bdfe536fe4890d943f25bd29bebf344", + "checker_sha256": "eab934985f3748c9c840d74975071389c9b7a4e8491a05bf4ef2a15e2a719111", + "baseline": { + "binary_sha256": "b9dea2faf85f096e93f9b93db4b431ec6e5617d5528be87db27e2bae0e614b89", + "app_source_sha256": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "app_header_sha256": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "text_bytes": 35546, + "data_bytes": 1968, + "app_controller_bytes": 856, + "scalar_bytes": 280, + "samples": [ + { + "apps": 1000, + "app_controller_bytes": 856, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2576, + "rss_saved_kib": 2640, + "rss_initialized_delta_kib": 1200, + "rss_saved_delta_kib": 1264, + "completed_actions": 1000, + "completed_transfers": 1005 + }, + { + "apps": 1000, + "app_controller_bytes": 856, + "scalar_bytes": 280, + "rss_before_kib": 1392, + "rss_initialized_kib": 2592, + "rss_saved_kib": 2656, + "rss_initialized_delta_kib": 1200, + "rss_saved_delta_kib": 1264, + "completed_actions": 1000, + "completed_transfers": 1005 + }, + { + "apps": 1000, + "app_controller_bytes": 856, + "scalar_bytes": 280, + "rss_before_kib": 1360, + "rss_initialized_kib": 2560, + "rss_saved_kib": 2624, + "rss_initialized_delta_kib": 1200, + "rss_saved_delta_kib": 1264, + "completed_actions": 1000, + "completed_transfers": 1005 + } + ], + "median_initialized_delta_kib": 1200, + "median_saved_delta_kib": 1264 + }, + "elastic": { + "binary_sha256": "738fecb38f2306264ef5356318a3e23b4e131355d88c2457ef541734b9c70a37", + "app_source_sha256": "cf19cbf7d7c60020b13198e9335e519f650633f1eb3fb25b61953ff56c3e4dd8", + "app_header_sha256": "622ec62cfb5f755fe0fb2582f5e1ce264172e8a45663078429afb910061e21a4", + "text_bytes": 35730, + "data_bytes": 1968, + "app_controller_bytes": 408, + "scalar_bytes": 280, + "samples": [ + { + "apps": 1000, + "app_controller_bytes": 408, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2128, + "rss_saved_kib": 2192, + "rss_initialized_delta_kib": 752, + "rss_saved_delta_kib": 816, + "completed_actions": 1000, + "completed_transfers": 1005 + }, + { + "apps": 1000, + "app_controller_bytes": 408, + "scalar_bytes": 280, + "rss_before_kib": 1392, + "rss_initialized_kib": 2144, + "rss_saved_kib": 2208, + "rss_initialized_delta_kib": 752, + "rss_saved_delta_kib": 816, + "completed_actions": 1000, + "completed_transfers": 1005 + }, + { + "apps": 1000, + "app_controller_bytes": 408, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2128, + "rss_saved_kib": 2192, + "rss_initialized_delta_kib": 752, + "rss_saved_delta_kib": 816, + "completed_actions": 1000, + "completed_transfers": 1005 + } + ], + "median_initialized_delta_kib": 752, + "median_saved_delta_kib": 816 + }, + "elastic_metrics": { + "binary_sha256": "bdf8be3979cdb5b76916e71771cf6304ea50c2184f9c490d63ab996d7852a5db", + "app_source_sha256": "cf19cbf7d7c60020b13198e9335e519f650633f1eb3fb25b61953ff56c3e4dd8", + "app_header_sha256": "622ec62cfb5f755fe0fb2582f5e1ce264172e8a45663078429afb910061e21a4", + "text_bytes": 35898, + "data_bytes": 1968, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "samples": [ + { + "apps": 1000, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2208, + "rss_saved_kib": 2272, + "rss_initialized_delta_kib": 832, + "rss_saved_delta_kib": 896, + "completed_actions": 1000, + "completed_transfers": 1005 + }, + { + "apps": 1000, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "rss_before_kib": 1392, + "rss_initialized_kib": 2224, + "rss_saved_kib": 2288, + "rss_initialized_delta_kib": 832, + "rss_saved_delta_kib": 896, + "completed_actions": 1000, + "completed_transfers": 1005 + }, + { + "apps": 1000, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2208, + "rss_saved_kib": 2272, + "rss_initialized_delta_kib": 832, + "rss_saved_delta_kib": 896, + "completed_actions": 1000, + "completed_transfers": 1005 + } + ], + "median_initialized_delta_kib": 832, + "median_saved_delta_kib": 896 + }, + "controller_saved_per_app_bytes": 448, + "metrics_storage_saved_per_app_bytes": 72, + "metrics_initialized_rss_median_saved_kib": 80, + "metrics_saved_rss_median_saved_kib": 80, + "initialized_rss_median_saved_kib": 448, + "saved_rss_median_saved_kib": 448 +} diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-cli-build.json b/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-cli-build.json new file mode 100644 index 0000000..25fdd72 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-cli-build.json @@ -0,0 +1,48 @@ +{ + "source": "/home/eunhhu/work/pure/prototypes/workbench/examples/routed-layout.pure", + "sourceSha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "zigVersion": "0.15.2", + "zigFlags": [ + "-O", + "ReleaseSafe", + "-fno-compiler-rt" + ], + "scalarProfile": "fixed-definition", + "zigRoot": "/home/eunhhu/work/pure/prototypes/workbench/native-fixed.zig", + "cFlags": [ + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror" + ], + "binary": "/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-native", + "binarySha256": "ca33aa955ac192db31f5684ff8535f05bded6adef2f7f982b4afd681b3ef11d0", + "librarySha256": "bcdfc29c7aaa42f3c50baa0f46c0b0cab341f7b8d5d7c75b1f10ddc9f94660f2", + "fixedProfileSha256": "adc229822531f3c62f3e4f16821ea21d74047808125e699f1ce03c5bfa7f9199", + "preparedSha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "desktop": false, + "desktopFlags": [], + "peer": false, + "peerFlags": [], + "implementationSha256": { + "prototypes/workbench/engine.zig": "1a58aa081fcfa0876c5499bbfa2768334641e4bd5a357ae4136003fd1e315b98", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/native-fixed.zig": "29b4cc1c8777e83c0d167ecabd66b6bb8c84dba9273f0c1d9e24e33f872f1125", + "prototypes/workbench/native/prepare.mjs": "2d2679ae71088ba8691f29029c85c849a5351cf5e49c9b691c60b56b15ceca0f", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f" + } +} diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-parity-final.json b/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-parity-final.json new file mode 100644 index 0000000..69a2446 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-parity-final.json @@ -0,0 +1,209 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": true, + "profile": "fixed-definition", + "general_instance_bytes": 20064, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 1576, + "fixed_profile_sha256": "f894c6e3ed9ab2aff461e4bf1e31a19d7eedc8e0d1618b433c1a733b08539e72", + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "eada952dee9afd7071370215364b2a6717a28f03e0513cdd0cae4695939ec52b", + "native_binary_sha256": "4dde0c70ea02eaf3def61e4d82c1707ffb825a60e81cea89487b83576afd6705", + "native_binary_bytes": 589832 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 1768, + "fixed_profile_sha256": "f6afa6aab5a41d915a68af4414372b1e3fd36b1486de31a97690dd1872f45ffc", + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "fc96ee38b256117a7bfe2859b7126e22262c19e96047a8d01fbe30d4d1a28764", + "native_binary_sha256": "0de137366b074a34090268a0e5198a1ed4fd39c8613594f1212c447df1a47b48", + "native_binary_bytes": 593464 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 2528, + "fixed_profile_sha256": "4b15bba6c5aec3cc97caa76a93c77398be13f54ccd536936fc41c0d6387ffde3", + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "7ba5fa094b0bc772202bb44a62b3c8377c4f3924db246ec429c2faee4948b09f", + "native_binary_sha256": "1fe2b0dfbf1bce6955b21d9a48b7751db0529db14402d47ce3b230838836e9ee", + "native_binary_bytes": 598024 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 3248, + "fixed_profile_sha256": "9dfd71e76ae0d2b42cf85b02d4169fc26f4f15d0ac0a9796962f892cb05efdb3", + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "27f0aa76eb3e12e289bcd6edf6c2ae72892a2dfe1ff094b619671f0583e2c8d3", + "native_binary_sha256": "cad1d5261438b6b643d728c015d138bb268f5e804a6f0c56a69a4f401b0084ab", + "native_binary_bytes": 600624 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 5456, + "fixed_profile_sha256": "adc229822531f3c62f3e4f16821ea21d74047808125e699f1ce03c5bfa7f9199", + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "native_binary_sha256": "12647b3270ca7022b82542cc10837b0bb4527e1f1892c8635c2d11a11df4c7f7", + "native_binary_bytes": 595000 + }, + { + "name": "layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 11792, + "fixed_profile_sha256": "5ede5580fe1f780269a6288fbfb8bc54487699f1e73aabbd3fc4d6200bba8602", + "fresh_process_restore": true, + "source": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n", + "source_sha256": "cebd9c3485a2be4ec16ae665c1ed4ee200d51fc56ad0151306c8e9c31138f346", + "prepared_sha256": "ed2fb846200749b500e3349b324bd30778a6938e867abd715b35f661f1d06e81", + "native_binary_sha256": "8e28b4cbad0538b948dd207958d83907cb31fc8e50dcc20d47ead8bc97c9a952", + "native_binary_bytes": 606864 + }, + { + "name": "shared-layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 5712, + "fixed_profile_sha256": "6892a498a7289d9e969e745271615ad963b692bcc4012e4d20519881ffe40d94", + "fresh_process_restore": true, + "source": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "be8296244b768f9adda99e7afc89249f34853fa27124a277aecb0841eb8176eb", + "prepared_sha256": "aed8c20aff78e0322434b3b6f9893f39084444d209337fef9345cc182965790d", + "native_binary_sha256": "6663289f823122261fd48ee307f7319c67793ca80dbcc0240552df361e2aa3eb", + "native_binary_bytes": 595400 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 1432, + "fixed_profile_sha256": "017becbebd7be3f6bb66fc758c49ff11fdd919ecbd28300f362ab5cb06481484", + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "c74315c1b0067e167a4678b30505d1770b0753b601e8acee27f6d598ac7b9ffb", + "native_binary_sha256": "0bf6d2cfd68d26a6f2e382feea6ca78e99364c8fdf22006bebd07a4bba11cae7", + "native_binary_bytes": 591968 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "instance_bytes": 1464, + "fixed_profile_sha256": "daf614a069ecd881b4e0acf0b45a61f64bb7fc5098a03bf55282dce04084b0a7", + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "6a415d33780227158ebeae2a75c7520bf13bad3c0a6fef7ee82f5c323be226f0", + "native_binary_sha256": "60fb0cc6dd2162ca3149096e23cc18a70a39044fe3cd5c972eb27b78e1b93541", + "native_binary_bytes": 591184 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 1320, + "fixed_profile_sha256": "9cf762238448174b8fc058577912f3aad318b9e84d50e4061862ad3f7a884fcd", + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "1c8f5ade56ee8ec931a62fd1f68ef3635b17297d3a54b20c954860c4a43b6155", + "native_binary_sha256": "63df7a3e4b2ac8cefc9074b97da9eda533660f86ce9ebe1f5f87d24d5d3351f9", + "native_binary_bytes": 588832 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 1384, + "fixed_profile_sha256": "933c81c100e38fd977057d4444210c0715af372183e002b649497f8f8c72daf4", + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e9a8b1542ecf6b008ad30188cb9e95d29638fab1c38d331c00bf706c40a06d3c", + "native_binary_sha256": "4ccc23a268a9cf6519380b1b692b4ee376f7e12aa094c35ac91fbb245adfa8b1", + "native_binary_bytes": 592032 + } + ], + "units": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload", + "scalar_instance_bytes": 5456, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "1a58aa081fcfa0876c5499bbfa2768334641e4bd5a357ae4136003fd1e315b98", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/native-fixed.zig": "29b4cc1c8777e83c0d167ecabd66b6bb8c84dba9273f0c1d9e24e33f872f1125", + "prototypes/workbench/manage.py": "e0cf9f9eb65bb8b8830797bf4f533ea2cd86045d401fef444e86fad654e4dd8f", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "2d2679ae71088ba8691f29029c85c849a5351cf5e49c9b691c60b56b15ceca0f", + "prototypes/workbench/native/test.c": "47488a2cfda2b378637f69b8f0d94f75e615d784e8418167b5b82c8ce141f7e8", + "prototypes/workbench/native/test-fixed.c": "725d109417ee6ca2b680b9b8ba82f604985083fe6173a6e7269f40d40eb7d18a", + "prototypes/workbench/native/check.mjs": "794ee0d44395c973d3ca2a7213dcd564733f291f0d22358e914815e41bdf3958", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "8fe2f501cb9639fea0817aa57e0b7292ca6779cda4ac156b7c6b3df31d7921e9", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "5ed7f2793d136aa79945c3115bac196ab2c97c1d5ef096bc2f3aba04bc9d13f4", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33020\t 0\t 0\t 33020\t 80fc\tprototypes/workbench/build/cache/tmp/ad67c6128f027364/general_zcu.o (ex /tmp/pure-native-parity-r76i5q/general.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/ad67c6128f027364/general_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-peer-build.json b/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-peer-build.json new file mode 100644 index 0000000..6b86d65 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/fixed-peer-build.json @@ -0,0 +1,52 @@ +{ + "source": "/home/eunhhu/work/pure/prototypes/workbench/examples/routed-layout.pure", + "sourceSha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "zigVersion": "0.15.2", + "zigFlags": [ + "-O", + "ReleaseSafe", + "-fno-compiler-rt" + ], + "scalarProfile": "fixed-definition", + "zigRoot": "/home/eunhhu/work/pure/prototypes/workbench/native-fixed.zig", + "cFlags": [ + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror" + ], + "binary": "/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-peer", + "binarySha256": "d62916cbaec4e84580127ef79134bd21f0298811be70b9cdd6e29e998bd4d5e0", + "librarySha256": "9d030a00ba5d3d24f9e7b4aaf967dc90a4fdad6afeac59b2959a2dac1564570a", + "fixedProfileSha256": "adc229822531f3c62f3e4f16821ea21d74047808125e699f1ce03c5bfa7f9199", + "preparedSha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "desktop": false, + "desktopFlags": [], + "peer": true, + "peerFlags": [ + "-lssl", + "-lcrypto" + ], + "implementationSha256": { + "prototypes/workbench/engine.zig": "1a58aa081fcfa0876c5499bbfa2768334641e4bd5a357ae4136003fd1e315b98", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/native-fixed.zig": "29b4cc1c8777e83c0d167ecabd66b6bb8c84dba9273f0c1d9e24e33f872f1125", + "prototypes/workbench/native/prepare.mjs": "2d2679ae71088ba8691f29029c85c849a5351cf5e49c9b691c60b56b15ceca0f", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/native/peer.c": "7d5865d499a4ba51f1d14e0d286480f605be2476616d28e1150b35ca1f87bf56", + "prototypes/workbench/native/peer-wire.c": "8fc3694db7a00a4f4899ca5a42dc2c0d96e39cb7d927c759ee8881901241733a", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f" + } +} diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/general-parity-final.json b/prototypes/workbench/results/2026-09-24-fixed-capacity/general-parity-final.json new file mode 100644 index 0000000..d8013a2 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/general-parity-final.json @@ -0,0 +1,209 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": true, + "profile": "general-authoring", + "general_instance_bytes": 20064, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "eada952dee9afd7071370215364b2a6717a28f03e0513cdd0cae4695939ec52b", + "native_binary_sha256": "ed239ce3033b402d454bc11752dd1c9559216acdda8c45ccefad1660ea482b45", + "native_binary_bytes": 591904 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "fc96ee38b256117a7bfe2859b7126e22262c19e96047a8d01fbe30d4d1a28764", + "native_binary_sha256": "aee4b4660bcf74176ca4ef3e83139e6a8d62af14ba35f6ec12cfd63982c7e388", + "native_binary_bytes": 592288 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "7ba5fa094b0bc772202bb44a62b3c8377c4f3924db246ec429c2faee4948b09f", + "native_binary_sha256": "ed9d13c86fafa6ac9a1d80177fd2c702b38cc374796df698143ca3c7eea687d1", + "native_binary_bytes": 592352 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "27f0aa76eb3e12e289bcd6edf6c2ae72892a2dfe1ff094b619671f0583e2c8d3", + "native_binary_sha256": "d536134e2997232b19d7672862b9ca9016da9f0d3554daf899a930b4aeccf9ec", + "native_binary_bytes": 593120 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "native_binary_sha256": "cabd1b99761af718c687068c53bb44879bf7f2b7654c32d8fb265f2714e4c24d", + "native_binary_bytes": 594272 + }, + { + "name": "layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n", + "source_sha256": "cebd9c3485a2be4ec16ae665c1ed4ee200d51fc56ad0151306c8e9c31138f346", + "prepared_sha256": "ed2fb846200749b500e3349b324bd30778a6938e867abd715b35f661f1d06e81", + "native_binary_sha256": "03c3f8a4d67da292dfe632758a687c22873bd852e2ef6e2986c110a0b26e26eb", + "native_binary_bytes": 594240 + }, + { + "name": "shared-layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "be8296244b768f9adda99e7afc89249f34853fa27124a277aecb0841eb8176eb", + "prepared_sha256": "aed8c20aff78e0322434b3b6f9893f39084444d209337fef9345cc182965790d", + "native_binary_sha256": "c335d630de485cc699a26409280408b9f025dc55321b1ff2ba7ff4f5a6ec71c0", + "native_binary_bytes": 594416 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "c74315c1b0067e167a4678b30505d1770b0753b601e8acee27f6d598ac7b9ffb", + "native_binary_sha256": "aca33b051685a745ee3d838c18dc581a15537f3aa9e5b46031b9410555d55b9c", + "native_binary_bytes": 591584 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "6a415d33780227158ebeae2a75c7520bf13bad3c0a6fef7ee82f5c323be226f0", + "native_binary_sha256": "e0d924e1d1fe5a6c7d4524443c97bb28199d8abf7d165f96a07b7235f97c14b6", + "native_binary_bytes": 591728 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "1c8f5ade56ee8ec931a62fd1f68ef3635b17297d3a54b20c954860c4a43b6155", + "native_binary_sha256": "0702180c9dd146f07811f5a95c1ff880039f0445666afa0401125ed358c613a1", + "native_binary_bytes": 591264 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e9a8b1542ecf6b008ad30188cb9e95d29638fab1c38d331c00bf706c40a06d3c", + "native_binary_sha256": "2bb2bcd328bf95ae473b6081a33c5bfd10f03398933d13b95cca429efd5afcd4", + "native_binary_bytes": 591664 + } + ], + "units": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload", + "scalar_instance_bytes": 20064, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "1a58aa081fcfa0876c5499bbfa2768334641e4bd5a357ae4136003fd1e315b98", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/native-fixed.zig": "29b4cc1c8777e83c0d167ecabd66b6bb8c84dba9273f0c1d9e24e33f872f1125", + "prototypes/workbench/manage.py": "e0cf9f9eb65bb8b8830797bf4f533ea2cd86045d401fef444e86fad654e4dd8f", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "2d2679ae71088ba8691f29029c85c849a5351cf5e49c9b691c60b56b15ceca0f", + "prototypes/workbench/native/test.c": "47488a2cfda2b378637f69b8f0d94f75e615d784e8418167b5b82c8ce141f7e8", + "prototypes/workbench/native/test-fixed.c": "725d109417ee6ca2b680b9b8ba82f604985083fe6173a6e7269f40d40eb7d18a", + "prototypes/workbench/native/check.mjs": "794ee0d44395c973d3ca2a7213dcd564733f291f0d22358e914815e41bdf3958", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "8fe2f501cb9639fea0817aa57e0b7292ca6779cda4ac156b7c6b3df31d7921e9", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "83834892d7b67b06aa30fb61dbca3c6bad89fa3064644269880d8c7c3e3286f7", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33020\t 0\t 0\t 33020\t 80fc\tprototypes/workbench/build/cache/tmp/2d18d0109be25ab7/general_zcu.o (ex /tmp/pure-native-parity-5YxKyV/general.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/2d18d0109be25ab7/general_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/instance-memory.json b/prototypes/workbench/results/2026-09-24-fixed-capacity/instance-memory.json new file mode 100644 index 0000000..33be16a --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/instance-memory.json @@ -0,0 +1,65 @@ +{ + "host": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "samples": { + "general": [ + { + "instances": 1000, + "instance_bytes": 20064, + "rss_before_kib": 1248, + "rss_after_kib": 21056, + "rss_delta_kib": 19808, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 20064, + "rss_before_kib": 1232, + "rss_after_kib": 21040, + "rss_delta_kib": 19808, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 20064, + "rss_before_kib": 1248, + "rss_after_kib": 21056, + "rss_delta_kib": 19808, + "checksum": 1000 + } + ], + "fixed": [ + { + "instances": 1000, + "instance_bytes": 5456, + "rss_before_kib": 1232, + "rss_after_kib": 6768, + "rss_delta_kib": 5536, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 5456, + "rss_before_kib": 1232, + "rss_after_kib": 6768, + "rss_delta_kib": 5536, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 5456, + "rss_before_kib": 1232, + "rss_after_kib": 6768, + "rss_delta_kib": 5536, + "checksum": 1000 + } + ] + }, + "binary_sha256": { + "general": "bb9b95ab306e0bb252c793055717409d3726729c408e73dd652d58cf4cb10b03", + "fixed": "faed0a646207e63d466f0afe78b655bfa875695aa3e45dc9e0d0d575eaa97768" + }, + "median_rss_delta_kib": { + "general": 19808, + "fixed": 5536 + } +} diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/peer-check.json b/prototypes/workbench/results/2026-09-24-fixed-capacity/peer-check.json new file mode 100644 index 0000000..301ba16 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/peer-check.json @@ -0,0 +1 @@ +{"alpn": "pass", "disconnect_release": "pass", "idle_deadline": "pass", "malformed_frames": "pass", "mutual_authentication": "pass", "persist_restart": "pass", "python_client": "pass", "revision_conflict": "pass", "server_connected_rss_kib": 8464, "server_hostname": "pass", "server_idle_rss_kib": 7888} diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/size.txt b/prototypes/workbench/results/2026-09-24-fixed-capacity/size.txt new file mode 100644 index 0000000..42841eb --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/size.txt @@ -0,0 +1,6 @@ + text data bss dec hex filename + 33020 0 0 33020 80fc prototypes/workbench/build/cache/tmp/13dbb99515d73cd6/pure-general-scalar_zcu.o (ex /tmp/pure-general-scalar.a) + 18252 0 0 18252 474c prototypes/workbench/build/cache/tmp/5e96d4ff68ad7b63/libpure-scalar-fixed_zcu.o (ex prototypes/workbench/build/native/libpure-scalar-fixed.a) + 62827 2040 16 64883 fd73 /tmp/pure-native-general + 48059 2040 16 50115 c3c3 prototypes/workbench/build/native/pure-native + 61201 2472 16 63689 f8c9 prototypes/workbench/build/native/pure-peer diff --git a/prototypes/workbench/results/2026-09-24-fixed-capacity/stripped.txt b/prototypes/workbench/results/2026-09-24-fixed-capacity/stripped.txt new file mode 100644 index 0000000..1897476 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-fixed-capacity/stripped.txt @@ -0,0 +1,3 @@ +/tmp/pure-native-general-stripped 68000 +/tmp/pure-native-fixed-stripped 68000 +/tmp/pure-peer-fixed-stripped 68400 diff --git a/prototypes/workbench/results/2026-09-24-idle-work-binding/app-memory.json b/prototypes/workbench/results/2026-09-24-idle-work-binding/app-memory.json new file mode 100644 index 0000000..14875e6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-idle-work-binding/app-memory.json @@ -0,0 +1,108 @@ +{ + "profile": "whole-native-app-idle-work-storage", + "prepared_sha256": "b7524e53a5d90d1ce16d2e2fbaac67ceb242f1cd27f280afc3a80857ad8c21ba", + "scalar_archive_sha256": "30a015c55d385b337639b54b2f31aea34b40bc37d1fe6c9e3c884660d26e4cbc", + "harness_sha256": "f6d6dde36c00847552e8a8f0c98fab700e33c7634cddd363202a8891424d789e", + "checker_sha256": "0360c8e60f769da76f2e6352ab11b753213c61a1f7b4d85660c6bb87d673eabe", + "baseline": { + "binary_sha256": "d76598743f43ebba0c11bbb70af7a2e24cc07172c794320bdeac0d35aac44f06", + "app_source_sha256": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "app_header_sha256": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "text_bytes": 35682, + "data_bytes": 1968, + "app_controller_bytes": 928, + "scalar_bytes": 280, + "samples": [ + { + "apps": 1000, + "app_controller_bytes": 928, + "scalar_bytes": 280, + "rss_before_kib": 1392, + "rss_initialized_kib": 2656, + "rss_saved_kib": 2720, + "rss_initialized_delta_kib": 1264, + "rss_saved_delta_kib": 1328, + "accepted": 3010, + "releases": 3010 + }, + { + "apps": 1000, + "app_controller_bytes": 928, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2640, + "rss_saved_kib": 2704, + "rss_initialized_delta_kib": 1264, + "rss_saved_delta_kib": 1328, + "accepted": 3010, + "releases": 3010 + }, + { + "apps": 1000, + "app_controller_bytes": 928, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2640, + "rss_saved_kib": 2704, + "rss_initialized_delta_kib": 1264, + "rss_saved_delta_kib": 1328, + "accepted": 3010, + "releases": 3010 + } + ], + "median_initialized_delta_kib": 1264, + "median_saved_delta_kib": 1328 + }, + "elastic": { + "binary_sha256": "a5d5f4d3344dcaa9a206261f8a423f39836afa3b3302933873ae5981383c95ef", + "app_source_sha256": "cf19cbf7d7c60020b13198e9335e519f650633f1eb3fb25b61953ff56c3e4dd8", + "app_header_sha256": "622ec62cfb5f755fe0fb2582f5e1ce264172e8a45663078429afb910061e21a4", + "text_bytes": 35946, + "data_bytes": 1968, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "samples": [ + { + "apps": 1000, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "rss_before_kib": 1392, + "rss_initialized_kib": 2224, + "rss_saved_kib": 2288, + "rss_initialized_delta_kib": 832, + "rss_saved_delta_kib": 896, + "accepted": 3010, + "releases": 3010 + }, + { + "apps": 1000, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "rss_before_kib": 1376, + "rss_initialized_kib": 2208, + "rss_saved_kib": 2272, + "rss_initialized_delta_kib": 832, + "rss_saved_delta_kib": 896, + "accepted": 3010, + "releases": 3010 + }, + { + "apps": 1000, + "app_controller_bytes": 480, + "scalar_bytes": 280, + "rss_before_kib": 1392, + "rss_initialized_kib": 2224, + "rss_saved_kib": 2288, + "rss_initialized_delta_kib": 832, + "rss_saved_delta_kib": 896, + "accepted": 3010, + "releases": 3010 + } + ], + "median_initialized_delta_kib": 832, + "median_saved_delta_kib": 896 + }, + "controller_saved_per_app_bytes": 448, + "initialized_rss_median_saved_kib": 432, + "saved_rss_median_saved_kib": 432 +} diff --git a/prototypes/workbench/results/2026-09-24-idle-work-binding/native-sanitized.json b/prototypes/workbench/results/2026-09-24-idle-work-binding/native-sanitized.json new file mode 100644 index 0000000..6a0da13 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-idle-work-binding/native-sanitized.json @@ -0,0 +1 @@ +{"native_instances":3,"checks":"independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload","scalar_instance_bytes":20064,"scalar_alignment":8,"app_controller_bytes":480,"action_bytes":32,"snapshot_bytes":244,"all_work_drained":true} diff --git a/prototypes/workbench/results/2026-09-24-idle-work-binding/native-unit.json b/prototypes/workbench/results/2026-09-24-idle-work-binding/native-unit.json new file mode 100644 index 0000000..6a0da13 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-idle-work-binding/native-unit.json @@ -0,0 +1 @@ +{"native_instances":3,"checks":"independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload","scalar_instance_bytes":20064,"scalar_alignment":8,"app_controller_bytes":480,"action_bytes":32,"snapshot_bytes":244,"all_work_drained":true} diff --git a/prototypes/workbench/results/2026-09-24-idle-work-binding/scalar-peer.json b/prototypes/workbench/results/2026-09-24-idle-work-binding/scalar-peer.json new file mode 100644 index 0000000..5f48783 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-idle-work-binding/scalar-peer.json @@ -0,0 +1 @@ +{"alpn": "pass", "disconnect_release": "pass", "idle_deadline": "pass", "malformed_frames": "pass", "mutual_authentication": "pass", "persist_restart": "pass", "python_client": "pass", "revision_conflict": "pass", "server_connected_rss_kib": 8624, "server_hostname": "pass", "server_idle_rss_kib": 7952} diff --git a/prototypes/workbench/results/2026-09-24-idle-work-binding/stack-baseline.su b/prototypes/workbench/results/2026-09-24-idle-work-binding/stack-baseline.su new file mode 100644 index 0000000..60579da --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-idle-work-binding/stack-baseline.su @@ -0,0 +1,9 @@ +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:16:13:allocate 32 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:33:19:read_value 176 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:15:13:release 0 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:121:19:write_value 336 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:203:20:transfer 288 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:167:12:pure_native_app_init 176 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:192:13:pure_native_app_action 208 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:222:13:pure_native_app_save 16 static +prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c:223:13:pure_native_app_restore 16 static diff --git a/prototypes/workbench/results/2026-09-24-idle-work-binding/stack-elastic.su b/prototypes/workbench/results/2026-09-24-idle-work-binding/stack-elastic.su new file mode 100644 index 0000000..265d96f --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-idle-work-binding/stack-elastic.su @@ -0,0 +1,9 @@ +prototypes/workbench/native/app.c:16:13:allocate 32 static +prototypes/workbench/native/app.c:33:19:read_value 176 static +prototypes/workbench/native/app.c:15:13:release 0 static +prototypes/workbench/native/app.c:121:19:write_value 336 static +prototypes/workbench/native/app.c:210:20:transfer 736 static +prototypes/workbench/native/app.c:167:12:pure_native_app_init 176 static +prototypes/workbench/native/app.c:192:13:pure_native_app_action 432 static +prototypes/workbench/native/app.c:235:13:pure_native_app_save 16 static +prototypes/workbench/native/app.c:236:13:pure_native_app_restore 16 static diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/README.md b/prototypes/workbench/results/2026-09-24-native-desktop/README.md new file mode 100644 index 0000000..4ed851e --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/README.md @@ -0,0 +1,38 @@ +# Linux 네이티브 작성 창 검증 — 2026-09-24 + +이 폴더는 성능 벤치마크가 아니라 실제 GTK 창의 동작, 실행 전용 비용과 선택형 편집기 비용의 +구분을 기록한다. 환경·바이너리 해시·섹션 크기·프로세스 메모리 관찰 값은 `manifest.json`에, +재실행 가능한 네이티브/Wasm 동작 비교와 C 호스트 sanitizer 결과는 `native-parity*.json`에 있다. +`core-budget.json`은 기존 6 KiB 코어 한도 재검사다. `sources/`는 검증 당시 변경 파일 사본이다. + +Xvfb 1400×800×24, `GTK_A11Y=none GDK_BACKEND=x11 GSK_RENDERER=cairo GDK_DISABLE=gl,vulkan`에서 +GTK 창을 열었다. `xdotool`로 다음을 실제 창에 보냈다: 도형 누름·이동·놓기, 저장, `GtkTextView` +끝에 새 `text` 출력 추가와 적용, 잘못된 선언 적용 및 수정, 다시 저장, 900×600으로 크기 변경. +새 CLI 프로세스에서 `load` 결과를 확인하고, 새 창에서도 수정된 소스와 저장 상태를 복원했다. +수치 입력은 별도 `document.pure` 창에서 99를 넣어 작성 규칙의 저장 결과 80을 확인했다. + +화면 자료: + +- `drag-and-save.png`: 놓은 위치와 저장 성공. +- `rejected-edit.png`: 잘못된 선언을 거절하고 직전 실행 화면을 유지. +- `recovered-edit.png`: 소스 복구 후 새 정의의 상태를 저장. +- `resized.png`, `resized-input.png`: 축소 후 화면 및 별도 드래그·키 입력. +- `reopened-state.png`: 새 프로세스의 새 정의·상태 복원. +- `numeric-input.png`: native 값 입력 및 상태 저장. + +화면은 `/tmp`에서 빌드한 `capture-x11.c`로 X 서버의 실제 픽셀을 PNG로 기록했다. +최종 작성 소스는 `authored-after.pure`에 있다. 프로세스 RSS/PSS는 작업 완료 후 +`/proc//status`와 `smaps_rollup`에서 관찰했다. 컴파일러 자식 프로세스·Xvfb·운영체제 +전체 메모리를 합친 값이 아니며, macOS의 비용도 아니다. + +선택 빌드 확인: + +```sh +python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure --zig /path/to/zig +python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure --desktop --zig /path/to/zig +node prototypes/workbench/native/check.mjs +node prototypes/workbench/native/check.mjs --sanitize +``` + +CLI의 직접 공유 라이브러리 의존성은 libc 하나였다. GTK 작성 창에는 GTK/Pango/Cairo/GLib가 +추가된다. 문서의 해석과 남은 범위는 [025 보고서](../../../../docs/research/025-native-authoring-shell.md)에 있다. diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/authored-after.pure b/prototypes/workbench/results/2026-09-24-native-desktop/authored-after.pure new file mode 100644 index 0000000..8065362 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/authored-after.pure @@ -0,0 +1,72 @@ +# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다. +state selected = 0 +record Tile { + x = 0; y = 0 + ox = 0; oy = 0 + sx = 0; sy = 0 + held = 0; id = 0 +} +item green = Tile(x=80, y=120, sx=80, sy=120, id=1) +item blue = Tile(x=280, y=160, sx=280, sy=160, id=2) +item amber = Tile(x=450, y=220, sx=450, sy=220, id=3) +input left = ArrowLeft +input right = ArrowRight +input up = ArrowUp +input down = ArrowDown +input drag = pointer { + green_box: green + blue_box: blue + amber_box: amber +} +box board(20,90,600,240,"#233a31") +text help(25,40,selected,"선택한 도형: ") +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +box amber_box(amber.x,amber.y,56,44,"#dfb184") + +rule grab(t: Tile) { + selected = t.id + t.sx = t.x; t.sy = t.y + t.ox = event_x - t.x; t.oy = event_y - t.y + t.held = 1 +} +rule move(t: Tile) { + t.x = clamp(event_x - t.ox, 20, 564) + t.y = clamp(event_y - t.oy, 90, 286) +} +rule drop(t: Tile) { t.held = 0 } +rule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 } +rule go_left(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) } +} +rule go_right(t: Tile) { + if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) } +} +rule go_up(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) } +} +rule go_down(t: Tile) { + if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) } +} + +on drag.down use grab(drag.target) +on drag.move use move(drag.target) +on drag.up use drop(drag.target) +on drag.cancel use cancel(drag.target) + +on left.down use go_left(green) +on right.down use go_right(green) +on up.down use go_up(green) +on down.down use go_down(green) + +on left.down use go_left(blue) +on right.down use go_right(blue) +on up.down use go_up(blue) +on down.down use go_down(blue) + +on left.down use go_left(amber) +on right.down use go_right(amber) +on up.down use go_up(amber) +on down.down use go_down(amber) + +text live(20,20,selected,"LIVE: " ) diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/capture-x11.c b/prototypes/workbench/results/2026-09-24-native-desktop/capture-x11.c new file mode 100644 index 0000000..2d03544 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/capture-x11.c @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +static unsigned char component(unsigned long pixel, unsigned long mask) { + if (!mask) return 0; + unsigned long shift = __builtin_ctzl(mask); + unsigned long bits = mask >> shift; + return (unsigned char)(((pixel & mask) >> shift) * 255 / bits); +} +int main(int argc, char **argv) { + if (argc != 2) return 2; + Display *d = XOpenDisplay(NULL); + if (!d) return 2; + int screen = DefaultScreen(d), width = DisplayWidth(d, screen), height = DisplayHeight(d, screen); + XImage *image = XGetImage(d, RootWindow(d, screen), 0, 0, width, height, AllPlanes, ZPixmap); + FILE *file = fopen(argv[1], "wb"); + if (!image || !file) return 2; + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + png_infop info = png_create_info_struct(png); + if (setjmp(png_jmpbuf(png))) return 2; + png_init_io(png, file); + png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + unsigned char *row = malloc((size_t)width * 3); + if (!row) return 2; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + unsigned long pixel = XGetPixel(image, x, y); + row[x * 3] = component(pixel, image->red_mask); + row[x * 3 + 1] = component(pixel, image->green_mask); + row[x * 3 + 2] = component(pixel, image->blue_mask); + } + png_write_row(png, row); + } + png_write_end(png, info); + free(row); png_destroy_write_struct(&png, &info); + XDestroyImage(image); fclose(file); XCloseDisplay(d); + return 0; +} diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/core-budget.json b/prototypes/workbench/results/2026-09-24-native-desktop/core-budget.json new file mode 100644 index 0000000..8c0caf7 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/core-budget.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/workbench/results/2026-09-24-native-desktop/drag-and-save.png b/prototypes/workbench/results/2026-09-24-native-desktop/drag-and-save.png new file mode 100644 index 0000000..3c1d630 Binary files /dev/null and b/prototypes/workbench/results/2026-09-24-native-desktop/drag-and-save.png differ diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/manifest.json b/prototypes/workbench/results/2026-09-24-native-desktop/manifest.json new file mode 100644 index 0000000..34482d8 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/manifest.json @@ -0,0 +1,141 @@ +{ + "kind": "correctness-and-cost-evidence-not-performance-benchmark", + "date": "2026-09-24", + "host": { + "machine": "aarch64", + "system": "Linux", + "cc": "cc (Debian 14.2.0-19) 14.2.0", + "gtk": "4.18.6", + "zig": "0.15.2" + }, + "renderer_for_final_qa": "GDK_BACKEND=x11 GSK_RENDERER=cairo GDK_DISABLE=gl,vulkan GTK_A11Y=none, Xvfb 1400x800x24", + "sections": { + "cli": { + "text": 62827, + "data": 2040, + "bss": 16 + }, + "desktop": { + "text": 78964, + "data": 2824, + "bss": 16 + } + }, + "stripped_elf_bytes": { + "cli": 68000, + "desktop": 134224 + }, + "direct_needed": { + "cli": [ + "libc.so.6" + ], + "desktop": [ + "libgtk-4.so.1", + "libpangocairo-1.0.so.0", + "libpango-1.0.so.0", + "libcairo.so.2", + "libgio-2.0.so.0", + "libgobject-2.0.so.0", + "libglib-2.0.so.0", + "libc.so.6" + ] + }, + "idle_kib_single_observation": { + "cli_vmrss": 1552, + "desktop_cairo_no_gpu_vmrss": 36512, + "desktop_cairo_no_gpu_pss": 23618, + "desktop_default_vmrss_other_run": 173808, + "desktop_default_pss_other_run": 139710 + }, + "observed_restored_state": { + "status": 0, + "stored_revision": 2, + "selected": 1, + "green_x": 167.07482993197277, + "green_y": 154.82993197278913, + "green_held": 0, + "new_view_live": 1 + }, + "files": { + "README.md": { + "bytes": 2596, + "sha256": "000e8d83d1809facacc32039618a7b6dce41d84bedf7c921ff11c37a4a7b2289" + }, + "authored-after.pure": { + "bytes": 2000, + "sha256": "7e0861e8116da883dce578963cca40073f4efaf98265d1c5ca79aa0a7855001c" + }, + "capture-x11.c": { + "bytes": 1768, + "sha256": "c3e21d4e79180ee60e9fa7845118fcc6ec57a4e1386471d78ae404831ebc83ae" + }, + "core-budget.json": { + "bytes": 1851, + "sha256": "37e9ae8320daf7bbe05983e7f35b59707e64a83d09574e3b126e8d9e13f3336c" + }, + "drag-and-save.png": { + "bytes": 134122, + "sha256": "5f8773c98f31736ba49419e1c0fa8b8e76c84b916f9fd2a92ff50accd9f35014" + }, + "native-parity-sanitized.json": { + "bytes": 13740, + "sha256": "c34e50c4e634a1bc3324b190d3fd54a0502986c5cc42bf3b3d8b62c788447ba1" + }, + "native-parity.json": { + "bytes": 13741, + "sha256": "24d7c37de7609fb1aa406f13cf92cbe6534c8f02ac1ab884cbd171e941fd48b4" + }, + "numeric-input.png": { + "bytes": 45958, + "sha256": "284139508ecd8049083e4128325c27b4f65ec4e3104147736037fb879535cf95" + }, + "recovered-edit.png": { + "bytes": 139858, + "sha256": "700895fbd4320617a3b428bed1a42eff3a5189a9647d6477a80b99a678114047" + }, + "rejected-edit.png": { + "bytes": 142502, + "sha256": "5a9acabbb1ab60d569158ab403e5f51ffe0e12c655ed281f24c196f325548082" + }, + "reopened-state.png": { + "bytes": 134717, + "sha256": "596bb7a9e6234143283db188605faaa94b65db9e17366a25bf0d80d0d578c046" + }, + "resized-input.png": { + "bytes": 108314, + "sha256": "ca29815234a244916f1a3674963c400549343866fd9be00acc942525f631f7c4" + }, + "resized.png": { + "bytes": 108735, + "sha256": "0685310a9b5c1e046a1133f54db81f29e035eefb9f1927e434a03e3b0f318f97" + }, + "sources/manage.py": { + "bytes": 6992, + "sha256": "afde3c7129d89cea0a559df4aa18cc9c3bd997146375db0967730340b66c16fe" + }, + "sources/native/app.c": { + "bytes": 11764, + "sha256": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca" + }, + "sources/native/app.h": { + "bytes": 1492, + "sha256": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729" + }, + "sources/native/check.mjs": { + "bytes": 8492, + "sha256": "286e23f1ab164a956f53c6d89f229ccb0ec5fdbb35426737a6c28cd553b92953" + }, + "sources/native/desktop.c": { + "bytes": 22779, + "sha256": "617d58ad2ec74de0f428ab76ca23e76bed4ca9c3234dd50213853843552d5c7c" + }, + "sources/native/test.c": { + "bytes": 9837, + "sha256": "47488a2cfda2b378637f69b8f0d94f75e615d784e8418167b5b82c8ce141f7e8" + } + }, + "binary_sha256": { + "cli": "b7add8a755a4959191417b3f0fd50805f71c72659eb4659905a689588351bda8", + "desktop": "eb4276076f4fb11268f976baef9cb7701399a8a82223bc11f93a0593e176174a" + } +} diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/native-parity-sanitized.json b/prototypes/workbench/results/2026-09-24-native-desktop/native-parity-sanitized.json new file mode 100644 index 0000000..c1f5006 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/native-parity-sanitized.json @@ -0,0 +1,159 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": true, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "eada952dee9afd7071370215364b2a6717a28f03e0513cdd0cae4695939ec52b", + "native_binary_sha256": "b51112856288b20947d7287c135f78ee889f4a67e15fcf4410900ac43299c9ab", + "native_binary_bytes": 591896 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "fc96ee38b256117a7bfe2859b7126e22262c19e96047a8d01fbe30d4d1a28764", + "native_binary_sha256": "1a309cdf3d564ce995f34c40bb14ad2989dfeee736ce59d93b67083873d6f566", + "native_binary_bytes": 592280 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "7ba5fa094b0bc772202bb44a62b3c8377c4f3924db246ec429c2faee4948b09f", + "native_binary_sha256": "ececc7e0b1c01a36ade2d376807ea0aeb99191c8feea1b1f202a254a5e9a7d2e", + "native_binary_bytes": 592344 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "27f0aa76eb3e12e289bcd6edf6c2ae72892a2dfe1ff094b619671f0583e2c8d3", + "native_binary_sha256": "60e9c0a7eeef1c3f74325bda42763096548e54969bd1c13dfe8a9d470c3eda46", + "native_binary_bytes": 593112 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "native_binary_sha256": "c0b9fdf9008d5c9c92eb4ff24c9beb38cf12f95551ec998e9a9e6a2393949083", + "native_binary_bytes": 594264 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "c74315c1b0067e167a4678b30505d1770b0753b601e8acee27f6d598ac7b9ffb", + "native_binary_sha256": "838098d0ad1158f12fdcedd532c9e3f69220e346d7bf0e0c6bdcc9694e123098", + "native_binary_bytes": 591576 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "6a415d33780227158ebeae2a75c7520bf13bad3c0a6fef7ee82f5c323be226f0", + "native_binary_sha256": "9b39b9b8d0493d13c641a964083289b478be5b57897d5bfa1991a7d68c812f83", + "native_binary_bytes": 591712 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "1c8f5ade56ee8ec931a62fd1f68ef3635b17297d3a54b20c954860c4a43b6155", + "native_binary_sha256": "6bc96dcfbe6a40f1ac2f21124eaecbe5a21ea5a61a28b4196328948b4441d382", + "native_binary_bytes": 591248 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e9a8b1542ecf6b008ad30188cb9e95d29638fab1c38d331c00bf706c40a06d3c", + "native_binary_sha256": "720b98f4a5022f38560245b352dbf85a2d70e9e248a483db365ce06c92c50bde", + "native_binary_bytes": 591656 + } + ], + "units": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload", + "scalar_instance_bytes": 20064, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "555b9ff3143888921d90a475bcb7957c18dbf7101ee9e3d2853c01a562cbf3f5", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/manage.py": "afde3c7129d89cea0a559df4aa18cc9c3bd997146375db0967730340b66c16fe", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "3989d4e545255fccf9d846afb79246e874f4cefb03eecef9ee1e2b1ce8fd257e", + "prototypes/workbench/native/test.c": "47488a2cfda2b378637f69b8f0d94f75e615d784e8418167b5b82c8ce141f7e8", + "prototypes/workbench/native/check.mjs": "286e23f1ab164a956f53c6d89f229ccb0ec5fdbb35426737a6c28cd553b92953", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "8fe2f501cb9639fea0817aa57e0b7292ca6779cda4ac156b7c6b3df31d7921e9", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "4e4082a2252de7b23758e438a246e654012231105b12e2619f45d4e82701238d", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33020\t 0\t 0\t 33020\t 80fc\tprototypes/workbench/build/cache/tmp/5d68cd21f0052e5a/libpure-scalar_zcu.o (ex /home/eunhhu/work/pure/prototypes/workbench/build/native/libpure-scalar.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/5d68cd21f0052e5a/libpure-scalar_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/native-parity.json b/prototypes/workbench/results/2026-09-24-native-desktop/native-parity.json new file mode 100644 index 0000000..f84b22e --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/native-parity.json @@ -0,0 +1,159 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": false, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "eada952dee9afd7071370215364b2a6717a28f03e0513cdd0cae4695939ec52b", + "native_binary_sha256": "a2e4c733de5edd2c1abcd3a9e186ee495bdbfd7df65cc6ba220d093ad80bfb19", + "native_binary_bytes": 213744 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "fc96ee38b256117a7bfe2859b7126e22262c19e96047a8d01fbe30d4d1a28764", + "native_binary_sha256": "bcd626e79163df2cc3eb0afc87fa301ed33ca9622d2b48cd2eb7e4d508af0940", + "native_binary_bytes": 213744 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "7ba5fa094b0bc772202bb44a62b3c8377c4f3924db246ec429c2faee4948b09f", + "native_binary_sha256": "eca35aa011c0a67eca954846be5241eb21ccc9cc979b491d519ead880acbf95e", + "native_binary_bytes": 213744 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "27f0aa76eb3e12e289bcd6edf6c2ae72892a2dfe1ff094b619671f0583e2c8d3", + "native_binary_sha256": "6d67ee421bf83e1e1c8ff081e9fe7e36c22a75791049f7887183bb84f5a03590", + "native_binary_bytes": 213744 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "native_binary_sha256": "768406f62dfe7ddcfe448e45dd3a4e0285363ffcce143f8b07850b02fcedf6e4", + "native_binary_bytes": 213744 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "c74315c1b0067e167a4678b30505d1770b0753b601e8acee27f6d598ac7b9ffb", + "native_binary_sha256": "75c2b688cacd060010f3a2f416b9f14c44df6782da172690bdd3e3b709f97e8e", + "native_binary_bytes": 213744 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "6a415d33780227158ebeae2a75c7520bf13bad3c0a6fef7ee82f5c323be226f0", + "native_binary_sha256": "9b2346f153516af265003a60f155e48805b354f4afaa7845940acba5971c3eee", + "native_binary_bytes": 213760 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "1c8f5ade56ee8ec931a62fd1f68ef3635b17297d3a54b20c954860c4a43b6155", + "native_binary_sha256": "74e48d74b3e3ff73a0b501aaad0f72b103e3001fa6f70373e49c056f740c417d", + "native_binary_bytes": 213720 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e9a8b1542ecf6b008ad30188cb9e95d29638fab1c38d331c00bf706c40a06d3c", + "native_binary_sha256": "0f6c413948c5e3b942edc268206baa6bbed04f0288d8b6d49f0f293fe0b4ab5b", + "native_binary_bytes": 213760 + } + ], + "units": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload", + "scalar_instance_bytes": 20064, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "555b9ff3143888921d90a475bcb7957c18dbf7101ee9e3d2853c01a562cbf3f5", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/manage.py": "afde3c7129d89cea0a559df4aa18cc9c3bd997146375db0967730340b66c16fe", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "3989d4e545255fccf9d846afb79246e874f4cefb03eecef9ee1e2b1ce8fd257e", + "prototypes/workbench/native/test.c": "47488a2cfda2b378637f69b8f0d94f75e615d784e8418167b5b82c8ce141f7e8", + "prototypes/workbench/native/check.mjs": "286e23f1ab164a956f53c6d89f229ccb0ec5fdbb35426737a6c28cd553b92953", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "8fe2f501cb9639fea0817aa57e0b7292ca6779cda4ac156b7c6b3df31d7921e9", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "4e4082a2252de7b23758e438a246e654012231105b12e2619f45d4e82701238d", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33020\t 0\t 0\t 33020\t 80fc\tprototypes/workbench/build/cache/tmp/5d68cd21f0052e5a/libpure-scalar_zcu.o (ex /home/eunhhu/work/pure/prototypes/workbench/build/native/libpure-scalar.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/5d68cd21f0052e5a/libpure-scalar_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/numeric-input.png b/prototypes/workbench/results/2026-09-24-native-desktop/numeric-input.png new file mode 100644 index 0000000..3c58d07 Binary files /dev/null and b/prototypes/workbench/results/2026-09-24-native-desktop/numeric-input.png differ diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/recovered-edit.png b/prototypes/workbench/results/2026-09-24-native-desktop/recovered-edit.png new file mode 100644 index 0000000..af11b13 Binary files /dev/null and b/prototypes/workbench/results/2026-09-24-native-desktop/recovered-edit.png differ diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/rejected-edit.png b/prototypes/workbench/results/2026-09-24-native-desktop/rejected-edit.png new file mode 100644 index 0000000..c4a674a Binary files /dev/null and b/prototypes/workbench/results/2026-09-24-native-desktop/rejected-edit.png differ diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/reopened-state.png b/prototypes/workbench/results/2026-09-24-native-desktop/reopened-state.png new file mode 100644 index 0000000..d4ce69a Binary files /dev/null and b/prototypes/workbench/results/2026-09-24-native-desktop/reopened-state.png differ diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/resized-input.png b/prototypes/workbench/results/2026-09-24-native-desktop/resized-input.png new file mode 100644 index 0000000..4434902 Binary files /dev/null and b/prototypes/workbench/results/2026-09-24-native-desktop/resized-input.png differ diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/resized.png b/prototypes/workbench/results/2026-09-24-native-desktop/resized.png new file mode 100644 index 0000000..3768cc1 Binary files /dev/null and b/prototypes/workbench/results/2026-09-24-native-desktop/resized.png differ diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/sources/manage.py b/prototypes/workbench/results/2026-09-24-native-desktop/sources/manage.py new file mode 100644 index 0000000..a95f6e2 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/sources/manage.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Build the authoring VM or serve its local browser adapter.""" +import argparse +from datetime import datetime, timezone +from functools import partial +import hashlib +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import shutil +import shlex +import subprocess + +ROOT = Path(__file__).resolve().parent + + +def sync_examples(): + target = ROOT / 'web/examples' + target.mkdir(exist_ok=True) + for source in (ROOT / 'examples').glob('*.pure'): + shutil.copyfile(source, target / source.name) + + +def build(zig): + compiler = shutil.which(zig) + if not compiler: + raise SystemExit('Zig 0.15.2가 필요합니다. --zig /path/to/zig로 지정할 수 있습니다.') + version = subprocess.check_output([compiler, 'version'], text=True).strip() + if version != '0.15.2': + raise SystemExit(f'이 실험은 Zig 0.15.2로 고정했습니다. 현재 {version}') + folder = ROOT / 'build' + folder.mkdir(exist_ok=True) + cache = ['--cache-dir', str(folder / 'cache'), '--global-cache-dir', str(folder / 'global-cache')] + source = ROOT / 'core.zig' + result = subprocess.run([compiler, 'test', str(source), *cache], capture_output=True, text=True) + print(result.stdout + result.stderr, end='', flush=True) + result.check_returncode() + flags = ['-target', 'wasm32-freestanding', '-O', 'ReleaseSafe', '-fstrip', '-fno-entry', + '-rdynamic', '--export-memory', '--stack', '16384', + '--initial-memory=65536', '--max-memory=65536'] + artifact = ROOT / 'web/core.wasm' + subprocess.run([compiler, 'build-exe', str(source), *flags, f'-femit-bin={artifact}', *cache], check=True) + sync_examples() + sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() + (folder / 'build.json').write_text(json.dumps({ + 'builtUtc': datetime.now(timezone.utc).isoformat(), 'zigVersion': version, + 'sourceSha256': sha(source), 'wasmSha256': sha(artifact), + 'evaluationSourceSha256': sha(ROOT / 'evaluation.zig'), + 'engineSourceSha256': sha(ROOT / 'engine.zig'), + 'wasmFileBytes': artifact.stat().st_size, 'flags': flags, + 'nativeTests': result.stdout + result.stderr, + }, indent=2) + '\n') + print(f'Built {artifact} ({artifact.stat().st_size:,} bytes)') + + +def native(zig, source, output, cc, desktop=False): + compiler = shutil.which(zig) + if not compiler or subprocess.check_output([compiler, 'version'], text=True).strip() != '0.15.2': + raise SystemExit('Zig 0.15.2가 필요합니다. --zig /path/to/zig로 지정하세요.') + folder = ROOT / 'build/native' + folder.mkdir(parents=True, exist_ok=True) + cache = ['--cache-dir', str(ROOT / 'build/cache'), '--global-cache-dir', str(ROOT / 'build/global-cache')] + library = folder / 'libpure-scalar.a' + subprocess.run([compiler, 'build-lib', str(ROOT / 'native.zig'), '-O', 'ReleaseSafe', + '-fno-compiler-rt', f'-femit-bin={library}', *cache], check=True) + prepared = folder / 'application.c' + subprocess.run(['node', str(ROOT / 'native/prepare.mjs'), str(source.resolve()), str(prepared)], check=True) + result = output.resolve() if output else folder / ('pure-desktop' if desktop else 'pure-native') + result.parent.mkdir(parents=True, exist_ok=True) + connections = ROOT.parent / 'connections' + inputs = [prepared, ROOT / ('native/desktop.c' if desktop else 'native/main.c'), + ROOT / 'native/app.c', ROOT / 'native/plan.c', + *(connections / name for name in ('runtime.c', 'evaluate.c', 'store.c', 'store-port.c')), library] + flags = ['-std=c17', '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror'] + desktop_flags = [] + if desktop: + desktop_flags = shlex.split(subprocess.check_output(['pkg-config', '--cflags', '--libs', 'gtk4'], text=True)) + flags.extend(['-DPURE_NATIVE_AUTHORING=1', f'-DPURE_PREPARE_PATH="{ROOT / "native/prepare.mjs"}"', + f'-DPURE_NATIVE_HEADERS="{ROOT / "native"}"', f'-DPURE_CC="{cc}"']) + subprocess.run([cc, *flags, '-I' + str(ROOT / 'native'), *(str(p) for p in inputs), + '-lm', *(desktop_flags if desktop else []), *(['-ldl'] if desktop else []), + '-o', str(result)], check=True) + sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() + (folder / 'build.json').write_text(json.dumps({'source': str(source.resolve()), 'sourceSha256': sha(source), + 'zigVersion': '0.15.2', 'zigFlags': ['-O', 'ReleaseSafe', '-fno-compiler-rt'], 'cFlags': flags, + 'binary': str(result), 'binarySha256': sha(result), 'librarySha256': sha(library), + 'preparedSha256': sha(prepared), 'desktop': desktop, 'desktopFlags': desktop_flags, + 'implementationSha256': {str(p.relative_to(ROOT.parent.parent)): sha(p) for p in [ + ROOT / 'engine.zig', ROOT / 'evaluation.zig', ROOT / 'native.zig', ROOT / 'native/prepare.mjs', + ROOT / 'native/scalar.h', ROOT / 'native/plan.h', ROOT / 'native/app.h', ROOT / 'web/compiler.mjs', + *(p for p in inputs if p.suffix == '.c' and p != prepared)]}}, indent=2) + '\n') + print(f'Built native application: {result}') + + +class Handler(SimpleHTTPRequestHandler): + extensions_map = {**SimpleHTTPRequestHandler.extensions_map, '.wasm': 'application/wasm', '.mjs': 'text/javascript', '.pure': 'text/plain'} + + def end_headers(self): + self.send_header('Cache-Control', 'no-store') + super().end_headers() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='action', required=True) + b = sub.add_parser('build') + b.add_argument('--zig', default='zig') + n = sub.add_parser('native', help='prepare an existing .pure definition as a native CLI or authoring desktop app') + n.add_argument('source', type=Path) + n.add_argument('--zig', default='zig') + n.add_argument('--cc', default='cc') + n.add_argument('--output', type=Path) + n.add_argument('--desktop', action='store_true', help='build the optional Linux GTK4 authoring shell') + s = sub.add_parser('serve') + s.add_argument('--port', type=int, default=8766) + args = parser.parse_args() + if args.action == 'build': + build(args.zig) + return + if args.action == 'native': + native(args.zig, args.source, args.output, args.cc, args.desktop) + return + if not 1 <= args.port <= 65535: + parser.error('port must be 1..65535') + if not (ROOT / 'web/core.wasm').is_file(): + parser.error('먼저 manage.py build를 실행하세요.') + sync_examples() + with ThreadingHTTPServer(('127.0.0.1', args.port), partial(Handler, directory=str(ROOT / 'web'))) as server: + print(f'Pure workbench: http://127.0.0.1:{args.port}', flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + main() diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c new file mode 100644 index 0000000..ad79c2f --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.c @@ -0,0 +1,239 @@ +#include "app.h" +#include "../../connections/store-port.h" +#include "../../connections/store.h" +#include +#include +#include + +_Static_assert(sizeof(double) == 8 && FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024, + "the current snapshot boundary requires IEEE binary64"); + +static const PureProgram connections = {.read = {.to_end = true}, .write = {.to_end = true}}; +static const unsigned char marker[8] = {'P','U','R','E','S','0','0','1'}; +enum { SNAPSHOT_HEADER = 44 }; + +static void release(void *memory, size_t size) { (void)size; free(memory); } +static bool allocate(void *context, size_t size, PureResource *out) { + (void)context; + void *memory = malloc(size); + if (!memory) return false; + *out = (PureResource){.body = {memory, size}, .allocation = memory, + .allocation_size = size, .release = release}; + return true; +} +static uint64_t decode(const unsigned char *bytes, unsigned count) { + uint64_t value = 0; + for (unsigned i = 0; i < count; ++i) value |= (uint64_t)bytes[i] << (8 * i); + return value; +} +static void encode(unsigned char *bytes, uint64_t value, unsigned count) { + for (unsigned i = 0; i < count; ++i) bytes[i] = (unsigned char)(value >> (8 * i)); +} + +static PureResult read_value(void *context, const char *key, PureRegion region, size_t minimum, + size_t available, PureResource *out, int64_t *revision) { + PureNativeApp *app = context; + if (!strcmp(key, "load")) { + PureHost store = pure_store_host(app->directory); + return store.read(store.context, app->document, region, minimum, available, out, revision); + } + if (strcmp(key, "snapshot")) return PURE_INVALID; + if (app->held) return PURE_BUSY; + size_t size = SNAPSHOT_HEADER + 8u * app->plan->state_count; + /* This host contract currently exports the complete typed snapshot. */ + if (minimum > size || !region.to_end || region.offset) return PURE_LIMIT; + if (size > available) return PURE_BUDGET; + if (!allocate(NULL, size, out)) return PURE_ALLOCATION; + unsigned char *bytes = out->allocation; + memcpy(bytes, marker, sizeof(marker)); + memcpy(bytes + 8, app->plan->identity, 32); + encode(bytes + 40, app->plan->state_count, 4); + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + double value = pure_scalar_get_state(app->scalar, i); + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + encode(bytes + SNAPSHOT_HEADER + i * 8, bits, 8); + } + *revision = app->revision; + return PURE_OK; +} + +static PureResult action(PureNativeApp *app, PureAction a) { + if (a.phase == PURE_RESET) { + if (app->held) return PURE_BUSY; + pure_scalar_reset_state(app->scalar); + return PURE_OK; + } + if (a.phase == PURE_FRAME) { + app->last_scalar_error = pure_scalar_dispatch(app->scalar, 0, 1.0 / 60.0); + return app->last_scalar_error ? PURE_INVALID : PURE_OK; + } + if (a.input >= app->plan->input_count || a.input >= 8) return PURE_INVALID; + PureScalarInputKind kind = app->plan->inputs[a.input].kind; + uint32_t bit = 1u << a.input, event; + if (kind == PURE_VALUE) { + if (a.phase != PURE_CHANGE) return PURE_INVALID; + app->last_scalar_error = pure_scalar_dispatch_value(app->scalar, 33 + a.input, a.x); + return app->last_scalar_error ? PURE_INVALID : PURE_OK; + } + if (kind == PURE_KEY) { + if ((a.phase != PURE_DOWN && a.phase != PURE_UP) || a.source >= 32) return PURE_INVALID; + uint32_t source = 1u << a.source, before = app->key_sources[a.input]; + if (a.phase == PURE_DOWN) { + app->key_sources[a.input] |= source; + app->held |= bit; + if (before) return PURE_OK; + } else { + if (!(before & source)) return PURE_OK; + app->key_sources[a.input] &= ~source; + if (app->key_sources[a.input]) return PURE_OK; + app->held &= ~bit; + } + event = a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u); + app->last_scalar_error = pure_scalar_dispatch(app->scalar, event, 0); + } else { + if (a.phase > PURE_CANCEL) return PURE_INVALID; + uint32_t slot = a.input; + if (kind == PURE_ROUTED) { + if (a.target >= app->plan->target_count || a.target >= 24 || app->plan->targets[a.target].input != a.input) return PURE_INVALID; + slot = 8 + a.target; + } + bit = 1u << slot; + if (a.phase == PURE_DOWN) { + if (app->held & bit) return app->pointer_owners[slot] == a.source ? PURE_OK : PURE_BUSY; + app->held |= bit; + app->pointer_owners[slot] = a.source; + } else { + if (!(app->held & bit) || app->pointer_owners[slot] != a.source) return PURE_INVALID; + if (a.phase == PURE_UP || a.phase == PURE_CANCEL) app->held &= ~bit; + } + event = a.phase == PURE_MOVE ? 17 + a.input : a.phase == PURE_CANCEL ? 25 + a.input + : a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u); + app->last_scalar_error = kind == PURE_ROUTED + ? pure_scalar_dispatch_target(app->scalar, event, a.target, a.x, a.y) + : pure_scalar_dispatch_point(app->scalar, event, a.x, a.y); + } + /* Input ownership follows the physical phases even if an authored handler + * fails; its scalar writes remain atomic. Each routed target has its owner. */ + return app->last_scalar_error ? PURE_INVALID : PURE_OK; +} + +static PureResult write_value(void *context, const char *key, int64_t expected, + PureSpan bytes, int64_t *revision) { + PureNativeApp *app = context; + if (!strcmp(key, "save")) return pure_store_put(app->directory, app->document, expected, bytes, revision); + *revision = app->revision; + if (expected != app->revision) return PURE_CONFLICT; + if (app->revision == INT64_MAX) return PURE_LIMIT; + app->last_scalar_error = 0; + if (!strcmp(key, "event")) { + if (bytes.size != sizeof(PureAction)) return PURE_LIMIT; + PureAction input; + memcpy(&input, bytes.data, sizeof(input)); + PureResult status = action(app, input); + if (status != PURE_OK) return status; +#ifdef PURE_NATIVE_AUTHORING + } else if (!strcmp(key, "plan")) { + if (bytes.size != sizeof(const PureScalarPlan *)) return PURE_LIMIT; + if (app->held) return PURE_BUSY; + const PureScalarPlan *candidate; + memcpy(&candidate, bytes.data, sizeof(candidate)); + if (!candidate || !candidate->states || !candidate->inputs || !candidate->views || + candidate->state_count > 32 || candidate->input_count > 8 || + candidate->target_count > 24 || candidate->view_count > 24) return PURE_INVALID; + app->last_scalar_error = pure_scalar_load(app->scalar, candidate, app->plan, NULL); + if (app->last_scalar_error) return PURE_INVALID; + app->plan = candidate; + memset(app->pointer_owners, 0, sizeof(app->pointer_owners)); + memset(app->key_sources, 0, sizeof(app->key_sources)); +#endif + } else if (!strcmp(key, "restore")) { + if (app->held) return PURE_BUSY; + if (bytes.size != SNAPSHOT_HEADER + 8u * app->plan->state_count || + memcmp(bytes.data, marker, sizeof(marker)) || memcmp(bytes.data + 8, app->plan->identity, 32) || + decode(bytes.data + 40, 4) != app->plan->state_count) return PURE_INVALID; + double values[32]; + for (uint32_t i = 0; i < app->plan->state_count; ++i) { + uint64_t bits = decode(bytes.data + SNAPSHOT_HEADER + 8 * i, 8); + memcpy(values + i, &bits, sizeof(bits)); + } + app->last_scalar_error = pure_scalar_load(app->scalar, app->plan, NULL, values); + if (app->last_scalar_error) return PURE_INVALID; + } else return PURE_INVALID; + *revision = ++app->revision; + return PURE_OK; +} + +PureResult pure_native_app_init(PureNativeApp *app, const PureScalarPlan *plan, void *memory, + size_t size, const char *directory, const char *key, size_t budget) { + if (!app || !plan || !directory || !key || !*key || strlen(key) > 64 || plan->state_count > 32) return PURE_INVALID; + for (const char *p = key; *p; ++p) + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')) return PURE_INVALID; + PureScalar *scalar = pure_scalar_initialize(memory, size); + if (!scalar) return PURE_INVALID; + if (pure_scalar_load(scalar, plan, NULL, NULL)) return PURE_INVALID; + PureResult status = pure_store_init(directory); + if (status != PURE_OK) return status; + *app = (PureNativeApp){.scalar = scalar, .plan = plan, .directory = directory}; + strcpy(app->document, key); + PureHost host = {.context = app, .allocate = allocate, .read = read_value, .write = write_value}; + return pure_runtime_init(&app->execution, app->slots, 2, &connections, host, budget) ? PURE_OK : PURE_INVALID; +} + +static PureOutcome finish(PureNativeApp *app, PureTicket ticket) { + PureOutcome result; + while (!pure_runtime_result(ticket, &result)) { + if (!pure_runtime_step(&app->execution)) return (PureOutcome){.status = PURE_INVALID}; + } + return result; +} + +PureOutcome pure_native_app_action(PureNativeApp *app, PureAction input) { + PureTicket ticket; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_WRITE, "event", app->revision, sizeof(input), &ticket); + if (status != PURE_OK) return (PureOutcome){.status = status}; + memcpy(pure_runtime_ingress(ticket, NULL), &input, sizeof(input)); + pure_runtime_ready(ticket); + PureOutcome result = finish(app, ticket); + pure_runtime_release(ticket); + return result; +} + +static PureOutcome transfer(PureNativeApp *app, bool restore) { + PureTicket source, destination; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_READ, restore ? "load" : "snapshot", 0, 0, &source); + if (status != PURE_OK) return (PureOutcome){.status = status}; + PureOutcome result = finish(app, source); + int64_t stored = result.revision; + if (result.status == PURE_OK) { + status = pure_runtime_forward(source, &app->execution, restore ? "restore" : "save", + restore ? app->revision : app->stored_revision, &destination); + pure_runtime_release(source); + if (status != PURE_OK) return (PureOutcome){.status = status}; + result = finish(app, destination); + pure_runtime_release(destination); + if (result.status == PURE_OK) app->stored_revision = restore ? stored : result.revision; + } else pure_runtime_release(source); + /* Bodies are consumed internally before this API returns. */ + result.body = (PureSpan){0}; + return result; +} +PureOutcome pure_native_app_save(PureNativeApp *app) { return transfer(app, false); } +PureOutcome pure_native_app_restore(PureNativeApp *app) { return transfer(app, true); } + +#ifdef PURE_NATIVE_AUTHORING +PureResult pure_native_app_replace_plan(PureNativeApp *app, const PureScalarPlan *candidate) { + if (!app || !candidate) return PURE_INVALID; + if (app->held || app->execution.active || app->execution.retained) return PURE_BUSY; + PureTicket ticket; + PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_WRITE, + "plan", app->revision, sizeof(candidate), &ticket); + if (status != PURE_OK) return status; + memcpy(pure_runtime_ingress(ticket, NULL), &candidate, sizeof(candidate)); + pure_runtime_ready(ticket); + PureOutcome result = finish(app, ticket); + pure_runtime_release(ticket); + return result.status; +} +#endif diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.h b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.h new file mode 100644 index 0000000..f5ce9b1 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/app.h @@ -0,0 +1,32 @@ +#ifndef PURE_NATIVE_APP_H +#define PURE_NATIVE_APP_H +#include "plan.h" +#include "../../connections/runtime.h" + +typedef enum { PURE_DOWN, PURE_UP, PURE_MOVE, PURE_CANCEL, PURE_CHANGE, PURE_FRAME, PURE_RESET } PureActionPhase; +/* In-process event value, never a persistent or network representation. */ +typedef struct { uint32_t input, phase, target, source; double x, y; } PureAction; +typedef struct { + PureScalar *scalar; + const PureScalarPlan *plan; + const char *directory; + char document[65]; + PureRuntime execution; + PureWork slots[2]; + int64_t revision, stored_revision; + uint32_t last_scalar_error, held, pointer_owners[32], key_sources[8]; +} PureNativeApp; + +/* Caller supplies instance memory and keeps plan/directory alive. This selected + * host uses the existing SQL-free store. Single owner; effects are synchronous. */ +PureResult pure_native_app_init(PureNativeApp *, const PureScalarPlan *, void *memory, + size_t size, const char *directory, const char *key, size_t budget); +PureOutcome pure_native_app_action(PureNativeApp *, PureAction); +PureOutcome pure_native_app_save(PureNativeApp *); +PureOutcome pure_native_app_restore(PureNativeApp *); +/* Authoring host only. Candidate plan stays alive on success; previous plan + * stays alive until this call returns. A failed reload retains the old app. */ +#ifdef PURE_NATIVE_AUTHORING +PureResult pure_native_app_replace_plan(PureNativeApp *, const PureScalarPlan *); +#endif +#endif diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/check.mjs b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/check.mjs new file mode 100644 index 0000000..02dcb5e --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/check.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { resolve, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { compile } from '../web/compiler.mjs'; +import { createRuntime } from '../web/runtime.mjs'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const repo = resolve(root, '../..'); +const connections = resolve(root, '../connections'); +const args = process.argv.slice(2); +const sanitized = args.includes('--sanitize'); +const index = args.indexOf('--output'); +const output = index >= 0 ? resolve(args[index + 1]) : null; +const temporary = await mkdtemp(join(tmpdir(), 'pure-native-parity-')); +const library = resolve(root, 'build/native/libpure-scalar.a'); +const wasm = await readFile(resolve(root, 'web/core.wasm')); +const hash = data => createHash('sha256').update(data).digest('hex'); +const compiler = process.env.CC || 'cc'; +const flags = ['-std=c17', sanitized ? '-O1' : '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror', + ...(sanitized ? ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] : []), '-I' + resolve(root, 'native')]; +function run(command, arguments_, options = {}) { + const p = spawnSync(command, arguments_, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, timeout: 60000, ...options }); + assert.ifError(p.error); + return p; +} +function successful(command, arguments_, options) { + const p = run(command, arguments_, options); + assert.equal(p.status, 0, p.stdout + p.stderr); + return p.stdout; +} +const units = [resolve(root, 'native/app.c'), resolve(root, 'native/plan.c'), + ...['runtime.c', 'evaluate.c', 'store.c', 'store-port.c'].map(name => resolve(connections, name))]; +const objects = []; +const cases = []; +let unitResult; +try { + for (const [i, path] of units.entries()) { + const object = join(temporary, `unit-${i}.o`); + successful(compiler, [...flags, '-c', path, '-o', object]); + objects.push(object); + } + const main = join(temporary, 'main.o'); + successful(compiler, [...flags, '-c', resolve(root, 'native/main.c'), '-o', main]); + const definition = name => readFile(resolve(root, 'examples', name + '.pure'), 'utf8'); + const repeat = (line, n) => Array(n).fill(line); + const definitions = [ + ['counter', await definition('counter'), ['key tap down', 'key tap down', 'key tap up', 'key tap down', 'key tap up']], + ['toggle', await definition('toggle'), ['key toggle down', 'key toggle up', ...Array.from({length: 13}, () => ['key move down', 'key move up']).flat()]], + ['charge', await definition('charge'), ['key jump down', ...repeat('frame', 12), 'key jump up', ...repeat('frame', 90)]], + ['direct', await definition('direct'), ['point drag down tile 90 180', 'point drag move tile 250 220', 'point drag up tile 250 220']], + ['routed', await definition('routed-layout'), ['point drag down green_box 90 130', 'point drag down blue_box 290 170', + 'point drag move green_box 200 210', 'point drag move blue_box 500 270', 'point drag cancel green_box 200 210', + 'point drag up blue_box 500 270', 'key left down', 'key left up']], + ['document', await definition('document'), ['value split 99', 'value split -10', 'value split 61.5', 'key reset down', 'key reset up']], + ['atomic-error', 'state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,"value: ")', + ['value n 0', 'value n 4']], + ['blank', '', []], + ['literal-boundary', 'state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,"한글 \\\" \\\\ \\n")', ['value n -0']], + ]; + for (const [name, source, commands] of definitions) { + const sourcePath = join(temporary, name + '.pure'), prepared = join(temporary, name + '.c'), binary = join(temporary, name); + await writeFile(sourcePath, source); + successful(process.execPath, [resolve(root, 'native/prepare.mjs'), sourcePath, prepared]); + successful(compiler, [...flags, prepared, main, ...objects, library, '-lm', '-o', binary]); + const native = run(binary, [join(temporary, name + '-store'), 'project'], { input: commands.concat(['save', 'quit']).join('\n') + '\n' }); + const rows = native.stdout.trim().split('\n').map(line => JSON.parse(line)); + const runtime = await createRuntime(wasm); + const plan = compile(source); + runtime.apply(source); + function equivalent(row) { + const snapshot = runtime.snapshot(); + assert.deepEqual(row.states, snapshot.states, name); + const views = Object.fromEntries(snapshot.views.map(view => [view.id, Object.fromEntries(Object.keys(plan.views.find(v => v.id === view.id).bindings).map(key => [key, view[key]]))])); + assert.deepEqual(row.views, views, name); + } + equivalent(rows[0]); + let failures = 0; + commands.forEach((command, i) => { + const [verb, input, phase, target, x, y] = command.split(' '); + let code = 0; + if (verb === 'key') code = phase === 'down' ? runtime.down(input) : runtime.up(input); + else if (verb === 'frame') code = runtime.frame(1); + else if (verb === 'value') code = runtime.change(input, Number(phase)); + else if (verb === 'point') { + const routed = plan.inputs.find(p => p.name === input).routes; + code = runtime.point(input, phase, Number(x), Number(y), target, routed ? target : undefined); + } + if (code) ++failures; + assert.equal(rows[i + 1].status === 0, code === 0, command); + if (code) assert.equal(rows[i + 1].scalar_error, code, command); + equivalent(rows[i + 1]); + }); + assert.equal(rows.length, commands.length + 2); + assert.equal(rows.at(-1).status, 0); + assert.equal(native.status, failures ? 1 : 0, native.stderr); + const restored = JSON.parse(successful(binary, [join(temporary, name + '-store'), 'project'], {input: 'load\nquit\n'}).trim().split('\n').at(-1)); + equivalent(restored); + assert.equal(restored.stored_revision, '1'); + if (name === 'routed') { + const unit = join(temporary, 'test-native'); + const authoring = join(temporary, 'app-author.o'); + successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', '-c', units[0], '-o', authoring]); + successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', prepared, resolve(root, 'native/test.c'), + authoring, ...objects.slice(1), library, '-lm', '-o', unit]); + unitResult = JSON.parse(successful(unit, [])); + } + cases.push({name, commands: commands.length, failed_events: failures, states_and_views_match: true, + fresh_process_restore: true, source, source_sha256: hash(source), prepared_sha256: hash(await readFile(prepared)), + native_binary_sha256: hash(await readFile(binary)), native_binary_bytes: (await readFile(binary)).length}); + } + const paths = [resolve(root, 'core.zig'), resolve(root, 'engine.zig'), resolve(root, 'evaluation.zig'), resolve(root, 'native.zig'), + resolve(root, 'manage.py'), ...['scalar.h','plan.h','plan.c','app.h','app.c','main.c','prepare.mjs','test.c','check.mjs'].map(name => resolve(root, 'native', name)), + ...['compiler.mjs','runtime.mjs','project.mjs','core.wasm'].map(name => resolve(root, 'web', name)), + ...['runtime.c','runtime.h','evaluate.c','program.h','flow.h','result.h','store.c','store.h','store-port.c','store-port.h'].map(name => resolve(connections, name))]; + const sourceHashes = Object.fromEntries(await Promise.all(paths.map(async path => [relative(repo, path), hash(await readFile(path))]))); + const result = {kind:'native-instance-and-authored-behavior-correctness-not-performance', sanitized_c_host: sanitized, + zig_mode:'ReleaseSafe; Zig object is not ASan-instrumented', compiler: successful(compiler,['--version']).split('\n')[0], + cases, units: unitResult, source_sha256: sourceHashes, scalar_archive_sha256: hash(await readFile(library)), + scalar_archive_size: successful('size',[library]), scalar_archive_undefined: successful('nm',['-u',library])}; + if (output) { + await mkdir(resolve(output, '..'), {recursive: true}); + await writeFile(output, JSON.stringify(result,null,2)+'\n', {flag:'wx'}); + } + console.log(JSON.stringify({cases:cases.length, event_comparisons:cases.reduce((n,c)=>n+c.commands,0), units:unitResult, sanitized_c_host:sanitized})); +} finally { + await rm(temporary, {recursive:true, force:true}); +} diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/desktop.c b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/desktop.c new file mode 100644 index 0000000..064b181 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/desktop.c @@ -0,0 +1,503 @@ +/* Linux authoring shell. GTK, Cairo, the compiler and module loader stay outside + * the application/execution core; the standalone CLI links none of them. */ +#include "app.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PURE_PREPARE_PATH +#error PURE_PREPARE_PATH must name the existing build-time compiler adapter +#endif +#ifndef PURE_NATIVE_HEADERS +#error PURE_NATIVE_HEADERS must name the native plan headers +#endif +#ifndef PURE_CC +#define PURE_CC "cc" +#endif + +extern const PureScalarPlan pure_app_plan; + +typedef struct { + PureNativeApp app; + void *scalar_memory, *module; + const char *source_path; + char *initial_source; + gsize initial_source_bytes; + char *scratch; + unsigned generation; + GtkWidget *window, *canvas, *status, *values, *source; + bool pointer_active; + uint32_t pointer_input, pointer_target; + double pointer_x, pointer_y, pointer_px, pointer_py; + uint32_t keys_down; +} Desktop; + +typedef struct { double field[4]; bool valid; } SceneView; +typedef struct { Desktop *desktop; uint32_t input; } ValueControl; + +static void free_value_control(gpointer data, GClosure *closure) { + (void)closure; + g_free(data); +} + +static void report(Desktop *d, const char *name, PureOutcome result) { + char message[200]; + snprintf(message, sizeof(message), "%s · status %u · scalar %u · local %" PRId64 " · stored %" PRId64, + name, (unsigned)result.status, d->app.last_scalar_error, d->app.revision, d->app.stored_revision); + gtk_label_set_text(GTK_LABEL(d->status), message); + gtk_widget_queue_draw(d->canvas); +} + +static bool scene(Desktop *d, SceneView result[24]) { + if (d->app.plan->view_count > 24) return false; + pure_scalar_begin_evaluation(d->app.scalar, 2048); + for (uint32_t i = 0; i < d->app.plan->view_count; ++i) { + const PureScalarView *view = &d->app.plan->views[i]; + result[i].valid = view->count <= 4; + for (uint32_t j = 0; j < view->count && j < 4; ++j) { + result[i].field[j] = pure_scalar_eval_bounded(d->app.scalar, view->fields[j].start, view->fields[j].length); + if (pure_scalar_evaluation_info(d->app.scalar, 0) || !isfinite(result[i].field[j])) result[i].valid = false; + } + if (view->count >= 3 && strcmp(view->type, "text") && result[i].field[2] < 0) result[i].valid = false; + if (view->count == 4 && result[i].field[3] < 0) result[i].valid = false; + } + return true; +} + +static void viewport(int width, int height, double *scale, double *left, double *top) { + *scale = fmin((double)width / 640.0, (double)height / 360.0); + if (*scale <= 0) *scale = 1; + *left = (width - 640 * *scale) / 2; + *top = (height - 360 * *scale) / 2; +} + +static void set_color(cairo_t *cr, const char *text) { + GdkRGBA color; + if (!text || !gdk_rgba_parse(&color, text)) gdk_rgba_parse(&color, "#79a7a1"); + cairo_set_source_rgba(cr, color.red, color.green, color.blue, color.alpha); +} + +static void draw(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data) { + Desktop *d = data; + cairo_set_source_rgb(cr, .055, .075, .09); + cairo_paint(cr); + double scale, left, top; + viewport(width, height, &scale, &left, &top); + cairo_save(cr); + cairo_translate(cr, left, top); + cairo_scale(cr, scale, scale); + cairo_set_source_rgb(cr, .08, .115, .14); + cairo_rectangle(cr, 0, 0, 640, 360); + cairo_fill(cr); + SceneView values[24] = {0}; + if (scene(d, values)) for (uint32_t i = 0; i < d->app.plan->view_count; ++i) { + const PureScalarView *v = &d->app.plan->views[i]; + const double *f = values[i].field; + if (!values[i].valid) continue; + if (!strcmp(v->type, "box") && v->count == 4) { + set_color(cr, v->decoration); + cairo_rectangle(cr, f[0], f[1], f[2], f[3]); + cairo_fill(cr); + } else if (!strcmp(v->type, "circle") && v->count == 3) { + set_color(cr, v->decoration); + cairo_arc(cr, f[0], f[1], f[2], 0, 2 * G_PI); + cairo_fill(cr); + } else if (!strcmp(v->type, "text") && v->count == 3) { + char number[64]; + snprintf(number, sizeof(number), "%.6g", f[2]); + char *label = g_strconcat(v->decoration ? v->decoration : "", number, NULL); + PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(area), label); + PangoFontDescription *font = pango_font_description_from_string("Sans 15"); + pango_layout_set_font_description(layout, font); + cairo_set_source_rgb(cr, .88, .92, .92); + cairo_move_to(cr, f[0], f[1]); + pango_cairo_show_layout(cr, layout); + pango_font_description_free(font); + g_object_unref(layout); + g_free(label); + } + } + cairo_restore(cr); +} + +static bool hit(Desktop *d, double px, double py, uint32_t *input, uint32_t *target) { + double scale, left, top; + viewport(gtk_widget_get_width(d->canvas), gtk_widget_get_height(d->canvas), &scale, &left, &top); + double x = (px - left) / scale, y = (py - top) / scale; + if (x < 0 || y < 0 || x >= 640 || y >= 360) return false; + SceneView views[24] = {0}; + if (!scene(d, views)) return false; + for (uint32_t ri = d->app.plan->view_count; ri; --ri) { + uint32_t i = ri - 1; + const PureScalarView *v = &d->app.plan->views[i]; + const double *f = views[i].field; + if (!views[i].valid) continue; + bool inside = (!strcmp(v->type, "box") && v->count == 4 && + x >= f[0] && x <= f[0] + f[2] && y >= f[1] && y <= f[1] + f[3]) || + (!strcmp(v->type, "circle") && v->count == 3 && + (x - f[0]) * (x - f[0]) + (y - f[1]) * (y - f[1]) <= f[2] * f[2]); + if (!inside) continue; + for (uint32_t j = 0; j < d->app.plan->target_count; ++j) + if (!strcmp(v->id, d->app.plan->targets[j].view)) { + *input = d->app.plan->targets[j].input; *target = j; return true; + } + for (uint32_t j = 0; j < d->app.plan->input_count; ++j) + if (d->app.plan->inputs[j].kind == PURE_POINT && + !strcmp(v->id, d->app.plan->inputs[j].target)) { + *input = j; *target = UINT32_MAX; return true; + } + } + return false; +} + +static void pointer(Desktop *d, PureActionPhase phase, double px, double py) { + double scale, left, top; + viewport(gtk_widget_get_width(d->canvas), gtk_widget_get_height(d->canvas), &scale, &left, &top); + d->pointer_px = px; + d->pointer_py = py; + d->pointer_x = (px - left) / scale; + d->pointer_y = (py - top) / scale; + PureAction a = {.input = d->pointer_input, .phase = phase, .target = d->pointer_target, + .source = 0, .x = d->pointer_x, .y = d->pointer_y}; + report(d, "pointer", pure_native_app_action(&d->app, a)); +} +static void drag_begin(GtkGestureDrag *gesture, double x, double y, gpointer data) { + (void)gesture; + Desktop *d = data; + if (d->pointer_active || !hit(d, x, y, &d->pointer_input, &d->pointer_target)) return; + gtk_widget_grab_focus(d->canvas); + pointer(d, PURE_DOWN, x, y); + uint32_t slot = d->pointer_target == UINT32_MAX ? d->pointer_input : 8 + d->pointer_target; + d->pointer_active = (d->app.held & (1u << slot)) != 0; +} +static void drag_update(GtkGestureDrag *gesture, double dx, double dy, gpointer data) { + Desktop *d = data; + if (!d->pointer_active) return; + double x, y; + if (gtk_gesture_drag_get_start_point(gesture, &x, &y)) pointer(d, PURE_MOVE, x + dx, y + dy); +} +static void drag_end(GtkGestureDrag *gesture, double dx, double dy, gpointer data) { + Desktop *d = data; + if (!d->pointer_active) return; + double x, y; + if (gtk_gesture_drag_get_start_point(gesture, &x, &y)) pointer(d, PURE_UP, x + dx, y + dy); + else pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py); + d->pointer_active = false; +} +static void drag_cancel(GtkGesture *gesture, GdkEventSequence *sequence, gpointer data) { + (void)gesture; (void)sequence; + Desktop *d = data; + if (d->pointer_active) { + pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py); + d->pointer_active = false; + } +} + +static bool key_matches(const char *key, guint value) { + if (!strcmp(key, "ArrowLeft")) return value == GDK_KEY_Left; + if (!strcmp(key, "ArrowRight")) return value == GDK_KEY_Right; + if (!strcmp(key, "ArrowUp")) return value == GDK_KEY_Up; + if (!strcmp(key, "ArrowDown")) return value == GDK_KEY_Down; + if (!strcmp(key, "Space")) return value == GDK_KEY_space; + if (strlen(key) == 4 && !memcmp(key, "Key", 3) && value < 128) + return g_ascii_toupper((char)value) == key[3]; + return false; +} +static gboolean key_event(GtkEventControllerKey *controller, guint keyval, guint keycode, + GdkModifierType modifiers, gpointer data) { + (void)keycode; (void)modifiers; + Desktop *d = data; + bool pressed = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(controller), "pressed")); + bool handled = false; + for (uint32_t i = 0; i < d->app.plan->input_count; ++i) { + const PureScalarInput *input = &d->app.plan->inputs[i]; + if (input->kind != PURE_KEY || !key_matches(input->key, keyval)) continue; + handled = true; + if (pressed) d->keys_down |= 1u << i; else d->keys_down &= ~(1u << i); + report(d, input->name, pure_native_app_action(&d->app, (PureAction){.input = i, + .phase = pressed ? PURE_DOWN : PURE_UP, .source = 0})); + } + return handled; +} +static gboolean key_down(GtkEventControllerKey *c, guint key, guint code, GdkModifierType mods, gpointer d) { + g_object_set_data(G_OBJECT(c), "pressed", GINT_TO_POINTER(1)); + return key_event(c, key, code, mods, d); +} +static void key_up(GtkEventControllerKey *c, guint key, guint code, GdkModifierType mods, gpointer d) { + g_object_set_data(G_OBJECT(c), "pressed", NULL); + (void)key_event(c, key, code, mods, d); +} +static void focus_left(GtkEventControllerFocus *controller, gpointer data) { + (void)controller; + Desktop *d = data; + if (d->pointer_active) { + pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py); + d->pointer_active = false; + } + for (uint32_t i = 0; i < d->app.plan->input_count; ++i) + if (d->keys_down & (1u << i)) + report(d, "key release", pure_native_app_action(&d->app, (PureAction){.input = i, .phase = PURE_UP})); + d->keys_down = 0; +} + +static void value_enter(GtkEntry *entry, gpointer data) { + ValueControl *control = data; + const char *s = gtk_editable_get_text(GTK_EDITABLE(entry)); + char *end; + double n = g_ascii_strtod(s, &end); + if (!*s || *end || !isfinite(n)) { + gtk_label_set_text(GTK_LABEL(control->desktop->status), "Value must be a finite number"); + return; + } + report(control->desktop, "value", pure_native_app_action(&control->desktop->app, + (PureAction){.input = control->input, .phase = PURE_CHANGE, .x = n})); +} +static void rebuild_values(Desktop *d) { + GtkWidget *child; + while ((child = gtk_widget_get_first_child(d->values))) gtk_box_remove(GTK_BOX(d->values), child); + for (uint32_t i = 0; i < d->app.plan->input_count; ++i) { + if (d->app.plan->inputs[i].kind != PURE_VALUE) continue; + GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + GtkWidget *label = gtk_label_new(d->app.plan->inputs[i].name); + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "number ↵"); + gtk_widget_set_hexpand(entry, TRUE); + gtk_box_append(GTK_BOX(row), label); + gtk_box_append(GTK_BOX(row), entry); + gtk_box_append(GTK_BOX(d->values), row); + ValueControl *control = g_new(ValueControl, 1); + *control = (ValueControl){d, i}; + g_signal_connect_data(entry, "activate", G_CALLBACK(value_enter), control, free_value_control, 0); + } +} + +static bool run(char **arguments, char **failure) { + GError *error = NULL; + char *output = NULL, *err = NULL; + int status = 0; + bool launched = g_spawn_sync(NULL, arguments, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, + &output, &err, &status, &error); + bool ok = launched && g_spawn_check_wait_status(status, &error); + if (!ok) { + if (err && *err) { + const char *begin = strstr(err, "Error: "); + if (!begin) begin = err; + const char *end = strchr(begin, '\n'); + *failure = g_strndup(begin, end ? (size_t)(end - begin) : 300); + } else *failure = g_strdup(error ? error->message : "tool failed"); + } + g_clear_error(&error); + g_free(output); g_free(err); + return ok; +} + +static void apply_source(Desktop *d, bool save_source) { + if (d->app.held) { + gtk_label_set_text(GTK_LABEL(d->status), "Release held inputs before Apply"); + return; + } + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer, &start, &end); + char *source = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); + size_t bytes = strlen(source); + if (bytes > 32768) { + gtk_label_set_text(GTK_LABEL(d->status), "Source exceeds the 32 KiB compiler limit"); + g_free(source); return; + } + char *input = g_build_filename(d->scratch, "source.pure", NULL); + char *output = g_strdup_printf("%s/plan-%u.c", d->scratch, ++d->generation); + char *module = g_strdup_printf("%s/plan-%u.so", d->scratch, d->generation); + char *failure = NULL; + GError *error = NULL; + if (!g_file_set_contents(input, source, bytes, &error)) { + failure = g_strdup(error->message); g_clear_error(&error); goto done; + } + char *prepare_argv[] = {"node", PURE_PREPARE_PATH, input, output, NULL}; + if (!run(prepare_argv, &failure)) goto done; + char *headers = g_strconcat("-I", PURE_NATIVE_HEADERS, NULL); + char *compile_argv[] = {PURE_CC, "-std=c17", "-O2", "-fPIC", "-shared", "-Wall", "-Wextra", "-Werror", + headers, output, "-o", module, NULL}; + bool built = run(compile_argv, &failure); + g_free(headers); + if (!built) goto done; + void *handle = dlopen(module, RTLD_NOW | RTLD_LOCAL); + if (!handle) { failure = g_strdup(dlerror()); goto done; } + const PureScalarPlan *candidate = dlsym(handle, "pure_app_plan"); + bool same = candidate && !memcmp(candidate->identity, d->app.plan->identity, sizeof(candidate->identity)); + if (!same) { + PureResult status = pure_native_app_replace_plan(&d->app, candidate); + if (status != PURE_OK) { + failure = g_strdup_printf("plan rejected: status %u, scalar %u", status, d->app.last_scalar_error); + dlclose(handle); goto done; + } + if (d->module) dlclose(d->module); + d->module = handle; + rebuild_values(d); + } else { + dlclose(handle); + } + if (save_source && !g_file_set_contents(d->source_path, source, bytes, &error)) { + failure = g_strdup_printf("Applied in memory; source save failed: %s", error->message); + g_clear_error(&error); + } else { + gtk_text_buffer_set_modified(buffer, FALSE); + report(d, "Applied source", (PureOutcome){.status = PURE_OK}); + } +done: + if (failure) { + char message[350]; + g_snprintf(message, sizeof(message), "Apply: %.335s", failure); + gtk_label_set_text(GTK_LABEL(d->status), message); + g_free(failure); + } + g_remove(input); g_remove(output); g_remove(module); + g_free(input); g_free(output); g_free(module); g_free(source); + gtk_widget_queue_draw(d->canvas); +} +static void apply(GtkButton *button, gpointer data) { + (void)button; + apply_source(data, true); +} + +static void save_state(GtkButton *button, gpointer data) { + (void)button; + Desktop *d = data; + report(d, "Save state", pure_native_app_save(&d->app)); +} +static void load_state(GtkButton *button, gpointer data) { + (void)button; + Desktop *d = data; + report(d, "Load state", pure_native_app_restore(&d->app)); +} +static void reset_state(GtkButton *button, gpointer data) { + (void)button; + Desktop *d = data; + report(d, "Reset", pure_native_app_action(&d->app, (PureAction){.phase = PURE_RESET})); +} + +static GtkWidget *button(GtkWidget *parent, const char *title, GCallback callback, Desktop *d) { + GtkWidget *item = gtk_button_new_with_label(title); + gtk_box_append(GTK_BOX(parent), item); + g_signal_connect(item, "clicked", callback, d); + return item; +} +static void activate(GtkApplication *application, gpointer data) { + Desktop *d = data; + d->window = gtk_application_window_new(application); + gtk_window_set_title(GTK_WINDOW(d->window), "Pure · native authoring"); + gtk_window_set_default_size(GTK_WINDOW(d->window), 1320, 720); + GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_margin_start(root, 12); gtk_widget_set_margin_end(root, 12); + gtk_widget_set_margin_top(root, 12); gtk_widget_set_margin_bottom(root, 12); + gtk_window_set_child(GTK_WINDOW(d->window), root); + GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_append(GTK_BOX(root), bar); + button(bar, "Apply source", G_CALLBACK(apply), d); + button(bar, "Save state", G_CALLBACK(save_state), d); + button(bar, "Load state", G_CALLBACK(load_state), d); + button(bar, "Reset", G_CALLBACK(reset_state), d); + d->status = gtk_label_new("Edit source and Apply; click the scene for keyboard input"); + gtk_label_set_xalign(GTK_LABEL(d->status), 0); + gtk_label_set_single_line_mode(GTK_LABEL(d->status), TRUE); + gtk_label_set_ellipsize(GTK_LABEL(d->status), PANGO_ELLIPSIZE_END); + gtk_widget_set_hexpand(d->status, TRUE); + gtk_box_append(GTK_BOX(bar), d->status); + GtkWidget *panes = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_vexpand(panes, TRUE); + gtk_box_append(GTK_BOX(root), panes); + GtkWidget *left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_paned_set_start_child(GTK_PANED(panes), left); + GtkWidget *source_title = gtk_label_new(d->source_path); + gtk_label_set_xalign(GTK_LABEL(source_title), 0); + gtk_box_append(GTK_BOX(left), source_title); + GtkWidget *scroll = gtk_scrolled_window_new(); + gtk_widget_set_hexpand(scroll, TRUE); gtk_widget_set_vexpand(scroll, TRUE); + gtk_box_append(GTK_BOX(left), scroll); + d->source = gtk_text_view_new(); + gtk_text_view_set_monospace(GTK_TEXT_VIEW(d->source), TRUE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(d->source), GTK_WRAP_NONE); + gtk_text_buffer_set_enable_undo(gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)), TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), d->source); + gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)), + d->initial_source, (int)d->initial_source_bytes); + GtkWidget *right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_paned_set_end_child(GTK_PANED(panes), right); + d->canvas = gtk_drawing_area_new(); + gtk_widget_set_hexpand(d->canvas, TRUE); gtk_widget_set_vexpand(d->canvas, TRUE); + gtk_widget_set_focusable(d->canvas, TRUE); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(d->canvas), draw, d, NULL); + gtk_box_append(GTK_BOX(right), d->canvas); + d->values = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_append(GTK_BOX(right), d->values); + rebuild_values(d); + GtkGesture *drag = gtk_gesture_drag_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY); + g_signal_connect(drag, "drag-begin", G_CALLBACK(drag_begin), d); + g_signal_connect(drag, "drag-update", G_CALLBACK(drag_update), d); + g_signal_connect(drag, "drag-end", G_CALLBACK(drag_end), d); + g_signal_connect(drag, "cancel", G_CALLBACK(drag_cancel), d); + gtk_widget_add_controller(d->canvas, GTK_EVENT_CONTROLLER(drag)); + GtkEventController *keys = gtk_event_controller_key_new(); + g_signal_connect(keys, "key-pressed", G_CALLBACK(key_down), d); + g_signal_connect(keys, "key-released", G_CALLBACK(key_up), d); + gtk_widget_add_controller(d->canvas, keys); + GtkEventController *focus = gtk_event_controller_focus_new(); + g_signal_connect(focus, "leave", G_CALLBACK(focus_left), d); + gtk_widget_add_controller(d->canvas, focus); + gtk_paned_set_position(GTK_PANED(panes), 560); + apply_source(d, false); + gtk_window_present(GTK_WINDOW(d->window)); +} + +int main(int argc, char **argv) { + if (argc != 4) { + fprintf(stderr, "pure-desktop STORE_DIRECTORY DOCUMENT_KEY SOURCE.pure\n"); + return 2; + } + Desktop d = {.source_path = argv[3]}; + GError *error = NULL; + if (!g_file_get_contents(d.source_path, &d.initial_source, &d.initial_source_bytes, &error)) { + fprintf(stderr, "source read failed: %s\n", error->message); + g_clear_error(&error); + return 2; + } + if (d.initial_source_bytes > 32768) { + fprintf(stderr, "source exceeds the 32 KiB compiler limit\n"); + g_free(d.initial_source); return 2; + } + if (!g_utf8_validate(d.initial_source, (gssize)d.initial_source_bytes, NULL)) { + fprintf(stderr, "source must be valid UTF-8 text\n"); + g_free(d.initial_source); return 2; + } + d.scalar_memory = malloc(pure_scalar_bytes()); + if (!d.scalar_memory) { g_free(d.initial_source); return 2; } + PureResult status = pure_native_app_init(&d.app, &pure_app_plan, d.scalar_memory, + pure_scalar_bytes(), argv[1], argv[2], 4096); + if (status != PURE_OK) { + fprintf(stderr, "initialization failed: %u\n", status); + free(d.scalar_memory); g_free(d.initial_source); return 2; + } + d.scratch = g_dir_make_tmp("pure-author-XXXXXX", &error); + if (!d.scratch) { + fprintf(stderr, "authoring scratch: %s\n", error->message); + g_clear_error(&error); free(d.scalar_memory); g_free(d.initial_source); return 2; + } + GtkApplication *application = gtk_application_new("dev.pure.authoring", G_APPLICATION_NON_UNIQUE); + g_signal_connect(application, "activate", G_CALLBACK(activate), &d); + int result = g_application_run(G_APPLICATION(application), 0, NULL); + g_object_unref(application); + pure_runtime_stop(&d.app.execution); + if (d.module) dlclose(d.module); + g_rmdir(d.scratch); g_free(d.scratch); + free(d.scalar_memory); + g_free(d.initial_source); + return result; +} diff --git a/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/test.c b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/test.c new file mode 100644 index 0000000..697fb64 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-native-desktop/sources/native/test.c @@ -0,0 +1,151 @@ +#define _POSIX_C_SOURCE 200809L +#include "app.h" +#include "../../connections/store.h" +#include +#include +#include +#include +#include +#include + +extern const PureScalarPlan pure_app_plan; +static void move(PureNativeApp *app, uint32_t target, double x, double y) { + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_DOWN, .target = target, .x = x, .y = y}).status == PURE_OK); + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_MOVE, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK); + assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_UP, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK); +} +static void drained(PureNativeApp *app) { + assert(app->execution.active == 0 && app->execution.retained == 0); + assert(app->execution.peak_active <= 2 && app->execution.peak_retained <= app->execution.budget); +} +static void values(PureNativeApp *app, double output[32]) { + for (uint32_t i = 0; i < app->plan->state_count; ++i) output[i] = pure_scalar_get_state(app->scalar, i); +} +static void same(PureNativeApp *app, const double *expected) { + for (uint32_t i = 0; i < app->plan->state_count; ++i) assert(pure_scalar_get_state(app->scalar, i) == expected[i]); +} + +int main(void) { + size_t size = pure_scalar_bytes(); + unsigned char *a = malloc(size), *b = malloc(size), *c = malloc(size); + assert(a && b && c); + assert(!pure_scalar_initialize(NULL, size)); + assert(!pure_scalar_initialize(a, size - 1)); + assert(pure_scalar_alignment() > 1 && !pure_scalar_initialize(a + 1, size)); + char path[] = "/tmp/pure-native-instance-XXXXXX"; + assert(mkdtemp(path)); + PureNativeApp first, second, limited; + assert(pure_native_app_init(&first, &pure_app_plan, a, size, path, "scene", 4096) == PURE_OK); + assert(pure_native_app_init(&second, &pure_app_plan, b, size, path, "scene", 4096) == PURE_OK); + assert(pure_native_app_init(&limited, &pure_app_plan, c, size, path, "limited", sizeof(PureAction) - 1) == PURE_OK); + assert(pure_native_app_action(&limited, (PureAction){.phase = PURE_FRAME}).status == PURE_BUDGET); + assert(pure_scalar_info(limited.scalar, 9) == 0); + + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .x = 90, .y = 130}).status == PURE_OK); + assert(pure_native_app_save(&first).status == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 1, .x = 230, .y = 210}).status == PURE_INVALID); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .x = 230, .y = 210}).status == PURE_OK); + move(&second, 1, 290, 170); + assert(pure_scalar_get_state(second.scalar, 1) == 80 && pure_scalar_get_state(second.scalar, 9) == 350); + assert(pure_scalar_get_state(first.scalar, 1) == 220 && pure_scalar_get_state(first.scalar, 9) == 280); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 0, .x = 230, .y = 210}).status == PURE_OK); + assert(first.held == 0 && second.held == 0); + assert(pure_native_app_save(&first).status == PURE_OK && first.stored_revision == 1); + double second_values[32] = {0}; + values(&second, second_values); + PureOutcome conflict = pure_native_app_save(&second); + assert(conflict.status == PURE_CONFLICT && conflict.revision == 1 && second.stored_revision == 0); + same(&second, second_values); + assert(pure_native_app_restore(&second).status == PURE_OK && second.stored_revision == 1); + assert(pure_scalar_get_state(second.scalar, 1) == 220 && pure_scalar_get_state(second.scalar, 9) == 280); + + /* Staging/error/evaluation state also belongs to the selected instance. */ + assert(pure_scalar_begin_program(first.scalar, 1, 0) == 0); + assert(pure_scalar_put_state(first.scalar, 0, 999, -1) == 0); + assert(pure_scalar_configure_evaluation(first.scalar, 2, 2) == 0); + move(&second, 2, 460, 230); + assert(pure_scalar_put_instruction(first.scalar, 999, 0, 0) != 0); + assert(pure_scalar_commit_program(first.scalar) != 0); + assert(pure_scalar_get_state(first.scalar, 1) == 220); + assert(pure_scalar_get_state(first.scalar, 17) == 450 && pure_scalar_get_state(second.scalar, 17) == 520); + assert(pure_native_app_save(&second).status == PURE_OK && second.stored_revision == 2); + assert(pure_native_app_save(&first).status == PURE_CONFLICT && first.stored_revision == 1); + + /* Mismatched definition and nonfinite restored values never replace state. */ + PureStoredView saved; + assert(pure_store_get(path, "scene", 300, &saved) == PURE_OK); + unsigned char invalid[300]; + assert(saved.body.size <= sizeof(invalid)); + size_t length = saved.body.size; + memcpy(invalid, saved.body.data, length); + pure_store_release(&saved); + invalid[8] ^= 1; + int64_t revision; + assert(pure_store_put(path, "scene", 2, (PureSpan){invalid, length}, &revision) == PURE_OK); + double first_values[32] = {0}; + values(&first, first_values); + assert(pure_native_app_restore(&first).status == PURE_INVALID && first.stored_revision == 1); + same(&first, first_values); + invalid[8] ^= 1; + const unsigned char nan[8] = {0,0,0,0,0,0,248,127}; + memcpy(invalid + 44, nan, 8); + assert(pure_store_put(path, "scene", 3, (PureSpan){invalid, length}, &revision) == PURE_OK); + assert(pure_native_app_restore(&first).status == PURE_INVALID && first.last_scalar_error == 5); + same(&first, first_values); + assert(first.stored_revision == 1); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .source = 11, .x = 230, .y = 210}).status == PURE_OK); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 1, .source = 22, .x = 290, .y = 170}).status == PURE_OK); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .source = 22}).status == PURE_INVALID); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_CANCEL, .target = 0, .source = 11}).status == PURE_OK); + assert(first.held && pure_native_app_save(&first).status == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 1, .source = 22}).status == PURE_OK); + assert(first.held == 0); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 1}).status == PURE_OK); + double after_key = pure_scalar_get_state(first.scalar, 9); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 2}).status == PURE_OK); + assert(pure_scalar_get_state(first.scalar, 9) == after_key); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 1}).status == PURE_OK && first.held); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 2}).status == PURE_OK && !first.held); + + /* The native editor swaps only a fully accepted definition. Matching state + * names survive; a new name starts at its declared initial value. */ + PureScalarState changed_states[32]; + memcpy(changed_states, pure_app_plan.states, pure_app_plan.state_count * sizeof(changed_states[0])); + changed_states[1] = (PureScalarState){"new.green.x", 123}; + PureScalarPlan changed = pure_app_plan; + changed.states = changed_states; + changed.identity[0] ^= 1; + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN}).status == PURE_OK); + assert(pure_native_app_replace_plan(&first, &changed) == PURE_BUSY); + assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP}).status == PURE_OK); + double old_y = pure_scalar_get_state(first.scalar, 2); + int64_t before_replace = first.revision; + uint64_t before_accept = first.execution.accepted; + assert(pure_native_app_replace_plan(&first, &changed) == PURE_OK); + assert(first.execution.accepted == before_accept + 1); + assert(first.plan == &changed && first.revision == before_replace + 1); + assert(pure_scalar_get_state(first.scalar, 1) == 123 && pure_scalar_get_state(first.scalar, 2) == old_y); + PureScalarInstruction invalid_program[512]; + assert(changed.instruction_count <= 512); + memcpy(invalid_program, changed.instructions, changed.instruction_count * sizeof(invalid_program[0])); + invalid_program[0].op = 999; + PureScalarPlan rejected = changed; + rejected.instructions = invalid_program; + before_replace = first.revision; + before_accept = first.execution.accepted; + assert(pure_native_app_replace_plan(&first, &rejected) == PURE_INVALID && first.last_scalar_error != 0); + assert(first.execution.accepted == before_accept + 1); + assert(first.plan == &changed && first.revision == before_replace); + assert(pure_scalar_get_state(first.scalar, 1) == 123 && pure_scalar_get_state(first.scalar, 2) == old_y); + drained(&first); drained(&second); drained(&limited); + + char file[256]; + snprintf(file, sizeof(file), "%s/obj-scene", path); assert(unlink(file) == 0); + snprintf(file, sizeof(file), "%s/.writer.lock", path); assert(unlink(file) == 0); + assert(rmdir(path) == 0); + free(a); free(b); free(c); + printf("{\"native_instances\":3,\"checks\":\"independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload\"," + "\"scalar_instance_bytes\":%zu,\"scalar_alignment\":%zu,\"app_controller_bytes\":%zu,\"action_bytes\":%zu," + "\"snapshot_bytes\":%zu,\"all_work_drained\":true}\n", size, pure_scalar_alignment(), sizeof(PureNativeApp), sizeof(PureAction), length); + return 0; +} diff --git a/prototypes/workbench/results/2026-09-24-secure-peer/build.json b/prototypes/workbench/results/2026-09-24-secure-peer/build.json new file mode 100644 index 0000000..38c1906 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-secure-peer/build.json @@ -0,0 +1,48 @@ +{ + "source": "/home/eunhhu/work/pure/prototypes/workbench/examples/routed-layout.pure", + "sourceSha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "zigVersion": "0.15.2", + "zigFlags": [ + "-O", + "ReleaseSafe", + "-fno-compiler-rt" + ], + "cFlags": [ + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror" + ], + "binary": "/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-peer", + "binarySha256": "3389497f5c9091d0ac971b1647b762bc4f4de556aa83dece729365b97eca0410", + "librarySha256": "ce4835b457ccae5f7d21b76c5d86b4d6b74ca99f3932efe96d9d1281b17429be", + "preparedSha256": "710503e7ffadf02b309c24a761caade4c5a093750d6801956cc698bfc263fc09", + "desktop": false, + "desktopFlags": [], + "peer": true, + "peerFlags": [ + "-lssl", + "-lcrypto" + ], + "implementationSha256": { + "prototypes/workbench/engine.zig": "555b9ff3143888921d90a475bcb7957c18dbf7101ee9e3d2853c01a562cbf3f5", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "fe6c445cdfb5a4bc27ea9f540f3880c45b65c33098eec761d4398d9a7fc5c9a6", + "prototypes/workbench/native/prepare.mjs": "3989d4e545255fccf9d846afb79246e874f4cefb03eecef9ee1e2b1ce8fd257e", + "prototypes/workbench/native/scalar.h": "b84a796bde2f45d59505cc0addb5f5afede0a9c4dc58d40a38dd37a8ac68d1fd", + "prototypes/workbench/native/plan.h": "2e9bd7e3c2ecc52f79c8938d09d2a696174c44a245e4bf52b70be2ed02cbbb57", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/native/peer.c": "7d5865d499a4ba51f1d14e0d286480f605be2476616d28e1150b35ca1f87bf56", + "prototypes/workbench/native/peer-wire.c": "8fc3694db7a00a4f4899ca5a42dc2c0d96e39cb7d927c759ee8881901241733a", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/plan.c": "f0bdf8466097cfad54c197120aace8acd5f430170931d575e26807b760ce2173", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f" + } +} diff --git a/prototypes/workbench/results/2026-09-24-secure-peer/peer-check.json b/prototypes/workbench/results/2026-09-24-secure-peer/peer-check.json new file mode 100644 index 0000000..257ef9e --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-secure-peer/peer-check.json @@ -0,0 +1 @@ +{"alpn": "pass", "disconnect_release": "pass", "idle_deadline": "pass", "malformed_frames": "pass", "mutual_authentication": "pass", "persist_restart": "pass", "python_client": "pass", "revision_conflict": "pass", "server_connected_rss_kib": 8480, "server_hostname": "pass", "server_idle_rss_kib": 7904} diff --git a/prototypes/workbench/results/2026-09-24-secure-peer/size.txt b/prototypes/workbench/results/2026-09-24-secure-peer/size.txt new file mode 100644 index 0000000..2c2547a --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-secure-peer/size.txt @@ -0,0 +1,4 @@ + text data bss dec hex filename + 5012 0 0 5012 1394 /tmp/pure-peer-runtime.o + 62827 2040 16 64883 fd73 prototypes/workbench/build/native/pure-native + 75969 2472 16 78457 13279 prototypes/workbench/build/native/pure-peer diff --git a/prototypes/workbench/results/2026-09-24-shared-program/cli-build.json b/prototypes/workbench/results/2026-09-24-shared-program/cli-build.json new file mode 100644 index 0000000..0618cc5 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/cli-build.json @@ -0,0 +1,49 @@ +{ + "source": "/home/eunhhu/work/pure/prototypes/workbench/examples/routed-layout.pure", + "sourceSha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "zigVersion": "0.15.2", + "zigFlags": [ + "-O", + "ReleaseSafe", + "-fno-compiler-rt" + ], + "scalarProfile": "fixed-definition", + "zigRoot": "/home/eunhhu/work/pure/prototypes/workbench/native-fixed.zig", + "cFlags": [ + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-DPURE_FIXED_DEFINITION=1" + ], + "binary": "/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-native", + "binarySha256": "75da17aa4518ac725a42fa0b96e2419d0c19f301af700fb6081bae0c9b562731", + "librarySha256": "30a015c55d385b337639b54b2f31aea34b40bc37d1fe6c9e3c884660d26e4cbc", + "fixedProfileSha256": "4854304aae01c28b5e6fd43a7d16ff3b08ddd711108b4fdde6848eb5f5beaa92", + "preparedSha256": "b7524e53a5d90d1ce16d2e2fbaac67ceb242f1cd27f280afc3a80857ad8c21ba", + "desktop": false, + "desktopFlags": [], + "peer": false, + "peerFlags": [], + "implementationSha256": { + "prototypes/workbench/engine.zig": "a2ea7923975eeeb11d9222b91887489b374efd91f3f50b312b8d96deec076339", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "5656e5e402359b5f1b4bd14c1e73366ed5e32a253a7469a16f58dd8e88104d43", + "prototypes/workbench/native-fixed.zig": "15418d44f2f697b65770ea96d8e896de84905dec98af1074784323a51cef5a2b", + "prototypes/workbench/native/prepare.mjs": "94b732b7cd7a1f8beaf6759bf484fb794449f9d93ed4ea1c3a3186501f8a519a", + "prototypes/workbench/native/scalar.h": "4a95675e08c4a3bd30f0bef123aeb7327f0cae58297b9b32fc1e7d2a208f1d1a", + "prototypes/workbench/native/plan.h": "98825678cb4e9beba4699f4f9679bb6d6637968b513639cac4173c052de206d2", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/plan.c": "aa484bfe9072addb00e6eb87edcc11b290b97bd4e362fe9c12105aca8b807f69", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f" + } +} diff --git a/prototypes/workbench/results/2026-09-24-shared-program/dependencies.txt b/prototypes/workbench/results/2026-09-24-shared-program/dependencies.txt new file mode 100644 index 0000000..89d1dd9 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/dependencies.txt @@ -0,0 +1,6 @@ +pure-native TEXTREL=absent +0x0000000000000001 (NEEDED) Shared library: [libc.so.6] +pure-peer TEXTREL=absent +0x0000000000000001 (NEEDED) Shared library: [libssl.so.3] +0x0000000000000001 (NEEDED) Shared library: [libcrypto.so.3] +0x0000000000000001 (NEEDED) Shared library: [libc.so.6] diff --git a/prototypes/workbench/results/2026-09-24-shared-program/desktop-build.json b/prototypes/workbench/results/2026-09-24-shared-program/desktop-build.json new file mode 100644 index 0000000..56604fd --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/desktop-build.json @@ -0,0 +1,84 @@ +{ + "source": "/home/eunhhu/work/pure/prototypes/workbench/examples/routed-layout.pure", + "sourceSha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "zigVersion": "0.15.2", + "zigFlags": [ + "-O", + "ReleaseSafe", + "-fno-compiler-rt" + ], + "scalarProfile": "general-authoring", + "zigRoot": "/home/eunhhu/work/pure/prototypes/workbench/native.zig", + "cFlags": [ + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-DPURE_NATIVE_AUTHORING=1", + "-DPURE_PREPARE_PATH=\"/home/eunhhu/work/pure/prototypes/workbench/native/prepare.mjs\"", + "-DPURE_NATIVE_HEADERS=\"/home/eunhhu/work/pure/prototypes/workbench/native\"", + "-DPURE_CC=\"cc\"" + ], + "binary": "/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-desktop", + "binarySha256": "c9dec132b2d4e980c75126ac51c8ea3bdd5417ee903b76962f5824feb5f346b2", + "librarySha256": "6916a374f8c6c9be93ca09b8d81eb60af889928b5a68376c3985bf5f87f6a2a7", + "fixedProfileSha256": null, + "preparedSha256": "b7524e53a5d90d1ce16d2e2fbaac67ceb242f1cd27f280afc3a80857ad8c21ba", + "desktop": true, + "desktopFlags": [ + "-I/usr/include/gtk-4.0", + "-I/usr/include/pango-1.0", + "-I/usr/include/glib-2.0", + "-I/usr/lib/aarch64-linux-gnu/glib-2.0/include", + "-I/usr/include/sysprof-6", + "-I/usr/include/harfbuzz", + "-I/usr/include/freetype2", + "-I/usr/include/libpng16", + "-I/usr/include/libmount", + "-I/usr/include/blkid", + "-I/usr/include/fribidi", + "-I/usr/include/cairo", + "-I/usr/include/pixman-1", + "-I/usr/include/gdk-pixbuf-2.0", + "-I/usr/include/aarch64-linux-gnu", + "-I/usr/include/webp", + "-I/usr/include/graphene-1.0", + "-I/usr/lib/aarch64-linux-gnu/graphene-1.0/include", + "-pthread", + "-lgtk-4", + "-lpangocairo-1.0", + "-lpango-1.0", + "-lharfbuzz", + "-lgdk_pixbuf-2.0", + "-lcairo-gobject", + "-lcairo", + "-lvulkan", + "-lgraphene-1.0", + "-lgio-2.0", + "-lgobject-2.0", + "-lglib-2.0" + ], + "peer": false, + "peerFlags": [], + "implementationSha256": { + "prototypes/workbench/engine.zig": "a2ea7923975eeeb11d9222b91887489b374efd91f3f50b312b8d96deec076339", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "5656e5e402359b5f1b4bd14c1e73366ed5e32a253a7469a16f58dd8e88104d43", + "prototypes/workbench/native-fixed.zig": "15418d44f2f697b65770ea96d8e896de84905dec98af1074784323a51cef5a2b", + "prototypes/workbench/native/prepare.mjs": "94b732b7cd7a1f8beaf6759bf484fb794449f9d93ed4ea1c3a3186501f8a519a", + "prototypes/workbench/native/scalar.h": "4a95675e08c4a3bd30f0bef123aeb7327f0cae58297b9b32fc1e7d2a208f1d1a", + "prototypes/workbench/native/plan.h": "98825678cb4e9beba4699f4f9679bb6d6637968b513639cac4173c052de206d2", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/native/desktop.c": "617d58ad2ec74de0f428ab76ca23e76bed4ca9c3234dd50213853843552d5c7c", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/plan.c": "aa484bfe9072addb00e6eb87edcc11b290b97bd4e362fe9c12105aca8b807f69", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f" + } +} diff --git a/prototypes/workbench/results/2026-09-24-shared-program/fixed-parity.json b/prototypes/workbench/results/2026-09-24-shared-program/fixed-parity.json new file mode 100644 index 0000000..7d56dd6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/fixed-parity.json @@ -0,0 +1,205 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": true, + "profile": "fixed-definition", + "general_instance_bytes": 20064, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 88, + "fixed_profile_sha256": "dfff2e26103b07d6ad79c664cca1a8b074784eade92e575c0cc760d3679b1b18", + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "b7a39d8499bd2e638ce614c24263160c7a16a379c33e2f405d10b04bd99dd780", + "native_binary_sha256": "3c85ede423237d68ade84bd27983e1cb2ba7a9a4eb86c384b091badfe4e51ed8", + "native_binary_bytes": 558928 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 96, + "fixed_profile_sha256": "aa3508055dece145608b3d98e7fdf6054560d6ef9d17de5a55e0f66e78f71631", + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "d3c278f4eb8796e422ef03384373fab6ae517cb682c7432f057c2432c6f8e8e5", + "native_binary_sha256": "5c7d08ff87abcc022310f8ec99a1172710912ae0e964e6925cbbe62e51f89ced", + "native_binary_bytes": 559000 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 112, + "fixed_profile_sha256": "19d850442b7a7043e3b6c11d4554095df296c1dfed07de35730f4fee36a725b8", + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "99846fb777cb7b1b7289d3fb0662b8138ea7891371798e4e451355cbc629ef10", + "native_binary_sha256": "e965e259831d65b6e5e6b2307a045e96e7b2133785b311d04c01df45842cb3d4", + "native_binary_bytes": 559912 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 136, + "fixed_profile_sha256": "376ec5d1eda3865897afc4538d01685ece57b63ec8b0ba8b7d79f67ed55e146d", + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "307257b3be2ec979114f85d9b8d37bc6309b4f6fd236bfcdf799f784af0b7644", + "native_binary_sha256": "50af581c01391749fa65a957fbb35b00a29a0fb47e7af9e53749dfac0900429e", + "native_binary_bytes": 560848 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 280, + "fixed_profile_sha256": "4854304aae01c28b5e6fd43a7d16ff3b08ddd711108b4fdde6848eb5f5beaa92", + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "b7524e53a5d90d1ce16d2e2fbaac67ceb242f1cd27f280afc3a80857ad8c21ba", + "native_binary_sha256": "bdd8277378c04ac6f336093d12f55747d046fec0f70cbe25286136687898de10", + "native_binary_bytes": 564736 + }, + { + "name": "layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 256, + "fixed_profile_sha256": "1c957ab0a71623233c0d122f209c3cc84071da93e44f0704e1b8d4410cbb3cba", + "fresh_process_restore": true, + "source": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n", + "source_sha256": "cebd9c3485a2be4ec16ae665c1ed4ee200d51fc56ad0151306c8e9c31138f346", + "prepared_sha256": "95a9cb69007033e06f62e8cf7c7f6ea63d8afa85fe90b856666e1facbba7f2f4", + "native_binary_sha256": "11d69b3a99c8a9d4d0a19effb28e3ec06fd543d4e9b1590cd4c1534a968424a7", + "native_binary_bytes": 563352 + }, + { + "name": "shared-layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 280, + "fixed_profile_sha256": "a2aa8b4aff56d2cff8994126b66777fc02350f6a896bbf6c648710757ec60cfb", + "fresh_process_restore": true, + "source": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "be8296244b768f9adda99e7afc89249f34853fa27124a277aecb0841eb8176eb", + "prepared_sha256": "a0a3a7618e74a615cb2de5d66388a073e15015a1e9a737b8384c70e96fc77fd4", + "native_binary_sha256": "521814d64a0c0fd766f3735e31e8ec020ddeb4229ffabe38bbe66461b2c0bf7b", + "native_binary_bytes": 563904 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 88, + "fixed_profile_sha256": "dfa3b8bc6975c3bb3a49e54880e55c8fdfff868eeebd448cae336ef18b9e3ee0", + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "905b28757ee3e93e69a1631cf88d9b2c63b70570f2484b9d8ca96367af15b5d6", + "native_binary_sha256": "2dd90e041e02f97454bacaff36c633577391f4d2ca1210984f4ce46d78660857", + "native_binary_bytes": 559000 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "instance_bytes": 96, + "fixed_profile_sha256": "d1da839b23ccfd107c3b5f9c84113d9c9832237e07975d094a4e61bbc085861e", + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "8fc04a4b7a6b9d6480374e5ec71379b8afba085c1c18ef7fc31c09eec29cede3", + "native_binary_sha256": "b196d28fdeb4adc8f89c28c033bde88c48d922890e4f256ca94d552c7d387dcc", + "native_binary_bytes": 558472 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 88, + "fixed_profile_sha256": "73d69c68a84728b0189fcd1d77366cd7e86261e966c4074ec16e3e2b9ca59f73", + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "0b788b46d577755be3dcd0b6f8126fd58de579079f048d57a240b830ef42446c", + "native_binary_sha256": "0b1177946d226c9058daf4ac48f1838ae13a722743642143983eff23a31bcc8a", + "native_binary_bytes": 548696 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 88, + "fixed_profile_sha256": "0cd50260d50ab3dc787ebea64cc6a973e7f43906244d59110e28ba610f579fd6", + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e07c5dfc5050f6ab50371d342a239deccc2eb392b804630a4ea0e07f24b378c3", + "native_binary_sha256": "89833bfc5fe82a69bd75a2b9c70f2b78e138b2100cb9442fa3e1d699333e6714", + "native_binary_bytes": 558936 + } + ], + "units": { + "native_instances": 2, + "scalar_instance_bytes": 280, + "shared_program_bytes": 2480, + "checks": "empty-state-independent-state-identity-capacity-atomic-restore-signed-zero-reset" + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "a2ea7923975eeeb11d9222b91887489b374efd91f3f50b312b8d96deec076339", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "5656e5e402359b5f1b4bd14c1e73366ed5e32a253a7469a16f58dd8e88104d43", + "prototypes/workbench/native-fixed.zig": "15418d44f2f697b65770ea96d8e896de84905dec98af1074784323a51cef5a2b", + "prototypes/workbench/manage.py": "6c3aba1b7a6d2b42ce967a0de8a1a74ae19a9751464607e2a349c76347cb62f2", + "prototypes/workbench/native/scalar.h": "4a95675e08c4a3bd30f0bef123aeb7327f0cae58297b9b32fc1e7d2a208f1d1a", + "prototypes/workbench/native/plan.h": "98825678cb4e9beba4699f4f9679bb6d6637968b513639cac4173c052de206d2", + "prototypes/workbench/native/plan.c": "aa484bfe9072addb00e6eb87edcc11b290b97bd4e362fe9c12105aca8b807f69", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "94b732b7cd7a1f8beaf6759bf484fb794449f9d93ed4ea1c3a3186501f8a519a", + "prototypes/workbench/native/test.c": "47488a2cfda2b378637f69b8f0d94f75e615d784e8418167b5b82c8ce141f7e8", + "prototypes/workbench/native/test-fixed.c": "a6a967239c6510ebcba4ffbcf30a3a08644dea7117f4fe96d4f0e611bbbc17ef", + "prototypes/workbench/native/check.mjs": "4beac79764664bfa30ae5693a9063f60879221eafa2fb5b51329dc27bab7fec7", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "1dcdfeb56a8fbfb4b4da033c2af5d694699292253f18e0b7d71296f656e3bdda", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33084\t 0\t 0\t 33084\t 813c\tprototypes/workbench/build/cache/tmp/40c7a059de114231/general_zcu.o (ex /tmp/pure-native-parity-dY4Kg7/general.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/40c7a059de114231/general_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-24-shared-program/general-parity.json b/prototypes/workbench/results/2026-09-24-shared-program/general-parity.json new file mode 100644 index 0000000..eecb8bb --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/general-parity.json @@ -0,0 +1,209 @@ +{ + "kind": "native-instance-and-authored-behavior-correctness-not-performance", + "sanitized_c_host": true, + "profile": "general-authoring", + "general_instance_bytes": 20064, + "zig_mode": "ReleaseSafe; Zig object is not ASan-instrumented", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "cases": [ + { + "name": "counter", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# Space or the tap control increments a state that is displayed and drawn.\nstate count = 0\ninput tap = Space\ntext label(80, 90, count, \"Count: \")\nbox tile(80, 150, 40 + min(count, 20) * 8, 60, \"#356548\")\non tap.down {\n count = count + 1\n}\n", + "source_sha256": "53cb3a93ff0f26a3d208a5876bfff9ecb1c39e9a01acc101566727733ba166a8", + "prepared_sha256": "b7a39d8499bd2e638ce614c24263160c7a16a379c33e2f405d10b04bd99dd780", + "native_binary_sha256": "a171412f698f3dce575b7975b5c12c4e8f7842045cce2cb11c805539a496286f", + "native_binary_bytes": 592464 + }, + { + "name": "toggle", + "commands": 28, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# Space toggles the circle. Right arrow moves the tile.\nstate visible = 1\nstate position = 120\ninput toggle = Space\ninput move = ArrowRight\ncircle marker(position, 140, visible * 24, \"#c78638\")\nbox tile(position - 24, 190, 48, 48, \"#356548\")\ntext value(80, 80, visible, \"Visible: \")\non toggle.down {\n visible = !visible\n}\non move.down {\n position = position + 40\n if position > 560 {\n position = 80\n }\n}\n", + "source_sha256": "3de97fab9d316c17ba6a0c82a16cccd3baafc6816b879bf9b0a174210c8a1915", + "prepared_sha256": "d3c278f4eb8796e422ef03384373fab6ae517cb682c7432f057c2432c6f8e8e5", + "native_binary_sha256": "e79bb3b4b319e249244512befc5ce074212d6b88c4daca3cf99cce1f194afda9", + "native_binary_bytes": 592848 + }, + { + "name": "charge", + "commands": 104, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# All holding, charging, gravity, jumping, and floor behavior is authored here.\nstate holding = 0\nstate charge = 0\nstate y = 240\nstate velocity = 0\ninput jump = Space\nbox floor(40, 261, 560, 4, \"#356548\")\ncircle actor(180, y, 20, \"#c78638\")\nbox meter(80, 80, charge * 200, 12, \"#c78638\")\ntext amount(80, 60, charge, \"Charge: \")\non jump.down {\n if y >= 240 {\n holding = 1\n charge = 0\n }\n}\non jump.up {\n if holding {\n velocity = -(160 + charge * 300)\n holding = 0\n charge = 0\n }\n}\non frame {\n if holding {\n charge = clamp(charge + dt, 0, 1)\n }\n velocity = velocity + 600 * dt\n y = y + velocity * dt\n if y >= 240 {\n y = 240\n velocity = 0\n }\n}\n", + "source_sha256": "8d69d9fd51d44ab754d560eb7ff5102ea8a34540753cd7170b3bd800e4294f56", + "prepared_sha256": "99846fb777cb7b1b7289d3fb0662b8138ea7891371798e4e451355cbc629ef10", + "native_binary_sha256": "9036e33de79b1d16be4b97d8b41729054f54099f2d4445259274c4f048648d2c", + "native_binary_bytes": 592912 + }, + { + "name": "direct", + "commands": 3, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.\nstate x = 80\nstate y = 160\nstate grab_x = 0\nstate grab_y = 0\nstate start_x = 80\nstate start_y = 160\nstate dragging = 0\n\ninput drag = pointer(tile)\ninput left = ArrowLeft\ninput right = ArrowRight\n\nbox left_area(20, 100, 280, 220, \"#233a31\")\nbox right_area(340, 100, 280, 220, \"#343b25\")\nbox tile(x, y, 56, 44, \"#c9f08b\")\ntext horizontal(30, 45, x, \"x: \")\ntext vertical(180, 45, y, \"y: \")\ntext region(350, 70, x >= 320, \"오른쪽 영역: \")\ntext active(30, 75, dragging, \"잡고 있음: \")\n\non drag.down {\n start_x = x\n start_y = y\n grab_x = event_x - x\n grab_y = event_y - y\n dragging = 1\n}\non drag.move {\n x = clamp(event_x - grab_x, 20, 564)\n y = clamp(event_y - grab_y, 100, 276)\n}\non drag.up {\n # 놓기는 마지막으로 표시한 위치를 확정합니다.\n dragging = 0\n}\non drag.cancel {\n x = start_x\n y = start_y\n dragging = 0\n}\non left.down {\n if dragging == 0 { x = max(20, x - 10) }\n}\non right.down {\n if dragging == 0 { x = min(564, x + 10) }\n}\n", + "source_sha256": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a", + "prepared_sha256": "307257b3be2ec979114f85d9b8d37bc6309b4f6fd236bfcdf799f784af0b7644", + "native_binary_sha256": "2f95aa15b0d12ec4696e5bfb47f7675c32f02b864723b68e6f6f27d7ee8af869", + "native_binary_bytes": 593680 + }, + { + "name": "routed", + "commands": 8, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 하나의 포인터 입력과 네 반응을 모든 도형이 공유합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag = pointer {\n green_box: green\n blue_box: blue\n amber_box: amber\n}\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag.down use grab(drag.target)\non drag.move use move(drag.target)\non drag.up use drop(drag.target)\non drag.cancel use cancel(drag.target)\n\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "prepared_sha256": "b7524e53a5d90d1ce16d2e2fbaac67ceb242f1cd27f280afc3a80857ad8c21ba", + "native_binary_sha256": "c072b4c631710945d1eaffe3ed5fd13a5ebe5463c98042294f93b6f6d4a8db7c", + "native_binary_bytes": 594832 + }, + { + "name": "layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 세 도형을 배치하고 배치 저장으로 결과를 보관하세요.\n# 마지막으로 잡은 도형은 방향키로 10씩 이동합니다.\nstate selected = 0\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\nstate x1 = 80\nstate y1 = 120\nstate ox1 = 0\nstate oy1 = 0\nstate sx1 = 80\nstate sy1 = 120\nstate held1 = 0\ninput drag1 = pointer(tile1)\nstate x2 = 280\nstate y2 = 160\nstate ox2 = 0\nstate oy2 = 0\nstate sx2 = 280\nstate sy2 = 160\nstate held2 = 0\ninput drag2 = pointer(tile2)\nstate x3 = 450\nstate y3 = 220\nstate ox3 = 0\nstate oy3 = 0\nstate sx3 = 450\nstate sy3 = 220\nstate held3 = 0\ninput drag3 = pointer(tile3)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox tile1(x1,y1,56,44,\"#c9f08b\")\nbox tile2(x2,y2,56,44,\"#74c4d4\")\nbox tile3(x3,y3,56,44,\"#dfb184\")\non drag1.down {\n selected = 1\n sx1 = x1; sy1 = y1\n ox1 = event_x - x1; oy1 = event_y - y1\n held1 = 1\n}\non drag1.move {\n x1 = clamp(event_x - ox1, 20, 564)\n y1 = clamp(event_y - oy1, 90, 286)\n}\non drag1.up { held1 = 0 }\non drag1.cancel { x1 = sx1; y1 = sy1; held1 = 0 }\non drag2.down {\n selected = 2\n sx2 = x2; sy2 = y2\n ox2 = event_x - x2; oy2 = event_y - y2\n held2 = 1\n}\non drag2.move {\n x2 = clamp(event_x - ox2, 20, 564)\n y2 = clamp(event_y - oy2, 90, 286)\n}\non drag2.up { held2 = 0 }\non drag2.cancel { x2 = sx2; y2 = sy2; held2 = 0 }\non drag3.down {\n selected = 3\n sx3 = x3; sy3 = y3\n ox3 = event_x - x3; oy3 = event_y - y3\n held3 = 1\n}\non drag3.move {\n x3 = clamp(event_x - ox3, 20, 564)\n y3 = clamp(event_y - oy3, 90, 286)\n}\non drag3.up { held3 = 0 }\non drag3.cancel { x3 = sx3; y3 = sy3; held3 = 0 }\non left.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + -10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + -10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + -10, 20, 564) }\n}\non right.down {\n if selected == 1 && held1 == 0 { x1 = clamp(x1 + 10, 20, 564) }\n if selected == 2 && held2 == 0 { x2 = clamp(x2 + 10, 20, 564) }\n if selected == 3 && held3 == 0 { x3 = clamp(x3 + 10, 20, 564) }\n}\non up.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + -10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + -10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + -10, 90, 286) }\n}\non down.down {\n if selected == 1 && held1 == 0 { y1 = clamp(y1 + 10, 90, 286) }\n if selected == 2 && held2 == 0 { y2 = clamp(y2 + 10, 90, 286) }\n if selected == 3 && held3 == 0 { y3 = clamp(y3 + 10, 90, 286) }\n}\n", + "source_sha256": "cebd9c3485a2be4ec16ae665c1ed4ee200d51fc56ad0151306c8e9c31138f346", + "prepared_sha256": "95a9cb69007033e06f62e8cf7c7f6ea63d8afa85fe90b856666e1facbba7f2f4", + "native_binary_sha256": "63acfed8146f6b2421b82ca7fba44207873e309badfa15bed933a8802366c56b", + "native_binary_bytes": 594800 + }, + { + "name": "shared-layout", + "commands": 7, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 대상별 값은 item에, 공통 반응은 rule에 한 번만 작성합니다.\nstate selected = 0\nrecord Tile {\n x = 0; y = 0\n ox = 0; oy = 0\n sx = 0; sy = 0\n held = 0; id = 0\n}\nitem green = Tile(x=80, y=120, sx=80, sy=120, id=1)\nitem blue = Tile(x=280, y=160, sx=280, sy=160, id=2)\nitem amber = Tile(x=450, y=220, sx=450, sy=220, id=3)\ninput left = ArrowLeft\ninput right = ArrowRight\ninput up = ArrowUp\ninput down = ArrowDown\ninput drag_green = pointer(green_box)\ninput drag_blue = pointer(blue_box)\ninput drag_amber = pointer(amber_box)\nbox board(20,90,600,240,\"#233a31\")\ntext help(25,40,selected,\"선택한 도형: \")\nbox green_box(green.x,green.y,56,44,\"#c9f08b\")\nbox blue_box(blue.x,blue.y,56,44,\"#74c4d4\")\nbox amber_box(amber.x,amber.y,56,44,\"#dfb184\")\n\nrule grab(t: Tile) {\n selected = t.id\n t.sx = t.x; t.sy = t.y\n t.ox = event_x - t.x; t.oy = event_y - t.y\n t.held = 1\n}\nrule move(t: Tile) {\n t.x = clamp(event_x - t.ox, 20, 564)\n t.y = clamp(event_y - t.oy, 90, 286)\n}\nrule drop(t: Tile) { t.held = 0 }\nrule cancel(t: Tile) { t.x = t.sx; t.y = t.sy; t.held = 0 }\nrule go_left(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x - 10, 20, 564) }\n}\nrule go_right(t: Tile) {\n if selected == t.id && t.held == 0 { t.x = clamp(t.x + 10, 20, 564) }\n}\nrule go_up(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y - 10, 90, 286) }\n}\nrule go_down(t: Tile) {\n if selected == t.id && t.held == 0 { t.y = clamp(t.y + 10, 90, 286) }\n}\n\non drag_green.down use grab(green)\non drag_green.move use move(green)\non drag_green.up use drop(green)\non drag_green.cancel use cancel(green)\non left.down use go_left(green)\non right.down use go_right(green)\non up.down use go_up(green)\non down.down use go_down(green)\n\non drag_blue.down use grab(blue)\non drag_blue.move use move(blue)\non drag_blue.up use drop(blue)\non drag_blue.cancel use cancel(blue)\non left.down use go_left(blue)\non right.down use go_right(blue)\non up.down use go_up(blue)\non down.down use go_down(blue)\n\non drag_amber.down use grab(amber)\non drag_amber.move use move(amber)\non drag_amber.up use drop(amber)\non drag_amber.cancel use cancel(amber)\non left.down use go_left(amber)\non right.down use go_right(amber)\non up.down use go_up(amber)\non down.down use go_down(amber)\n", + "source_sha256": "be8296244b768f9adda99e7afc89249f34853fa27124a277aecb0841eb8176eb", + "prepared_sha256": "a0a3a7618e74a615cb2de5d66388a073e15015a1e9a737b8384c70e96fc77fd4", + "native_binary_sha256": "ad57a6507cb23f0a7a1429df8c47df4cd226e21d3f1f7ca4ce406d566f8440f2", + "native_binary_bytes": 594976 + }, + { + "name": "document", + "commands": 5, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "# 웹 컨트롤이 요청한 분할 비율을 받습니다. 레이아웃과 문서는 브라우저가 소유합니다.\nstate ratio = 50\ninput split = value\ninput reset = KeyR\non split.change { ratio = clamp(event_value, 20, 80) }\non reset.down { ratio = 50 }\n", + "source_sha256": "057d2e665d72dd3fce7ac59d232a65e9e0e1a6ca1f8c1e1b36ae766e480367d1", + "prepared_sha256": "905b28757ee3e93e69a1631cf88d9b2c63b70570f2484b9d8ca96367af15b5d6", + "native_binary_sha256": "3d31de6e4ef3c707f82165c1283edac64883e6487d898f0d3f76b0acced5ef67", + "native_binary_bytes": 592144 + }, + { + "name": "atomic-error", + "commands": 2, + "failed_events": 1, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,\"value: \")", + "source_sha256": "61a5478e7147ef281d617017910af36ad1bae3dabc97c03c8569c5d8b0e5fe19", + "prepared_sha256": "8fc04a4b7a6b9d6480374e5ec71379b8afba085c1c18ef7fc31c09eec29cede3", + "native_binary_sha256": "54b6a9b0c2f4d6814a6654be8d740ae95c1f4d941e0bb7720d9051e698f89544", + "native_binary_bytes": 592280 + }, + { + "name": "blank", + "commands": 0, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "", + "source_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "prepared_sha256": "0b788b46d577755be3dcd0b6f8126fd58de579079f048d57a240b830ef42446c", + "native_binary_sha256": "370e38fbb95d9fdcc34b302060321495669d068e4f674956b9c1ae6e6e57a9ca", + "native_binary_bytes": 591824 + }, + { + "name": "literal-boundary", + "commands": 1, + "failed_events": 0, + "states_and_views_match": true, + "instance_bytes": 20064, + "fixed_profile_sha256": null, + "fresh_process_restore": true, + "source": "state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,\"한글 \\\" \\\\ \\n\")", + "source_sha256": "7f480fa50bbddaf4e3a42bbd2132701e2091ef12b499e30c1bdda1dcd947dcc1", + "prepared_sha256": "e07c5dfc5050f6ab50371d342a239deccc2eb392b804630a4ea0e07f24b378c3", + "native_binary_sha256": "bbe8cef640977f1d42315f1a545c49a5f07b599e2ada602cbcd6c148464b6708", + "native_binary_bytes": 592224 + } + ], + "units": { + "native_instances": 3, + "checks": "independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload", + "scalar_instance_bytes": 20064, + "scalar_alignment": 8, + "app_controller_bytes": 928, + "action_bytes": 32, + "snapshot_bytes": 244, + "all_work_drained": true + }, + "source_sha256": { + "prototypes/workbench/core.zig": "ea237848df0dbd2ce620c2874e64bc5ecbe569bf77f3737e9a4fe1e64fea40cc", + "prototypes/workbench/engine.zig": "a2ea7923975eeeb11d9222b91887489b374efd91f3f50b312b8d96deec076339", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "5656e5e402359b5f1b4bd14c1e73366ed5e32a253a7469a16f58dd8e88104d43", + "prototypes/workbench/native-fixed.zig": "15418d44f2f697b65770ea96d8e896de84905dec98af1074784323a51cef5a2b", + "prototypes/workbench/manage.py": "6c3aba1b7a6d2b42ce967a0de8a1a74ae19a9751464607e2a349c76347cb62f2", + "prototypes/workbench/native/scalar.h": "4a95675e08c4a3bd30f0bef123aeb7327f0cae58297b9b32fc1e7d2a208f1d1a", + "prototypes/workbench/native/plan.h": "98825678cb4e9beba4699f4f9679bb6d6637968b513639cac4173c052de206d2", + "prototypes/workbench/native/plan.c": "aa484bfe9072addb00e6eb87edcc11b290b97bd4e362fe9c12105aca8b807f69", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/main.c": "dd28168741e79ffaa73cf74e60b974a1ca84b0473a43aafe089019f61e732aab", + "prototypes/workbench/native/prepare.mjs": "94b732b7cd7a1f8beaf6759bf484fb794449f9d93ed4ea1c3a3186501f8a519a", + "prototypes/workbench/native/test.c": "47488a2cfda2b378637f69b8f0d94f75e615d784e8418167b5b82c8ce141f7e8", + "prototypes/workbench/native/test-fixed.c": "f89cf711090ac5c152b6713d3afc15081d0d41d3bb78c54705ef400bde8b6131", + "prototypes/workbench/native/check.mjs": "4beac79764664bfa30ae5693a9063f60879221eafa2fb5b51329dc27bab7fec7", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/web/core.wasm": "e5187c44f8d2df952a85e4cf8102b71dbec6c43420893b3544a6f8514a1bff5c", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6" + }, + "scalar_archive_sha256": "766e7a761e36aaa4f7d5cc38605d5fe2f625f8db3e221901fcfeffa654ed42a1", + "scalar_archive_size": " text\t data\t bss\t dec\t hex\tfilename\n 33084\t 0\t 0\t 33084\t 813c\tprototypes/workbench/build/cache/tmp/62dc1b2e933cdcfa/general_zcu.o (ex /tmp/pure-native-parity-RFDTHK/general.a)\n", + "scalar_archive_undefined": "\nprototypes/workbench/build/cache/tmp/62dc1b2e933cdcfa/general_zcu.o:\n U memcpy\n U memset\n" +} diff --git a/prototypes/workbench/results/2026-09-24-shared-program/instance-memory.json b/prototypes/workbench/results/2026-09-24-shared-program/instance-memory.json new file mode 100644 index 0000000..91329ee --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/instance-memory.json @@ -0,0 +1,71 @@ +{ + "host": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "samples": { + "general": [ + { + "instances": 1000, + "instance_bytes": 20064, + "rss_before_kib": 1248, + "rss_after_kib": 21056, + "rss_delta_kib": 19808, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 20064, + "rss_before_kib": 1216, + "rss_after_kib": 21024, + "rss_delta_kib": 19808, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 20064, + "rss_before_kib": 1232, + "rss_after_kib": 21040, + "rss_delta_kib": 19808, + "checksum": 1000 + } + ], + "shared_fixed": [ + { + "instances": 1000, + "instance_bytes": 280, + "rss_before_kib": 1232, + "rss_after_kib": 1712, + "rss_delta_kib": 480, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 280, + "rss_before_kib": 1200, + "rss_after_kib": 1680, + "rss_delta_kib": 480, + "checksum": 1000 + }, + { + "instances": 1000, + "instance_bytes": 280, + "rss_before_kib": 1216, + "rss_after_kib": 1696, + "rss_delta_kib": 480, + "checksum": 1000 + } + ] + }, + "binary_sha256": { + "general": "b25a49909a01510d38c0c4fe125c7542b2dc343d460203f3c446ba278696acfe", + "shared_fixed": "3dddca197b168c404c308bef618a71020b5dece98c43c24c3fad1881e5401c46" + }, + "median_rss_delta_kib": { + "general": 19808, + "shared_fixed": 480 + }, + "zig_version": "0.15.2", + "build": { + "zig": "ReleaseSafe -fno-compiler-rt", + "c": "-std=c17 -O2 -g -Wall -Wextra -Wpedantic -Werror", + "fixed_c_define": "PURE_FIXED_DEFINITION=1" + } +} diff --git a/prototypes/workbench/results/2026-09-24-shared-program/peer-build.json b/prototypes/workbench/results/2026-09-24-shared-program/peer-build.json new file mode 100644 index 0000000..8499d96 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/peer-build.json @@ -0,0 +1,53 @@ +{ + "source": "/home/eunhhu/work/pure/prototypes/workbench/examples/routed-layout.pure", + "sourceSha256": "656e81577c250df704ba744b1ea6ec9c987ab3b8ee90c758062ac4beaf195650", + "zigVersion": "0.15.2", + "zigFlags": [ + "-O", + "ReleaseSafe", + "-fno-compiler-rt" + ], + "scalarProfile": "fixed-definition", + "zigRoot": "/home/eunhhu/work/pure/prototypes/workbench/native-fixed.zig", + "cFlags": [ + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-DPURE_FIXED_DEFINITION=1" + ], + "binary": "/home/eunhhu/work/pure/prototypes/workbench/build/native/pure-peer", + "binarySha256": "058eff07a94e0fefb655263acb804e07332fed0f685951d81be31b6b39642a22", + "librarySha256": "aeb0052b016704cd77e0f673c1e270df7a26816a0f03e4d778077db8c902423a", + "fixedProfileSha256": "4854304aae01c28b5e6fd43a7d16ff3b08ddd711108b4fdde6848eb5f5beaa92", + "preparedSha256": "b7524e53a5d90d1ce16d2e2fbaac67ceb242f1cd27f280afc3a80857ad8c21ba", + "desktop": false, + "desktopFlags": [], + "peer": true, + "peerFlags": [ + "-lssl", + "-lcrypto" + ], + "implementationSha256": { + "prototypes/workbench/engine.zig": "a2ea7923975eeeb11d9222b91887489b374efd91f3f50b312b8d96deec076339", + "prototypes/workbench/evaluation.zig": "ce04f0785180aaf6d1dcddc4461817251ad61abf1625ccd04ce673c89774b165", + "prototypes/workbench/native.zig": "5656e5e402359b5f1b4bd14c1e73366ed5e32a253a7469a16f58dd8e88104d43", + "prototypes/workbench/native-fixed.zig": "15418d44f2f697b65770ea96d8e896de84905dec98af1074784323a51cef5a2b", + "prototypes/workbench/native/prepare.mjs": "94b732b7cd7a1f8beaf6759bf484fb794449f9d93ed4ea1c3a3186501f8a519a", + "prototypes/workbench/native/scalar.h": "4a95675e08c4a3bd30f0bef123aeb7327f0cae58297b9b32fc1e7d2a208f1d1a", + "prototypes/workbench/native/plan.h": "98825678cb4e9beba4699f4f9679bb6d6637968b513639cac4173c052de206d2", + "prototypes/workbench/native/app.h": "32c738e06f007894457b6b4369ff75c9c2286737885a04159e257c1dcdfa3729", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/native/peer.c": "7d5865d499a4ba51f1d14e0d286480f605be2476616d28e1150b35ca1f87bf56", + "prototypes/workbench/native/peer-wire.c": "8fc3694db7a00a4f4899ca5a42dc2c0d96e39cb7d927c759ee8881901241733a", + "prototypes/workbench/native/app.c": "96ab7faf4848b5eaba5a1fb672eec2ea7882b3e554a4cba1adfac9c202a2b4ca", + "prototypes/workbench/native/plan.c": "aa484bfe9072addb00e6eb87edcc11b290b97bd4e362fe9c12105aca8b807f69", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f" + } +} diff --git a/prototypes/workbench/results/2026-09-24-shared-program/peer-check.json b/prototypes/workbench/results/2026-09-24-shared-program/peer-check.json new file mode 100644 index 0000000..4025aa6 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/peer-check.json @@ -0,0 +1 @@ +{"alpn": "pass", "disconnect_release": "pass", "idle_deadline": "pass", "malformed_frames": "pass", "mutual_authentication": "pass", "persist_restart": "pass", "python_client": "pass", "revision_conflict": "pass", "server_connected_rss_kib": 8448, "server_hostname": "pass", "server_idle_rss_kib": 7856} diff --git a/prototypes/workbench/results/2026-09-24-shared-program/size.txt b/prototypes/workbench/results/2026-09-24-shared-program/size.txt new file mode 100644 index 0000000..a815c80 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/size.txt @@ -0,0 +1,5 @@ + text data bss dec hex filename + 10396 0 0 10396 289c prototypes/workbench/build/cache/tmp/0d9c963faf84813d/libpure-scalar-fixed_zcu.o (ex prototypes/workbench/build/native/libpure-scalar-fixed.a) + 37135 2040 16 39191 9917 prototypes/workbench/build/native/pure-native + 50277 2472 16 52765 ce1d prototypes/workbench/build/native/pure-peer + 33084 0 0 33084 813c prototypes/workbench/build/cache/tmp/0bfbc00652c46618/pure-shared-general-scalar-final_zcu.o (ex /tmp/pure-shared-general-scalar-final.a) diff --git a/prototypes/workbench/results/2026-09-24-shared-program/stripped.txt b/prototypes/workbench/results/2026-09-24-shared-program/stripped.txt new file mode 100644 index 0000000..a63dc8e --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/stripped.txt @@ -0,0 +1,2 @@ +pure-native original=196688 stripped=68000 +pure-peer original=241416 stripped=68400 diff --git a/prototypes/workbench/results/2026-09-24-shared-program/symbols.txt b/prototypes/workbench/results/2026-09-24-shared-program/symbols.txt new file mode 100644 index 0000000..75dab69 --- /dev/null +++ b/prototypes/workbench/results/2026-09-24-shared-program/symbols.txt @@ -0,0 +1,4 @@ +pure-native +0000000000007348 00000000000009b0 r engine.shared_program +pure-peer +000000000000a1e8 00000000000009b0 r engine.shared_program diff --git a/prototypes/workbench/routing.test.mjs b/prototypes/workbench/routing.test.mjs new file mode 100644 index 0000000..bae04de --- /dev/null +++ b/prototypes/workbench/routing.test.mjs @@ -0,0 +1,114 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {spawnSync} from 'node:child_process'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const shared=await readFile(new URL('./examples/shared-layout.pure',import.meta.url),'utf8'); +const routed=await readFile(new URL('./examples/routed-layout.pure',import.meta.url),'utf8'); +const fresh=()=>createRuntime(wasm,{cacheEntries:32}); +const fixture=n=>`input drag=pointer {\n${Array.from({length:n},(_,i)=>`v${i}:c${i}`).join('\n')}\n}\non drag.move use move(drag.target)\nrule move(t:Cell) {t.x=event_x}\nrecord Cell {x=0}\n`+Array.from({length:n},(_,i)=>`item c${i}=Cell()\nbox v${i}(c${i}.x,0,10,10,"#123456")`).join('\n'); + +test('one input and one handler serve up to 24 static targets without instruction or handler expansion',async()=>{ + for(const n of [1,3,9,24]){ + const p=compile(fixture(n));assert.equal(p.inputs.length,1);assert.equal(p.handlers.length,1); + assert.equal(p.handlers[0].length,2);assert.equal(p.pointerTargets.length,n); + assert.equal(p.handlers[0].parameterBase,255); + const r=await fresh();r.apply(fixture(n)); + r.point('drag','down',0,0,'p',`v${n-1}`);r.point('drag','move',37,0,'p',`v${n-1}`);r.point('drag','up',0,0,'p',`v${n-1}`); + const s=r.snapshot();assert.equal(s.states[`c${n-1}.x`],37); + for(let i=0;i{ + const base=fixture(2); + for(const source of [ + base.replace('v0:c0','missing:c0'),base.replace('v0:c0','v0:missing'), + base.replace('v1:c1','v0:c1'),base.replace('v1:c1','v1:c0'), + base.replace('c1=Cell()','c1=Other()')+'\nrecord Other {x=0}', + base.replace('use move(drag.target)','use move(other.target)'), + base.replace('use move(drag.target)','use move(drag.missing)'), + base+'\ninput another=pointer(v0)', + base+'\ninput other=pointer { v2:c0 }\nbox v2(0,0,1,1,"#000000")', + 'input empty=pointer {}', + base.replace('on drag.move','on frame'), + base.replace('rule move(t:Cell)','rule move(t:Other)')+'\nrecord Other {x=0}', + ])assert.throws(()=>compile(source),source); +}); + +test('grouped layout matches explicit bindings through concurrent capture, partial cancel, keyboard and restore',async()=>{ + const a=await fresh(),b=await fresh();a.apply(shared);b.apply(routed); + const compare=()=>{const x=a.snapshot(),y=b.snapshot();assert.deepEqual(y.states,x.states);assert.deepEqual(y.views,x.views);assert.equal(y.lastError,x.lastError);}; + const point=(name,phase,x,y)=>{a.point(`drag_${name}`,phase,x,y,name);b.point('drag',phase,x,y,name,`${name}_box`);compare();}; + point('green','down',90,130);point('blue','down',290,170); + assert.equal(b.snapshot().held.length,2); + assert.throws(()=>b.apply(routed),/놓은/);assert.throws(()=>b.exportDocument(),/조작/); + assert.throws(()=>b.point('drag','down',0,0,'intruder','green_box'),/다른 포인터/); + b.point('drag','move',999,999,'intruder','green_box');compare(); + point('green','move',200,210);point('blue','move',500,270);point('green','cancel',200,210); + assert.equal(b.snapshot().held.length,1);point('blue','up',290,170); + for(const key of ['left','up','right','down']){a.down(key);b.down(key);a.up(key);b.up(key);compare();} + point('amber','down',460,230);point('amber','move',900,500);point('amber','up',460,230); + const c=await fresh();c.restoreDocument(JSON.parse(JSON.stringify(b.exportDocument()))); + assert.deepEqual(c.snapshot().states,b.snapshot().states); + for(const r of [b,c]){r.point('drag','down',570,290,'new','amber_box');r.point('drag','move',100,150,'new','amber_box');r.releaseAll();} + assert.deepEqual(c.snapshot().states,b.snapshot().states); + assert.throws(()=>b.point('drag','down',0,0),/도형/); + assert.throws(()=>b.point('drag','down',0,0,'p','board'),/도형/); + assert.throws(()=>b.down('drag'),/좌표/); +}); + +test('capture remains bound to its target and releaseAll cancels each target exactly once',async()=>{ + const r=await fresh();r.apply(routed+'\nstate cancelled=0\non drag.cancel {cancelled=cancelled+1}'); + r.point('drag','down',90,130,'a','green_box');r.point('drag','move',200,200,'a','green_box'); + r.point('drag','down',290,170,'b','blue_box');r.point('drag','move',400,250,'b','blue_box'); + r.point('drag','up',0,0,'a','blue_box');assert.equal(r.snapshot().held.length,2); + r.releaseAll();r.releaseAll();const s=r.snapshot();assert.equal(s.held.length,0); + assert.equal(s.states.cancelled,2);assert.equal(s.states['green.x'],80);assert.equal(s.states['blue.x'],280); +}); + +test('reorder and remove/readd rebuild routes by names without persisting physical indices',async()=>{ + const r=await fresh();r.apply(fixture(3)); + r.point('drag','down',0,0,'p','v2');r.point('drag','move',55,0,'p','v2');r.releaseAll(); + const reordered=fixture(3).replace('v0:c0\nv1:c1\nv2:c2','v2:c2\nv1:c1\nv0:c0').replace('item c0=Cell()','').replace('item c2=Cell()','item c2=Cell()\nitem c0=Cell()'); + r.apply(reordered);assert.equal(r.snapshot().states['c2.x'],55); + r.point('drag','down',0,0,'p','v2');r.point('drag','move',66,0,'p','v2');r.releaseAll(); + assert.equal(r.snapshot().states['c2.x'],66);assert.equal(r.snapshot().states['c0.x'],0); + const saved=r.exportDocument();const c=await fresh();c.restoreDocument(saved);assert.deepEqual(c.snapshot().states,r.snapshot().states); + r.apply(fixture(2));r.apply(fixture(3));assert.equal(r.snapshot().states['c2.x'],0); + r.point('drag','move',999,0,'p','v2');assert.equal(r.snapshot().states['c2.x'],0); +}); + +test('routed writes invalidate cached displays and failed handlers rollback without losing another target owner',async()=>{ + const source=fixture(2)+'\nstate total=0\non drag.move {total=total+1; total=1/event_y}'; + const r=await fresh();r.apply(source);r.snapshot();r.snapshot(); + r.point('drag','down',0,0,'a','v0');r.point('drag','down',0,0,'b','v1'); + assert.equal(r.point('drag','move',50,2,'a','v0'),0);assert.equal(r.snapshot().views[0].x,50); + assert.equal(r.point('drag','move',99,0,'b','v1'),4);let s=r.snapshot(); + assert.equal(s.states['c1.x'],0);assert.equal(s.states['c0.x'],50);assert.equal(s.states.total,0.5); + assert.equal(s.lastFailure.target,'v1');r.releaseAll();assert.equal(r.snapshot().lastFailure.status,4); + const previous=r.currentProgram;assert.throws(()=>r.apply(source.replace('v0:c0','v0:missing'))); + assert.equal(r.currentProgram,previous);r.reset();assert.equal(r.snapshot().held.length,0); +}); + +test('CLI sends grouped pointer events and rejects an unknown target',()=>{ + const path=new URL('./examples/routed-layout.pure',import.meta.url).pathname; + const run=input=>spawnSync(process.execPath,[new URL('./cli.mjs',import.meta.url).pathname,path],{input,encoding:'utf8'}); + const ok=run('point-target drag green_box down 90 130\npoint-target drag green_box move 210 220\npoint-target drag green_box up 90 130\nquit\n'); + assert.equal(ok.status,0,ok.stderr);const rows=ok.stdout.trim().split('\n').map(JSON.parse); + assert.equal(rows.at(-1).states['green.x'],200);assert.equal(rows.at(-1).states['blue.x'],280);assert.equal(rows.at(-1).held.length,0); + const bad=run('point-target drag missing down 0 0\nquit\n');assert.equal(bad.status,1);assert.match(bad.stderr,/도형/); +}); + + +test('a route from a different input group cannot be delivered through the named group',async()=>{ + const source=fixture(2).replace('v1:c1','')+'\ninput second=pointer {v1:c1}\non second.move use move(second.target)'; + const r=await fresh();r.apply(source); + assert.throws(()=>r.point('drag','down',0,0,'p','v1'),/도형/); + assert.throws(()=>r.point('second','down',0,0,'p','v0'),/도형/); + r.point('second','down',0,0,'p','v1');r.point('second','move',77,0,'p','v1');r.releaseAll(); + assert.equal(r.snapshot().states['c0.x'],0);assert.equal(r.snapshot().states['c1.x'],77); +}); diff --git a/prototypes/workbench/rules.test.mjs b/prototypes/workbench/rules.test.mjs new file mode 100644 index 0000000..b4c4969 --- /dev/null +++ b/prototypes/workbench/rules.test.mjs @@ -0,0 +1,94 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +import {parseProject} from './web/project.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const shared=await readFile(new URL('./examples/shared-layout.pure',import.meta.url),'utf8'); +const explicit=await readFile(new URL('./examples/layout.pure',import.meta.url),'utf8'); +const fresh=()=>createRuntime(wasm,{cacheEntries:32}); +const named=['green','blue','amber']; + +function simple(n){return 'record Cell { value=1 }\ninput tap=Space\nrule inc(t: Cell) { t.value=t.value+1 }\n'+Array.from({length:n},(_,i)=>`item cell${i}=Cell()\non tap.down use inc(cell${i})`).join('\n');} + +test('binding more objects shares one VM body instead of expanding its instructions',()=>{ + for(const n of [1,2,4,8]){ + const p=compile(simple(n)); + assert.equal(p.instructions.length,4);assert.equal(p.handlers.length,n); + assert.deepEqual(p.handlers.map(h=>h.start),Array(n).fill(0)); + assert.deepEqual(p.handlers.map(h=>[h.parameterBase,h.parameterCount]),Array.from({length:n},(_,i)=>[i,1])); + } + assert.equal(compile('record R { value=0 }\nrule unused(t:R) { t.value=t.value+1 }').instructions.length,0); +}); + +test('records and rules resolve forward names and reject invalid types fields and bindings',()=>{ + const p=compile('on tap.down use inc(a)\nitem a=R(v=4)\ninput tap=Space\nrule inc(t:R) { t.v=t.v+1 }\nrecord R { v=0 }'); + assert.equal(p.states[0].name,'a.v');assert.deepEqual([p.handlers[0].parameterBase,p.handlers[0].parameterCount],[0,1]); + for(const code of [ + 'record R {}','record R { x=0; x=1 }','record R { event_x=0 }', + 'item a=Missing()','record R {x=0}\nitem a=R(y=1)', + 'record R {x=0}\nitem a=R(x=1,x=2)','record R {x=0}\nstate a=0\nitem a=R()', + 'record R {x=0}\nrule bad(t:R) {t.missing=0}', + 'record R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {t.y=1}', + 'state value=0\nrecord R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {value=t.y}', + 'rule bad(t:Missing) {}','record R {x=0}\nitem a=R()\ninput k=Space\non k.down use missing(a)', + 'record R {x=0}\nrule r(t:R) {}\ninput k=Space\non k.down use r(missing)', + 'record R {x=0}\nrecord S {x=0}\nitem a=S()\nrule r(t:R) {}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_x}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_y}\non frame use r(a)', + 'record R {x=0}\nitem a=R()\ntext label(0,0,t.x)', + `record R { ${Array.from({length:9},(_,i)=>`f${i}=0`).join(';')} }`, + ]) assert.throws(()=>compile(code),code); + assert.throws(()=>compile(simple(33)),/32 states/); +}); + +test('shared layout matches the existing authored layout across independent and simultaneous gestures',async()=>{ + const a=await fresh(),b=await fresh();a.apply(explicit);b.apply(shared); + const compare=()=>{ + const old=a.snapshot(),next=b.snapshot();assert.equal(next.lastError,old.lastError); + assert.equal(next.states.selected,old.states.selected); + for(let i=1;i<=3;i++)for(const field of ['x','y','ox','oy','sx','sy','held'])assert.equal(next.states[`${named[i-1]}.${field}`],old.states[`${field}${i}`]); + for(let i=0;i{a.point(`drag${i}`,phase,x,y,`p${i}`);b.point(`drag_${named[i-1]}`,phase,x,y,`p${i}`);compare();}; + compare();point(1,'down',90,130);point(2,'down',290,170);point(1,'move',350,200);point(2,'move',500,300);point(2,'up',290,170);point(1,'cancel',350,200); + for(const key of ['left','right','up','down']){a.down(key);b.down(key);a.up(key);b.up(key);compare();} + point(3,'down',460,230);point(3,'move',900,500);point(3,'up',460,230); + const saved=JSON.parse(JSON.stringify(b.exportDocument()));const c=await fresh();c.restoreDocument(saved); + assert.deepEqual(c.snapshot().states,b.snapshot().states);assert.deepEqual(c.snapshot().views,b.snapshot().views); + c.down('left');c.up('left');assert.equal(c.snapshot().states['amber.x'],b.snapshot().states['amber.x']-10); +}); + +test('field names retain identity across schema and instance reorder, removal and restoration',async()=>{ + const r=await fresh(); + const source='record R { x=1;y=2 }\nitem a=R(x=10)\nitem b=R(x=20)\ninput k=Space\nrule step(t:R) {t.x=t.x+1}\non k.down use step(b)'; + r.apply(source);r.down('k');r.up('k'); + r.apply(source.replace('x=1;y=2','y=2;x=1').replace('item a=R(x=10)\nitem b=R(x=20)','item b=R(x=20)\nitem a=R(x=10)')); + r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],22);assert.equal(r.snapshot().states['a.x'],10); + r.apply(source.replace('item a=R(x=10)\n',''));r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); + const saved=r.exportDocument();r.apply(source);assert.equal(r.snapshot().states['a.x'],10); + r.restoreDocument(saved);assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); +}); + +test('bindings share one event transaction including global effects and multiple uses of one item',async()=>{ + const r=await fresh();r.apply('state total=0\nrecord R {x=8;divisor=2}\nitem a=R()\nitem b=R(divisor=0)\ninput k=Space\nrule divide(t:R) {total=total+1\nt.x=t.x/t.divisor}\non k.down use divide(a)\non k.down use divide(b)'); + r.down('k');assert.equal(r.snapshot().lastError,4);assert.equal(r.snapshot().states.total,0);assert.equal(r.snapshot().states['a.x'],8); + r.up('k');r.apply(simple(1)+'\non tap.down use inc(cell0)');r.down('tap');r.up('tap');assert.equal(r.snapshot().states['cell0.value'],3); +}); + +test('bound writes invalidate affected display caches and failed edits preserve active bindings',async()=>{ + const r=await fresh();const source=simple(2)+'\ntext first(0,0,cell0.value)\ntext second(0,0,cell1.value)';r.apply(source);r.snapshot();r.snapshot(); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[2,2]); + const before=r.snapshot();assert.throws(()=>r.apply(source.replace('inc(cell1)','inc(missing)')));assert.deepEqual(r.snapshot(),before); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[3,3]); +}); + +test('generated qualified names cannot make a valid exported document unreadable by its own loader',async()=>{ + const field='f'.repeat(18000); + const source=`record R { ${field}=1 }\n`+Array.from({length:32},(_,i)=>`item i${i}=R()`).join('\n'); + const r=await fresh();r.apply(source); + const saved=JSON.stringify(r.exportDocument('\u0001'.repeat(32768))); + r.restoreDocument(parseProject(saved)); + assert.equal(r.snapshot().states[`i31.${field}`],1); +}); diff --git a/prototypes/workbench/runtime.test.mjs b/prototypes/workbench/runtime.test.mjs new file mode 100644 index 0000000..86bc9ba --- /dev/null +++ b/prototypes/workbench/runtime.test.mjs @@ -0,0 +1,197 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const fresh = () => createRuntime(wasm); +const countSource = `state total = 3 +input add = KeyA +box bar(20, 80, total * 10, 30, "#abcdef") +text label(20, 60, total, "Total: ") +on add.down { total = total + 2 } +on add.up { total = total + 1 } +`; + +test('blank startup and applied empty source contain no application objects or behavior', async () => { + const runtime = await fresh(); + assert.equal(runtime.currentProgram, null); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + runtime.apply(''); + runtime.frame(60); + assert.deepEqual(runtime.snapshot().states, {}); + assert.deepEqual(runtime.snapshot().views, []); + assert.equal(runtime.snapshot().tick, 60); + assert.throws(() => runtime.down('jump'), /jump/); +}); + +test('author can add unrelated states inputs views and behavior entirely through source', async () => { + const runtime = await fresh(); + runtime.apply(countSource); + runtime.down('add'); runtime.up('add'); + assert.equal(runtime.snapshot().states.total, 6); + const added = `${countSource}\nstate radius = 12 +input inflate = KeyI +circle bubble(240, 160, radius, "#123456") +on inflate.down { radius = radius + total } +on frame { radius = radius + 60 * dt } +`; + runtime.apply(added); + runtime.down('inflate'); runtime.up('inflate'); runtime.frame(2); + assert.deepEqual(runtime.snapshot().states, { total: 6, radius: 20 }); + assert.equal(runtime.snapshot().views.find(view => view.id === 'bubble').radius, 20); + assert.equal(runtime.snapshot().views.length, 3); +}); + +test('keyboard and pointer sources aggregate first down and last up; repeats are ignored', async () => { + const runtime = await fresh(); runtime.apply(countSource); + runtime.down('add', 'keyboard'); runtime.down('add', 'keyboard'); runtime.down('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 5); + runtime.up('add', 'unknown'); runtime.up('add', 'keyboard'); + assert.equal(runtime.snapshot().states.total, 5); + assert.deepEqual(runtime.snapshot().held, ['add']); + runtime.releaseAll(); runtime.up('add', 'pointer'); + assert.equal(runtime.snapshot().states.total, 6); + assert.deepEqual(runtime.snapshot().held, []); + assert.equal(runtime.snapshot().successfulEvents, 2); +}); + +test('held input rejects apply without changing applied source, metadata, or values', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); + const before = runtime.snapshot(); + assert.throws(() => runtime.apply('state changed = 9'), /놓은 뒤/); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + runtime.up('add'); runtime.apply('state changed = 9'); + assert.deepEqual(runtime.snapshot().states, { changed: 9 }); +}); + +test('invalid syntax, unknown references and capacity overflow preserve the previous program', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (const source of ['state broken =', 'state n = 0\non frame { n = missing }', + Array.from({ length: 33 }, (_, i) => `state v${i} = ${i}`).join('\n'), '#'.repeat(32769)]) { + assert.throws(() => runtime.apply(source)); + assert.deepEqual(runtime.snapshot(), before); + assert.equal(runtime.exportProject().source, countSource); + } +}); + +test('state preservation follows names across reorder/add/remove; reset uses new initializers', async () => { + const runtime = await fresh(); + runtime.apply('state x = 11\nstate y = 22'); + runtime.apply('state z = 30\nstate y = 2\nstate x = 1'); + assert.deepEqual(runtime.snapshot().states, { z: 30, y: 22, x: 11 }); + runtime.apply('state y = 200\nstate renamed = 9'); + assert.deepEqual(runtime.snapshot().states, { y: 22, renamed: 9 }); + runtime.reset(); + assert.deepEqual(runtime.snapshot().states, { y: 200, renamed: 9 }); + assert.equal(runtime.snapshot().revision, 3); +}); + +test('callers cannot mutate active metadata and corrupt name-based state preservation', async () => { + const runtime = await fresh(); const program = runtime.apply('state x = 11\nstate y = 22'); + assert.throws(() => program.states.reverse(), TypeError); + assert.throws(() => { runtime.currentProgram.states[0].name = 'y'; }, TypeError); + runtime.apply('state x = 0\nstate y = 0'); + assert.deepEqual(runtime.snapshot().states, { x: 11, y: 22 }); +}); + +test('conditions execute once, and assignments and multiple handlers follow source order', async () => { + const runtime = await fresh(); + runtime.apply(`state x = 0 +state seen = 0 +on frame { + if x == 0 { + x = 1 + seen = x + 1 + } + x = x + 3 +} +on frame { seen = seen + x } +`); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 4, seen: 6 }); + runtime.frame(); + assert.deepEqual(runtime.snapshot().states, { x: 7, seen: 13 }); +}); + +test('a failed event rolls back all handlers and keeps its source diagnostic through release', async () => { + const runtime = await fresh(); + const source = `state x = 7 +state y = 9 +input breakit = Space +on breakit.down { x = 99 } +on breakit.down { + y = 2 + x = 1 / 0 +} +`; + runtime.apply(source); + assert.equal(runtime.down('breakit'), 4); + assert.deepEqual(runtime.snapshot().states, { x: 7, y: 9 }); + assert.equal(runtime.snapshot().successfulEvents, 0); + assert.equal(runtime.snapshot().errorLine, 7); + runtime.up('breakit'); runtime.frame(); + assert.equal(runtime.snapshot().lastEvent.status, 0); + assert.equal(runtime.snapshot().lastFailure.phase, 'down'); + assert.equal(runtime.snapshot().lastError, 4); + assert.equal(runtime.snapshot().errorLine, 7); + assert.throws(() => runtime.apply('bad')); + assert.equal(runtime.snapshot().lastError, 4); + runtime.apply(source.replace('1 / 0', '10')); + assert.equal(runtime.snapshot().lastError, 0); + runtime.down('breakit'); runtime.up('breakit'); + assert.deepEqual(runtime.snapshot().states, { x: 10, y: 2 }); +}); + +test('reset clears held inputs and latched errors; invalid frame counts have no effects', async () => { + const runtime = await fresh(); + runtime.apply('state x = 5\ninput tap = Space\non tap.down { x = 1 / 0 }'); + runtime.down('tap'); runtime.reset(); + const before = runtime.snapshot(); + assert.equal(before.lastError, 0); assert.equal(before.lastFailure, null); + assert.deepEqual(before.held, []); + for (const count of [-1, 601, 0.5, NaN, Infinity]) assert.throws(() => runtime.frame(count)); + assert.deepEqual(runtime.snapshot(), before); +}); + +test('render evaluation is pure and invalid binding is distinct from an event rollback', async () => { + const runtime = await fresh(); runtime.apply(countSource); runtime.down('add'); runtime.up('add'); + const before = runtime.snapshot(); + for (let i = 0; i < 100; i++) assert.deepEqual(runtime.snapshot(), before); + runtime.apply('state n = 2\ncircle bad(20, 20, -n, "#123456")\ntext fine(0, 0, n)'); + const result = runtime.snapshot(); + assert.equal(result.lastError, 100); assert.equal(result.errorLine, 2); + assert.deepEqual(result.states, { n: 2 }); assert.equal(result.views.length, 1); + assert.equal(result.lastFailure, null); + assert.deepEqual(runtime.snapshot(), result); +}); + +test('independent adapters replay the same author program without sharing live state', async () => { + const left = await fresh(), right = await fresh(); + left.apply(countSource); right.apply(countSource); + for (const runtime of [left, right]) { + runtime.down('add', 'keyboard'); runtime.frame(3); runtime.up('add', 'keyboard'); + } + assert.deepEqual(left.snapshot(), right.snapshot()); + left.down('add'); + assert.equal(right.snapshot().states.total, 6); + assert.equal(left.snapshot().states.total, 8); +}); + +test('Wasm has no imports and reports fixed memory and exact active IR/state storage', async () => { + const module = await WebAssembly.compile(wasm); + assert.deepEqual(WebAssembly.Module.imports(module), []); + const runtime = await fresh(); const program = runtime.apply(countSource); + const before = runtime.snapshot().memory; + assert.equal(before.linearBytes, 65536); + assert.equal(before.instructionBytes, 16); assert.equal(before.handlerBytes, 16); + assert.equal(before.activeIRBytes, program.instructions.length * 16 + program.handlers.length * 16); + assert.equal(before.activeStateBytes, program.states.length * 8); + assert(before.stagingReservedBytes < before.reservedEngineBytes); + assert(before.reservedEngineBytes < before.linearBytes); + for (let i = 0; i < 20; i++) runtime.frame(600); + assert.deepEqual(runtime.snapshot().memory, before); +}); diff --git a/prototypes/workbench/value-input.test.mjs b/prototypes/workbench/value-input.test.mjs new file mode 100644 index 0000000..3044048 --- /dev/null +++ b/prototypes/workbench/value-input.test.mjs @@ -0,0 +1,50 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {spawnSync} from 'node:child_process'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const source=await readFile(new URL('./examples/document.pure',import.meta.url),'utf8'); + +test('numeric controls use explicit change context and cannot read pointer or frame payloads',()=>{ + const p=compile(source);assert.equal(p.states.length,1);assert.equal(p.views.length,0);assert.equal(p.handlers[0].event,33); + for(const s of [ + 'state x=0\ninput n=value\non n.down {x=1}', + 'state x=0\ninput k=Space\non k.change {x=1}', + 'state x=0\ninput n=value\non n.change {x=event_x}', + 'state x=0\non frame {x=event_value}', + 'input n=value\ntext v(0,0,event_value)', + 'record R {x=0}\nitem a=R()\ninput k=Space\nrule r(t:R){t.x=event_value}\non k.down use r(a)', + 'state event_value=0', + ])assert.throws(()=>compile(s),s); +}); + +test('numeric input handles scalar rules, rollback, cache invalidation and signed zero',async()=>{ + const r=await createRuntime(wasm,{cacheEntries:32}); + const code='record R {x=10}\nitem a=R()\ninput n=value\nrule r(t:R){t.x=event_value}\non n.change use r(a)\ntext v(0,0,a.x)'; + r.apply(code);r.snapshot();r.snapshot();r.change('n',25);assert.equal(r.snapshot().views[0].value,25); + r.change('n',-0);assert(Object.is(r.snapshot().states['a.x'],-0));assert.deepEqual(r.snapshot().held,[]); + const before=r.snapshot();assert.throws(()=>r.change('n',Infinity));assert.throws(()=>r.down('n'));assert.throws(()=>r.point('n','down',1,2)); + assert.equal(r.snapshot().successfulEvents,before.successfulEvents); + r.apply(code+'\non n.change {a.x=1/0}');assert.equal(r.change('n',80),4);assert(Object.is(r.snapshot().states['a.x'],-0)); + assert.equal(r.snapshot().lastFailure.value,80);assert.throws(()=>r.exportDocument(),/오류/); + r.apply(code);const saved=r.exportDocument();const next=await createRuntime(wasm);next.restoreDocument(saved);next.change('n',32);assert.equal(next.snapshot().states['a.x'],32); +}); + +test('the document policy receives range, keyboard and pointer requests without layouts or text in VM state',async()=>{ + const r=await createRuntime(wasm);r.apply(source); + for(const [value,expected] of [[60,60],[1000,80],[-1000,20],[45.5,45.5]]){assert.equal(r.change('split',value,'control'),0);assert.deepEqual(r.snapshot().states,{ratio:expected});} + r.down('reset');r.up('reset');assert.deepEqual(r.snapshot().states,{ratio:50}); + assert.equal(r.snapshot().views.length,0);assert.equal(r.snapshot().memory.activeStateBytes,8); + const published=[];const target={begin(){},view(){assert.fail('no declared views');},value(){assert.fail('no view values');},state(i,v){published.push(v);},end(n){assert.equal(n,0);}}; + r.present(target);assert.deepEqual(published,[50]); + r.apply(source.replace('20, 80','30, 70'));r.change('split',10);assert.equal(r.snapshot().states.ratio,30); + const current=r.currentProgram;assert.throws(()=>r.apply(source+'\non split.down {ratio=0}'));assert.equal(r.currentProgram,current); +}); + +test('CLI supplies the same numeric control value and rejects nonfinite input',()=>{ + const run=input=>spawnSync(process.execPath,[new URL('./cli.mjs',import.meta.url).pathname,new URL('./examples/document.pure',import.meta.url).pathname],{input,encoding:'utf8'}); + const ok=run('change split 95\nquit\n');assert.equal(ok.status,0,ok.stderr);assert.equal(JSON.parse(ok.stdout.trim()).states.ratio,80); + const bad=run('change split NaN\nquit\n');assert.equal(bad.status,1);assert.match(bad.stderr,/유한/); +}); diff --git a/prototypes/workbench/web/app.js b/prototypes/workbench/web/app.js new file mode 100644 index 0000000..9e53ee3 --- /dev/null +++ b/prototypes/workbench/web/app.js @@ -0,0 +1,411 @@ +import { createRuntime } from './runtime.mjs'; +import { createOutput } from './output.mjs'; +import { bindSceneInput } from './scene-input.mjs'; +import { parseProject, MAX_PROJECT_BYTES } from './project.mjs'; + +const $ = id => document.getElementById(id); +const source = $('source'); +const MAX_SOURCE_BYTES = 32768; +const encoder = new TextEncoder(); +const output = createOutput(document); +const snippets = { + state: 'state count = 0\nstate x = 80\n', + input: 'input tap = Space\non tap.down {\n count = count + 1\n}\n', + view: 'box tile(x, 180, 40, 40, "#7ed9ae")\ntext label(40, 60, count, "Count: ")\n', + frame: 'on frame {\n x = x + 30 * dt\n if x > 600 { x = 0 }\n}\n', +}; +let runtime = null; +let appliedSource = ''; +let running = false; +let previousTime = null; +let accumulator = 0; +let droppedTicks = 0; +let diagnosticLocation = null; +let draftGeneration = 0; +const keyboardSources = new Map(); +const pointerSources = new Map(); +const inputButtons = new Map(); +const sceneInput = bindSceneInput($('scene'), () => runtime, safeAction); + +const bytes = text => encoder.encode(text).byteLength; +const dirty = () => source.value !== appliedSource; +// DOM is the published value. Avoid rewriting unchanged diagnostics/controls. +function text(id, value) { + const node = $(id); + if (node.textContent !== value) node.textContent = value; +} +function pressed(node, value) { + const next = String(value); + if (node.getAttribute('aria-pressed') !== next) node.setAttribute('aria-pressed', next); +} +function draftStatus() { + const changed = dirty(); + text('draft-status', changed ? '미적용 변경' : runtime?.currentProgram ? '적용됨' : '빈 작업물'); + $('draft-status').classList.toggle('dirty', changed); +} + +function setDiagnostic(message, kind = '', location = null) { + $('diagnostic-text').textContent = message; + $('diagnostic').className = `diagnostic ${kind}`; + diagnosticLocation = location; + $('error-location').hidden = !location; + source.setAttribute('aria-invalid', kind === 'error' && location ? 'true' : 'false'); +} + +function cursorPosition() { + const before = source.value.slice(0, source.selectionStart); + const lines = before.split('\n'); + text('cursor-position', `${lines.length}행 · ${lines.at(-1).length + 1}열`); +} + +function draftChanged() { + draftGeneration++; + // Read scroll position before writes; this path runs only for source edits. + const scrollTop = source.scrollTop; + const count = source.value.split('\n').length; + const size = bytes(source.value); + text('line-numbers', Array.from({ length: count }, (_, i) => i + 1).join('\n')); + $('line-numbers').scrollTop = scrollTop; + text('source-size', `${size.toLocaleString('en-US')} / 32,768 bytes`); + $('source-size').style.color = size > MAX_SOURCE_BYTES ? 'var(--danger)' : ''; + draftStatus(); + cursorPosition(); +} + +function pause() { + running = false; + previousTime = null; + accumulator = 0; + text('play', '▶ 실행'); + pressed($('play'), false); +} + +function safeAction(action) { + if (!runtime) return; + try { action(); } + catch (error) { pause(); setDiagnostic(error.message, 'error', error.line ? { line: error.line, column: error.column || 1 } : null); } + refresh(); +} + +function releaseEverything() { + keyboardSources.clear(); + pointerSources.clear(); + sceneInput.releaseCaptures(); + if (runtime) safeAction(() => runtime.releaseAll()); +} + +function loseFocus() { + pause(); + releaseEverything(); + refresh(); +} + +function refresh() { + if (!runtime) return; + const snapshot = runtime.present(output); + if (snapshot.lastError) pause(); + const held = new Set((snapshot.held || []).map(input => typeof input === 'string' ? input : input.name)); + for (const [name, button] of inputButtons) { + button.classList.toggle('held', held.has(name)); + pressed(button, held.has(name)); + } + text('scene-status', runtime.currentProgram ? (running ? '실행 중' : '정지') : '적용 전'); + text('clock-status', `${snapshot.tick || 0} frame · ${running ? '60 Hz' : '정지'}`); + text('revision', `revision ${snapshot.revision || 0}`); + for (const id of ['play', 'step', 'reset', 'download-layout']) { + if ($(id).disabled !== !runtime.currentProgram) $(id).disabled = !runtime.currentProgram; + } + const event = snapshot.lastEvent; + text('last-event', event ? `${event.name}${event.phase && event.phase !== 'frame' ? `.${event.phase}` : ''}${event.target ? ` → ${event.target}` : ''} · ${event.status ? '실패' : '완료'}${event.line ? ` · ${event.line}행` : ''}` : '아직 없음'); + if ($('runtime-error').hidden !== !snapshot.lastError) $('runtime-error').hidden = !snapshot.lastError; + const displayError = snapshot.lastError === 100 || snapshot.lastError === 101; + let message = snapshot.lastError ? `${displayError ? '표시 오류' : '마지막 실패'} · ${snapshot.errorLine ? `${snapshot.errorLine}행 · ` : ''}${snapshot.errorMessage || `실행 오류 ${snapshot.lastError}`}${snapshot.lastError === 101 ? ' — 이번 장면을 표시하지 않았습니다. 상태 변경은 유지됩니다.' : displayError ? ' — 해당 도형을 표시할 수 없습니다.' : ' — 실패한 이벤트의 상태 변경은 취소됐습니다. 코드를 적용하거나 초깃값으로 되돌리면 이 기록을 지웁니다.'}` : ''; + if (!displayError && snapshot.displayError?.code === 101) + message += ` · ${snapshot.displayError.message}`; + text('runtime-error', message); + output.costs(snapshot, runtime.currentProgram?.metrics, droppedTicks); +} + +function endPointer(event) { + const entry = pointerSources.get(event.pointerId); + if (!entry) return; + event.preventDefault(); + pointerSources.delete(event.pointerId); + safeAction(() => runtime.up(entry.name, entry.source)); +} + +function rebuildInputControls() { + inputButtons.clear(); + const children = []; + for (const input of runtime.currentProgram?.inputs || []) { + if (input.kind === 'value') { + const form = document.createElement('form'), label = document.createElement('label'); + const field = document.createElement('input'), button = document.createElement('button'); + label.textContent = input.name + ' '; field.type = 'number'; field.step = 'any'; field.value = '0'; field.required = true; + button.type = 'submit'; button.textContent = '값 전달'; label.append(field); form.append(label, button); + form.addEventListener('submit', event => { event.preventDefault(); safeAction(() => runtime.change(input.name, field.valueAsNumber, 'control')); }); + children.push(form); continue; + } + if (input.target || input.routes) { + const note = document.createElement('p'); + note.className = 'empty-note'; + note.textContent = `${input.name} · 장면의 ${input.target || input.routes.map(route => route.target).join(', ')}을 잡아 조작하세요.`; + children.push(note); + continue; + } + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'input-button'; + button.dataset.input = input.name; + button.setAttribute('aria-pressed', 'false'); + const name = document.createElement('span'); name.textContent = input.name; + const key = document.createElement('kbd'); key.textContent = input.key; + button.append(name, key); + button.addEventListener('pointerdown', event => { + if (event.button !== 0) return; + event.preventDefault(); + const identity = `pointer:${event.pointerId}`; + pointerSources.set(event.pointerId, { name: input.name, source: identity }); + button.setPointerCapture(event.pointerId); + safeAction(() => runtime.down(input.name, identity)); + }); + button.addEventListener('pointerup', endPointer); + button.addEventListener('pointercancel', endPointer); + button.addEventListener('lostpointercapture', endPointer); + // Keyboard activation and assistive technology clicks are complete taps. + // Pointer clicks already delivered down/up via the handlers above. + button.addEventListener('click', event => { + if (event.detail !== 0) return; + safeAction(() => { runtime.down(input.name, 'activation'); runtime.up(input.name, 'activation'); }); + }); + inputButtons.set(input.name, button); + children.push(button); + } + if (!children.length) { + const message = document.createElement('p'); + message.className = 'empty-note'; + message.textContent = '입력을 선언하면 여기에 버튼이 생깁니다.'; + children.push(message); + } + $('input-controls').replaceChildren(...children); +} + +function isEditing(target) { + return target instanceof Element && Boolean(target.closest('textarea,input,select,button,[contenteditable="true"],[role="textbox"]')); +} + +$('scene').addEventListener('pointerdown', () => $('scene').focus()); + +document.addEventListener('keydown', event => { + if (!runtime || event.repeat || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || isEditing(event.target)) return; + const input = runtime.currentProgram?.inputs.find(item => item.key === event.code); + if (!input) return; + event.preventDefault(); + if (keyboardSources.has(event.code)) return; + keyboardSources.set(event.code, input.name); + safeAction(() => runtime.down(input.name, `keyboard:${event.code}`)); +}); + +document.addEventListener('keyup', event => { + const name = keyboardSources.get(event.code); + if (!name) return; + event.preventDefault(); + keyboardSources.delete(event.code); + safeAction(() => runtime.up(name, `keyboard:${event.code}`)); +}); + +source.addEventListener('input', () => { + draftChanged(); + if (diagnosticLocation) setDiagnostic('코드를 수정했습니다. 적용해서 다시 확인하세요.'); +}); +source.addEventListener('scroll', () => { $('line-numbers').scrollTop = source.scrollTop; }); +for (const name of ['click', 'keyup', 'select']) source.addEventListener(name, cursorPosition); +source.addEventListener('keydown', event => { + if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); + source.setRangeText(' ', source.selectionStart, source.selectionEnd, 'end'); + draftChanged(); + } +}); + +$('apply').addEventListener('click', () => { + if (!runtime) return; + try { + runtime.apply(source.value); + appliedSource = source.value; + rebuildInputControls(); + setDiagnostic('코드를 적용했습니다. 같은 이름의 상태는 유지됩니다.', 'success'); + } catch (error) { + const location = error.line ? { line: error.line, column: error.column || 1 } : null; + setDiagnostic(`${location ? `${location.line}행 ${location.column}열 · ` : ''}${error.message} · 기존 장면은 그대로 유지됩니다.`, 'error', location); + } + draftStatus(); + refresh(); +}); + +$('error-location').addEventListener('click', () => { + if (!diagnosticLocation) return; + const lines = source.value.split('\n'); + const lineIndex = Math.min(lines.length - 1, diagnosticLocation.line - 1); + const start = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0); + const at = start + Math.min(lines[lineIndex].length, diagnosticLocation.column - 1); + source.focus(); + source.setSelectionRange(at, Math.min(at + 1, source.value.length)); + const lineHeight = parseFloat(getComputedStyle(source).lineHeight); + source.scrollTop = Math.max(0, lineIndex * lineHeight - source.clientHeight / 3); + $('line-numbers').scrollTop = source.scrollTop; + cursorPosition(); +}); + +$('play').addEventListener('click', () => { + if (running) pause(); + else { running = true; previousTime = null; accumulator = 0; $('play').textContent = 'Ⅱ 정지'; $('play').setAttribute('aria-pressed', 'true'); } + refresh(); +}); +$('step').addEventListener('click', () => { pause(); safeAction(() => runtime.frame()); }); +$('reset').addEventListener('click', () => { + pause(); releaseEverything(); droppedTicks = 0; + safeAction(() => runtime.reset()); + setDiagnostic('적용된 코드의 초깃값으로 되돌렸습니다. 편집 중인 코드는 유지됩니다.'); +}); + +function replaceDraft(text, message) { + source.value = text; + source.scrollTop = 0; + source.setSelectionRange(0, 0); + setDiagnostic(message); + draftChanged(); +} + +function mayReplaceDraft() { + return !source.value || window.confirm('현재 편집 중인 코드를 바꿀까요? 저장하지 않은 내용은 사라집니다. 실행 장면은 적용 전까지 유지됩니다.'); +} + +$('new').addEventListener('click', () => { + if (mayReplaceDraft()) replaceDraft('', '편집기만 비웠습니다. 빈 코드를 적용하면 장면도 비워집니다.'); +}); + +for (const button of document.querySelectorAll('.insert-snippet')) { + button.addEventListener('click', () => { + const at = source.selectionStart; + const prefix = at > 0 && source.value[at - 1] !== '\n' ? '\n' : ''; + source.setRangeText(prefix + snippets[button.dataset.snippet], source.selectionStart, source.selectionEnd, 'end'); + source.focus(); + draftChanged(); + setDiagnostic('조각을 삽입했습니다. 이름과 동작을 원하는 대로 바꾸고 적용하세요.'); + }); +} + +$('load-example').addEventListener('click', async () => { + if (!mayReplaceDraft()) return; + const selected = $('example').value; + const before = source.value; + const button = $('load-example'); + button.disabled = true; + try { + const response = await fetch(`./examples/${selected}.pure`); + if (!response.ok) throw new Error(`예제를 불러오지 못했습니다 (${response.status}).`); + const text = await response.text(); + if (bytes(text) > MAX_SOURCE_BYTES) throw new Error('예제가 코드 크기 제한을 넘었습니다.'); + if (source.value !== before) throw new Error('불러오는 동안 코드가 바뀌었습니다. 변경 내용을 지키기 위해 불러오기를 취소했습니다.'); + replaceDraft(text, '예제를 편집기에 불러왔습니다. 아직 적용하지 않았습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } + finally { button.disabled = false; } +}); + +function download(filename, content, mime, message = '편집 중인 코드를 파일로 저장했습니다. 적용되지 않은 변경도 포함합니다.') { + const url = URL.createObjectURL(new Blob([content], { type: mime })); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + setDiagnostic(message); +} +$('download').addEventListener('click', () => download('my-work.pure', source.value, 'text/plain;charset=utf-8')); +$('download-project').addEventListener('click', () => download('my-work.pure.json', JSON.stringify({ format: 'pure-workbench', version: 1, source: source.value }, null, 2) + '\n', 'application/json')); +$('download-layout').addEventListener('click', () => { + try { + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 저장하세요.'); + const project = runtime.exportDocument(source.value); + download('my-layout.pure.json', JSON.stringify(project, null, 2) + '\n', 'application/json', + '적용된 코드·현재 배치·미적용 초안을 함께 저장했습니다.'); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); +$('upload').addEventListener('click', () => $('file-input').click()); +$('file-input').addEventListener('change', async event => { + const file = event.target.files[0]; + event.target.value = ''; + if (!file) return; + try { + if (file.size > MAX_PROJECT_BYTES) throw new Error('파일이 너무 큽니다. 코드 제한은 32 KiB입니다.'); + const before = draftGeneration; + const active = runtime?.currentProgram; + const events = runtime?.snapshot().successfulEvents; + let content = await file.text(); + if (file.name.toLowerCase().endsWith('.json')) { + const project = parseProject(content); + if (project.version === 2) { + if (!runtime) throw new Error('실행기가 준비되지 않았습니다.'); + if (draftGeneration !== before || runtime.currentProgram !== active || runtime.snapshot().successfulEvents !== events) + throw new Error('파일을 읽는 동안 작업이 바뀌었습니다. 정지한 뒤 다시 불러오세요.'); + if (running) throw new Error('시간 진행을 정지한 뒤 배치를 복원하세요.'); + if ((source.value || appliedSource) && !window.confirm('현재 코드와 배치를 파일의 내용으로 복원할까요? 저장하지 않은 작업은 사라집니다.')) return; + const restored = runtime.restoreDocument(project); + pause(); droppedTicks = 0; + appliedSource = restored.source; + rebuildInputControls(); + replaceDraft(restored.draft, '배치를 복원했습니다. 미적용 초안도 보존했고, 시간은 정지 상태입니다.'); + refresh(); + return; + } + content = project.source; + } + if (bytes(content) > MAX_SOURCE_BYTES) throw new Error('코드가 32 KiB 제한을 넘었습니다.'); + if (draftGeneration !== before) throw new Error('파일을 읽는 동안 코드가 바뀌었습니다. 불러오기를 다시 시도하세요.'); + if (mayReplaceDraft()) replaceDraft(content, `${file.name}을 편집기에 불러왔습니다. 아직 적용하지 않았습니다.`); + } catch (error) { setDiagnostic(error.message, 'error'); } +}); + +window.addEventListener('blur', loseFocus); +document.addEventListener('visibilitychange', () => { if (document.hidden) loseFocus(); }); +window.addEventListener('beforeunload', event => { + // Applying is not persistence: even applied code can be lost when leaving. + if (source.value || appliedSource) { event.preventDefault(); event.returnValue = ''; } +}); + +function animate(time) { + if (running && runtime) { + if (previousTime !== null) { + accumulator += Math.max(0, time - previousTime); + const wanted = Math.floor(accumulator / (1000 / 60)); + if (wanted > 0) { + const steps = Math.min(6, wanted); + accumulator -= wanted * (1000 / 60); + droppedTicks += wanted - steps; + safeAction(() => runtime.frame(steps)); + } + } + previousTime = running ? time : null; + } + requestAnimationFrame(animate); +} + +Object.defineProperty(window, 'pureWorkbench', { + configurable: false, + writable: false, + value: Object.freeze({ snapshot: () => structuredClone({ ...(runtime?.snapshot() || {}), ready: Boolean(runtime), source: source.value, appliedSource, dirty: dirty(), running, droppedTicks, diagnostic: $('diagnostic-text').textContent }) }), +}); + +draftChanged(); +requestAnimationFrame(animate); +try { + runtime = await createRuntime(); + $('ready-status').textContent = '실행기 준비됨'; + $('ready-dot').classList.add('ready'); + $('apply').disabled = false; + refresh(); +} catch (error) { + $('ready-status').textContent = '실행기 준비 실패'; + $('ready-dot').classList.add('failed'); + setDiagnostic(`실행기를 열지 못했습니다. ${error.message}`, 'error'); +} diff --git a/prototypes/workbench/web/baseline.html b/prototypes/workbench/web/baseline.html new file mode 100644 index 0000000..cc9f569 --- /dev/null +++ b/prototypes/workbench/web/baseline.html @@ -0,0 +1,108 @@ + + + + +Pure 비교 실험 · 일반 JavaScript + +← 빈 작업물 제작기로 +

일반 JavaScript로 작성한 충전 점프

+

동일한 충전·점프 동작을 직접 작성한 비교용 앱입니다. 실행을 켜고 Space 또는 아래 버튼을 누르다가 놓아보세요. 정지 중에는 한 프레임씩 진행할 수 있습니다.

+ + + + + Charge: 0 + +
+ + + + + 정지 · 0 프레임 +
+

+

이 앱은 고정된 JavaScript 프로그램입니다. 코드를 편집하고 다시 불러와 동작을 바꿀 수 있지만, 제작기의 정의 적용·상태 보존 기능은 구현하지 않았습니다. 아래 파일과 충전 예제를 비교할 수 있습니다.

+

행동과 상태 소스 · baseline.mjs · 동일 동작의 Pure 정의 · charge.pure

+ + diff --git a/prototypes/workbench/web/baseline.mjs b/prototypes/workbench/web/baseline.mjs new file mode 100644 index 0000000..3873e31 --- /dev/null +++ b/prototypes/workbench/web/baseline.mjs @@ -0,0 +1,68 @@ +// Handwritten JavaScript reference for examples/charge.pure. +// This fixed app has no source compiler, live apply, or state-name migration. +// Its scene and behavior must be changed by editing this JavaScript file. +export function createChargeModel() { + const held = new Set(); + let holding = 0, charge = 0, y = 240, velocity = 0, tick = 0; + const checkInput = name => { + if (name !== 'jump') throw new Error(`Unknown input: ${name}`); + }; + + const model = { + down(name = 'jump', source = 'external') { + checkInput(name); + const wasDown = held.size > 0; + held.add(source); + if (!wasDown && y >= 240) { + holding = 1; + charge = 0; + } + return 0; + }, + up(name = 'jump', source = 'external') { + checkInput(name); + if (!held.delete(source) || held.size) return 0; + if (holding) { + velocity = -(160 + charge * 300); + holding = 0; + charge = 0; + } + return 0; + }, + frame(count = 1) { + if (!Number.isInteger(count) || count < 0 || count > 600) { + throw new Error('Frame count must be an integer from 0 through 600.'); + } + const dt = 1 / 60; + for (let frame = 0; frame < count; frame++) { + if (holding) charge = Math.min(Math.max(charge + dt, 0), 1); + velocity = velocity + 600 * dt; + y = y + velocity * dt; + if (y >= 240) { y = 240; velocity = 0; } + tick++; + } + return 0; + }, + releaseAll() { + for (const source of [...held]) model.up('jump', source); + return 0; + }, + reset() { + held.clear(); + holding = 0; charge = 0; y = 240; velocity = 0; tick = 0; + }, + snapshot() { + return { + states: { holding, charge, y, velocity }, tick, + held: held.size ? ['jump'] : [], + views: [ + { id: 'floor', type: 'box', x: 40, y: 261, width: 560, height: 4, color: '#356548' }, + { id: 'actor', type: 'circle', x: 180, y, radius: 20, color: '#c78638' }, + { id: 'meter', type: 'box', x: 80, y: 80, width: charge * 200, height: 12, color: '#c78638' }, + { id: 'amount', type: 'text', x: 80, y: 60, value: charge, prefix: 'Charge: ' }, + ], + }; + }, + }; + return Object.freeze(model); +} diff --git a/prototypes/workbench/web/compiler.mjs b/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/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/workbench/web/core.wasm b/prototypes/workbench/web/core.wasm new file mode 100755 index 0000000..7aa443c Binary files /dev/null and b/prototypes/workbench/web/core.wasm differ diff --git a/prototypes/workbench/web/document.css b/prototypes/workbench/web/document.css new file mode 100644 index 0000000..7e7ed97 --- /dev/null +++ b/prototypes/workbench/web/document.css @@ -0,0 +1,3 @@ +:root{color-scheme:dark;--bg:#101815;--panel:#19241e;--line:#35483c;--text:#e5eade;--muted:#a6b3a7;--accent:#c9f08b;font-family:system-ui,-apple-system,sans-serif;color:var(--text);background:var(--bg)} +*{box-sizing:border-box}body{margin:0}header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding:22px max(24px,calc((100vw - 1500px)/2))}.brand{font-size:22px;font-weight:700;letter-spacing:-1px;text-decoration:none;color:var(--accent)}.brand span{font-size:14px;color:var(--muted);font-weight:400;letter-spacing:0}.badge,.eyebrow{color:var(--muted);font-size:12px;letter-spacing:.1em}main{max-width:1550px;margin:auto;padding:28px 24px}.intro{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:25px}h1{font-size:clamp(23px,3vw,34px);font-weight:550;letter-spacing:-.04em;word-break:keep-all;margin:8px 0}.intro p:not(.eyebrow){color:var(--muted);font-size:14px;line-height:1.65;margin-bottom:0}button,input,textarea{font:inherit;color:inherit}button{background:#273a2e;border:1px solid #456049;padding:9px 14px;border-radius:7px;cursor:pointer;white-space:nowrap}button:hover{background:#334d3d}button:disabled{opacity:.45;cursor:default}input:not([type=range]){background:var(--bg);border:1px solid var(--line);border-radius:5px;padding:8px;max-width:180px}button:focus-visible,input:focus-visible,textarea:focus-visible,[tabindex]:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;background:var(--panel);padding:14px;border:1px solid var(--line);border-radius:9px 9px 0 0}.toolbar label{font-size:13px;color:var(--muted)}#document-status{font-size:12px;color:var(--muted);margin-left:auto;max-width:100%;overflow-wrap:anywhere}.layout-tools{display:flex;align-items:center;gap:12px;padding:15px 0;font-size:13px}.layout-tools input{width:145px;accent-color:var(--accent)}.layout-tools output{min-width:42px;font-variant-numeric:tabular-nums}.layout-tools button{padding:5px 10px}.hint{color:var(--muted);font-size:12px;margin-left:auto}.panes{display:grid;grid-template-columns:minmax(0,calc((100% - 18px)*var(--split))) 18px minmax(0,1fr);height:clamp(400px,65vh,850px)}.pane{min-width:0;min-height:0;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:8px;overflow:hidden}.pane-heading{display:flex;align-items:center;justify-content:space-between;padding:13px 18px;border-bottom:1px solid var(--line);flex:0 0 auto}.pane-heading h2{font-size:14px;font-weight:550;margin:0}.pane-heading span{font-size:10px;letter-spacing:.12em;color:var(--muted)}#document-text{width:100%;flex:1;min-height:0;resize:none;background:transparent;border:0;padding:22px;font:15px/1.8 ui-monospace,monospace;outline-offset:-3px;tab-size:4}#document-text::placeholder{color:#84998b}#reading-scroll{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;outline-offset:-3px;padding:26px clamp(18px,3vw,42px);font-size:17px;line-height:1.95;background:#e5e8dc;color:#26352d}#reading-text{white-space:pre-wrap;overflow-wrap:anywhere}#reading-empty{color:#637566;margin:0}#divider{cursor:col-resize;touch-action:none;display:flex;align-items:center;justify-content:center;outline-offset:-2px}#divider span{height:44px;width:4px;background:#617768;border-radius:4px}#divider:hover span,#divider.dragging span,#divider:focus-visible span{background:var(--accent)}#divider[aria-disabled=true]{pointer-events:none}.policy{margin-top:22px;border:1px solid var(--line);padding:14px 18px;border-radius:8px}.policy summary{cursor:pointer;color:var(--muted);font-size:13px}.policy p{font-size:13px;color:var(--muted);line-height:1.7}.policy textarea{display:block;width:100%;height:160px;resize:vertical;background:#121b16;border:1px solid var(--line);border-radius:5px;font:13px/1.65 ui-monospace,monospace;padding:12px;margin:12px 0}.policy span{font-size:12px;margin-left:12px;color:var(--muted)}footer{font-size:12px;color:var(--muted);line-height:1.7;padding:22px 0}a{color:var(--accent)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important} +@media(max-width:720px){header{padding:18px}.badge{display:none}main{padding:20px 16px}.intro{align-items:flex-start}.intro button{font-size:12px;padding:8px}.toolbar input:not([type=range]){max-width:155px}.toolbar label{width:100%}#document-status{width:100%;margin:2px 0}.layout-tools{display:none}.panes{display:flex;flex-direction:column;height:auto;gap:18px}.pane{height:430px}.read-pane{height:500px}#divider{display:none}.policy span{display:block;margin:10px 0 0}.hint{display:none}#reading-scroll{padding:22px}} diff --git a/prototypes/workbench/web/document.html b/prototypes/workbench/web/document.html new file mode 100644 index 0000000..7e96674 --- /dev/null +++ b/prototypes/workbench/web/document.html @@ -0,0 +1,32 @@ + + + + + + Pure · 문서 작업 + + +
pure / 문서 작업텍스트 워크스페이스
+
+

WRITE / READ

쓰고, 읽고, 공간을 조절하세요.

가운데 경계를 움직여 편한 폭을 만드세요. 좁은 화면에서는 위아래로 이어집니다.

+
+ + + + UTF-8 텍스트 파일을 열거나 바로 작성하세요. +
+
+ 50% + 경계에서 ← → · Shift로 크게 · Esc로 취소 +
+
+

작성

TEXT
+ +

읽기

PREVIEW

작성한 내용이 여기에 나타납니다.

+
+
분할 규칙 수정

비율을 받는 규칙을 수정할 수 있습니다. 문서 내용은 유지됩니다. 숫자 입력 이름은 split, 결과 상태는 ratio입니다.

실행기를 준비하고 있습니다.
+
텍스트는 일반 .txt 파일로 저장됩니다. 줄바꿈과 글자 배치는 브라우저가 처리합니다. 기존 작업 공간 ↗
+
+ + + diff --git a/prototypes/workbench/web/document.mjs b/prototypes/workbench/web/document.mjs new file mode 100644 index 0000000..1f19322 --- /dev/null +++ b/prototypes/workbench/web/document.mjs @@ -0,0 +1,132 @@ +import {createRuntime} from './runtime.mjs'; +import {compile} from './compiler.mjs'; +import {bindSplit} from './split-input.mjs'; +const $=id=>document.getElementById(id), text=$('document-text'), reading=$('reading-text'); +const MAX_BYTES=2*1024*1024, stats={geometryReads:0,previewWrites:0,ratioWrites:0}; +let runtime=null, splitter=null, previewFrame=0, composing=false, generation=0, request=0, dirty=false; +const message=(id,value)=>{$(id).textContent=value;}; +const narrow=matchMedia('(max-width:720px)'); +const sample=`작업 노트 — 인터페이스의 경계 + +1. 이 문서에서 해볼 일 + +창의 폭을 줄여보세요. 읽기 화면의 글자는 새 폭에 맞춰 줄바꿈됩니다. 화면이 좁아지면 작성과 읽기가 위아래로 놓입니다. 문서 자체를 다른 형식으로 바꿀 필요는 없습니다. + +넓은 화면에서는 두 영역 사이의 경계를 잡아 움직일 수 있습니다. 경계에 키보드 초점을 두고 방향키를 눌러도 됩니다. 손을 놓으면 현재 비율을 유지하고, 움직이는 도중 Esc를 누르면 시작한 비율로 돌아갑니다. + +2. 작성과 읽기 + +왼쪽은 브라우저의 텍스트 입력입니다. 이 문장을 고치고, 몇 줄을 더 적어보세요. 한글을 조합하고 복사하거나 붙여넣고, 실행 취소를 해볼 수 있습니다. 읽기 화면에서는 같은 글을 다른 폭으로 살펴봅니다. + +양쪽의 스크롤은 독립적입니다. 읽던 곳을 옮긴다고 작성 커서가 움직이지 않습니다. 분할 비율을 바꿀 때도 글자나 커서, 문서를 새로 만들지 않습니다. + +3. 연결을 유지하기 + +일반 UTF-8 텍스트 파일을 열 수 있습니다. 작성한 결과도 .txt로 내려받습니다. 다른 편집기에서 그대로 읽을 수 있는 경로를 유지하는 것이 이 작업의 조건입니다. + +아래 문자열은 코드가 아니라 문서의 일부입니다. +
이 글은 화면의 HTML 요소로 해석되지 않습니다.
+ +4. 작업 메모 + +여기에 실제 프로젝트의 메모를 적어도 됩니다. + +하려던 일: + +실제로 일어난 일: + +기대했던 일: + +다음 수정: + +5. 남겨둘 질문 + +입력이 바뀌었을 때 꼭 다시 계산해야 하는 것은 무엇일까요? 문서가 바뀌지 않았는데 문서를 다시 전달할 이유가 있을까요? 화면의 폭과 문서의 의미를 같은 상태로 보관할 필요가 있을까요? + +이 화면이 답하는 범위는 작습니다. 실제 브라우저의 편집과 배치를 사용하면서, 조작 규칙을 어디에 둘 수 있는지를 확인합니다. +`; +function publishText() { + previewFrame=0; + if(composing)return; + reading.textContent=text.value;stats.previewWrites++; + $('reading-empty').hidden=text.value.length!==0; +} +function scheduleText(){if(!previewFrame)previewFrame=requestAnimationFrame(publishText);} +text.addEventListener('compositionstart',()=>{composing=true;}); +text.addEventListener('compositionend',()=>{composing=false;scheduleText();}); +text.addEventListener('input',()=>{generation++;dirty=true;scheduleText();message('document-status','편집 중 · 저장하지 않은 변경이 있습니다.');}); +function replaceText(value,name) { + text.value=value;text.scrollTop=0;$('reading-scroll').scrollTop=0; + if(name)$('filename').value=name; + generation++;dirty=false;scheduleText(); +} +$('sample').addEventListener('click',()=>{ + if(dirty&&!confirm('편집한 내용을 예제 문서로 바꿀까요?'))return; + request++;replaceText(sample,'interface-notes.txt');dirty=true;message('document-status','예제 문서를 불러왔습니다. 자유롭게 고쳐보세요.'); +}); +$('open').addEventListener('click',()=>$('file-input').click()); +$('file-input').addEventListener('change',async event=>{ + const file=event.target.files[0];event.target.value='';if(!file)return; + if(dirty&&!confirm('저장하지 않은 내용을 파일의 내용으로 바꿀까요?'))return; + const ticket=++request, version=generation; + try { + if(file.size>MAX_BYTES)throw new Error('2 MiB 이하의 텍스트 파일을 선택하세요.'); + const bytes=await file.arrayBuffer(); + if(ticket!==request||version!==generation)throw new Error('파일을 읽는 동안 문서가 바뀌어 열기를 취소했습니다.'); + if(bytes.byteLength>MAX_BYTES)throw new Error('파일 크기 한도를 넘었습니다.'); + const value=new TextDecoder('utf-8',{fatal:true}).decode(bytes); + replaceText(value,file.name);message('document-status',`${file.name} 파일을 열었습니다.`); + } catch(error){if(ticket===request)message('document-status',error instanceof TypeError?'UTF-8 텍스트로 읽을 수 없습니다. 현재 문서는 유지됩니다.':error.message);} +}); +$('save').addEventListener('click',()=>{ + const bytes=new TextEncoder().encode(text.value); + if(bytes.byteLength>MAX_BYTES){message('document-status','저장은 UTF-8 기준 2 MiB까지 지원합니다. 문서는 그대로 유지됩니다.');return;} + const name=$('filename').value.trim()||'notes.txt'; + const url=URL.createObjectURL(new Blob([bytes],{type:'text/plain;charset=utf-8'})); + const link=document.createElement('a');link.href=url;link.download=name.endsWith('.txt')?name:`${name}.txt`; + link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);dirty=false;message('document-status','텍스트 파일을 내려받았습니다.'); +}); +window.addEventListener('beforeunload',event=>{if(dirty){event.preventDefault();event.returnValue='';}}); +const output={ + begin(){},view(){},value(){},end(){}, + state(index,value){ + if(runtime.currentProgram.states[index].name!=='ratio')return; + // Invalid authored output never produces a broken grid or invalid ARIA value. + if(value<20||value>80)throw new Error('이 화면의 분할 비율은 20~80% 범위여야 합니다. 규칙을 수정하세요.'); + const ratio=String(value); + if($('divider').getAttribute('aria-valuenow')===ratio)return; + $('divider').setAttribute('aria-valuenow',ratio); + $('panes').style.setProperty('--split',String(value/100)); + $('split-range').value=ratio;$('split-output').value=`${Math.round(value)}%`;stats.ratioWrites++; + }, +}; +function publish(){const result=runtime.present(output);if(result.lastError)throw new Error(result.errorMessage);} +function writeRatio(value,source) { + if(!runtime)return; + try {runtime.change('split',value,source);publish();message('policy-status','적용된 규칙으로 분할 비율을 조절합니다.');} + catch(error){$('split-range').value=$('divider').getAttribute('aria-valuenow');message('policy-status',error.message);} +} +function applyPolicy() { + if(splitter?.isDragging())throw new Error('경계를 놓거나 취소한 뒤 규칙을 적용하세요.'); + const source=$('policy-source').value, p=compile(source), ratio=p.states.find(state=>state.name==='ratio'); + if(!ratio||ratio.initial<20||ratio.initial>80||!p.inputs.some(input=>input.name==='split'&&input.kind==='value')||!p.inputs.some(input=>input.name==='reset'&&input.key==='KeyR')||p.views.length) + throw new Error('ratio 상태(초깃값 20~80), split 숫자 입력, reset = KeyR 입력을 유지하세요. 이 화면은 HTML로 표시합니다.'); + runtime.apply(source);publish();message('policy-status','규칙을 적용했습니다. 문서와 편집 커서는 유지됩니다.'); +} +$('apply-policy').addEventListener('click',()=>{try{applyPolicy();}catch(error){message('policy-status',error.message);}}); +$('split-range').addEventListener('input',event=>{splitter?.cancel();writeRatio(event.target.valueAsNumber,'range');}); +$('reset-split').addEventListener('click',()=>{ + splitter?.cancel();if(!runtime)return; + try{runtime.down('reset','control');runtime.up('reset','control');publish();message('policy-status','분할 비율을 초기화했습니다.');}catch(error){message('policy-status',error.message);} +}); +try { + const [instance,response]=await Promise.all([createRuntime(),fetch('./examples/document.pure',{cache:'no-store'})]); + if(!response.ok)throw new Error('분할 규칙을 불러올 수 없습니다.'); + runtime=instance;$('policy-source').value=await response.text();applyPolicy(); + splitter=bindSplit({container:$('panes'),handle:$('divider'),narrow,stats, + readRatio:()=>Number($('divider').getAttribute('aria-valuenow')),writeRatio}); + for(const id of ['split-range','reset-split','apply-policy'])$(id).disabled=false; + $('divider').setAttribute('aria-disabled','false'); +} catch(error){message('policy-status',`분할 실행기를 열지 못했습니다: ${error.message} 문서 작성과 파일 저장은 계속 사용할 수 있습니다.`);} +window.pureDocument=Object.freeze({snapshot:()=>({ready:Boolean(splitter),stats:{...stats}, + runtime:runtime?.snapshot()??null,dragging:splitter?.isDragging()??false,composing,dirty,generation})}); diff --git a/prototypes/workbench/web/index.html b/prototypes/workbench/web/index.html new file mode 100644 index 0000000..089302c --- /dev/null +++ b/prototypes/workbench/web/index.html @@ -0,0 +1,95 @@ + + + + + + + Pure · 빈 작업물에서 시작하기 + + + + +
+ pure / workbench + 제작 경험 실험 004 +
실행기 준비 중
+
+ +
+
+

빈 코드 · 직접 만든 동작

무엇을 움직이게 할까요?

+

상태를 만들고, 입력에 동작을 연결하세요.
코드를 적용하면 장면이 바뀝니다. 같은 이름의 상태는 이어집니다.

+
+ +
+
+

01 코드

빈 작업물
+
+ + + + + + +
+ +
+ + +
+ +

편집만으로 실행 중인 코드는 바뀌지 않습니다.

+
아직 작성한 코드가 없습니다.
+
+ +
+
+

02 실행 장면

적용 전
+
+ 사용자가 코드로 만든 장면. 현재 비어 있습니다. +
아직 아무것도 없습니다도형이나 글자를 선언하고 코드를 적용하세요.
+ +
+
0 frame · 정지
+

정지하면 시간만 멈춥니다. 아래 입력은 정지 중에도 작동합니다.

+

내가 선언한 입력

눌렀을 때 .down / 놓았을 때 .up
+

입력을 선언하면 여기에 버튼이 생깁니다.

+

장면을 클릭하면 키보드로 입력할 수 있습니다. 코드나 폼을 편집하는 동안에는 전달되지 않습니다.

+
+ +
+

03 현재 상태

revision 0
+

선언한 상태의 값이 표시됩니다.

+
마지막 입력아직 없음
+ +
실행 비용과 범위

코어가 보고한 WebAssembly 메모리입니다. 브라우저·편집기·렌더러의 메모리와 프로세스 전체 사용량은 포함하지 않습니다. 예약 공간 안에 사용 중인 코드·상태·스테이징 공간이 포함되므로 합산하지 않습니다.

시간 지연으로 건너뛴 프레임: 0

+
+
+
+ +
+

언어 안내

조각을 조합해 자신의 동작을 만드세요.
+
+

1. 기억할 값

이름은 영문으로, 처음 값은 숫자로 정합니다.

state count = 0
+state x = 80
+

2. 입력과 반응

버튼 이름과 키를 선언하고 .down / .up에 연결합니다.

input tap = Space
+on tap.down {
+  count = count + 1
+}
+

3. 보일 것

좌표와 크기를 상태에 연결할 수 있습니다.

box tile(x, 180, 40, 40, "#7ed9ae")
+text label(40, 60, count, "Count: ")
+

4. 시간에 따른 변화

실행하면 매 프레임 반응합니다. dt는 1/60초입니다.

on frame {
+  x = x + 30 * dt
+  if x > 600 { x = 0 }
+}
+
+
표현식 · 입력 키 · 제한 보기

표현식
+ - * /, < <= > >= == !=, && || !, 괄호, min(a,b), max(a,b), clamp(x,lo,hi)를 사용합니다. 논리 연산은 양쪽을 모두 계산합니다.

도형
box id(x,y,width,height,"#rrggbb")
circle id(x,y,radius,"#rrggbb")
text id(x,y,value,"접두사")
글자의 value는 숫자 표현식입니다. 문자 접두사는 생략할 수 있습니다.

입력
Space, Enter, ArrowLeft/Right/Up/Down, KeyA~KeyZ, Digit0~Digit9. 같은 키는 하나의 입력에 연결합니다.

숫자 입력
input level = value와 on level.change { amount = event_value }로 숫자 컨트롤을 연결합니다. 웹 호스트와 CLI에서도 같은 값을 전달할 수 있습니다.

직접 조작
input drag = pointer(tile)로 도형에 입력을 연결합니다. on drag.down / .move / .up / .cancel에서 event_x, event_y는 장면 좌표입니다. 이동 방식과 취소 시 동작도 코드로 정합니다. 초점이나 포인터 캡처를 잃으면 .cancel이 전달됩니다.

값과 규칙 재사용
record Tile { x=0; y=0 }로 필드를 정하고 item green = Tile(x=80)으로 대상을 만듭니다. rule move(t: Tile) { t.x=event_x }를 on drag.move use move(green)으로 연결합니다. 화면에서는 green.x를 읽습니다. 규칙은 연결한 대상의 값을 직접 바꿉니다.

여러 도형의 입력 묶기
input drag = pointer { green_box: green; blue_box: blue }로 연결하고 on drag.move use move(drag.target)으로 현재 조작 대상을 전달합니다. 같은 레코드 타입의 대상끼리 묶으며, 각 도형은 따로 잡고 놓을 수 있습니다.

이 실험의 범위
상태 32개 · 입력 8개 · 도형/글자 24개 · 반응 64개 · 컴파일된 명령 512개 · 코드 32 KiB. 반복문, 함수 선언, 자동 충돌·물리는 없습니다. 줄바꿈 또는 세미콜론으로 구분하고 #으로 주석을 씁니다.

0으로 나누기 등 계산 실패 시 해당 이벤트의 상태 변경 전체를 취소합니다. 입력을 누르고 있을 때는 새 코드를 적용할 수 없습니다.

+

문서 편집 · 반응형 레이아웃 작업 화면 ↗

+

선택한 코드를 편집기에 불러옵니다. 자동으로 적용되지 않습니다.

+
같은 동작을 일반 JavaScript로 만든 비교 앱 ↗충전 예제와 비교할 수 있습니다. 새 탭에서 열립니다.
+
+
Pure · authoring experiment

원하는 동작을 만들어보고, 하려던 일 / 실제로 일어난 일 / 기대했던 일을 남겨주세요.

+
+ + + diff --git a/prototypes/workbench/web/output.mjs b/prototypes/workbench/web/output.mjs new file mode 100644 index 0000000..694fce6 --- /dev/null +++ b/prototypes/workbench/web/output.mjs @@ -0,0 +1,103 @@ +// Bind authored declarations to concrete DOM destinations once per apply. +// The DOM holds the published values; no parallel scene/value model is kept. +const NS = 'http://www.w3.org/2000/svg'; +const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString(); +const attributes = { box: { x: 'x', y: 'y', width: 'width', height: 'height' }, + circle: { x: 'cx', y: 'cy', radius: 'r' }, text: { x: 'x', y: 'y' } }; + +function text(node, value) { + if (node.textContent !== value) node.textContent = value; +} +function attribute(node, name, value) { + const next = String(value); + if (node.getAttribute(name) !== next) node.setAttribute(name, next); +} + +export function createOutput(document) { + const get = id => document.getElementById(id); + const scene = get('scene-content'), states = get('state-values'); + const empty = get('empty-scene'), description = get('scene-description'); + let active = undefined, nodes = [], amounts = []; + const costNames = ['Wasm 선형 메모리', '코어 예약 공간', '사용 중인 명령·연결', + '현재 상태 값', '새 코드 준비 공간 (예약에 포함)', '평가 스택 용량', + '마지막 이벤트 실행 명령', '적용된 소스 크기']; + const costs = costNames.map(label => { + const dt = document.createElement('dt'), dd = document.createElement('dd'); + dt.textContent = label; + return { dt, dd }; + }); + get('cost-values').replaceChildren(...costs.flatMap(({dt, dd}) => [dt, dd])); + + return { + begin(program) { + if (program === active) return; + active = program; + const targets = new Map(); + for (const input of program?.inputs ?? []) { + if (input.target) targets.set(input.target, {name:input.name}); + for (const route of input.routes ?? []) targets.set(route.target, {name:input.name,target:route.target}); + } + scene.ownerSVGElement.classList.toggle('direct-input', targets.size > 0); + nodes = (program?.views ?? []).map(view => { + const node = document.createElementNS(NS, {box:'rect', circle:'circle', text:'text'}[view.type]); + node.setAttribute('data-view-id', view.id); + if (targets.has(view.id)) { + const input = targets.get(view.id); + node.setAttribute('data-point-input', input.name); + if (input.target) node.setAttribute('data-point-target', input.target); + } + node.setAttribute('fill', view.color ?? '#e7ece6'); + if (view.type === 'text') { + node.setAttribute('font-family', 'ui-monospace, monospace'); + node.setAttribute('font-size', '20'); + } + return node; + }); + scene.replaceChildren(...nodes); + const holders = []; + amounts = (program?.states ?? []).map(state => { + const dl = document.createElement('dl'), dt = document.createElement('dt'), dd = document.createElement('dd'); + dl.className = 'state-value'; dt.textContent = state.name; dd.dataset.state = state.name; + dl.append(dt, dd); holders.push(dl); + return dd; + }); + if (!holders.length) { + const note = document.createElement('p'); + note.className = 'empty-note'; note.textContent = '선언한 상태의 값이 표시됩니다.'; + holders.push(note); + } + states.replaceChildren(...holders); + }, + state(index, value) { text(amounts[index], number(value)); }, + view(index, valid) { + const node = nodes[index]; + if (valid) { if (node.hasAttribute('display')) node.removeAttribute('display'); } + else attribute(node, 'display', 'none'); + }, + value(index, name, value) { + const view = active.views[index], node = nodes[index]; + if (name === 'value') text(node, `${view.prefix || ''}${number(value)}`); + else attribute(node, attributes[view.type][name], value); + }, + end(visible) { + const hidden = visible !== 0; + if (empty.hidden !== hidden) empty.hidden = hidden; + const failed = !visible && nodes.length > 0; + text(empty.querySelector('strong'), failed ? '장면을 표시할 수 없습니다' : '아직 아무것도 없습니다'); + text(empty.querySelector('span:last-child'), failed ? '현재 상태 아래의 표시 오류를 확인하세요.' : '도형이나 글자를 선언하고 코드를 적용하세요.'); + text(description, visible ? `사용자가 만든 장면. 도형과 글자 ${visible}개.` : failed ? '표시 오류로 장면을 표시할 수 없습니다.' : '사용자가 코드로 만든 장면. 현재 비어 있습니다.'); + }, + costs(status, metrics, droppedTicks) { + const m = status.memory; + const values = [m.linearBytes, m.reservedEngineBytes, m.activeIRBytes, m.activeStateBytes, + m.stagingReservedBytes, m.evalStackBytes, status.lastOps, metrics?.sourceBytes]; + for (let i = 0; i < values.length; i++) { + const value = values[i], {dt, dd} = costs[i], hidden = !Number.isFinite(value); + if (dt.hidden !== hidden) dt.hidden = hidden; + if (dd.hidden !== hidden) dd.hidden = hidden; + if (!hidden) text(dd, i === 6 ? `${value}개` : `${value.toLocaleString('en-US')} B`); + } + text(get('dropped-ticks'), `시간 지연으로 건너뛴 프레임: ${droppedTicks}`); + }, + }; +} diff --git a/prototypes/workbench/web/project.mjs b/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/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/workbench/web/runtime.mjs b/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/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/workbench/web/scene-input.mjs b/prototypes/workbench/web/scene-input.mjs new file mode 100644 index 0000000..0daf079 --- /dev/null +++ b/prototypes/workbench/web/scene-input.mjs @@ -0,0 +1,84 @@ +// This adapter owns browser capture only. Authored state changes stay in the VM. +export function bindSceneInput(scene, runtime, act) { + const captures = new Map(); + const events = scene.ownerDocument.defaultView; + const coordinates = event => { + const matrix = scene.getScreenCTM(); + if (!matrix) return null; + try { + const p = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse()); + return Number.isFinite(p.x) && Number.isFinite(p.y) ? p : null; + } catch { return null; } + }; + const release = id => { + if (scene.hasPointerCapture(id)) scene.releasePointerCapture(id); + }; + function finish(event, phase, pointerId = event.pointerId) { + const entry = captures.get(pointerId); + if (!entry) return; + event.preventDefault(); + // Capture loss/cancel may not carry a meaningful position. Use the last + // delivered coordinates. Up must use its own final position. + const p = phase === 'up' ? coordinates(event) : null; + if (p) { entry.x = p.x; entry.y = p.y; } + if (phase === 'up' && !p) phase = 'cancel'; + captures.delete(pointerId); + release(pointerId); + act(() => runtime().point(entry.name, phase, entry.x, entry.y, entry.source, entry.target)); + } + scene.addEventListener('pointerdown', event => { + if (event.button !== 0 || !runtime() || captures.has(event.pointerId)) return; + const target = event.target.closest?.('[data-point-input]'); + if (!target || !scene.contains(target)) return; + const name = target.getAttribute('data-point-input'); + const targetId = target.getAttribute('data-point-target') ?? undefined; + // Each target has one coordinate owner. A second contact on it must not + // steal the gesture; other authored targets can be manipulated independently. + if ([...captures.values()].some(entry => entry.name === name && entry.target === targetId)) return; + const p = coordinates(event); + if (!p) return; + event.preventDefault(); scene.focus(); + act(() => { + scene.setPointerCapture(event.pointerId); + const entry = {name, target:targetId, source:`scene:${event.pointerId}`, pointerType:event.pointerType, x:p.x, y:p.y}; + captures.set(event.pointerId, entry); + try { runtime().point(name, 'down', p.x, p.y, entry.source, entry.target); } + catch (error) { captures.delete(event.pointerId); release(event.pointerId); throw error; } + }); + }); + scene.addEventListener('pointermove', event => { + const entry = captures.get(event.pointerId); + if (!entry) return; + const p = coordinates(event); + if (!p) { finish(event, 'cancel'); return; } + event.preventDefault(); + entry.x = p.x; entry.y = p.y; + act(() => runtime().point(entry.name, 'move', p.x, p.y, entry.source, entry.target)); + }); + // A host may deliver release outside the SVG even while it owns capture. + // Observe termination before target handlers or capture cleanup can cancel it. + events.addEventListener('pointerup', event => finish(event, 'up'), true); + events.addEventListener('pointercancel', event => finish(event, 'cancel'), true); + events.addEventListener('mouseup', event => { + if (event.button !== 0) return; + // Mouse-only fallback when the host delivers mouseup without pointerup. + // A normal pointerup already removed the owner; touch compatibility events + // must never finish a different contact. + for (const [id, entry] of captures) { + if (entry.pointerType === 'mouse') finish(event, 'up', id); + } + }, true); + scene.addEventListener('lostpointercapture', event => { + // Descendant capture transitions bubble here but do not release our owner. + if (event.target === scene) finish(event, 'cancel'); + }); + return Object.freeze({ + releaseCaptures() { + const ids = [...captures.keys()]; + captures.clear(); + for (const id of ids) release(id); + // The caller subsequently invokes runtime.releaseAll(), which dispatches + // each pending cancel once. Clearing first makes lostcapture harmless. + }, + }); +} diff --git a/prototypes/workbench/web/split-input.mjs b/prototypes/workbench/web/split-input.mjs new file mode 100644 index 0000000..294e9a3 --- /dev/null +++ b/prototypes/workbench/web/split-input.mjs @@ -0,0 +1,57 @@ +// Only gesture ownership and transient geometry live here. Layout belongs to CSS. +export function bindSplit({container, handle, readRatio, writeRatio, narrow, stats}) { + const events=container.ownerDocument.defaultView, abort=new AbortController(); + const listen=(node,type,callback,options={})=>node.addEventListener(type,callback,{...options,signal:abort.signal}); + let drag=null, frame=0, dirty=true, rect=null, handleWidth=0; + const invalidate=()=>{dirty=true;}; + const observer=new ResizeObserver(invalidate);observer.observe(container);observer.observe(handle); + function measure() { + rect=container.getBoundingClientRect();const h=handle.getBoundingClientRect(); + handleWidth=h.width;stats.geometryReads+=2;dirty=false;return h; + } + function flush() { + if(frame){cancelAnimationFrame(frame);frame=0;} + if(!drag || drag.x===null)return; + if(dirty)measure(); + const width=rect.width-handleWidth; + if(width>0)writeRatio(100*(drag.x-rect.left-handleWidth/2-drag.offset)/width,'pointer'); + drag.x=null; + } + function finish(cancel) { + if(!drag)return; + if(cancel){if(frame)cancelAnimationFrame(frame);frame=0;}else flush(); + const previous=drag;drag=null; + if(handle.hasPointerCapture(previous.id))handle.releasePointerCapture(previous.id); + handle.classList.remove('dragging'); + if(cancel)writeRatio(previous.before,'cancel'); + } + listen(handle,'pointerdown',event=>{ + if(event.button!==0 || drag || narrow.matches)return; + const h=measure();if(rect.width<=handleWidth)return; + event.preventDefault();handle.focus(); + handle.setPointerCapture(event.pointerId); + drag={id:event.pointerId,type:event.pointerType,before:readRatio(),offset:event.clientX-(h.left+h.width/2),x:null}; + handle.classList.add('dragging'); + }); + listen(handle,'pointermove',event=>{ + if(!drag || drag.id!==event.pointerId)return; + event.preventDefault();drag.x=event.clientX; + if(!frame)frame=requestAnimationFrame(()=>{frame=0;flush();}); + }); + listen(events,'pointerup',event=>{if(drag?.id===event.pointerId)finish(false);},{capture:true}); + listen(events,'pointercancel',event=>{if(drag?.id===event.pointerId)finish(true);},{capture:true}); + listen(events,'mouseup',event=>{if(event.button===0&&drag?.type==='mouse')finish(false);},{capture:true}); + listen(handle,'lostpointercapture',event=>{if(event.target===handle&&drag?.id===event.pointerId)finish(true);}); + listen(events,'blur',()=>finish(true)); + listen(container.ownerDocument,'visibilitychange',()=>{if(container.ownerDocument.hidden)finish(true);}); + listen(events,'resize',invalidate);listen(events,'scroll',invalidate,{capture:true,passive:true}); + listen(narrow,'change',()=>{invalidate();if(narrow.matches)finish(true);}); + listen(handle,'keydown',event=>{ + if(event.key==='Escape'){event.preventDefault();finish(true);return;} + if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return; + event.preventDefault();finish(true); + writeRatio(event.key==='Home'?20:event.key==='End'?80:readRatio()+(event.key==='ArrowLeft'?-1:1)*(event.shiftKey?10:1),'keyboard'); + }); + return Object.freeze({cancel:()=>finish(true),isDragging:()=>drag!==null, + dispose(){finish(true);observer.disconnect();abort.abort();}}); +} diff --git a/prototypes/workbench/web/style.css b/prototypes/workbench/web/style.css new file mode 100644 index 0000000..702a6e4 --- /dev/null +++ b/prototypes/workbench/web/style.css @@ -0,0 +1,10 @@ +:root{color-scheme:dark;--bg:#151918;--panel:#1d2220;--field:#151a18;--line:#353e38;--text:#e7ece6;--muted:#a9b6aa;--faint:#7b8e7e;--accent:#c9f08b;--accent-dark:#222e18;--danger:#f6afa0;--radius:14px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-synthesis:none} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text)}button,input,select,textarea{font:inherit}button,select{border:1px solid var(--line);border-radius:7px;background:#282f2a;color:var(--text);padding:9px 12px;cursor:pointer;font-size:12px;font-weight:600;min-height:36px}button:hover:not(:disabled){background:#344035;border-color:#70836d}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button.primary{background:var(--accent);color:var(--accent-dark);border-color:var(--accent);padding:11px 19px;font-size:14px;display:flex;gap:28px;justify-content:space-between;align-items:center}button.primary:hover:not(:disabled){background:#d8f7ad}button.secondary{background:#c9f08b16;border-color:#637545;color:#d7f0b5}button.quiet{background:transparent;border-color:transparent;color:var(--muted);padding:6px 8px;min-height:30px}button.quiet:hover{color:var(--text)}.masthead{height:76px;padding:0 4.5%;display:flex;align-items:center;gap:28px;border-bottom:1px solid var(--line)}.wordmark{font-size:23px;font-weight:750;letter-spacing:-1px;color:var(--accent);text-decoration:none}.wordmark span{font-size:13px;color:var(--muted);letter-spacing:0;font-weight:400}.experiment{font-size:11px;color:var(--faint);border-left:1px solid var(--line);padding-left:24px}.connection{margin-left:auto;display:flex;gap:9px;align-items:center;font-size:11px;color:var(--muted)}.dot{width:6px;height:6px;border-radius:100%;background:#aa8e5c}.dot.ready{background:var(--accent)}.dot.failed{background:var(--danger)}main{max-width:1540px;margin:auto;padding:38px 4.5% 0}.intro{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin:0 0 27px}.eyebrow{font-size:10px;letter-spacing:1.4px;font-weight:600;color:var(--accent);margin:0 0 10px}h1{font-size:clamp(23px,2.5vw,35px);letter-spacing:-1px;margin:0;font-weight:600}.intro>p{font-size:12px;line-height:1.9;color:var(--muted);margin:0 0 2px}.intro strong{color:var(--text);font-weight:500}.workspace{display:grid;grid-template-columns:minmax(340px,1fr) minmax(380px,1.05fr);gap:22px;align-items:start}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.panel-heading{height:56px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line)}h2{margin:0;font-size:13px;font-weight:650;display:flex;gap:10px;align-items:center}.section-index{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;font-weight:400;color:var(--faint)}.badge{font-size:10px;border:1px solid var(--line);border-radius:4px;padding:4px 7px;white-space:nowrap;color:var(--muted)}.badge.dirty{color:var(--accent);border-color:#5f7247}.file-tools{display:flex;align-items:center;gap:3px;padding:8px 10px;border-bottom:1px solid var(--line);flex-wrap:wrap}.code-editor{height:514px;display:flex;overflow:hidden;background:var(--field);position:relative}.line-numbers{padding:20px 10px 20px 16px;min-width:45px;text-align:right;color:#667567;user-select:none;white-space:pre;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:hidden;flex-shrink:0;border-right:1px solid #2a322b}textarea#source{border:0;border-radius:0;outline:0;resize:none;padding:20px 16px;display:block;width:100%;height:100%;color:#d4e3ca;background:transparent;white-space:pre;overflow:auto;tab-size:2;font:12px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;caret-color:var(--accent)}textarea#source:focus{background:#171e19}textarea#source::placeholder{color:#7c8b7d;font:12px/1.9 ui-sans-serif,system-ui,sans-serif;white-space:pre-wrap}textarea#source[aria-invalid=true]{box-shadow:inset 3px 0 0 var(--danger)}.editor-footer{padding:8px 15px;font-size:10px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--faint);display:flex;justify-content:space-between;border-top:1px solid var(--line)}.apply-row{display:flex;gap:18px;align-items:center;padding:16px 18px 12px}.apply-row p{font-size:10px;color:var(--muted);line-height:1.7;margin:0;max-width:185px}.diagnostic{margin:0 18px 17px;font-size:11px;line-height:1.8;min-height:22px;color:var(--muted);display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}.diagnostic.error{color:var(--danger)}.diagnostic.success{color:#c7dea9}.right-column{display:grid;gap:18px}.stage-wrap{position:relative;background-color:#121613;background-image:linear-gradient(#a6c39907 1px,transparent 1px),linear-gradient(90deg,#a6c39907 1px,transparent 1px);background-size:24px 24px}#scene{width:100%;height:auto;aspect-ratio:640/360;display:block;overflow:hidden}.empty-scene{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;gap:9px;color:var(--faint);padding:24px;text-align:center}.empty-scene[hidden]{display:none}.empty-scene strong{font-size:13px;color:var(--muted);font-weight:400}.empty-scene>span:not(.empty-cross){font-size:11px}.empty-cross{font-size:31px;color:#52614d;font-weight:200;margin-bottom:7px}.stage-dimensions{position:absolute;right:14px;bottom:10px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#6d7f6d;pointer-events:none}.transport{display:flex;gap:7px;align-items:center;padding:14px 16px 0;flex-wrap:wrap}.transport button{font-size:11px;padding:7px 10px}.transport #clock-status{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:var(--muted);margin-left:auto}.control-note,.keyboard-note{font-size:10px;line-height:1.7;color:var(--faint);margin:10px 18px 15px}.input-heading{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:0 18px;padding-top:13px;border-top:1px solid var(--line)}h3{font-size:11px;font-weight:550;margin:0}.input-heading>span{font-size:9px;color:var(--faint)}.input-controls{display:flex;flex-wrap:wrap;gap:7px;padding:11px 17px 0;min-height:39px}.input-button{display:flex;align-items:center;gap:14px;touch-action:none;user-select:none;-webkit-user-select:none}.input-button kbd{font:9px ui-monospace,SFMono-Regular,Consolas,monospace;border:1px solid #5a6957;border-radius:3px;padding:3px 4px;color:var(--muted);min-width:22px;text-align:center}.input-button.held{background:var(--accent);border-color:var(--accent);color:var(--accent-dark)}.input-button.held kbd{color:var(--accent-dark);border-color:#829a62}.empty-note{font-size:11px;line-height:1.7;color:var(--faint);margin:0}.keyboard-note{margin-bottom:13px;font-size:9px}.state-values{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));padding:15px 19px;gap:13px;max-height:210px;overflow:auto}.state-value{display:flex;flex-direction:column;gap:6px;min-width:0}.state-value dt{font:10px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);overflow-wrap:anywhere}.state-value dd{font:17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--accent);margin:0;overflow-wrap:anywhere}.state-value dl{margin:0}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.subdued{color:var(--muted);font-size:10px}.event-line{padding:0 19px 13px;display:flex;gap:16px;align-items:baseline;flex-wrap:wrap}.event-line #last-event{color:var(--muted);font-size:10px;overflow-wrap:anywhere}.runtime-error{margin:0 18px 14px;padding:10px;border:1px solid #705049;border-radius:5px;background:#432c272e;color:var(--danger);font-size:11px;line-height:1.6}.cost-details{border-top:1px solid var(--line);padding:12px 19px;font-size:10px;color:var(--muted)}summary{cursor:pointer;font-size:11px;color:var(--muted)}.cost-details p{font-size:10px;line-height:1.8;color:var(--faint)}.cost-values{display:grid;grid-template-columns:1fr auto;gap:8px 20px;margin:16px 0}.cost-values dt,.cost-values dd{margin:0}.cost-values dd{text-align:right;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--text)}.reference{margin-top:25px}.reference-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));padding:22px 20px;gap:24px}.reference-grid article{min-width:0;display:flex;flex-direction:column;align-items:flex-start}.reference-grid h3{font-size:12px;color:#d2ddc8}.reference-grid p{font-size:10px;line-height:1.8;color:var(--muted);min-height:36px;margin:8px 0 10px}.reference-grid pre{font:10px/1.85 ui-monospace,SFMono-Regular,Consolas,monospace;background:#151b16;color:#bacead;width:100%;padding:10px;overflow:auto;border-radius:6px;min-height:115px;margin:0 0 8px}.reference-grid .quiet{font-size:10px;padding:5px 0}.syntax-details{padding:16px 20px;border-top:1px solid var(--line)}.syntax-columns{display:grid;grid-template-columns:1fr 1fr;gap:35px;margin-top:12px}.syntax-columns p{font-size:11px;line-height:1.95;color:var(--muted)}.syntax-columns strong{color:var(--text);font-weight:500}.syntax-columns code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;color:#d0dcbe;overflow-wrap:anywhere}.example-row{padding:18px 20px;border-top:1px solid var(--line);display:flex;align-items:center;gap:10px}.example-row>div{margin-right:auto}.example-row label{font-size:11px;color:var(--text)}.example-row p{font-size:10px;color:var(--faint);margin:5px 0 0}.example-row button,.example-row select{font-size:10px}.page-footer{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:26px 0 30px;color:var(--faint);font-size:10px}.page-footer>span{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:9px}.page-footer p{margin:0;line-height:1.8}.page-footer strong{font-weight:400;color:var(--muted)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important} +.baseline-row{border-top:1px solid var(--line);padding:15px 20px;display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center}.baseline-row a{font-size:11px;color:var(--accent);text-underline-offset:3px}.baseline-row span{font-size:10px;color:var(--faint)}#scene:focus-visible{outline:2px solid var(--accent);outline-offset:-3px} +@media(min-width:1450px){.code-editor{height:558px}.reference-grid pre{font-size:11px}} +@media(max-width:1050px){.masthead{padding:0 25px}.experiment{display:none}main{padding:29px 25px 0}.workspace{grid-template-columns:minmax(300px,1fr) minmax(340px,1fr);gap:15px}.intro>p{font-size:11px;max-width:290px}.reference-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:25px}.reference-grid p{min-height:0}.reference-grid pre{min-height:110px;font-size:11px}.transport #clock-status{width:100%;margin:3px 0 0}.code-editor{height:533px}.panel-heading{padding:0 16px}.apply-row{gap:10px;padding-left:14px;padding-right:14px}.apply-row p{font-size:9px}.input-heading{align-items:flex-start;flex-direction:column;gap:5px}.intro{gap:20px}button.primary{gap:14px;padding:11px 13px}} +@media(max-width:760px){.masthead{height:63px;padding:0 20px}.wordmark{font-size:21px}.wordmark span{font-size:11px}.connection{font-size:10px}main{padding:25px 18px 0}.intro{display:block;margin-bottom:22px}.intro h1{font-size:28px}.intro>p{margin-top:14px;max-width:none;font-size:11px}.workspace{grid-template-columns:minmax(0,1fr);gap:17px}.code-editor{height:340px}.editor-panel{width:100%}.transport #clock-status{width:auto;margin-left:auto}.right-column{gap:17px}.input-heading{flex-direction:row;align-items:center}.reference{margin-top:20px}.reference-grid{padding:20px 16px;gap:24px 17px}.reference-grid pre{font-size:10px}.reference>.panel-heading{height:auto;min-height:57px;padding-top:12px;padding-bottom:12px;align-items:flex-start;flex-direction:column;gap:6px}.syntax-columns{grid-template-columns:1fr;gap:0}.example-row{flex-wrap:wrap;padding:17px 16px}.example-row>div{width:100%;margin-bottom:4px}.example-row select{flex:1}.page-footer{align-items:flex-start;flex-direction:column;gap:10px;padding:23px 0}.apply-row{padding:14px 16px 10px;gap:17px}.apply-row p{font-size:10px;max-width:none}.diagnostic{margin-left:16px}.reference-grid article{overflow:hidden}} +@media(max-width:420px){main{padding-left:13px;padding-right:13px}.masthead{padding:0 15px}.wordmark span{font-size:10px}.connection{font-size:9px;gap:6px}.intro h1{font-size:25px}.transport{gap:5px;padding-left:12px;padding-right:12px}.transport button{font-size:10px;padding:7px 8px}.transport #clock-status{font-size:9px}.input-heading>span{font-size:8px}.reference-grid{grid-template-columns:minmax(0,1fr)}.reference-grid pre{min-height:0;font-size:11px}.reference-grid p{margin-bottom:8px}.file-tools{gap:0;padding:7px 5px}.file-tools .quiet{font-size:10px;padding:6px 7px}.apply-row p{font-size:9px}.line-numbers{padding-left:10px;min-width:36px}textarea#source{padding-left:12px;font-size:11px}.line-numbers{font-size:11px}} +@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}} + +#scene.direct-input{touch-action:none;user-select:none;-webkit-user-select:none}#scene [data-point-input]{cursor:grab} diff --git a/prototypes/workbench/workspace-check.mjs b/prototypes/workbench/workspace-check.mjs new file mode 100644 index 0000000..063ba42 --- /dev/null +++ b/prototypes/workbench/workspace-check.mjs @@ -0,0 +1,62 @@ +// Compare the archived separate-storage build with the shared-workspace build. +// This is a correctness and storage check, not another speed benchmark. +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { createRuntime } from './web/runtime.mjs'; +import { createRuntime as createPrevious } from './results/2026-09-22-evaluation/sources/web/runtime.mjs'; + +const args = process.argv.slice(2); +if (args.length !== 2 || args[0] !== '--output') throw new Error('Use --output NEW.json'); +const root = new URL('.', import.meta.url); +const archived = new URL('./results/2026-09-22-evaluation/sources/', root); +const sha = bytes => createHash('sha256').update(bytes).digest('hex'); +const manifest = JSON.parse(await readFile(new URL('manifest.json', archived), 'utf8')); +for (const [path, expected] of Object.entries(manifest)) + assert.equal(sha(await readFile(new URL(path, archived))), expected, path); +const oldWasm = await readFile(new URL('web/core.wasm', archived)); +const wasm = await readFile(new URL('web/core.wasm', root)); +const record = {utc:new Date().toISOString(),kind:'workspace-reuse-correctness-and-storage', + baselineManifestSha256:sha(await readFile(new URL('manifest.json', archived))), + sourceSha256:{},cases:[],node:process.version,platform:process.platform,arch:process.arch}; +for (const path of ['core.zig','evaluation.zig','web/runtime.mjs','web/compiler.mjs','web/core.wasm','workspace-check.mjs']) + record.sourceSha256[path] = sha(await readFile(new URL(path,root))); + +for (const name of ['counter','toggle','charge']) { + const path = `examples/${name}.pure`, source = await readFile(new URL(path,root),'utf8'); + record.sourceSha256[path] = sha(source); + const old = await createPrevious(oldWasm,{cacheEntries:32}); + const current = await createRuntime(wasm,{cacheEntries:32}); + const program = old.apply(source); current.apply(source); + const input = program.inputs[0].name; + let snapshots = 0; + function check() { + const before = old.snapshot(), after = current.snapshot(); + const {memory:oldMemory,...oldObservable} = before; + const {memory:newMemory,...newObservable} = after; + assert.deepEqual(newObservable,oldObservable); + for (const field of ['work','instructions','cacheHits']) + assert.equal(current.evaluationStats()[field],old.evaluationStats()[field]); + assert.equal(oldMemory.linearBytes,newMemory.linearBytes); + ++snapshots; + } + check(); + for (let i=0;i<256;i++) { + for (const r of [old,current]) { + if (i%23===0) {r.down(input);r.up(input);} + if (i%31===0) r.reset(); + if (i%47===0) r.apply(source+`\n# replacement ${i}\n`); + if (i%7===0) r.forgetEvaluation('history'); + if (i%13===0) r.forgetEvaluation('all'); + assert.equal(r.frame(),0); + } + check(); + } + record.cases.push({name,snapshots,before:old.snapshot().memory,after:current.snapshot().memory, + evaluation:current.evaluationStats()}); +} +record.wasmFileBytes = {before:oldWasm.length,after:wasm.length}; +await writeFile(args[1],JSON.stringify(record,null,2)+'\n',{flag:'wx'}); +console.log(JSON.stringify({checkedSnapshots:record.cases.reduce((sum,c)=>sum+c.snapshots,0), + engineBefore:record.cases[0].before.reservedEngineBytes,engineAfter:record.cases[0].after.reservedEngineBytes, + wasmFileBytes:record.wasmFileBytes,evaluation:record.cases[0].evaluation})); diff --git a/prototypes/workbench/workspace.test.mjs b/prototypes/workbench/workspace.test.mjs new file mode 100644 index 0000000..799f056 --- /dev/null +++ b/prototypes/workbench/workspace.test.mjs @@ -0,0 +1,91 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { compile } from './web/compiler.mjs'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const source = factor => `state x = 10 +input next = KeyN +text label(0, 0, x * ${factor}) +on next.down { x = x + 0.25 } +`; + +function begin(core, program, preserve) { + assert.equal(core.begin_program(program.states.length, program.inputs.length), 0); + program.states.forEach((state, i) => assert.equal(core.put_state(i, state.initial, preserve ? i : -1), 0)); +} + +function finish(core, program, during = () => {}) { + for (const op of program.instructions) { + assert.equal(core.put_instruction(op.op, op.arg, op.value), 0); + during(); + } + for (const handler of program.handlers) + assert.equal(core.put_handler(handler.event, handler.start, handler.length), 0); + assert.equal(core.commit_program(), 0); +} + +function observe(core, binding) { + core.begin_evaluation(100); + const value = core.eval_bounded(binding.start, binding.length); + assert.equal(core.evaluation_info(0), 0); + return value; +} + +test('Wasm executes old events and output during staged replacement without corrupting either phase', async () => { + const {instance:{exports:core}} = await WebAssembly.instantiate(wasm); + core.reset_all(); + const old = compile(source(2)), next = compile(source(3)); + begin(core, old, false); finish(core, old); + assert.equal(core.configure_evaluation(32, 8), 0); + const binding = old.views[0].bindings.value; + assert.equal(observe(core, binding), 20); + assert.equal(observe(core, binding), 20); + assert.equal(core.evaluation_info(3), 1); + begin(core, next, true); + let value = 10; + finish(core, next, () => { + assert.equal(core.evaluation_info(9), 0); + assert.equal(core.dispatch(1, 0), 0); value += 0.25; + assert.equal(core.configure_evaluation(32, 8), 0); + core.forget_evaluation(3); + assert.equal(observe(core, binding), value * 2); + assert.equal(core.evaluation_info(3), 0); + }); + assert.equal(core.evaluation_info(9), 1); + assert.equal(core.get_state(0), value); + const replaced = next.views[0].bindings.value; + assert.equal(observe(core, replaced), value * 3); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.evaluation_info(3), 1); + + const revision = core.info(4); + begin(core, old, true); + assert.equal(core.put_instruction(0, 0, Infinity), 5); + assert.equal(core.commit_program(), 5); + assert.equal(observe(core, replaced), value * 3); + core.abort_program(); + assert.equal(core.info(4), revision); + assert.equal(core.get_state(0), value); + assert.equal(core.evaluation_info(9), 1); + assert.equal(observe(core, replaced), value * 3); + assert.equal(core.memory.buffer.byteLength, 65536); +}); + +test('workspace accounting reports shared payload and safety tag without double-counting', async () => { + const runtime = await createRuntime(wasm, {cacheEntries:32}); + runtime.apply(source(2)); runtime.snapshot(); + const stats = runtime.evaluationStats(), memory = runtime.snapshot().memory; + assert.equal(stats.dedicatedBytes, 48); + assert.equal(stats.sharedBytes, 1152); + assert.equal(stats.workspaceTagBytes, 8); + assert.equal(stats.reservedBytes, stats.dedicatedBytes + stats.sharedBytes); + const restoreStorage = memory.stagingReservedBytes - memory.programReservedBytes; + assert.equal(restoreStorage, 264); // 32 values + mask/alignment, only in pending storage. + assert.equal(memory.reservedEngineBytes, 19072 + stats.dedicatedBytes + stats.workspaceTagBytes + restoreStorage + 2 * 64 * (memory.handlerBytes - 12) + 2 * 80); + // Each program adds 24 three-byte routes, count/mask and alignment (80 B). + assert.equal(memory.pointerTargetBytes, 3); + assert(stats.sharedBytes < memory.stagingReservedBytes); + assert.equal(stats.reuseAvailable, true); +}); diff --git a/research/experiments/001-checkpoint-granularity/README.md b/research/experiments/001-checkpoint-granularity/README.md new file mode 100644 index 0000000..4c04950 --- /dev/null +++ b/research/experiments/001-checkpoint-granularity/README.md @@ -0,0 +1,110 @@ +# Experiment 001: checkpoint granularity + +This is a native measurement apparatus for one known space/time tradeoff. It is +not a new algorithm, a general dependency-graph runtime, or the product's language +choice. The apparatus uses C17 and Python's standard library so it can be run +with the tools present on the initial Linux host and with a C compiler on macOS. + +## Question and pre-measurement expectations + +How much does storing intermediate results at different intervals save, once the +editable program and scheduling state are included? How much extra work does it +cause when a rule changes? + +Expected, not measured at the time of writing this section: + +- Uniformly spaced checkpoints shrink the result cache at the cost of starting + recomputation before the changed instruction. +- Root input changes offer no prefix to reuse. Fine checkpoints may only add + cache writes and comparisons in this workload. +- Edits near the end favor fine checkpoints. This deliberately favorable case + must not stand in for all edits. +- The editable model stays resident, so shrinking the cache 16-fold will not + shrink total core storage 16-fold. + +## Semantics and comparison + +The model consists of independent, statically connected chains. Each instruction +is an 8-byte record with a scalar operator and parameter. Values are uint64_t; +overflow wraps. Supported operators are add, XOR, multiply, and rotate/XOR. +Only the final result of each chain is observed. + +One update applies a batch of changes, then computes all affected observed +outputs. Changes alter roots, operators, and parameters, not graph topology. +There is no renderer, effectful I/O, concurrent execution, or state migration. + +- Block 1 stores every intermediate result (fine incremental baseline). +- Blocks 4, 16, and 64 store results at block ends. +- A block equal to chain length recomputes the whole affected chain, retaining + only its final output (full recomputation baseline). + +All variants share program representation, input handling and per-chain dirty +ranges. Unaffected chains are skipped by every variant. Fine and full variants +use dedicated loops. Fine/block variants stop at an unchanged cached boundary +only after passing the last changed instruction in the batch. + +The chain structure makes dependency information implicit. No general graph +edge storage or graph partitioning is needed. This is an optimistic case for +compact checkpoints; it cannot establish results for arbitrary graphs. + +For G chains of length L and checkpoint spacing B, the requested persistent +storage in this implementation is: + +```text +sizeof(Engine) + 8*G*L + 16*G + 8*G*ceil(L/B) + model inputs+ cached values, + dirty including outputs +``` + +This includes all live core arrays and the engine descriptor, but not allocator +bookkeeping, stack, executable code, trace storage or OS pages. Trace size is +recorded separately. It is not RSS, physical footprint, or total application RAM. +There are no allocations in the timed update path. + +## Workloads + +The fixed initial matrix uses 16 chains, lengths 256 and 1024, and 256 updates per +trace. Workload seed: 20260922. + +- `roots`: every chain's input changes on every update. +- `uniform`: one uniformly selected instruction changes per update. +- `tail`: one instruction in the last eight positions changes per update. +- `dense`: eight instruction edits per chain per update, with replacement. + +Changes are XOR deltas so repeated traces continue changing the model. Two +warm-up passes precede timing. Even measured replay counts leave identical final +models across strategies; the runner checks their output checksums. Differential +tests additionally compare every output after every update against independent +from-scratch evaluation across multiple seeds, odd sizes and edge cases. + +## Reproduce + +From this directory, with Python 3 and a C compiler with ASan/UBSan support: + +```sh +python3 run.py --check-only +python3 run.py --output results/local-run +``` + +The runner refuses to overwrite an existing result directory. `CC` can select a +compiler, such as `CC=clang`. It builds into a temporary directory. On Linux it +pins itself and child processes to one available CPU and records that choice. + +Default measurements use seven trials with randomized strategy order within +each trial. Pilot timings choose an even replay count per case, targeting 20 ms +per measurement. All raw samples, memory counts, instruction counts, compiler +flags, source hashes and host metadata are retained. Timing excludes program +creation, trace creation, warm-up, and output verification. It includes change +application, scheduling and recomputation. + +`median_ns_per_tick` is the median of batch-average processing times. It is not a +P50/P99 input-to-display latency measurement. A shared host without controlled +power or temperature permits preliminary comparisons, not hardware guarantees. + +The reported storage reduction is largely predicted by the formula. The useful +observations are the whole-core reduction, processing cost and losing workloads. +This experiment cannot establish automatic policy quality, novel research, +editor usability, M3 performance, or universal memory savings. + +The first run and its interpretation are available in the +[Korean research report](../../../docs/research/002-checkpoint-results.md). diff --git a/research/experiments/001-checkpoint-granularity/benchmark.c b/research/experiments/001-checkpoint-granularity/benchmark.c new file mode 100644 index 0000000..1c3f98b --- /dev/null +++ b/research/experiments/001-checkpoint-granularity/benchmark.c @@ -0,0 +1,276 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include + +/* Experimental apparatus, not the Pure runtime. All arithmetic is modulo 2^64. */ +typedef struct { uint32_t salt, control; } Op; +typedef struct { + uint32_t chain, index; + uint64_t mask; +} Change; +typedef struct { + uint32_t chains, length, block, slots; + Op *ops; + uint64_t *inputs, *cache; + uint32_t *first, *last; +} Engine; +typedef struct { + uint32_t ticks, per_tick; + Change *changes; +} Trace; + +static void die(const char *s) { fprintf(stderr, "%s\n", s); exit(1); } +static void require(int condition, const char *s) { if (!condition) die(s); } +static void *allocate(size_t count, size_t size) { + if (size && count > SIZE_MAX / size) die("allocation size overflow"); + void *p = calloc(count, size); + if (!p) die("allocation failed"); + return p; +} +static uint64_t random64(uint64_t *s) { + uint64_t z = (*s += UINT64_C(0x9e3779b97f4a7c15)); + z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb); + return z ^ (z >> 31); +} +static uint64_t rotate(uint64_t x, uint32_t n) { + return (x << n) | (x >> (64 - n)); +} +static uint64_t evaluate(Op op, uint64_t x) { + switch (op.control & 3u) { + case 0: return x + op.salt; + case 1: return x ^ ((uint64_t)op.salt << 17); + case 2: return x * ((uint64_t)op.salt | 1u); + default: return rotate(x, 1u + ((op.control >> 2) % 63u)) ^ op.salt; + } +} +static uint64_t now_ns(void) { + struct timespec t; + if (clock_gettime(CLOCK_MONOTONIC, &t)) die("clock_gettime failed"); + return (uint64_t)t.tv_sec * UINT64_C(1000000000) + (uint64_t)t.tv_nsec; +} +static uint64_t output(const Engine *e, uint32_t c) { + return e->cache[(size_t)c * e->slots + e->slots - 1]; +} +static uint64_t reference_output(const Engine *e, uint32_t c) { + uint64_t x = e->inputs[c]; + for (uint32_t n = 0; n < e->length; ++n) + x = evaluate(e->ops[(size_t)c * e->length + n], x); + return x; +} +static Engine create_engine(uint32_t chains, uint32_t length, + uint32_t block, uint64_t seed) { + Engine e = { .chains = chains, .length = length, .block = block, + .slots = (length + block - 1) / block }; + e.ops = allocate((size_t)chains * length, sizeof(*e.ops)); + e.inputs = allocate(chains, sizeof(*e.inputs)); + e.cache = allocate((size_t)chains * e.slots, sizeof(*e.cache)); + e.first = allocate(chains, sizeof(*e.first)); + e.last = allocate(chains, sizeof(*e.last)); + for (uint32_t c = 0; c < chains; ++c) { + e.inputs[c] = random64(&seed); + uint64_t x = e.inputs[c]; + for (uint32_t n = 0; n < length; ++n) { + Op *op = &e.ops[(size_t)c * length + n]; + op->salt = (uint32_t)random64(&seed); + op->control = (uint32_t)random64(&seed); + x = evaluate(*op, x); + if ((n + 1) % block == 0 || n + 1 == length) + e.cache[(size_t)c * e.slots + n / block] = x; + } + e.first[c] = length; + } + return e; +} +static void destroy_engine(Engine *e) { + free(e->ops); free(e->inputs); free(e->cache); + free(e->first); free(e->last); +} +static void apply(Engine *e, Change change) { + uint32_t c = change.chain, n = change.index; + require(c < e->chains && n <= e->length, "invalid change"); + if (!change.mask) return; + if (n == e->length) { + e->inputs[c] ^= change.mask; + n = 0; + } else { + Op *op = &e->ops[(size_t)c * e->length + n]; + op->salt ^= (uint32_t)change.mask; + op->control ^= (uint32_t)(change.mask >> 32); + } + if (n < e->first[c]) e->first[c] = n; + if (n > e->last[c]) e->last[c] = n; +} +static uint64_t update(Engine *e) { + uint64_t work = 0; + for (uint32_t c = 0; c < e->chains; ++c) { + uint32_t first = e->first[c], last = e->last[c]; + if (first == e->length) continue; + const Op *ops = e->ops + (size_t)c * e->length; + uint64_t *cache = e->cache + (size_t)c * e->slots; + /* Full and fine have dedicated loops: no block-loop penalty for B=1. */ + if (e->block == e->length) { + uint64_t x = e->inputs[c]; + for (uint32_t n = 0; n < e->length; ++n) x = evaluate(ops[n], x); + cache[0] = x; + work += e->length; + } else if (e->block == 1) { + uint64_t x = first ? cache[first - 1] : e->inputs[c]; + uint32_t n = first; + while (n < e->length) { + uint64_t old = cache[n]; + cache[n] = x = evaluate(ops[n], x); + ++n; + /* Do not skip a later edit in the same batch. */ + if (n > last && x == old) break; + } + work += n - first; + } else { + uint32_t start = first / e->block; + uint64_t x = start ? cache[start - 1] : e->inputs[c]; + for (uint32_t b = start; b < e->slots; ++b) { + uint32_t begin = b * e->block, end = begin + e->block; + if (end > e->length) end = e->length; + for (uint32_t n = begin; n < end; ++n) x = evaluate(ops[n], x); + uint64_t old = cache[b]; + cache[b] = x; + work += end - begin; + if (end > last && x == old) break; + } + } + e->first[c] = e->length; + e->last[c] = 0; + } + return work; +} +static uint64_t checksum(const Engine *e) { + uint64_t h = UINT64_C(0xcbf29ce484222325); + for (uint32_t c = 0; c < e->chains; ++c) + h = (h ^ output(e, c)) * UINT64_C(0x100000001b3); + return h; +} +static Trace create_trace(const char *kind, uint32_t chains, uint32_t length, + uint32_t ticks, uint64_t seed) { + uint32_t per_tick = 1; + int roots = !strcmp(kind, "roots"); + int dense = !strcmp(kind, "dense"); + int uniform = !strcmp(kind, "uniform"); + int tail = !strcmp(kind, "tail"); + require(roots || dense || uniform || tail, "unknown workload"); + if (roots) per_tick = chains; + if (dense) per_tick = chains * 8; + Trace t = { .ticks = ticks, .per_tick = per_tick }; + t.changes = allocate((size_t)ticks * per_tick, sizeof(*t.changes)); + for (uint32_t i = 0; i < ticks; ++i) { + for (uint32_t k = 0; k < per_tick; ++k) { + Change *p = &t.changes[(size_t)i * per_tick + k]; + p->chain = roots ? k : dense ? k / 8 : (uint32_t)(random64(&seed) % chains); + p->index = roots ? length : (uint32_t)(random64(&seed) % length); + if (tail) { + uint32_t width = length < 8 ? length : 8; + p->index = length - width + p->index % width; + } + /* XOR changes keep every replay active; fixed assignments would not. */ + p->mask = random64(&seed) | 1u; + } + } + return t; +} +static uint64_t replay(Engine *e, const Trace *t) { + uint64_t work = 0; + for (uint32_t i = 0; i < t->ticks; ++i) { + for (uint32_t k = 0; k < t->per_tick; ++k) + apply(e, t->changes[(size_t)i * t->per_tick + k]); + work += update(e); + } + return work; +} +static size_t owned_bytes(const Engine *e) { + return sizeof(*e) + (size_t)e->chains * e->length * sizeof(*e->ops) + + (size_t)e->chains * (sizeof(*e->inputs) + sizeof(*e->first) + sizeof(*e->last)) + + (size_t)e->chains * e->slots * sizeof(*e->cache); +} +static void check_outputs(const Engine *e) { + for (uint32_t c = 0; c < e->chains; ++c) + require(output(e, c) == reference_output(e, c), "output disagrees with full reference"); +} +static void self_test(void) { + const uint32_t lengths[] = {1, 2, 7, 17, 65, 257}; + const uint32_t blocks[] = {1, 2, 4, 16, 64, 256}; + uint64_t checked_ticks = 0; + for (uint64_t seed = 1; seed <= 8; ++seed) { + for (size_t l = 0; l < sizeof(lengths) / sizeof(*lengths); ++l) { + uint32_t length = lengths[l]; + for (size_t b = 0; b <= sizeof(blocks) / sizeof(*blocks); ++b) { + uint32_t block = b == sizeof(blocks) / sizeof(*blocks) ? length : blocks[b]; + if (block > length) continue; + Engine e = create_engine(3, length, block, seed); + check_outputs(&e); + uint64_t rng = seed; + for (uint32_t tick = 0; tick < 500; ++tick) { + uint32_t count = (uint32_t)(random64(&rng) % 9); + for (uint32_t k = 0; k < count; ++k) { + Change change = { + .chain = (uint32_t)(random64(&rng) % e.chains), + .index = (uint32_t)(random64(&rng) % (length + 1)), + .mask = random64(&rng) + }; + if (tick % 11 == 0) change.mask = 0; + apply(&e, change); + if (tick % 13 == 0) apply(&e, change); /* cancelling edits */ + } + (void)update(&e); + check_outputs(&e); + require(update(&e) == 0, "clean update performed work"); + ++checked_ticks; + } + destroy_engine(&e); + } + } + } + /* An unchanged early checkpoint must not suppress a later dirty operation. */ + for (uint32_t block = 1; block <= 4; block *= 2) { + Engine e = create_engine(1, 8, block, 77); + Change cancel = {0, 0, 123}, later = {0, 7, 987}; + apply(&e, cancel); apply(&e, cancel); apply(&e, later); + (void)update(&e); check_outputs(&e); destroy_engine(&e); + } + printf("{\"checked_ticks\":%" PRIu64 ",\"status\":\"passed\"}\n", checked_ticks); +} +static uint32_t number(const char *s, uint32_t max) { + char *end; + errno = 0; + unsigned long n = strtoul(s, &end, 10); + if (errno || end == s || *end || !n || n > max) die("invalid positive integer"); + return (uint32_t)n; +} +int main(int argc, char **argv) { + if (argc == 2 && !strcmp(argv[1], "--check")) { self_test(); return 0; } + if (argc != 8) die("usage: benchmark CHAINS LENGTH BLOCK WORKLOAD TICKS REPLAYS SEED | --check"); + uint32_t chains = number(argv[1], 1024), length = number(argv[2], 65536); + uint32_t block = number(argv[3], length), ticks = number(argv[5], 4096); + uint32_t replays = number(argv[6], 4096), seed = number(argv[7], UINT32_MAX); + require((uint64_t)chains * length <= UINT64_C(1048576), "model too large for this lab"); + Engine e = create_engine(chains, length, block, seed); + Trace t = create_trace(argv[4], chains, length, ticks, seed + UINT64_C(1234567)); + (void)replay(&e, &t); (void)replay(&e, &t); /* two warm-up replays */ + uint64_t work = 0, start = now_ns(); + for (uint32_t r = 0; r < replays; ++r) work += replay(&e, &t); + uint64_t elapsed = now_ns() - start; + check_outputs(&e); /* correctness check is outside the measured interval */ + printf("{\"elapsed_ns\":%" PRIu64 ",\"ticks\":%" PRIu64 + ",\"node_evaluations\":%" PRIu64 ",\"core_owned_bytes\":%zu" + ",\"model_bytes\":%zu,\"cache_bytes\":%zu,\"engine_bytes\":%zu" + ",\"trace_bytes\":%zu,\"checksum\":\"%016" PRIx64 "\"}\n", + elapsed, (uint64_t)ticks * replays, work, owned_bytes(&e), + (size_t)chains * length * sizeof(Op), + (size_t)chains * e.slots * sizeof(uint64_t), sizeof(Engine), + sizeof(t) + (size_t)t.ticks * t.per_tick * sizeof(Change), checksum(&e)); + free(t.changes); destroy_engine(&e); + return 0; +} diff --git a/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/environment.json b/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/environment.json new file mode 100644 index 0000000..1e967ad --- /dev/null +++ b/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/environment.json @@ -0,0 +1,330 @@ +{ + "started_utc": "2026-09-22T09:32:06.820074+00:00", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0\nCopyright (C) 2024 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.", + "flags": [ + "-std=c17", + "-O3", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror" + ], + "python": "3.14.7", + "source_sha256": "9e1b1108f58a8725458531207bd1edf06d9af43ae3255c3b97d4388c2b58c707", + "runner_sha256": "4c3e8ae899eb5068813df9529a8c7953d63730ee3ca507a0a7dbe58963b1db01", + "trials": 7, + "target_ms": 20.0, + "seed": 20260922, + "timing": "batch elapsed time / update ticks; not input latency", + "memory": "sizeof engine + requested live array bytes; excludes allocator, harness, code, OS", + "power_control": "not controlled; shared host; preliminary measurements", + "original_affinity": [ + 0, + 1, + 2, + 3 + ], + "measurement_affinity": [ + 0 + ], + "cpu_description": [ + "CPU architecture: 8", + "CPU implementer\t: 0x41", + "CPU part\t: 0xd0b", + "Model\t\t: Raspberry Pi 5 Model B Rev 1.1" + ], + "release_check": { + "checked_ticks": 108000, + "status": "passed" + }, + "sanitizer_check": { + "checked_ticks": 108000, + "status": "passed" + }, + "calibration_replays": [ + { + "chains": 16, + "length": 256, + "workload": "roots", + "block": 1, + "replays": 6 + }, + { + "chains": 16, + "length": 256, + "workload": "roots", + "block": 4, + "replays": 4 + }, + { + "chains": 16, + "length": 256, + "workload": "roots", + "block": 16, + "replays": 6 + }, + { + "chains": 16, + "length": 256, + "workload": "roots", + "block": 64, + "replays": 6 + }, + { + "chains": 16, + "length": 256, + "workload": "roots", + "block": 256, + "replays": 6 + }, + { + "chains": 16, + "length": 256, + "workload": "uniform", + "block": 1, + "replays": 130 + }, + { + "chains": 16, + "length": 256, + "workload": "uniform", + "block": 4, + "replays": 110 + }, + { + "chains": 16, + "length": 256, + "workload": "uniform", + "block": 16, + "replays": 130 + }, + { + "chains": 16, + "length": 256, + "workload": "uniform", + "block": 64, + "replays": 124 + }, + { + "chains": 16, + "length": 256, + "workload": "uniform", + "block": 256, + "replays": 74 + }, + { + "chains": 16, + "length": 256, + "workload": "tail", + "block": 1, + "replays": 968 + }, + { + "chains": 16, + "length": 256, + "workload": "tail", + "block": 4, + "replays": 870 + }, + { + "chains": 16, + "length": 256, + "workload": "tail", + "block": 16, + "replays": 730 + }, + { + "chains": 16, + "length": 256, + "workload": "tail", + "block": 64, + "replays": 384 + }, + { + "chains": 16, + "length": 256, + "workload": "tail", + "block": 256, + "replays": 86 + }, + { + "chains": 16, + "length": 256, + "workload": "dense", + "block": 1, + "replays": 4 + }, + { + "chains": 16, + "length": 256, + "workload": "dense", + "block": 4, + "replays": 4 + }, + { + "chains": 16, + "length": 256, + "workload": "dense", + "block": 16, + "replays": 4 + }, + { + "chains": 16, + "length": 256, + "workload": "dense", + "block": 64, + "replays": 4 + }, + { + "chains": 16, + "length": 256, + "workload": "dense", + "block": 256, + "replays": 4 + }, + { + "chains": 16, + "length": 1024, + "workload": "roots", + "block": 1, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "roots", + "block": 4, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "roots", + "block": 16, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "roots", + "block": 64, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "roots", + "block": 1024, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "uniform", + "block": 1, + "replays": 24 + }, + { + "chains": 16, + "length": 1024, + "workload": "uniform", + "block": 4, + "replays": 22 + }, + { + "chains": 16, + "length": 1024, + "workload": "uniform", + "block": 16, + "replays": 24 + }, + { + "chains": 16, + "length": 1024, + "workload": "uniform", + "block": 64, + "replays": 24 + }, + { + "chains": 16, + "length": 1024, + "workload": "uniform", + "block": 1024, + "replays": 14 + }, + { + "chains": 16, + "length": 1024, + "workload": "tail", + "block": 1, + "replays": 912 + }, + { + "chains": 16, + "length": 1024, + "workload": "tail", + "block": 4, + "replays": 834 + }, + { + "chains": 16, + "length": 1024, + "workload": "tail", + "block": 16, + "replays": 708 + }, + { + "chains": 16, + "length": 1024, + "workload": "tail", + "block": 64, + "replays": 384 + }, + { + "chains": 16, + "length": 1024, + "workload": "tail", + "block": 1024, + "replays": 14 + }, + { + "chains": 16, + "length": 1024, + "workload": "dense", + "block": 1, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "dense", + "block": 4, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "dense", + "block": 16, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "dense", + "block": 64, + "replays": 2 + }, + { + "chains": 16, + "length": 1024, + "workload": "dense", + "block": 1024, + "replays": 2 + } + ], + "completed_utc": "2026-09-22T09:32:24.887753+00:00", + "binary_bytes": 71208 +} diff --git a/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/raw.csv b/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/raw.csv new file mode 100644 index 0000000..1ed88ef --- /dev/null +++ b/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/raw.csv @@ -0,0 +1,281 @@ +trial,chains,length,workload,block,replays,elapsed_ns,ticks,node_evaluations,core_owned_bytes,model_bytes,cache_bytes,engine_bytes,trace_bytes,checksum,ns_per_tick,evaluations_per_tick +0,16,256,uniform,256,74,19423930,18944,4849664,33208,32768,128,56,4112,07e316e8e83163f0,1025.3341427364865,256.0 +0,16,256,dense,256,4,27889701,1024,4194304,33208,32768,128,56,524304,07e316e8e83163f0,27236.0361328125,4096.0 +0,16,256,dense,64,4,27252683,1024,4080128,33592,32768,512,56,524304,07e316e8e83163f0,26613.9482421875,3984.5 +0,16,256,tail,16,730,16031339,186880,2990080,35128,32768,2048,56,4112,07e316e8e83163f0,85.78413420376712,16.0 +0,16,1024,roots,1,2,58533196,512,8388608,262456,131072,131072,56,65552,cc440e349b9b5a57,114322.6484375,16384.0 +0,16,1024,uniform,1,24,20859539,6144,3279504,262456,131072,131072,56,4112,cc440e349b9b5a57,3395.1072591145835,533.7734375 +0,16,256,roots,256,6,23781630,1536,6291456,33208,32768,128,56,65552,07e316e8e83163f0,15482.83203125,4096.0 +0,16,1024,dense,4,2,63661600,512,7458720,164152,131072,32768,56,524304,cc440e349b9b5a57,124339.0625,14567.8125 +0,16,1024,uniform,1024,14,22677705,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6327.484654017857,1024.0 +0,16,256,tail,1,968,15760618,247808,1128688,65848,32768,32768,56,4112,07e316e8e83163f0,63.60011783316116,4.5546875 +0,16,1024,tail,4,834,14631637,213504,1277688,164152,131072,32768,56,4112,cc440e349b9b5a57,68.53097365857315,5.984375 +0,16,1024,tail,1,912,15252766,233472,1063392,262456,131072,131072,56,4112,cc440e349b9b5a57,65.33017235471492,4.5546875 +0,16,256,dense,4,4,28737719,1024,3753408,41272,32768,8192,56,524304,07e316e8e83163f0,28064.1787109375,3665.4375 +0,16,1024,roots,64,2,54914217,512,8388608,133432,131072,2048,56,65552,cc440e349b9b5a57,107254.330078125,16384.0 +0,16,256,roots,1,6,30769606,1536,6291456,65848,32768,32768,56,65552,07e316e8e83163f0,20032.295572916668,4096.0 +0,16,1024,roots,16,2,57387697,512,8388608,139576,131072,8192,56,65552,cc440e349b9b5a57,112085.345703125,16384.0 +0,16,1024,roots,1024,2,55510329,512,8388608,131512,131072,128,56,65552,cc440e349b9b5a57,108418.611328125,16384.0 +0,16,1024,uniform,64,24,20219114,6144,3472896,133432,131072,2048,56,4112,cc440e349b9b5a57,3290.8714192708335,565.25 +0,16,1024,roots,4,2,69964336,512,8388608,164152,131072,32768,56,65552,cc440e349b9b5a57,136649.09375,16384.0 +0,16,1024,uniform,16,24,20636818,6144,3323520,139576,131072,8192,56,4112,cc440e349b9b5a57,3358.8570963541665,540.9375 +0,16,256,roots,4,4,21918928,1024,4194304,41272,32768,8192,56,65552,07e316e8e83163f0,21405.203125,4096.0 +0,16,1024,uniform,4,22,21167687,5632,3014264,164152,131072,32768,56,4112,cc440e349b9b5a57,3758.4671519886365,535.203125 +0,16,256,uniform,4,110,17970005,28160,3722840,41272,32768,8192,56,4112,07e316e8e83163f0,638.1393821022727,132.203125 +0,16,1024,tail,16,708,15733117,181248,2899968,139576,131072,8192,56,4112,cc440e349b9b5a57,86.80436197916667,16.0 +0,16,256,roots,16,6,25329389,1536,6291456,35128,32768,2048,56,65552,07e316e8e83163f0,16490.487630208332,4096.0 +0,16,1024,dense,64,2,55407347,512,7682176,133432,131072,2048,56,524304,cc440e349b9b5a57,108217.474609375,15004.25 +0,16,1024,tail,64,384,18079579,98304,6291456,133432,131072,2048,56,4112,cc440e349b9b5a57,183.91498819986978,64.0 +0,16,1024,dense,16,2,57109105,512,7506624,139576,131072,8192,56,524304,cc440e349b9b5a57,111541.220703125,14661.375 +0,16,256,uniform,1,130,19115671,33280,4352140,65848,32768,32768,56,4112,07e316e8e83163f0,574.3891526442308,130.7734375 +0,16,256,uniform,16,130,18864226,33280,4590560,35128,32768,2048,56,4112,07e316e8e83163f0,566.8337139423077,137.9375 +0,16,256,dense,1,4,26646406,1024,3729584,65848,32768,32768,56,524304,07e316e8e83163f0,26021.880859375,3642.171875 +0,16,1024,dense,1024,2,60125695,512,8388608,131512,131072,128,56,524304,cc440e349b9b5a57,117432.998046875,16384.0 +0,16,256,tail,4,870,15279896,222720,1332840,41272,32768,8192,56,4112,07e316e8e83163f0,68.60585488505747,5.984375 +0,16,256,tail,64,384,18427282,98304,6291456,33592,32768,512,56,4112,07e316e8e83163f0,187.4520060221354,64.0 +0,16,256,tail,256,86,19077985,22016,5636096,33208,32768,128,56,4112,07e316e8e83163f0,866.5509175145348,256.0 +0,16,256,uniform,64,124,19114097,31744,5150464,33592,32768,512,56,4112,07e316e8e83163f0,602.1325919858871,162.25 +0,16,256,dense,16,4,27271257,1024,3842048,35128,32768,2048,56,524304,07e316e8e83163f0,26632.0869140625,3752.0 +0,16,1024,dense,1,2,56247180,512,7446690,262456,131072,131072,56,524304,cc440e349b9b5a57,109857.7734375,14544.31640625 +0,16,1024,tail,1024,14,22547316,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6291.103794642857,1024.0 +0,16,256,roots,64,6,24836741,1536,6291456,33592,32768,512,56,65552,07e316e8e83163f0,16169.753255208334,4096.0 +1,16,256,tail,16,730,15723896,186880,2990080,35128,32768,2048,56,4112,07e316e8e83163f0,84.13899828767123,16.0 +1,16,1024,roots,64,2,53830403,512,8388608,133432,131072,2048,56,65552,cc440e349b9b5a57,105137.505859375,16384.0 +1,16,256,uniform,64,124,19164986,31744,5150464,33592,32768,512,56,4112,07e316e8e83163f0,603.7356980846774,162.25 +1,16,1024,roots,16,2,57857808,512,8388608,139576,131072,8192,56,65552,cc440e349b9b5a57,113003.53125,16384.0 +1,16,256,tail,256,86,18910930,22016,5636096,33208,32768,128,56,4112,07e316e8e83163f0,858.9630268895348,256.0 +1,16,256,uniform,1,130,19119244,33280,4352140,65848,32768,32768,56,4112,07e316e8e83163f0,574.496514423077,130.7734375 +1,16,256,uniform,256,74,19320449,18944,4849664,33208,32768,128,56,4112,07e316e8e83163f0,1019.8716744087837,256.0 +1,16,256,dense,64,4,27044701,1024,4080128,33592,32768,512,56,524304,07e316e8e83163f0,26410.8408203125,3984.5 +1,16,1024,tail,4,834,14780378,213504,1277688,164152,131072,32768,56,4112,cc440e349b9b5a57,69.22763976318944,5.984375 +1,16,1024,tail,1,912,15955191,233472,1063392,262456,131072,131072,56,4112,cc440e349b9b5a57,68.33877724095395,4.5546875 +1,16,256,dense,256,4,27838608,1024,4194304,33208,32768,128,56,524304,07e316e8e83163f0,27186.140625,4096.0 +1,16,256,uniform,4,110,18108468,28160,3722840,41272,32768,8192,56,4112,07e316e8e83163f0,643.0563920454546,132.203125 +1,16,1024,uniform,64,24,20199430,6144,3472896,133432,131072,2048,56,4112,cc440e349b9b5a57,3287.6676432291665,565.25 +1,16,256,tail,4,870,15577488,222720,1332840,41272,32768,8192,56,4112,07e316e8e83163f0,69.94202586206896,5.984375 +1,16,1024,dense,4,2,63503118,512,7458720,164152,131072,32768,56,524304,cc440e349b9b5a57,124029.52734375,14567.8125 +1,16,1024,roots,1,2,60590399,512,8388608,262456,131072,131072,56,65552,cc440e349b9b5a57,118340.623046875,16384.0 +1,16,1024,dense,16,2,58815196,512,7506624,139576,131072,8192,56,524304,cc440e349b9b5a57,114873.4296875,14661.375 +1,16,1024,uniform,1,24,20999687,6144,3279504,262456,131072,131072,56,4112,cc440e349b9b5a57,3417.9178059895835,533.7734375 +1,16,1024,dense,64,2,55368532,512,7682176,133432,131072,2048,56,524304,cc440e349b9b5a57,108141.6640625,15004.25 +1,16,256,roots,64,6,25027166,1536,6291456,33592,32768,512,56,65552,07e316e8e83163f0,16293.727864583334,4096.0 +1,16,256,dense,4,4,28726459,1024,3753408,41272,32768,8192,56,524304,07e316e8e83163f0,28053.1826171875,3665.4375 +1,16,1024,dense,1024,2,59377529,512,8388608,131512,131072,128,56,524304,cc440e349b9b5a57,115971.736328125,16384.0 +1,16,1024,uniform,1024,14,22664872,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6323.904017857143,1024.0 +1,16,1024,tail,16,708,16137451,181248,2899968,139576,131072,8192,56,4112,cc440e349b9b5a57,89.03519487111582,16.0 +1,16,1024,dense,1,2,59209844,512,7446690,262456,131072,131072,56,524304,cc440e349b9b5a57,115644.2265625,14544.31640625 +1,16,1024,tail,1024,14,28440553,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,7935.422154017857,1024.0 +1,16,1024,uniform,16,24,22939408,6144,3323520,139576,131072,8192,56,4112,cc440e349b9b5a57,3733.6276041666665,540.9375 +1,16,1024,roots,4,2,68243319,512,8388608,164152,131072,32768,56,65552,cc440e349b9b5a57,133287.732421875,16384.0 +1,16,1024,uniform,4,22,22284502,5632,3014264,164152,131072,32768,56,4112,cc440e349b9b5a57,3956.7652698863635,535.203125 +1,16,1024,tail,64,384,19017726,98304,6291456,133432,131072,2048,56,4112,cc440e349b9b5a57,193.45831298828125,64.0 +1,16,1024,roots,1024,2,54456218,512,8388608,131512,131072,128,56,65552,cc440e349b9b5a57,106359.80078125,16384.0 +1,16,256,roots,1,6,29430274,1536,6291456,65848,32768,32768,56,65552,07e316e8e83163f0,19160.334635416668,4096.0 +1,16,256,tail,1,968,16229803,247808,1128688,65848,32768,32768,56,4112,07e316e8e83163f0,65.4934586454029,4.5546875 +1,16,256,uniform,16,130,18842745,33280,4590560,35128,32768,2048,56,4112,07e316e8e83163f0,566.1882512019231,137.9375 +1,16,256,roots,256,6,25712943,1536,6291456,33208,32768,128,56,65552,07e316e8e83163f0,16740.197265625,4096.0 +1,16,256,roots,16,6,24999092,1536,6291456,35128,32768,2048,56,65552,07e316e8e83163f0,16275.450520833334,4096.0 +1,16,256,tail,64,384,18145560,98304,6291456,33592,32768,512,56,4112,07e316e8e83163f0,184.586181640625,64.0 +1,16,256,roots,4,4,21467262,1024,4194304,41272,32768,8192,56,65552,07e316e8e83163f0,20964.123046875,4096.0 +1,16,256,dense,1,4,26614572,1024,3729584,65848,32768,32768,56,524304,07e316e8e83163f0,25990.79296875,3642.171875 +1,16,256,dense,16,4,27211406,1024,3842048,35128,32768,2048,56,524304,07e316e8e83163f0,26573.638671875,3752.0 +2,16,1024,dense,64,2,55177792,512,7682176,133432,131072,2048,56,524304,cc440e349b9b5a57,107769.125,15004.25 +2,16,1024,roots,1024,2,56337550,512,8388608,131512,131072,128,56,65552,cc440e349b9b5a57,110034.27734375,16384.0 +2,16,256,uniform,256,74,19340633,18944,4849664,33208,32768,128,56,4112,07e316e8e83163f0,1020.9371304898649,256.0 +2,16,1024,roots,4,2,70127632,512,8388608,164152,131072,32768,56,65552,cc440e349b9b5a57,136968.03125,16384.0 +2,16,256,uniform,16,130,18852485,33280,4590560,35128,32768,2048,56,4112,07e316e8e83163f0,566.4809194711538,137.9375 +2,16,256,uniform,64,124,19214726,31744,5150464,33592,32768,512,56,4112,07e316e8e83163f0,605.3026083669355,162.25 +2,16,256,dense,4,4,29007978,1024,3753408,41272,32768,8192,56,524304,07e316e8e83163f0,28328.103515625,3665.4375 +2,16,1024,roots,64,2,55147181,512,8388608,133432,131072,2048,56,65552,cc440e349b9b5a57,107709.337890625,16384.0 +2,16,1024,uniform,16,24,20757910,6144,3323520,139576,131072,8192,56,4112,cc440e349b9b5a57,3378.5660807291665,540.9375 +2,16,256,roots,16,6,24347186,1536,6291456,35128,32768,2048,56,65552,07e316e8e83163f0,15851.032552083334,4096.0 +2,16,1024,uniform,1024,14,23230334,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6481.678013392857,1024.0 +2,16,1024,dense,1024,2,59534306,512,8388608,131512,131072,128,56,524304,cc440e349b9b5a57,116277.94140625,16384.0 +2,16,1024,uniform,1,24,20842391,6144,3279504,262456,131072,131072,56,4112,cc440e349b9b5a57,3392.3162434895835,533.7734375 +2,16,256,uniform,1,130,19198967,33280,4352140,65848,32768,32768,56,4112,07e316e8e83163f0,576.8920372596153,130.7734375 +2,16,256,uniform,4,110,17994653,28160,3722840,41272,32768,8192,56,4112,07e316e8e83163f0,639.0146661931818,132.203125 +2,16,256,dense,64,4,27086276,1024,4080128,33592,32768,512,56,524304,07e316e8e83163f0,26451.44140625,3984.5 +2,16,256,roots,4,4,21994113,1024,4194304,41272,32768,8192,56,65552,07e316e8e83163f0,21478.6259765625,4096.0 +2,16,1024,roots,16,2,57820049,512,8388608,139576,131072,8192,56,65552,cc440e349b9b5a57,112929.783203125,16384.0 +2,16,256,dense,256,4,27941793,1024,4194304,33208,32768,128,56,524304,07e316e8e83163f0,27286.9072265625,4096.0 +2,16,1024,tail,16,708,16133895,181248,2899968,139576,131072,8192,56,4112,cc440e349b9b5a57,89.01557534427967,16.0 +2,16,1024,tail,64,384,18182820,98304,6291456,133432,131072,2048,56,4112,cc440e349b9b5a57,184.9652099609375,64.0 +2,16,256,tail,256,86,18779634,22016,5636096,33208,32768,128,56,4112,07e316e8e83163f0,852.9993640988372,256.0 +2,16,256,roots,256,6,22007890,1536,6291456,33208,32768,128,56,65552,07e316e8e83163f0,14328.053385416666,4096.0 +2,16,1024,tail,1024,14,22562372,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6295.3046875,1024.0 +2,16,1024,tail,1,912,15219507,233472,1063392,262456,131072,131072,56,4112,cc440e349b9b5a57,65.18771844161184,4.5546875 +2,16,256,tail,1,968,16217302,247808,1128688,65848,32768,32768,56,4112,07e316e8e83163f0,65.4430123321281,4.5546875 +2,16,256,tail,16,730,15889117,186880,2990080,35128,32768,2048,56,4112,07e316e8e83163f0,85.02310038527398,16.0 +2,16,256,roots,64,6,24043260,1536,6291456,33592,32768,512,56,65552,07e316e8e83163f0,15653.1640625,4096.0 +2,16,256,tail,64,384,18128542,98304,6291456,33592,32768,512,56,4112,07e316e8e83163f0,184.4130655924479,64.0 +2,16,256,roots,1,6,28856219,1536,6291456,65848,32768,32768,56,65552,07e316e8e83163f0,18786.600911458332,4096.0 +2,16,256,dense,1,4,26688073,1024,3729584,65848,32768,32768,56,524304,07e316e8e83163f0,26062.5712890625,3642.171875 +2,16,1024,roots,1,2,59012937,512,8388608,262456,131072,131072,56,65552,cc440e349b9b5a57,115259.642578125,16384.0 +2,16,1024,tail,4,834,15146304,213504,1277688,164152,131072,32768,56,4112,cc440e349b9b5a57,70.94154676258992,5.984375 +2,16,1024,uniform,4,22,21112966,5632,3014264,164152,131072,32768,56,4112,cc440e349b9b5a57,3748.751065340909,535.203125 +2,16,1024,uniform,64,24,20180244,6144,3472896,133432,131072,2048,56,4112,cc440e349b9b5a57,3284.544921875,565.25 +2,16,256,dense,16,4,27251979,1024,3842048,35128,32768,2048,56,524304,07e316e8e83163f0,26613.2607421875,3752.0 +2,16,1024,dense,1,2,56115124,512,7446690,262456,131072,131072,56,524304,cc440e349b9b5a57,109599.8515625,14544.31640625 +2,16,256,tail,4,870,15574118,222720,1332840,41272,32768,8192,56,4112,07e316e8e83163f0,69.92689475574713,5.984375 +2,16,1024,dense,4,2,63355452,512,7458720,164152,131072,32768,56,524304,cc440e349b9b5a57,123741.1171875,14567.8125 +2,16,1024,dense,16,2,59015622,512,7506624,139576,131072,8192,56,524304,cc440e349b9b5a57,115264.88671875,14661.375 +3,16,1024,dense,1024,2,59351733,512,8388608,131512,131072,128,56,524304,cc440e349b9b5a57,115921.353515625,16384.0 +3,16,256,tail,64,384,18358098,98304,6291456,33592,32768,512,56,4112,07e316e8e83163f0,186.74822998046875,64.0 +3,16,256,tail,256,86,18854430,22016,5636096,33208,32768,128,56,4112,07e316e8e83163f0,856.3967114825581,256.0 +3,16,1024,tail,4,834,14976119,213504,1277688,164152,131072,32768,56,4112,cc440e349b9b5a57,70.14444225869305,5.984375 +3,16,1024,uniform,1024,14,22676798,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6327.231584821428,1024.0 +3,16,1024,tail,1024,14,22703465,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6334.672154017857,1024.0 +3,16,256,dense,64,4,27086110,1024,4080128,33592,32768,512,56,524304,07e316e8e83163f0,26451.279296875,3984.5 +3,16,256,roots,256,6,25762944,1536,6291456,33208,32768,128,56,65552,07e316e8e83163f0,16772.75,4096.0 +3,16,256,uniform,1,130,19111541,33280,4352140,65848,32768,32768,56,4112,07e316e8e83163f0,574.2650540865385,130.7734375 +3,16,1024,roots,4,2,70026373,512,8388608,164152,131072,32768,56,65552,cc440e349b9b5a57,136770.259765625,16384.0 +3,16,1024,tail,1,912,16096766,233472,1063392,262456,131072,131072,56,4112,cc440e349b9b5a57,68.94516687225877,4.5546875 +3,16,256,dense,16,4,27379350,1024,3842048,35128,32768,2048,56,524304,07e316e8e83163f0,26737.646484375,3752.0 +3,16,256,roots,16,6,24957185,1536,6291456,35128,32768,2048,56,65552,07e316e8e83163f0,16248.167317708334,4096.0 +3,16,256,tail,16,730,15562896,186880,2990080,35128,32768,2048,56,4112,07e316e8e83163f0,83.27748287671233,16.0 +3,16,256,roots,1,6,28879071,1536,6291456,65848,32768,32768,56,65552,07e316e8e83163f0,18801.478515625,4096.0 +3,16,256,dense,256,4,27899146,1024,4194304,33208,32768,128,56,524304,07e316e8e83163f0,27245.259765625,4096.0 +3,16,1024,uniform,16,24,20733021,6144,3323520,139576,131072,8192,56,4112,cc440e349b9b5a57,3374.51513671875,540.9375 +3,16,1024,roots,16,2,58642974,512,8388608,139576,131072,8192,56,65552,cc440e349b9b5a57,114537.05859375,16384.0 +3,16,256,roots,4,4,21769853,1024,4194304,41272,32768,8192,56,65552,07e316e8e83163f0,21259.6220703125,4096.0 +3,16,1024,dense,4,2,63265711,512,7458720,164152,131072,32768,56,524304,cc440e349b9b5a57,123565.841796875,14567.8125 +3,16,256,uniform,4,110,17831783,28160,3722840,41272,32768,8192,56,4112,07e316e8e83163f0,633.2309303977273,132.203125 +3,16,1024,roots,1,2,59387770,512,8388608,262456,131072,131072,56,65552,cc440e349b9b5a57,115991.73828125,16384.0 +3,16,256,dense,1,4,26625294,1024,3729584,65848,32768,32768,56,524304,07e316e8e83163f0,26001.263671875,3642.171875 +3,16,256,uniform,64,124,19081356,31744,5150464,33592,32768,512,56,4112,07e316e8e83163f0,601.1011844758065,162.25 +3,16,1024,roots,64,2,54379514,512,8388608,133432,131072,2048,56,65552,cc440e349b9b5a57,106209.98828125,16384.0 +3,16,256,roots,64,6,23421927,1536,6291456,33592,32768,512,56,65552,07e316e8e83163f0,15248.650390625,4096.0 +3,16,1024,tail,64,384,18163616,98304,6291456,133432,131072,2048,56,4112,cc440e349b9b5a57,184.76985677083334,64.0 +3,16,256,uniform,256,74,19326707,18944,4849664,33208,32768,128,56,4112,07e316e8e83163f0,1020.2020164695946,256.0 +3,16,256,tail,1,968,16443302,247808,1128688,65848,32768,32768,56,4112,07e316e8e83163f0,66.35500871642562,4.5546875 +3,16,256,uniform,16,130,18945597,33280,4590560,35128,32768,2048,56,4112,07e316e8e83163f0,569.2787560096153,137.9375 +3,16,256,tail,4,870,15606951,222720,1332840,41272,32768,8192,56,4112,07e316e8e83163f0,70.07431303879311,5.984375 +3,16,1024,uniform,4,22,21160558,5632,3014264,164152,131072,32768,56,4112,cc440e349b9b5a57,3757.201349431818,535.203125 +3,16,1024,tail,16,708,16127914,181248,2899968,139576,131072,8192,56,4112,cc440e349b9b5a57,88.98257635946328,16.0 +3,16,1024,dense,1,2,56464457,512,7446690,262456,131072,131072,56,524304,cc440e349b9b5a57,110282.142578125,14544.31640625 +3,16,1024,dense,16,2,58168790,512,7506624,139576,131072,8192,56,524304,cc440e349b9b5a57,113610.91796875,14661.375 +3,16,1024,uniform,1,24,21266872,6144,3279504,262456,131072,131072,56,4112,cc440e349b9b5a57,3461.4049479166665,533.7734375 +3,16,1024,uniform,64,24,20355706,6144,3472896,133432,131072,2048,56,4112,cc440e349b9b5a57,3313.1031901041665,565.25 +3,16,1024,roots,1024,2,55149236,512,8388608,131512,131072,128,56,65552,cc440e349b9b5a57,107713.3515625,16384.0 +3,16,256,dense,4,4,28735811,1024,3753408,41272,32768,8192,56,524304,07e316e8e83163f0,28062.3154296875,3665.4375 +3,16,1024,dense,64,2,55532161,512,7682176,133432,131072,2048,56,524304,cc440e349b9b5a57,108461.251953125,15004.25 +4,16,1024,roots,16,2,58275974,512,8388608,139576,131072,8192,56,65552,cc440e349b9b5a57,113820.26171875,16384.0 +4,16,1024,uniform,1,24,20898336,6144,3279504,262456,131072,131072,56,4112,cc440e349b9b5a57,3401.421875,533.7734375 +4,16,1024,tail,4,834,14866877,213504,1277688,164152,131072,32768,56,4112,cc440e349b9b5a57,69.63277971372902,5.984375 +4,16,1024,tail,16,708,16058932,181248,2899968,139576,131072,8192,56,4112,cc440e349b9b5a57,88.60198181497175,16.0 +4,16,1024,roots,64,2,54187366,512,8388608,133432,131072,2048,56,65552,cc440e349b9b5a57,105834.69921875,16384.0 +4,16,256,uniform,4,110,18060098,28160,3722840,41272,32768,8192,56,4112,07e316e8e83163f0,641.3387073863636,132.203125 +4,16,1024,uniform,4,22,21147503,5632,3014264,164152,131072,32768,56,4112,cc440e349b9b5a57,3754.8833451704545,535.203125 +4,16,256,roots,64,6,24374259,1536,6291456,33592,32768,512,56,65552,07e316e8e83163f0,15868.658203125,4096.0 +4,16,1024,dense,4,2,63382933,512,7458720,164152,131072,32768,56,524304,cc440e349b9b5a57,123794.791015625,14567.8125 +4,16,256,dense,64,4,27028257,1024,4080128,33592,32768,512,56,524304,07e316e8e83163f0,26394.7822265625,3984.5 +4,16,1024,tail,64,384,18112820,98304,6291456,133432,131072,2048,56,4112,cc440e349b9b5a57,184.25313313802084,64.0 +4,16,1024,uniform,1024,14,22717760,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6338.660714285715,1024.0 +4,16,1024,dense,1,2,56368123,512,7446690,262456,131072,131072,56,524304,cc440e349b9b5a57,110093.990234375,14544.31640625 +4,16,256,dense,16,4,27298090,1024,3842048,35128,32768,2048,56,524304,07e316e8e83163f0,26658.291015625,3752.0 +4,16,256,tail,256,86,18729560,22016,5636096,33208,32768,128,56,4112,07e316e8e83163f0,850.7249273255813,256.0 +4,16,256,tail,4,870,15490173,222720,1332840,41272,32768,8192,56,4112,07e316e8e83163f0,69.54998653017242,5.984375 +4,16,1024,dense,1024,2,59373955,512,8388608,131512,131072,128,56,524304,cc440e349b9b5a57,115964.755859375,16384.0 +4,16,256,uniform,64,124,19073782,31744,5150464,33592,32768,512,56,4112,07e316e8e83163f0,600.8625882056451,162.25 +4,16,256,roots,16,6,25373629,1536,6291456,35128,32768,2048,56,65552,07e316e8e83163f0,16519.289713541668,4096.0 +4,16,1024,uniform,64,24,20210356,6144,3472896,133432,131072,2048,56,4112,cc440e349b9b5a57,3289.4459635416665,565.25 +4,16,256,uniform,256,74,19265226,18944,4849664,33208,32768,128,56,4112,07e316e8e83163f0,1016.9566089527027,256.0 +4,16,1024,uniform,16,24,20671781,6144,3323520,139576,131072,8192,56,4112,cc440e349b9b5a57,3364.5476888020835,540.9375 +4,16,1024,roots,1024,2,54991477,512,8388608,131512,131072,128,56,65552,cc440e349b9b5a57,107405.228515625,16384.0 +4,16,256,dense,1,4,26646128,1024,3729584,65848,32768,32768,56,524304,07e316e8e83163f0,26021.609375,3642.171875 +4,16,256,roots,1,6,27778090,1536,6291456,65848,32768,32768,56,65552,07e316e8e83163f0,18084.694010416668,4096.0 +4,16,256,tail,16,730,15578766,186880,2990080,35128,32768,2048,56,4112,07e316e8e83163f0,83.36240368150685,16.0 +4,16,1024,tail,1024,14,22511631,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6281.147042410715,1024.0 +4,16,256,dense,256,4,27817794,1024,4194304,33208,32768,128,56,524304,07e316e8e83163f0,27165.814453125,4096.0 +4,16,256,roots,256,6,23618612,1536,6291456,33208,32768,128,56,65552,07e316e8e83163f0,15376.700520833334,4096.0 +4,16,1024,tail,1,912,15076693,233472,1063392,262456,131072,131072,56,4112,cc440e349b9b5a57,64.57602196408992,4.5546875 +4,16,256,dense,4,4,28763201,1024,3753408,41272,32768,8192,56,524304,07e316e8e83163f0,28089.0634765625,3665.4375 +4,16,256,uniform,16,130,18825430,33280,4590560,35128,32768,2048,56,4112,07e316e8e83163f0,565.66796875,137.9375 +4,16,256,uniform,1,130,19054708,33280,4352140,65848,32768,32768,56,4112,07e316e8e83163f0,572.5573317307692,130.7734375 +4,16,256,tail,1,968,16474654,247808,1128688,65848,32768,32768,56,4112,07e316e8e83163f0,66.48152602014463,4.5546875 +4,16,256,roots,4,4,21753002,1024,4194304,41272,32768,8192,56,65552,07e316e8e83163f0,21243.166015625,4096.0 +4,16,1024,roots,4,2,65107229,512,8388608,164152,131072,32768,56,65552,cc440e349b9b5a57,127162.556640625,16384.0 +4,16,1024,roots,1,2,59380214,512,8388608,262456,131072,131072,56,65552,cc440e349b9b5a57,115976.98046875,16384.0 +4,16,1024,dense,64,2,55434866,512,7682176,133432,131072,2048,56,524304,cc440e349b9b5a57,108271.22265625,15004.25 +4,16,1024,dense,16,2,58074048,512,7506624,139576,131072,8192,56,524304,cc440e349b9b5a57,113425.875,14661.375 +4,16,256,tail,64,384,18460486,98304,6291456,33592,32768,512,56,4112,07e316e8e83163f0,187.7897745768229,64.0 +5,16,256,uniform,64,124,19329263,31744,5150464,33592,32768,512,56,4112,07e316e8e83163f0,608.9107547883065,162.25 +5,16,1024,roots,16,2,57375679,512,8388608,139576,131072,8192,56,65552,cc440e349b9b5a57,112061.873046875,16384.0 +5,16,1024,tail,1024,14,22579761,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6300.156529017857,1024.0 +5,16,1024,tail,4,834,14960082,213504,1277688,164152,131072,32768,56,4112,cc440e349b9b5a57,70.0693289118705,5.984375 +5,16,256,tail,1,968,16278580,247808,1128688,65848,32768,32768,56,4112,07e316e8e83163f0,65.69029248450413,4.5546875 +5,16,1024,dense,16,2,57445975,512,7506624,139576,131072,8192,56,524304,cc440e349b9b5a57,112199.169921875,14661.375 +5,16,1024,tail,64,384,18213134,98304,6291456,133432,131072,2048,56,4112,cc440e349b9b5a57,185.2735799153646,64.0 +5,16,256,roots,1,6,27856293,1536,6291456,65848,32768,32768,56,65552,07e316e8e83163f0,18135.607421875,4096.0 +5,16,256,dense,64,4,27057813,1024,4080128,33592,32768,512,56,524304,07e316e8e83163f0,26423.6455078125,3984.5 +5,16,256,dense,1,4,26671387,1024,3729584,65848,32768,32768,56,524304,07e316e8e83163f0,26046.2763671875,3642.171875 +5,16,256,uniform,16,130,18836911,33280,4590560,35128,32768,2048,56,4112,07e316e8e83163f0,566.0129507211539,137.9375 +5,16,1024,uniform,1,24,20823781,6144,3279504,262456,131072,131072,56,4112,cc440e349b9b5a57,3389.2872721354165,533.7734375 +5,16,256,tail,4,870,15190562,222720,1332840,41272,32768,8192,56,4112,07e316e8e83163f0,68.2047503591954,5.984375 +5,16,1024,tail,1,912,15649600,233472,1063392,262456,131072,131072,56,4112,cc440e349b9b5a57,67.02987938596492,4.5546875 +5,16,256,roots,256,6,24620815,1536,6291456,33208,32768,128,56,65552,07e316e8e83163f0,16029.176432291666,4096.0 +5,16,1024,dense,4,2,63311137,512,7458720,164152,131072,32768,56,524304,cc440e349b9b5a57,123654.564453125,14567.8125 +5,16,1024,uniform,4,22,21326706,5632,3014264,164152,131072,32768,56,4112,cc440e349b9b5a57,3786.702059659091,535.203125 +5,16,256,tail,256,86,19151578,22016,5636096,33208,32768,128,56,4112,07e316e8e83163f0,869.8936228197674,256.0 +5,16,256,roots,4,4,22301428,1024,4194304,41272,32768,8192,56,65552,07e316e8e83163f0,21778.73828125,4096.0 +5,16,256,dense,4,4,29056293,1024,3753408,41272,32768,8192,56,524304,07e316e8e83163f0,28375.2861328125,3665.4375 +5,16,256,uniform,256,74,19565633,18944,4849664,33208,32768,128,56,4112,07e316e8e83163f0,1032.8142419763512,256.0 +5,16,256,uniform,4,110,18057894,28160,3722840,41272,32768,8192,56,4112,07e316e8e83163f0,641.2604403409091,132.203125 +5,16,1024,dense,1024,2,60140195,512,8388608,131512,131072,128,56,524304,cc440e349b9b5a57,117461.318359375,16384.0 +5,16,1024,tail,16,708,16254099,181248,2899968,139576,131072,8192,56,4112,cc440e349b9b5a57,89.67877714512711,16.0 +5,16,1024,dense,1,2,56663290,512,7446690,262456,131072,131072,56,524304,cc440e349b9b5a57,110670.48828125,14544.31640625 +5,16,256,dense,256,4,27836682,1024,4194304,33208,32768,128,56,524304,07e316e8e83163f0,27184.259765625,4096.0 +5,16,256,uniform,1,130,19151874,33280,4352140,65848,32768,32768,56,4112,07e316e8e83163f0,575.476983173077,130.7734375 +5,16,256,dense,16,4,27268739,1024,3842048,35128,32768,2048,56,524304,07e316e8e83163f0,26629.6279296875,3752.0 +5,16,1024,roots,1024,2,55194940,512,8388608,131512,131072,128,56,65552,cc440e349b9b5a57,107802.6171875,16384.0 +5,16,256,roots,64,6,24760574,1536,6291456,33592,32768,512,56,65552,07e316e8e83163f0,16120.165364583334,4096.0 +5,16,1024,uniform,16,24,20702411,6144,3323520,139576,131072,8192,56,4112,cc440e349b9b5a57,3369.5330403645835,540.9375 +5,16,256,tail,16,730,15642655,186880,2990080,35128,32768,2048,56,4112,07e316e8e83163f0,83.70427547089041,16.0 +5,16,1024,roots,1,2,58483900,512,8388608,262456,131072,131072,56,65552,cc440e349b9b5a57,114226.3671875,16384.0 +5,16,1024,roots,64,2,54285033,512,8388608,133432,131072,2048,56,65552,cc440e349b9b5a57,106025.455078125,16384.0 +5,16,256,tail,64,384,18131930,98304,6291456,33592,32768,512,56,4112,07e316e8e83163f0,184.4475301106771,64.0 +5,16,1024,uniform,1024,14,22782001,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6356.585100446428,1024.0 +5,16,1024,roots,4,2,70559817,512,8388608,164152,131072,32768,56,65552,cc440e349b9b5a57,137812.142578125,16384.0 +5,16,1024,uniform,64,24,20210762,6144,3472896,133432,131072,2048,56,4112,cc440e349b9b5a57,3289.5120442708335,565.25 +5,16,256,roots,16,6,25681295,1536,6291456,35128,32768,2048,56,65552,07e316e8e83163f0,16719.593098958332,4096.0 +5,16,1024,dense,64,2,55456236,512,7682176,133432,131072,2048,56,524304,cc440e349b9b5a57,108312.9609375,15004.25 +6,16,1024,roots,1024,2,54870791,512,8388608,131512,131072,128,56,65552,cc440e349b9b5a57,107169.513671875,16384.0 +6,16,1024,uniform,16,24,20742114,6144,3323520,139576,131072,8192,56,4112,cc440e349b9b5a57,3375.9951171875,540.9375 +6,16,256,tail,16,730,15687340,186880,2990080,35128,32768,2048,56,4112,07e316e8e83163f0,83.94338613013699,16.0 +6,16,1024,dense,1024,2,59332807,512,8388608,131512,131072,128,56,524304,cc440e349b9b5a57,115884.388671875,16384.0 +6,16,1024,uniform,64,24,20222374,6144,3472896,133432,131072,2048,56,4112,cc440e349b9b5a57,3291.4020182291665,565.25 +6,16,1024,roots,1,2,58901437,512,8388608,262456,131072,131072,56,65552,cc440e349b9b5a57,115041.869140625,16384.0 +6,16,1024,tail,64,384,18025357,98304,6291456,133432,131072,2048,56,4112,cc440e349b9b5a57,183.36341349283853,64.0 +6,16,256,roots,256,6,25008240,1536,6291456,33208,32768,128,56,65552,07e316e8e83163f0,16281.40625,4096.0 +6,16,1024,dense,4,2,63551748,512,7458720,164152,131072,32768,56,524304,cc440e349b9b5a57,124124.5078125,14567.8125 +6,16,1024,roots,64,2,56267698,512,8388608,133432,131072,2048,56,65552,cc440e349b9b5a57,109897.84765625,16384.0 +6,16,256,tail,256,86,18806949,22016,5636096,33208,32768,128,56,4112,07e316e8e83163f0,854.2400526889535,256.0 +6,16,1024,roots,4,2,68814522,512,8388608,164152,131072,32768,56,65552,cc440e349b9b5a57,134403.36328125,16384.0 +6,16,256,dense,256,4,27934608,1024,4194304,33208,32768,128,56,524304,07e316e8e83163f0,27279.890625,4096.0 +6,16,256,tail,4,870,15578303,222720,1332840,41272,32768,8192,56,4112,07e316e8e83163f0,69.94568516522989,5.984375 +6,16,256,dense,4,4,28803219,1024,3753408,41272,32768,8192,56,524304,07e316e8e83163f0,28128.1435546875,3665.4375 +6,16,1024,uniform,4,22,21089521,5632,3014264,164152,131072,32768,56,4112,cc440e349b9b5a57,3744.5882457386365,535.203125 +6,16,1024,tail,4,834,14739063,213504,1277688,164152,131072,32768,56,4112,cc440e349b9b5a57,69.03413050809353,5.984375 +6,16,1024,uniform,1,24,20910965,6144,3279504,262456,131072,131072,56,4112,cc440e349b9b5a57,3403.4773763020835,533.7734375 +6,16,256,roots,4,4,22458464,1024,4194304,41272,32768,8192,56,65552,07e316e8e83163f0,21932.09375,4096.0 +6,16,1024,dense,1,2,56255976,512,7446690,262456,131072,131072,56,524304,cc440e349b9b5a57,109874.953125,14544.31640625 +6,16,256,tail,64,384,18071820,98304,6291456,33592,32768,512,56,4112,07e316e8e83163f0,183.8360595703125,64.0 +6,16,256,tail,1,968,15814117,247808,1128688,65848,32768,32768,56,4112,07e316e8e83163f0,63.81600674715909,4.5546875 +6,16,1024,roots,16,2,57245493,512,8388608,139576,131072,8192,56,65552,cc440e349b9b5a57,111807.603515625,16384.0 +6,16,1024,dense,16,2,57808882,512,7506624,139576,131072,8192,56,524304,cc440e349b9b5a57,112907.97265625,14661.375 +6,16,256,roots,64,6,25547351,1536,6291456,33592,32768,512,56,65552,07e316e8e83163f0,16632.389973958332,4096.0 +6,16,1024,tail,16,708,15922932,181248,2899968,139576,131072,8192,56,4112,cc440e349b9b5a57,87.85162870762711,16.0 +6,16,256,dense,64,4,27213979,1024,4080128,33592,32768,512,56,524304,07e316e8e83163f0,26576.1513671875,3984.5 +6,16,256,uniform,256,74,19487226,18944,4849664,33208,32768,128,56,4112,07e316e8e83163f0,1028.6753589527027,256.0 +6,16,1024,tail,1,912,16061655,233472,1063392,262456,131072,131072,56,4112,cc440e349b9b5a57,68.79478053042763,4.5546875 +6,16,1024,dense,64,2,55728624,512,7682176,133432,131072,2048,56,524304,cc440e349b9b5a57,108844.96875,15004.25 +6,16,256,uniform,4,110,17905042,28160,3722840,41272,32768,8192,56,4112,07e316e8e83163f0,635.8324573863637,132.203125 +6,16,256,dense,16,4,27274238,1024,3842048,35128,32768,2048,56,524304,07e316e8e83163f0,26634.998046875,3752.0 +6,16,256,uniform,1,130,18982300,33280,4352140,65848,32768,32768,56,4112,07e316e8e83163f0,570.3816105769231,130.7734375 +6,16,256,uniform,64,124,19114171,31744,5150464,33592,32768,512,56,4112,07e316e8e83163f0,602.1349231350806,162.25 +6,16,256,roots,1,6,29285699,1536,6291456,65848,32768,32768,56,65552,07e316e8e83163f0,19066.210286458332,4096.0 +6,16,256,dense,1,4,26745535,1024,3729584,65848,32768,32768,56,524304,07e316e8e83163f0,26118.6865234375,3642.171875 +6,16,256,uniform,16,130,18815023,33280,4590560,35128,32768,2048,56,4112,07e316e8e83163f0,565.3552584134616,137.9375 +6,16,256,roots,16,6,24256649,1536,6291456,35128,32768,2048,56,65552,07e316e8e83163f0,15792.089192708334,4096.0 +6,16,1024,uniform,1024,14,22782186,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6356.63671875,1024.0 +6,16,1024,tail,1024,14,22543335,3584,3670016,131512,131072,128,56,4112,cc440e349b9b5a57,6289.993024553572,1024.0 diff --git a/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/summary.csv b/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/summary.csv new file mode 100644 index 0000000..861c9d1 --- /dev/null +++ b/research/experiments/001-checkpoint-granularity/results/2026-09-22-linux-aarch64/summary.csv @@ -0,0 +1,41 @@ +chains,length,workload,block,median_ns_per_tick,min_ns_per_tick,max_ns_per_tick,core_owned_bytes,cache_bytes,evaluations_per_tick +16,256,roots,1,18801.478515625,18084.694010416668,20032.295572916668,65848,32768,4096.0 +16,256,roots,4,21405.203125,20964.123046875,21932.09375,41272,8192,4096.0 +16,256,roots,16,16275.450520833334,15792.089192708334,16719.593098958332,35128,2048,4096.0 +16,256,roots,64,16120.165364583334,15248.650390625,16632.389973958332,33592,512,4096.0 +16,256,roots,256,16029.176432291666,14328.053385416666,16772.75,33208,128,4096.0 +16,256,uniform,1,574.3891526442308,570.3816105769231,576.8920372596153,65848,32768,130.7734375 +16,256,uniform,4,639.0146661931818,633.2309303977273,643.0563920454546,41272,8192,132.203125 +16,256,uniform,16,566.1882512019231,565.3552584134616,569.2787560096153,35128,2048,137.9375 +16,256,uniform,64,602.1349231350806,600.8625882056451,608.9107547883065,33592,512,162.25 +16,256,uniform,256,1020.9371304898649,1016.9566089527027,1032.8142419763512,33208,128,256.0 +16,256,tail,1,65.4934586454029,63.60011783316116,66.48152602014463,65848,32768,4.5546875 +16,256,tail,4,69.92689475574713,68.2047503591954,70.07431303879311,41272,8192,5.984375 +16,256,tail,16,83.94338613013699,83.27748287671233,85.78413420376712,35128,2048,16.0 +16,256,tail,64,184.586181640625,183.8360595703125,187.7897745768229,33592,512,64.0 +16,256,tail,256,856.3967114825581,850.7249273255813,869.8936228197674,33208,128,256.0 +16,256,dense,1,26021.880859375,25990.79296875,26118.6865234375,65848,32768,3642.171875 +16,256,dense,4,28089.0634765625,28053.1826171875,28375.2861328125,41272,8192,3665.4375 +16,256,dense,16,26632.0869140625,26573.638671875,26737.646484375,35128,2048,3752.0 +16,256,dense,64,26451.279296875,26394.7822265625,26613.9482421875,33592,512,3984.5 +16,256,dense,256,27236.0361328125,27165.814453125,27286.9072265625,33208,128,4096.0 +16,1024,roots,1,115259.642578125,114226.3671875,118340.623046875,262456,131072,16384.0 +16,1024,roots,4,136649.09375,127162.556640625,137812.142578125,164152,32768,16384.0 +16,1024,roots,16,112929.783203125,111807.603515625,114537.05859375,139576,8192,16384.0 +16,1024,roots,64,106209.98828125,105137.505859375,109897.84765625,133432,2048,16384.0 +16,1024,roots,1024,107713.3515625,106359.80078125,110034.27734375,131512,128,16384.0 +16,1024,uniform,1,3401.421875,3389.2872721354165,3461.4049479166665,262456,131072,533.7734375 +16,1024,uniform,4,3757.201349431818,3744.5882457386365,3956.7652698863635,164152,32768,535.203125 +16,1024,uniform,16,3374.51513671875,3358.8570963541665,3733.6276041666665,139576,8192,540.9375 +16,1024,uniform,64,3289.5120442708335,3284.544921875,3313.1031901041665,133432,2048,565.25 +16,1024,uniform,1024,6338.660714285715,6323.904017857143,6481.678013392857,131512,128,1024.0 +16,1024,tail,1,67.02987938596492,64.57602196408992,68.94516687225877,262456,131072,4.5546875 +16,1024,tail,4,69.63277971372902,68.53097365857315,70.94154676258992,164152,32768,5.984375 +16,1024,tail,16,88.98257635946328,86.80436197916667,89.67877714512711,139576,8192,16.0 +16,1024,tail,64,184.76985677083334,183.36341349283853,193.45831298828125,133432,2048,64.0 +16,1024,tail,1024,6295.3046875,6281.147042410715,7935.422154017857,131512,128,1024.0 +16,1024,dense,1,110093.990234375,109599.8515625,115644.2265625,262456,131072,14544.31640625 +16,1024,dense,4,123794.791015625,123565.841796875,124339.0625,164152,32768,14567.8125 +16,1024,dense,16,113425.875,111541.220703125,115264.88671875,139576,8192,14661.375 +16,1024,dense,64,108271.22265625,107769.125,108844.96875,133432,2048,15004.25 +16,1024,dense,1024,115971.736328125,115884.388671875,117461.318359375,131512,128,16384.0 diff --git a/research/experiments/001-checkpoint-granularity/run.py b/research/experiments/001-checkpoint-granularity/run.py new file mode 100644 index 0000000..b2d66f2 --- /dev/null +++ b/research/experiments/001-checkpoint-granularity/run.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Build, check, measure, and preserve raw data using only the Python stdlib.""" +import argparse +import csv +import hashlib +import json +import math +import os +from pathlib import Path +import platform +import random +import shlex +import statistics +import subprocess +import tempfile +from datetime import datetime, timezone + + +ROOT = Path(__file__).resolve().parent +SOURCE = ROOT / "benchmark.c" +FLAGS = ["-std=c17", "-O3", "-Wall", "-Wextra", "-Wpedantic", "-Werror"] + + +def command(args): + return subprocess.check_output(args, text=True).strip() + + +def write_csv(path, rows): + with path.open("w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path) + parser.add_argument("--trials", type=int, default=7) + parser.add_argument("--target-ms", type=float, default=20.0) + parser.add_argument("--check-only", action="store_true") + args = parser.parse_args() + if args.trials < 3 or not 1 <= args.target_ms <= 1000: + parser.error("use at least 3 trials and a target from 1 to 1000 ms") + if not args.check_only and args.output is None: + parser.error("--output is required for measurements") + cc = shlex.split(os.environ.get("CC", "cc")) + environment = { + "started_utc": datetime.now(timezone.utc).isoformat(), + "platform": platform.platform(), "machine": platform.machine(), + "compiler": command(cc + ["--version"]), "flags": FLAGS, + "python": platform.python_version(), + "source_sha256": hashlib.sha256(SOURCE.read_bytes()).hexdigest(), + "runner_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + "trials": args.trials, "target_ms": args.target_ms, + "seed": 20260922, "timing": "batch elapsed time / update ticks; not input latency", + "memory": "sizeof engine + requested live array bytes; excludes allocator, harness, code, OS", + "power_control": "not controlled; shared host; preliminary measurements", + } + # One CPU reduces migration noise. This is recorded, not assumed to remove noise. + if hasattr(os, "sched_getaffinity"): + affinity = sorted(os.sched_getaffinity(0)) + environment["original_affinity"] = affinity + os.sched_setaffinity(0, {affinity[0]}) + environment["measurement_affinity"] = sorted(os.sched_getaffinity(0)) + cpuinfo = Path("/proc/cpuinfo") + if cpuinfo.exists(): + allowed = ("model name", "CPU implementer", "CPU architecture", "CPU part", "Model") + environment["cpu_description"] = sorted({ + line for line in cpuinfo.read_text().splitlines() + if line.split(":", 1)[0].strip() in allowed + }) + with tempfile.TemporaryDirectory(prefix="pure-checkpoints-") as tmp: + binary = Path(tmp) / "benchmark" + subprocess.run(cc + FLAGS + [str(SOURCE), "-o", str(binary)], check=True) + environment["release_check"] = json.loads(command([str(binary), "--check"])) + checked = Path(tmp) / "benchmark-checked" + check_flags = ["-std=c17", "-O1", "-g", "-Wall", "-Wextra", "-Wpedantic", "-Werror", + "-fsanitize=address,undefined", "-fno-omit-frame-pointer"] + subprocess.run(cc + check_flags + [str(SOURCE), "-o", str(checked)], check=True) + environment["sanitizer_check"] = json.loads(command([str(checked), "--check"])) + print("Differential checks passed in release and ASan/UBSan builds.", flush=True) + if args.check_only: + print(json.dumps(environment, indent=2)) + return + # Fail before measurements if a previous run would be overwritten. + args.output.mkdir(parents=True, exist_ok=False) + + cases = [] + for chains, length in [(16, 256), (16, 1024)]: + for workload in ["roots", "uniform", "tail", "dense"]: + for block in [1, 4, 16, 64, length]: + cases.append((chains, length, workload, block)) + + def sample(case, replays): + chains, length, workload, block = case + return json.loads(command([str(binary), str(chains), str(length), str(block), + workload, "256", str(replays), str(environment["seed"])])) + + calibration = {} + for case in cases: + pilot = sample(case, 2) + wanted = math.ceil(2 * args.target_ms * 1e6 / max(1, pilot["elapsed_ns"])) + # Even replay counts leave the same final model in every strategy. + calibration[case] = min(2048, max(2, wanted + wanted % 2)) + environment["calibration_replays"] = [ + {"chains": c[0], "length": c[1], "workload": c[2], "block": c[3], "replays": r} + for c, r in calibration.items() + ] + rows = [] + checksums = {} + rng = random.Random(81173) + for trial in range(args.trials): + order = cases.copy() + rng.shuffle(order) + for case in order: + result = sample(case, calibration[case]) + chains, length, workload, block = case + key = (chains, length, workload) + if checksums.setdefault(key, result["checksum"]) != result["checksum"]: + raise RuntimeError(f"cross-strategy output mismatch: {case}") + row = {"trial": trial, "chains": chains, "length": length, + "workload": workload, "block": block, "replays": calibration[case], + **result} + row["ns_per_tick"] = result["elapsed_ns"] / result["ticks"] + row["evaluations_per_tick"] = result["node_evaluations"] / result["ticks"] + rows.append(row) + write_csv(args.output / "raw.csv", rows) + print(f"Completed trial {trial + 1}/{args.trials}.", flush=True) + summary = [] + for case in cases: + chains, length, workload, block = case + group = [r for r in rows if (r["chains"], r["length"], r["workload"], r["block"]) == case] + times = [r["ns_per_tick"] for r in group] + summary.append({"chains": chains, "length": length, "workload": workload, "block": block, + "median_ns_per_tick": statistics.median(times), + "min_ns_per_tick": min(times), "max_ns_per_tick": max(times), + "core_owned_bytes": group[0]["core_owned_bytes"], + "cache_bytes": group[0]["cache_bytes"], + "evaluations_per_tick": group[0]["evaluations_per_tick"]}) + write_csv(args.output / "summary.csv", summary) + environment["completed_utc"] = datetime.now(timezone.utc).isoformat() + environment["binary_bytes"] = binary.stat().st_size + (args.output / "environment.json").write_text(json.dumps(environment, indent=2) + "\n") + print(f"Saved raw data, summary, and environment to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/research/experiments/002-buffer-retention/README.md b/research/experiments/002-buffer-retention/README.md new file mode 100644 index 0000000..d43940d --- /dev/null +++ b/research/experiments/002-buffer-retention/README.md @@ -0,0 +1,165 @@ +# Experiment 002: buffer retention under backpressure + +This is a native C17 comparison apparatus, **not an implementation of the Pure +runtime and not a Sonic, Node.js, or .NET benchmark**. It follows +[research question 006](../../../docs/research/006-problem-landscape.md). + +Question: when a producer selects part of each record and a delayed consumer +retains that selection, what costs remain after applying ordinary copy policies, +bounded queues, pooling, a shared byte budget, and early field projection? + +## Work and observable output + +The CLI reads a sequence of experimental binary frames from stdin. Each frame +has three little-endian uint32 fields: payload byte length, selection offset, +and selection byte length, followed by the complete payload. The output is a +little-endian uint32 selection length followed by the selected bytes. Input order +is preserved. A payload is 1 byte to 8 MiB; a selection is nonempty and in bounds. +The format has no magic, version negotiation, JSON syntax, or production claims. + +The Python generator materializes every byte, with deterministic seed, varying +offsets, and source-size/selection-size distributions. It independently computes +the expected output digest; tests additionally compare bytes against a Python +reference decoder. Every measured run's complete output is checked. Input data is +synthetic, not a production trace. Large temporary fixtures are reproducible from +the saved plan, generator hash, and fixture manifest; they are not checked in. + +All variants read all payload bytes. `project` can discard bytes through a fixed +64 KiB scratch area because this fixture's header supplies the selected range. +That is an intentionally strong existing optimization when a format allows it. +Its results do not establish that arbitrary JSON, compression, or a random-access +document supports the same projection. + +## Variants and allocator + +| Variant | Decision | +|---|---| +| borrow | Keep the complete source buffer until output completes. | +| copy | Copy the selected bytes to an independent buffer, then release the source. | +| threshold2/4/8 | Copy when rounded source capacity / rounded selection capacity is at least 2/4/8. | +| pressure | Copy a smaller selection when current live buffer charges exceed half the usable budget. No future lifetime information. | +| project | Read the selection directly into its output buffer; read and discard the rest through scratch. | +| copy_no_pool | Same copy policy without the reusable buffer cache. | + +`pressure` is a simple candidate online heuristic written in ordinary C. It is +not described as a novel policy or Pure advantage. All thresholds were recorded +before measurement; all results are reported, with no fitted or held-out claim. + +Blocks use power-of-two capacity classes, minimum 64 bytes. Pool-enabled variants +reuse exactly matching classes. At most 16 idle blocks can be cached, consuming +at most one quarter of the total byte budget. Idle blocks are evicted as needed +before admitting another allocation. Pool-disabled runs use the same capacity +rounding so that this comparison isolates reuse rather than size classes. +This small explicit cache is an experimental baseline, not a best-in-class allocator. + +Each record has one owner at a time and there is one FIFO consumer. There is no +fan-out, shared-owner reference counting, persistent result cache, or async device +backend. Copy temporarily needs both source and destination; this overlap is +charged. A budget too small to admit a record returns `budget_too_small` instead +of waiting forever. Existing accepted output may already have been written. + +## Scheduling and budget + +The same single-threaded scheduler serves every policy. A queued output becomes +eligible `hold_us` after enqueue. Consecutive output completions are separated by +at least `service_us`. Ready output takes priority over another input record; +otherwise the producer reads until a queue slot or byte credit is unavailable. +Backpressure then services the oldest queued output, sleeping if needed. + +This uses actual reads, writes, allocations, copies, a monotonic clock, and +`nanosleep`; it is not a simulated memory counter. The consumer delays are +synthetic. A requested 20 us service interval is not a 20 us guarantee; kernel +wakeup granularity and host scheduling affect the realized interval. + +The byte budget includes the engine, queue array, scratch, histogram, pool pointer +array, block headers, active buffers, and cached idle buffers. It bounds requested +storage owned by the apparatus, **not whole-process RSS**. It excludes allocator +bookkeeping, libc/loader, executable pages, other stack frames, kernel I/O buffers, +filesystem page cache, and the external Python verification process. + +## Metrics and measurement limits + +- `peak_reserved_bytes`: maximum fixed apparatus bytes plus active and cached + block charges. `peak_active_bytes` excludes cached idle blocks but includes the + fixed apparatus. Allocator bookkeeping is excluded from both. +- `active_byte_seconds`: sum over active block leases of charged bytes times + elapsed lifetime. Includes transient input/output overlap and block headers; + excludes fixed apparatus storage and idle cache residency. Reused leases are + measured separately, without double counting a block's cached interval. +- `copy_bytes`: only explicit selection `memcpy` bytes. Reads, writes, kernel, + allocator, and generator copies are outside this counter. Zero does not mean + no data movement in the process or system. +- `latency_*`: starts immediately before reading a record header and ends after + its selected output is written. Includes admission waits after reading that + header. It excludes time a record waits outside the process before its header + is read. There is no independent open-loop arrival source, so these are not + service request P99 measurements. P99 is a histogram upper bound (up to 12.5% + bin rounding at normal durations), not an exact percentile. +- `elapsed_ns`: file I/O, deliberate delays, scheduling, instrumentation and + buffer cleanup. Excludes build, process startup, fixtures and verification. + `cpu_ns` uses the process CPU clock; reports deliberate waiting separately. +- `rss_peak_bytes`: approximate current-image `VmHWM` from Linux + `/proc/self/status`, read after the timed region. Zero with source `unavailable` + means no current-image implementation on another OS. `rusage_maxrss_bytes` is + retained separately (Linux KiB normalized to bytes; macOS already bytes). + Linux rusage survives exec and was contaminated by launcher history in the + first run; it cannot substitute for the child's current-image memory. + Neither measurement is added to requested buffer storage. + +Instrumentation is enabled equally in all variants, including block lifetime +timestamps. Its overhead is not separately calibrated in this first experiment. +Timing results must not be presented as uninstrumented application throughput. +Output goes to a regular temporary file without `fsync`; this measures cached +file I/O rather than durable storage. Each variant gets a warm-up run followed by +five fresh-process measured runs in randomized variant order within each case. +Cases run in fixed order. Linux runs pin to one available CPU; thermal and power +conditions are uncontrolled. Medians and ranges are descriptive, not significance +tests. The original plan predates the first measurement. Plan v2 adds an explicitly +exploratory 64-slot sensitivity case after the first run exposed queue occupancy +differences. Original data and source snapshots are preserved with an audit note; +the v2 run is the current result. Neither plan is a production SLO. + +Lower peak RAM can permit a longer queue under the same byte cap and thereby +increase observed in-process latency. Compare queue occupancy and latency as +well as memory; equal byte/slot limits do not imply equal latency. No application +SLO or success threshold has been invented for these synthetic inputs. + +## Reproduction and checks + +From the repository root, using Python 3 and a C compiler with ASan/UBSan: + +```sh +python3 research/experiments/002-buffer-retention/run.py --check-only +python3 research/experiments/002-buffer-retention/run.py --output /tmp/pure-retention-results +``` + +The result directory must not already exist. `CC` selects the compiler. The +runner builds into a temporary directory, verifies normal and sanitized builds, +and saves the plan, source hashes, build flags/hash/size, environment, fixture +digests, checks, every raw sample and summaries. Build size includes debug info. +`complete.json` appears only after all measurements and output hashes pass. +LeakSanitizer needs an execution environment without ptrace tracing; its fatal +diagnostics are treated as a failed verification, never suppressed. + +For direct use: + +```sh +cc -std=c17 -O2 -Wall -Wextra -Wpedantic -Werror research/experiments/002-buffer-retention/benchmark.c -o /tmp/pure-retention +/tmp/pure-retention --policy threshold --ratio 4 --budget 8388608 --hold-us 5000 < input.frames > selected.frames 2> metrics.json +``` + +Exit 0 indicates completion; exit 2 indicates an error; exit 3 indicates +cancellation. SIGINT/SIGTERM cancel synchronous work and release outstanding +buffers. The output may contain a completed prefix or a partially written final +frame on I/O failure. This is not an atomic-output guarantee. Tests include exact +output equivalence, empty input, malformed/truncated input, budget refusal, +deterministic cancellation, real signal cancellation, and `/dev/full` where +available. These synchronous tests do not verify async cancellation races. + +The RSS correction follows the documented preservation of +[resource usage across exec](https://man7.org/linux/man-pages/man2/getrusage.2.html). +Linux documents [VmHWM as approximate](https://man7.org/linux/man-pages/man5/proc_pid_status.5.html); +requested storage counters remain the precise apparatus accounting used here. + +The first result and its interpretation are recorded in +[report 007](../../../docs/research/007-buffer-retention-results.md). diff --git a/research/experiments/002-buffer-retention/benchmark.c b/research/experiments/002-buffer-retention/benchmark.c new file mode 100644 index 0000000..c9e8120 --- /dev/null +++ b/research/experiments/002-buffer-retention/benchmark.c @@ -0,0 +1,338 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* A measurement apparatus, not the Pure runtime or a general document parser. */ +enum { CACHE_SLOTS = 16, SCRATCH = 65536, HIST_SIZE = 512, MAX_FRAME = 8*1024*1024 }; +typedef struct Block { size_t capacity; uint64_t since; unsigned char bytes[]; } Block; +typedef struct Item { + Block *block; + size_t offset, length; + uint64_t start, ready; +} Item; +typedef struct Engine { + Item *queue; + Block *cache[CACHE_SLOTS]; + unsigned char scratch[SCRATCH]; + uint64_t histogram[HIST_SIZE]; + size_t slots, head, count, fixed, budget, reserved, active, cached; + size_t peak_reserved, peak_active, peak_queue; + uint64_t allocations, reuses, copies, read_bytes, written_bytes; + uint64_t accepted, emitted, copied_records, borrowed_records, pressure_copies; + uint64_t budget_waits, slot_waits, sleep_ns, hold_ns, service_ns, last_emit; + uint64_t elapsed_ns, cpu_ns, latency_sum, latency_max, start, cancel_after; + long double active_byte_ns; + unsigned ratio; + bool pool; + const char *policy, *status; +} Engine; +static volatile sig_atomic_t interrupted; +static void stop(int sig) { (void)sig; interrupted = 1; } +static uint64_t ns(clockid_t clock) { + struct timespec t; + if (clock_gettime(clock, &t)) { perror("clock_gettime"); exit(2); } + return (uint64_t)t.tv_sec * UINT64_C(1000000000) + (uint64_t)t.tv_nsec; +} +static size_t charge(const Block *b) { return sizeof(*b) + b->capacity; } +static size_t rounded(size_t n) { + size_t r = 64; + while (r < n) r *= 2; + return r; +} +static void peaks(Engine *e) { + assert(e->active + e->cached == e->reserved); + assert(e->fixed + e->reserved <= e->budget); + if (e->reserved > e->peak_reserved) e->peak_reserved = e->reserved; + if (e->active > e->peak_active) e->peak_active = e->active; +} +static void evict(Engine *e, size_t i) { + Block *b = e->cache[i]; + if (!b) return; + e->cached -= charge(b); e->reserved -= charge(b); + free(b); e->cache[i] = NULL; +} +static Block *take(Engine *e, size_t n) { + size_t capacity = rounded(n), need = sizeof(Block) + capacity; + if (e->pool) { + for (size_t i = 0; i < CACHE_SLOTS; ++i) { + Block *b = e->cache[i]; + if (b && b->capacity == capacity) { + e->cache[i] = NULL; e->cached -= need; e->active += need; + ++e->reuses; b->since = ns(CLOCK_MONOTONIC); peaks(e); return b; + } + } + } + for (size_t i = 0; i < CACHE_SLOTS && need > e->budget-e->fixed-e->reserved; ++i) + evict(e, i); + if (need > e->budget-e->fixed-e->reserved) return NULL; + Block *b = malloc(need); + if (!b) { e->status = "allocation_failed"; return NULL; } + b->capacity = capacity; b->since = ns(CLOCK_MONOTONIC); + e->reserved += need; e->active += need; ++e->allocations; peaks(e); return b; +} +static void give(Engine *e, Block *b) { + if (!b) return; + size_t bytes = charge(b); + e->active_byte_ns += (long double)bytes * (ns(CLOCK_MONOTONIC)-b->since); + e->active -= bytes; + if (e->pool && bytes <= e->budget/4-e->cached) { + for (size_t i = 0; i < CACHE_SLOTS; ++i) if (!e->cache[i]) { + e->cache[i] = b; e->cached += bytes; peaks(e); return; + } + } + e->reserved -= bytes; free(b); peaks(e); +} +static bool sleep_until(Engine *e, uint64_t deadline) { + uint64_t t = ns(CLOCK_MONOTONIC), start = t; + while (t < deadline && !interrupted) { + uint64_t delta = deadline-t; + struct timespec request = {(time_t)(delta/1000000000), (long)(delta%1000000000)}; + if (nanosleep(&request, NULL) && errno != EINTR) { e->status="sleep_failed"; return false; } + t = ns(CLOCK_MONOTONIC); + } + e->sleep_ns += t-start; + return !interrupted; +} +/* floor(log2(ns)) with eight subdivisions: upper bound error <= 12.5%. */ +static unsigned bucket(uint64_t n) { + unsigned exponent = 0; + for (uint64_t x = n; x > 1; x >>= 1) ++exponent; + if (exponent < 3) return (unsigned)n; + return exponent*8 + (unsigned)((n-(UINT64_C(1)<>(exponent-3)); +} +static uint64_t upper(unsigned b) { + if (b < 24) return b+1; + unsigned exponent = b/8; + if (exponent >= 63) return UINT64_MAX; + return (UINT64_C(1)<status = "output_failed"; return false; } + p += written; n -= (size_t)written; e->written_bytes += (uint64_t)written; + } + return !interrupted; +} +static bool emit(Engine *e) { + assert(e->count); + Item *item = &e->queue[e->head]; + uint64_t ready = item->ready; + if (e->last_emit + e->service_ns > ready) ready = e->last_emit + e->service_ns; + if (!sleep_until(e, ready)) return false; + uint32_t len = (uint32_t)item->length; + unsigned char header[4] = {(unsigned char)len, (unsigned char)(len>>8), + (unsigned char)(len>>16), (unsigned char)(len>>24)}; + if (!write_all(e, header, sizeof header) || + !write_all(e, item->block->bytes+item->offset, item->length)) return false; + uint64_t now = ns(CLOCK_MONOTONIC), latency = now-item->start; + ++e->histogram[bucket(latency)]; e->latency_sum += latency; + if (latency > e->latency_max) e->latency_max = latency; + e->last_emit = now; ++e->emitted; + give(e, item->block); item->block = NULL; + e->head = (e->head+1)%e->slots; --e->count; + if (e->cancel_after && e->emitted >= e->cancel_after) { + e->status = "cancelled"; return false; + } + return true; +} +static Block *acquire(Engine *e, size_t n) { + for (;;) { + Block *b = take(e, n); + if (b) return b; + if (strcmp(e->status, "ok")) return NULL; + if (!e->count) { e->status = "budget_too_small"; return NULL; } + ++e->budget_waits; + if (!emit(e)) return NULL; + } +} +/* Returns 1 on complete read, 0 on clean EOF before any byte, -1 on failure. */ +static int read_exact(Engine *e, void *dst, size_t n) { + unsigned char *p = dst; size_t done = 0; + while (done < n && !interrupted) { + ssize_t got = read(STDIN_FILENO, p+done, n-done); + if (got < 0 && errno == EINTR) continue; + if (got < 0) { e->status="input_failed"; return -1; } + if (!got) { if (!done) return 0; e->status="truncated_input"; return -1; } + done += (size_t)got; e->read_bytes += (uint64_t)got; + } + return interrupted ? -1 : 1; +} +static bool discard(Engine *e, size_t n) { + while (n) { + size_t chunk = n < SCRATCH ? n : SCRATCH; + if (read_exact(e, e->scratch, chunk) != 1) { + if (!strcmp(e->status,"ok")) e->status="truncated_input"; + return false; + } + n -= chunk; + } + return true; +} +static uint32_t le32(const unsigned char *p) { + return (uint32_t)p[0] | (uint32_t)p[1]<<8 | (uint32_t)p[2]<<16 | (uint32_t)p[3]<<24; +} +/* Linux's rusage high-water mark survives exec. Read the current mm's HWM + * separately; proc RSS accounting is approximate, not allocator accounting. */ +static uint64_t image_peak_rss(void) { +#if defined(__linux__) + FILE *status = fopen("/proc/self/status", "r"); + if (!status) return 0; + char line[256]; unsigned long long kib = 0; + while (fgets(line, sizeof line, status)) + if (sscanf(line, "VmHWM: %llu kB", &kib) == 1) break; + fclose(status); + return (uint64_t)kib * 1024; +#else + return 0; /* No current-image peak implemented for other OSes. */ +#endif +} +static void run(Engine *e) { + for (;;) { + if (interrupted) break; + if (e->count) { + Item *head = &e->queue[e->head]; + uint64_t now = ns(CLOCK_MONOTONIC); + if (now >= head->ready && now >= e->last_emit+e->service_ns) { + if (!emit(e)) break; + continue; + } + } + if (e->count == e->slots) { ++e->slot_waits; if (!emit(e)) break; } + unsigned char header[12]; + uint64_t start = ns(CLOCK_MONOTONIC); + int got = read_exact(e, header, sizeof header); + if (got != 1) { + if (!got) while (e->count && !interrupted) if (!emit(e)) break; + break; + } + size_t payload = le32(header), offset = le32(header+4), length = le32(header+8); + if (!payload || payload > MAX_FRAME || !length || offset > payload || length > payload-offset) { + e->status="invalid_frame"; break; + } + bool project = !strcmp(e->policy,"project"); + Block *source = acquire(e, project ? length : payload); + if (!source) break; + bool valid; + if (project) { + valid = discard(e, offset) && read_exact(e, source->bytes, length)==1 && + discard(e, payload-offset-length); + offset = 0; + } else valid = read_exact(e, source->bytes, payload)==1; + if (!valid) { + if (!strcmp(e->status,"ok")) e->status="truncated_input"; + give(e, source); break; + } + bool copy = !strcmp(e->policy,"copy") || + (!strcmp(e->policy,"threshold") && rounded(payload)/rounded(length) >= e->ratio); + if (!strcmp(e->policy,"pressure")) { + /* Online rule using current charged live buffers; no future lifetime. */ + copy = rounded(length) < source->capacity && + e->active > (e->budget-e->fixed)/2; + if (copy) ++e->pressure_copies; + } + if (!project && copy) { + Block *small = acquire(e, length); + if (!small) { give(e, source); break; } + memcpy(small->bytes, source->bytes+offset, length); + e->copies += length; ++e->copied_records; + give(e, source); source = small; offset = 0; + } else if (!project) ++e->borrowed_records; + size_t tail = (e->head+e->count)%e->slots; + e->queue[tail] = (Item){source, offset, length, start, ns(CLOCK_MONOTONIC)+e->hold_ns}; + ++e->count; ++e->accepted; + if (e->count > e->peak_queue) e->peak_queue=e->count; + } + if (interrupted) e->status="cancelled"; +} +static uint64_t number(const char *s, uint64_t maximum) { + char *end; errno=0; + unsigned long long n = strtoull(s, &end, 10); + if (!*s || *s=='-' || *end || errno || n>maximum) { fprintf(stderr,"invalid number\n"); exit(2); } + return n; +} +int main(int argc, char **argv) { + Engine *e = calloc(1, sizeof *e); + if (!e) return 2; + e->policy="borrow"; e->status="ok"; e->budget=8*1024*1024; e->slots=512; + e->ratio=4; e->pool=true; + for (int i=1; ipolicy=value; + else if (!strcmp(key,"--budget")) e->budget=(size_t)number(value,1073741824); + else if (!strcmp(key,"--slots")) e->slots=(size_t)number(value,4096); + else if (!strcmp(key,"--hold-us")) e->hold_ns=number(value,10000000)*1000; + else if (!strcmp(key,"--service-us")) e->service_ns=number(value,10000000)*1000; + else if (!strcmp(key,"--ratio")) e->ratio=(unsigned)number(value,UINT_MAX); + else if (!strcmp(key,"--pool")) e->pool=number(value,1)!=0; + else if (!strcmp(key,"--cancel-after")) e->cancel_after=number(value,UINT64_MAX); + else { fprintf(stderr,"unknown option\n"); free(e); return 2; } + } + bool known = !strcmp(e->policy,"borrow") || !strcmp(e->policy,"copy") || + !strcmp(e->policy,"threshold") || !strcmp(e->policy,"pressure") || !strcmp(e->policy,"project"); + if (!known || !e->slots || e->slots>4096 || e->ratio<2 || e->budget>UINT64_C(1073741824) || + e->hold_ns>UINT64_C(10000000000) || e->service_ns>UINT64_C(10000000000)) { + fprintf(stderr,"invalid configuration\n"); free(e); return 2; + } + e->fixed=sizeof(*e)+e->slots*sizeof(Item); + if (e->budget < e->fixed) { fprintf(stderr,"budget below fixed metadata\n"); free(e); return 2; } + e->queue=calloc(e->slots,sizeof(Item)); + if (!e->queue) { free(e); return 2; } + signal(SIGINT,stop); signal(SIGTERM,stop); signal(SIGPIPE,SIG_IGN); + e->start=ns(CLOCK_MONOTONIC); uint64_t cpu=ns(CLOCK_PROCESS_CPUTIME_ID); + run(e); + size_t live_at_stop=e->active, cached_at_stop=e->cached; + while (e->count) { + give(e,e->queue[e->head].block); e->head=(e->head+1)%e->slots; --e->count; + } + for (size_t i=0;iactive && !e->reserved && !e->cached); + e->elapsed_ns=ns(CLOCK_MONOTONIC)-e->start; e->cpu_ns=ns(CLOCK_PROCESS_CPUTIME_ID)-cpu; + uint64_t target=e->emitted-e->emitted/100, sum=0, p99=0; + for (unsigned b=0; e->emitted && bhistogram[b]; if (sum>=target) { p99=upper(b); break; } + } + struct rusage usage; long rss=0; + if (!getrusage(RUSAGE_SELF,&usage)) rss=usage.ru_maxrss; +#if defined(__APPLE__) + uint64_t rusage_bytes=(uint64_t)rss; +#else + uint64_t rusage_bytes=(uint64_t)rss*1024; +#endif + uint64_t rss_bytes=image_peak_rss(); + fprintf(stderr,"{\"status\":\"%s\",\"policy\":\"%s\",\"pool\":%s," + "\"budget_bytes\":%zu,\"fixed_bytes\":%zu,\"peak_reserved_bytes\":%zu," + "\"peak_active_bytes\":%zu,\"peak_queue\":%zu,\"rss_peak_bytes\":%" PRIu64 "," + "\"rusage_maxrss_bytes\":%" PRIu64 ",\"rss_peak_source\":\"%s\"," + "\"elapsed_ns\":%" PRIu64 ",\"cpu_ns\":%" PRIu64 ",\"input_bytes\":%" PRIu64 "," + "\"output_bytes\":%" PRIu64 ",\"accepted\":%" PRIu64 ",\"emitted\":%" PRIu64 "," + "\"copy_bytes\":%" PRIu64 ",\"copied_records\":%" PRIu64 ",\"borrowed_records\":%" PRIu64 "," + "\"pressure_copies\":%" PRIu64 ",\"allocations\":%" PRIu64 ",\"reuses\":%" PRIu64 "," + "\"budget_waits\":%" PRIu64 ",\"slot_waits\":%" PRIu64 ",\"sleep_ns\":%" PRIu64 "," + "\"latency_mean_ns\":%.3f,\"latency_p99_upper_ns\":%" PRIu64 ",\"latency_max_ns\":%" PRIu64 "," + "\"active_byte_seconds\":%.6Lf,\"live_at_stop_bytes\":%zu,\"cached_at_stop_bytes\":%zu," + "\"unreleased_buffer_bytes\":%zu}\n", + e->status,e->policy,e->pool?"true":"false",e->budget,e->fixed,e->fixed+e->peak_reserved, + e->fixed+e->peak_active,e->peak_queue,rss_bytes,rusage_bytes, + rss_bytes?"linux_proc_vmhwm_approximate":"unavailable",e->elapsed_ns,e->cpu_ns,e->read_bytes, + e->written_bytes,e->accepted,e->emitted,e->copies,e->copied_records,e->borrowed_records, + e->pressure_copies,e->allocations,e->reuses,e->budget_waits,e->slot_waits,e->sleep_ns, + e->emitted?(double)e->latency_sum/(double)e->emitted:0,p99,e->latency_max, + e->active_byte_ns/1e9L,live_at_stop,cached_at_stop,e->reserved); + int result=!strcmp(e->status,"ok")?0:!strcmp(e->status,"cancelled")?3:2; + free(e->queue); free(e); return result; +} diff --git a/research/experiments/002-buffer-retention/plan.json b/research/experiments/002-buffer-retention/plan.json new file mode 100644 index 0000000..1737f45 --- /dev/null +++ b/research/experiments/002-buffer-retention/plan.json @@ -0,0 +1,35 @@ +{ + "version": 2, + "written_before_measurement": true, + "revision_note": "The v1 run is preserved. Before this v2 run, RSS instrumentation was corrected and a 64-slot sparse sensitivity case was added after observing different queue occupancy. This is an exploratory follow-up, not a pre-registered confirmation.", + "seed": 20260922, + "repeats": 5, + "slots": 512, + "policies": [ + {"name": "borrow", "policy": "borrow", "pool": 1}, + {"name": "copy", "policy": "copy", "pool": 1}, + {"name": "threshold2", "policy": "threshold", "ratio": 2, "pool": 1}, + {"name": "threshold4", "policy": "threshold", "ratio": 4, "pool": 1}, + {"name": "threshold8", "policy": "threshold", "ratio": 8, "pool": 1}, + {"name": "pressure", "policy": "pressure", "pool": 1}, + {"name": "project", "policy": "project", "pool": 1}, + {"name": "copy_no_pool", "policy": "copy", "pool": 0} + ], + "cases": [ + {"name": "sparse_slow", "shape": "sparse", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "sparse_slots64", "shape": "sparse", "records": 2048, "budget": 8388608, "slots": 64, "hold_us": 5000, "service_us": 20}, + {"name": "dense_slow", "shape": "dense", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "mixed_slow", "shape": "mixed", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "mixed_tight", "shape": "mixed", "records": 2048, "budget": 2097152, "hold_us": 5000, "service_us": 20}, + {"name": "sparse_fast", "shape": "sparse", "records": 2048, "budget": 8388608, "hold_us": 0, "service_us": 0} + ], + "expectations": [ + "Borrow retains full source allocations for small selections.", + "Copy can add CPU and transient source plus destination memory for dense selections.", + "A current-pressure rule need not outperform a static size rule or early projection.", + "Pool reuse can reduce allocation calls while increasing reserved idle memory.", + "No Pure-specific advantage, application SLO, or statistical significance is presumed." + ], + "comparison_rule": "Report all presets and repetitions; no post-hoc winner is called an out-of-sample tuned policy.", + "limits": "Single FIFO local pipeline with synthetic delayed sink. No Sonic/Node/.NET benchmark, fan-out, persistent cache, generic JSON parser, external open-loop arrivals, or production SLO." +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/complete.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/complete.json new file mode 100644 index 0000000..0aa14f4 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/complete.json @@ -0,0 +1,5 @@ +{ + "samples": 240, + "all_output_hashes_match": true, + "finished_utc": "2026-09-22T12:24:32Z" +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/environment.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/environment.json new file mode 100644 index 0000000..b2b8d87 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/environment.json @@ -0,0 +1,57 @@ +{ + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "python": "3.14.7", + "cpu_affinity": 0, + "compiler": "cc (Debian 14.2.0-19) 14.2.0\nCopyright (C) 2024 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n", + "builds": [ + { + "command": [ + "cc", + "-std=c17", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-O2", + "-g", + "/home/eunhhu/work/pure/research/experiments/002-buffer-retention/benchmark.c", + "-o", + "/tmp/pure-retention-ktukd04y/bench" + ], + "stderr": "", + "sha256": "61e0dcf961714992da8d8f2846a09caeda95ce1ac183e2b82dbb80828c6ae43c", + "size_bytes": 101072 + }, + { + "command": [ + "cc", + "-std=c17", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-O2", + "-g", + "-fsanitize=address,undefined", + "-fno-omit-frame-pointer", + "/home/eunhhu/work/pure/research/experiments/002-buffer-retention/benchmark.c", + "-o", + "/tmp/pure-retention-ktukd04y/bench-sanitized" + ], + "stderr": "", + "sha256": "a4a383b9a31ec0d3cd2ac60a1697d863f0424a303608c69ed6faff969dd5d6de", + "size_bytes": 257680 + } + ], + "source_sha256": { + "benchmark.c": "8ac37467fd01421f23924d0e92c890cc161dbd7b3d55642ce91d47e8f12a1e52", + "run.py": "f40df6109d1270afa3a3241b4d22c6842d3dbf360ccf3f563ec067df5f9b1fb3", + "plan.json": "0e07742173d3a415af1791e2d92b3ce9319152c3bca8dfbe6e3cd8de6e13a9f4" + }, + "clock": "CLOCK_MONOTONIC; CPU = CLOCK_PROCESS_CPUTIME_ID", + "timing": "includes file IO, scheduling, deliberate sleep, metrics, buffer cleanup; excludes startup/build/fixture/verification", + "limitations": "shared host; unregulated thermals; cached files; output page cache, no fsync; synthetic closed-loop arrivals", + "started_utc": "2026-09-22T12:23:38Z", + "cpuinfo": "processor\t: 0\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 1\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 2\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 3\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nRevision\t: d04171\nSerial\t\t: d3ba8e625dbe42ac\nModel\t\t: Raspberry Pi 5 Model B Rev 1.1\n" +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/fixtures.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/fixtures.json new file mode 100644 index 0000000..2fe2bb6 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/fixtures.json @@ -0,0 +1,68 @@ +[ + { + "path": "input.frames", + "shape": "sparse", + "records": 2048, + "seed": 20260922, + "input_bytes": 134242304, + "input_sha256": "161377ac76f4b7d35b13db7edf3b93bb279ddd83f6514051020e8b49e743d1c0", + "expected_output_bytes": 139264, + "expected_output_sha256": "a01c7bff45f73512f3bc0dbd02d7c3a1f4ceb09f574d7c25b926cd3d30bb1bfa", + "case": "sparse_slow" + }, + { + "path": "input.frames", + "shape": "sparse", + "records": 2048, + "seed": 20260922, + "input_bytes": 134242304, + "input_sha256": "161377ac76f4b7d35b13db7edf3b93bb279ddd83f6514051020e8b49e743d1c0", + "expected_output_bytes": 139264, + "expected_output_sha256": "a01c7bff45f73512f3bc0dbd02d7c3a1f4ceb09f574d7c25b926cd3d30bb1bfa", + "case": "sparse_slots64" + }, + { + "path": "input.frames", + "shape": "dense", + "records": 2048, + "seed": 20260922, + "input_bytes": 134242304, + "input_sha256": "fb28c2da0af227079bd6888c3f549fa5a48abd0b790c3939dc330111e80c0b7f", + "expected_output_bytes": 100671488, + "expected_output_sha256": "d96dd7f80d696895e66521630fd30f13adabf63bea3b84b0a709fbc8c4095aa5", + "case": "dense_slow" + }, + { + "path": "input.frames", + "shape": "mixed", + "records": 2048, + "seed": 20260922, + "input_bytes": 177631232, + "input_sha256": "5cfcb8e74d5f30c3a611f373ddf53fd5bc843841d83de42c763e1d931403521d", + "expected_output_bytes": 53735632, + "expected_output_sha256": "f6b4eb83e7ca9aa4b5b2e0675d34b656815c82c992700ee9cbd7e0a15594cb0b", + "case": "mixed_slow" + }, + { + "path": "input.frames", + "shape": "mixed", + "records": 2048, + "seed": 20260922, + "input_bytes": 177631232, + "input_sha256": "5cfcb8e74d5f30c3a611f373ddf53fd5bc843841d83de42c763e1d931403521d", + "expected_output_bytes": 53735632, + "expected_output_sha256": "f6b4eb83e7ca9aa4b5b2e0675d34b656815c82c992700ee9cbd7e0a15594cb0b", + "case": "mixed_tight" + }, + { + "path": "input.frames", + "shape": "sparse", + "records": 2048, + "seed": 20260922, + "input_bytes": 134242304, + "input_sha256": "161377ac76f4b7d35b13db7edf3b93bb279ddd83f6514051020e8b49e743d1c0", + "expected_output_bytes": 139264, + "expected_output_sha256": "a01c7bff45f73512f3bc0dbd02d7c3a1f4ceb09f574d7c25b926cd3d30bb1bfa", + "case": "sparse_fast" + } +] diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/plan.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/plan.json new file mode 100644 index 0000000..23db315 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/plan.json @@ -0,0 +1,113 @@ +{ + "version": 2, + "written_before_measurement": true, + "revision_note": "The v1 run is preserved. Before this v2 run, RSS instrumentation was corrected and a 64-slot sparse sensitivity case was added after observing different queue occupancy. This is an exploratory follow-up, not a pre-registered confirmation.", + "seed": 20260922, + "repeats": 5, + "slots": 512, + "policies": [ + { + "name": "borrow", + "policy": "borrow", + "pool": 1 + }, + { + "name": "copy", + "policy": "copy", + "pool": 1 + }, + { + "name": "threshold2", + "policy": "threshold", + "ratio": 2, + "pool": 1 + }, + { + "name": "threshold4", + "policy": "threshold", + "ratio": 4, + "pool": 1 + }, + { + "name": "threshold8", + "policy": "threshold", + "ratio": 8, + "pool": 1 + }, + { + "name": "pressure", + "policy": "pressure", + "pool": 1 + }, + { + "name": "project", + "policy": "project", + "pool": 1 + }, + { + "name": "copy_no_pool", + "policy": "copy", + "pool": 0 + } + ], + "cases": [ + { + "name": "sparse_slow", + "shape": "sparse", + "records": 2048, + "budget": 8388608, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "sparse_slots64", + "shape": "sparse", + "records": 2048, + "budget": 8388608, + "slots": 64, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "dense_slow", + "shape": "dense", + "records": 2048, + "budget": 8388608, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "mixed_slow", + "shape": "mixed", + "records": 2048, + "budget": 8388608, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "mixed_tight", + "shape": "mixed", + "records": 2048, + "budget": 2097152, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "sparse_fast", + "shape": "sparse", + "records": 2048, + "budget": 8388608, + "hold_us": 0, + "service_us": 0 + } + ], + "expectations": [ + "Borrow retains full source allocations for small selections.", + "Copy can add CPU and transient source plus destination memory for dense selections.", + "A current-pressure rule need not outperform a static size rule or early projection.", + "Pool reuse can reduce allocation calls while increasing reserved idle memory.", + "No Pure-specific advantage, application SLO, or statistical significance is presumed." + ], + "comparison_rule": "Report all presets and repetitions; no post-hoc winner is called an out-of-sample tuned policy.", + "limits": "Single FIFO local pipeline with synthetic delayed sink. No Sonic/Node/.NET benchmark, fan-out, persistent cache, generic JSON parser, external open-loop arrivals, or production SLO." +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.csv b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.csv new file mode 100644 index 0000000..b9160ae --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.csv @@ -0,0 +1,241 @@ +case,variant,repeat,status,policy,pool,budget_bytes,fixed_bytes,peak_reserved_bytes,peak_active_bytes,peak_queue,rss_peak_bytes,rusage_maxrss_bytes,rss_peak_source,elapsed_ns,cpu_ns,input_bytes,output_bytes,accepted,emitted,copy_bytes,copied_records,borrowed_records,pressure_copies,allocations,reuses,budget_waits,slot_waits,sleep_ns,latency_mean_ns,latency_p99_upper_ns,latency_max_ns,active_byte_seconds,live_at_stop_bytes,cached_at_stop_bytes,unreleased_buffer_bytes +sparse_slow,copy,0,ok,copy,True,8388608,90560,197072,197072,512,1687552,27246592,linux_proc_vmhwm_approximate,160143537,26459012,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1524,137390253,34696998.27,41943040,41171715,6863.022101,0,66752,0 +sparse_slow,threshold4,0,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,157308101,31229598,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1480,130450222,33900498.29,41943040,39440182,6941.796558,0,66752,0 +sparse_slow,threshold2,0,ok,threshold,True,8388608,90560,197072,197072,512,1720320,27246592,linux_proc_vmhwm_approximate,155496866,24536501,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1532,134789830,33617642.818,41943040,38582611,6573.377656,0,66752,0 +sparse_slow,copy_no_pool,0,ok,copy,False,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,155097886,24003453,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1537,135038766,33344720.225,41943040,38228742,6474.089644,0,0,0 +sparse_slow,project,0,ok,project,True,8388608,90560,131520,131520,512,1654784,27246592,linux_proc_vmhwm_approximate,154147870,24607892,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1536,133284576,33266783.578,37748736,37610633,5450.353596,0,1280,0 +sparse_slow,threshold8,0,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,159785538,24425471,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1534,139188295,34401146.148,41943040,40673920,6694.694537,0,66752,0 +sparse_slow,borrow,0,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,107974911,48356230,134242304,139264,2048,2048,0,0,2048,0,126,1922,1100,0,61689899,6299431.416,9437184,9257676,842052.147088,0,1048832,0 +sparse_slow,pressure,0,ok,pressure,True,8388608,90560,4321888,4318208,512,5832704,27246592,linux_proc_vmhwm_approximate,153730223,27269441,134242304,139264,2048,2048,113984,1781,267,1781,514,3315,0,1480,130232809,32945208.971,41943040,38336797,525465.223488,0,590528,0 +sparse_slow,borrow,1,ok,borrow,True,8388608,90560,8350112,8350112,126,9469952,27246592,linux_proc_vmhwm_approximate,106374934,48221069,134242304,139264,2048,2048,0,0,2048,0,126,1922,1095,0,60129493,6201710.718,9437184,9283991,829040.966005,0,1048832,0 +sparse_slow,copy_no_pool,1,ok,copy,False,8388608,90560,197072,197072,512,1687552,27246592,linux_proc_vmhwm_approximate,160320037,25896167,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1525,138363559,34524425.147,41943040,40334217,6779.619735,0,0,0 +sparse_slow,threshold2,1,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,160914091,25835642,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1519,138817024,34925615.31,46137344,42672599,6869.973543,0,66752,0 +sparse_slow,threshold4,1,ok,threshold,True,8388608,90560,197072,197072,512,1687552,27246592,linux_proc_vmhwm_approximate,154184073,23590708,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134309537,33299901.746,37748736,37858502,6477.65682,0,66752,0 +sparse_slow,threshold8,1,ok,threshold,True,8388608,90560,197072,197072,512,1687552,27246592,linux_proc_vmhwm_approximate,154103999,23187579,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134573427,33283383.04,37748736,37891279,6458.021349,0,66752,0 +sparse_slow,project,1,ok,project,True,8388608,90560,131520,131520,512,1638400,27246592,linux_proc_vmhwm_approximate,157860470,28682548,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1516,133519836,34200243.611,41943040,39812385,5603.286251,0,1280,0 +sparse_slow,pressure,1,ok,pressure,True,8388608,90560,4321888,4316848,512,5849088,27246592,linux_proc_vmhwm_approximate,157264009,27287611,134242304,139264,2048,2048,113408,1772,276,1772,514,3306,0,1470,133795355,32665634.037,41943040,42380378,542285.660244,0,656000,0 +sparse_slow,copy,1,ok,copy,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,159054504,24707642,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1536,138332046,34466956.647,41943040,41828861,6705.336146,0,66752,0 +sparse_slow,threshold4,2,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,155128904,25543272,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1516,133541383,33533790.057,41943040,38416722,6610.674553,0,66752,0 +sparse_slow,copy_no_pool,2,ok,copy,False,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,154376239,24141589,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1537,134070369,33348471.497,37748736,37918872,6490.973039,0,0,0 +sparse_slow,threshold2,2,ok,threshold,True,8388608,90560,197072,197072,512,1720320,27246592,linux_proc_vmhwm_approximate,154320869,23690719,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134338369,33321815.102,37748736,37742483,6490.329618,0,66752,0 +sparse_slow,pressure,2,ok,pressure,True,8388608,90560,4321888,4318768,512,5832704,27246592,linux_proc_vmhwm_approximate,151916969,26295422,134242304,139264,2048,2048,114176,1784,264,1784,514,3318,0,1489,129297978,32537118.75,37748736,37291707,513114.693547,0,590528,0 +sparse_slow,copy,2,ok,copy,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,154140610,23189876,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134662671,33298003.958,37748736,37866187,6456.771667,0,66752,0 +sparse_slow,project,2,ok,project,True,8388608,90560,131520,131520,512,1654784,27246592,linux_proc_vmhwm_approximate,154094443,24230112,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1537,133556453,33259314.794,37748736,37585114,5449.134094,0,1280,0 +sparse_slow,threshold8,2,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,154652646,24092189,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1534,134423307,33402890.832,37748736,37866335,6511.54798,0,66752,0 +sparse_slow,borrow,2,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,129845439,45497318,134242304,139264,2048,2048,0,0,2048,0,126,1922,1501,0,87228642,7656609.5,9437184,9287806,1022546.229446,0,1048832,0 +sparse_slow,project,3,ok,project,True,8388608,90560,131520,131520,512,1638400,27246592,linux_proc_vmhwm_approximate,154249202,24700764,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1537,133261937,33273652.65,37748736,37535317,5451.483102,0,1280,0 +sparse_slow,borrow,3,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,137924379,44501920,134242304,139264,2048,2048,0,0,2048,0,126,1922,1704,0,96547980,8155969.084,9437184,9259085,1088953.248436,0,1048832,0 +sparse_slow,pressure,3,ok,pressure,True,8388608,90560,4321888,4318128,512,5849088,27246592,linux_proc_vmhwm_approximate,152102968,26353180,134242304,139264,2048,2048,113984,1781,267,1781,514,3315,0,1489,129366758,32586897.428,37748736,37391725,520107.889954,0,590528,0 +sparse_slow,threshold8,3,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,154077370,23197132,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134591611,33277920.587,37748736,37816187,6453.368258,0,66752,0 +sparse_slow,threshold2,3,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,154022055,23087202,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134592710,33270464.792,37748736,37834187,6450.425541,0,66752,0 +sparse_slow,copy,3,ok,copy,True,8388608,90560,197072,197072,512,1720320,27246592,linux_proc_vmhwm_approximate,159640187,27408125,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1444,128856855,34666958.779,46137344,43466300,7402.36243,0,66752,0 +sparse_slow,copy_no_pool,3,ok,copy,False,8388608,90560,197072,197072,512,1720320,27246592,linux_proc_vmhwm_approximate,155595440,25939365,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1537,134098060,33573592.059,41943040,37962687,6570.059349,0,0,0 +sparse_slow,threshold4,3,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,156340011,29539317,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1516,131960968,33615642.627,41943040,38156538,6719.71558,0,66752,0 +sparse_slow,copy_no_pool,4,ok,copy,False,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,162066143,36838768,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1413,131813978,34186582.359,46137344,44277743,7046.191505,0,0,0 +sparse_slow,copy,4,ok,copy,True,8388608,90560,197072,197072,512,1720320,27246592,linux_proc_vmhwm_approximate,154776516,28160466,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1467,130788967,33183203.845,41943040,39792181,6689.95279,0,66752,0 +sparse_slow,threshold4,4,ok,threshold,True,8388608,90560,197072,197072,512,1720320,27246592,linux_proc_vmhwm_approximate,154788109,23986070,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134524839,33403921.35,41943040,37878020,6520.274636,0,66752,0 +sparse_slow,threshold8,4,ok,threshold,True,8388608,90560,197072,197072,512,1720320,27246592,linux_proc_vmhwm_approximate,154198518,23692568,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1536,134186642,33304172.097,37748736,37802706,6492.439639,0,66752,0 +sparse_slow,borrow,4,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,141446795,44722082,134242304,139264,2048,2048,0,0,2048,0,126,1922,1729,0,99993943,8382742.796,9437184,9272177,1119170.099864,0,1048832,0 +sparse_slow,threshold2,4,ok,threshold,True,8388608,90560,197072,197072,512,1703936,27246592,linux_proc_vmhwm_approximate,154113537,24093342,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,133716023,33269305.586,37748736,37632576,6504.166448,0,66752,0 +sparse_slow,pressure,4,ok,pressure,True,8388608,90560,4321888,4318688,512,5849088,27246592,linux_proc_vmhwm_approximate,154850386,27901892,134242304,139264,2048,2048,114112,1783,265,1783,514,3317,0,1468,130766867,33232147.448,41943040,39951922,524904.925261,0,590528,0 +sparse_slow,project,4,ok,project,True,8388608,90560,131520,131520,512,1638400,27246592,linux_proc_vmhwm_approximate,157284342,26489150,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1510,134748166,33888730.885,41943040,39144776,5552.251467,0,1280,0 +sparse_slots64,project,0,ok,project,True,8388608,72640,77760,77760,64,1572864,27246592,linux_proc_vmhwm_approximate,167149794,26582076,134242304,139264,2048,2048,0,0,0,0,64,1984,0,1974,144371389,5130099.306,7864320,8958215,840.438441,0,1280,0 +sparse_slots64,threshold2,0,ok,threshold,True,8388608,72640,143312,143312,64,1622016,27246592,linux_proc_vmhwm_approximate,167663571,26966696,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1961,144092606,5142490.023,7864320,9061177,2058.951339,0,66752,0 +sparse_slots64,copy_no_pool,0,ok,copy,False,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,168181366,26871012,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1965,145357926,5163779.497,7864320,8918178,2024.80935,0,0,0 +sparse_slots64,borrow,0,ok,borrow,True,8388608,72640,4267968,4267968,64,5292032,27246592,linux_proc_vmhwm_approximate,167216739,43926056,134242304,139264,2048,2048,0,0,2048,0,64,1984,0,1888,126773157,5126680.831,7340032,7718922,688195.924843,0,1048832,0 +sparse_slots64,pressure,0,ok,pressure,True,8388608,72640,4268048,4268048,64,5767168,27246592,linux_proc_vmhwm_approximate,167185425,45108627,134242304,139264,2048,2048,2048,32,2016,32,65,2015,0,1714,125584463,5128742.545,6291456,6639537,677659.57207,0,1048832,0 +sparse_slots64,copy,0,ok,copy,True,8388608,72640,143312,143312,64,1622016,27246592,linux_proc_vmhwm_approximate,168358292,24933808,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1982,147266280,5168010.066,7864320,9057715,1951.688735,0,66752,0 +sparse_slots64,threshold4,0,ok,threshold,True,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,166820573,24630999,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1982,145973315,5129540.053,7864320,9056604,1927.278894,0,66752,0 +sparse_slots64,threshold8,0,ok,threshold,True,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,166777389,24643191,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1983,145938953,5126620.092,7864320,9062437,1926.54795,0,66752,0 +sparse_slots64,pressure,1,ok,pressure,True,8388608,72640,4268048,4268048,64,5783552,27246592,linux_proc_vmhwm_approximate,167023277,43214247,134242304,139264,2048,2048,2048,32,2016,32,65,2015,0,1923,127480383,5125737.978,7340032,8171940,677260.281048,0,1048832,0 +sparse_slots64,threshold2,1,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,167465182,25416979,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1982,146065308,5147482.869,7864320,9043937,1953.686249,0,66752,0 +sparse_slots64,threshold4,1,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,168266032,25648470,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1982,146554478,5170189.028,7864320,8853271,1979.901257,0,66752,0 +sparse_slots64,copy_no_pool,1,ok,copy,False,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,167352998,26167797,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1982,145161257,5140939.162,7864320,9037974,1975.871965,0,0,0 +sparse_slots64,threshold8,1,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,167352294,26149112,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1982,145164575,5140639.04,7864320,9055067,1998.899707,0,66752,0 +sparse_slots64,project,1,ok,project,True,8388608,72640,77760,77760,64,1589248,27246592,linux_proc_vmhwm_approximate,167537794,26582472,134242304,139264,2048,2048,0,0,0,0,64,1984,0,1983,144932270,5144362.881,7864320,9070492,842.776507,0,1280,0 +sparse_slots64,copy,1,ok,copy,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,177068451,34395657,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1964,148816905,5443811.067,9437184,9497139,2287.854324,0,66752,0 +sparse_slots64,borrow,1,ok,borrow,True,8388608,72640,4267968,4267968,64,5292032,27246592,linux_proc_vmhwm_approximate,169327288,47272489,134242304,139264,2048,2048,0,0,2048,0,64,1984,0,1559,125366124,5174744.393,7864320,7999125,694635.953387,0,1048832,0 +sparse_slots64,threshold4,2,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,166910296,25474992,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1982,145193155,5128818.551,7864320,9004900,1987.487947,0,66752,0 +sparse_slots64,copy_no_pool,2,ok,copy,False,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,167128888,26220522,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1980,144715144,5134877.701,7864320,9062770,2002.587942,0,0,0 +sparse_slots64,threshold2,2,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,167148609,25957735,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1977,145051954,5131955.022,7864320,9073752,1998.277517,0,66752,0 +sparse_slots64,threshold8,2,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,169611954,32276554,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1914,142094763,5206380.796,7864320,8901622,2300.08379,0,66752,0 +sparse_slots64,project,2,ok,project,True,8388608,72640,77760,77760,64,1589248,27246592,linux_proc_vmhwm_approximate,171072487,27568676,134242304,139264,2048,2048,0,0,0,0,64,1984,0,1973,147623196,5258257.061,7864320,9512213,861.43609,0,1280,0 +sparse_slots64,copy,2,ok,copy,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,169338770,27312589,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1973,146307420,5201400.59,7864320,9016640,2055.277015,0,66752,0 +sparse_slots64,pressure,2,ok,pressure,True,8388608,72640,4268048,4268048,64,5767168,27246592,linux_proc_vmhwm_approximate,170783358,46177893,134242304,139264,2048,2048,2048,32,2016,32,65,2015,0,1534,127950932,5236882.494,7340032,7222257,691939.11555,0,1048832,0 +sparse_slots64,borrow,2,ok,borrow,True,8388608,72640,4267968,4267968,64,5292032,27246592,linux_proc_vmhwm_approximate,167156999,43876108,134242304,139264,2048,2048,0,0,2048,0,64,1984,0,1884,127058453,5126798.896,6815744,6645517,688213.378653,0,1048832,0 +sparse_slots64,threshold4,3,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,166770166,25212214,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1984,145249611,5126749.497,7864320,9015048,1964.863682,0,66752,0 +sparse_slots64,copy,3,ok,copy,True,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,167909533,27850375,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1956,144091763,5137672.764,7864320,9072455,2094.171152,0,66752,0 +sparse_slots64,threshold8,3,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,168528180,32536976,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1897,140338194,5169990.139,7864320,8583716,2319.792062,0,66752,0 +sparse_slots64,copy_no_pool,3,ok,copy,False,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,166715000,24659146,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1984,145786851,5125611.619,7864320,9016234,1919.204343,0,0,0 +sparse_slots64,threshold2,3,ok,threshold,True,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,167074462,26663004,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1972,144308149,5134098.172,7864320,9056067,2041.253864,0,66752,0 +sparse_slots64,project,3,ok,project,True,8388608,72640,77760,77760,64,1572864,27246592,linux_proc_vmhwm_approximate,166784685,25542781,134242304,139264,2048,2048,0,0,0,0,64,1984,0,1985,144995208,5128089.141,7864320,9028215,840.114114,0,1280,0 +sparse_slots64,pressure,3,ok,pressure,True,8388608,72640,4268048,4268048,64,5750784,27246592,linux_proc_vmhwm_approximate,167121239,44137216,134242304,139264,2048,2048,2048,32,2016,32,65,2015,0,1831,126710351,5127462.262,7340032,7853292,677483.645644,0,1048832,0 +sparse_slots64,borrow,3,ok,borrow,True,8388608,72640,4267968,4267968,64,5292032,27246592,linux_proc_vmhwm_approximate,167212479,44714000,134242304,139264,2048,2048,0,0,2048,0,64,1984,0,1813,126101064,5127098.881,6291456,6795851,688253.763142,0,1048832,0 +sparse_slots64,threshold8,4,ok,threshold,True,8388608,72640,143312,143312,64,1622016,27246592,linux_proc_vmhwm_approximate,166717963,24687036,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1983,145714382,5125971.683,7864320,8962826,1940.992711,0,66752,0 +sparse_slots64,copy,4,ok,copy,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,166680851,24341624,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1985,146024444,5125321.995,7864320,9060326,1923.416256,0,66752,0 +sparse_slots64,threshold4,4,ok,threshold,True,8388608,72640,143312,143312,64,1654784,27246592,linux_proc_vmhwm_approximate,166829203,24368638,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1984,146127572,5127589.86,7864320,9064418,1922.285206,0,66752,0 +sparse_slots64,project,4,ok,project,True,8388608,72640,77760,77760,64,1589248,27246592,linux_proc_vmhwm_approximate,167505794,25505816,134242304,139264,2048,2048,0,0,0,0,64,1984,0,1985,145758292,5149157.839,7864320,9024826,843.565622,0,1280,0 +sparse_slots64,copy_no_pool,4,ok,copy,False,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,167165609,25932182,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1976,145075045,5133720.043,7864320,9029714,1986.249813,0,0,0 +sparse_slots64,borrow,4,ok,borrow,True,8388608,72640,4267968,4267968,64,5292032,27246592,linux_proc_vmhwm_approximate,167597886,45634483,134242304,139264,2048,2048,0,0,2048,0,64,1984,0,1768,125625299,5136396.266,7340032,8047606,689499.931113,0,1048832,0 +sparse_slots64,threshold2,4,ok,threshold,True,8388608,72640,143312,143312,64,1638400,27246592,linux_proc_vmhwm_approximate,166724425,24304145,134242304,139264,2048,2048,131072,2048,0,0,65,4031,0,1984,146112652,5125215.218,7864320,8987715,1915.595622,0,66752,0 +sparse_slots64,pressure,4,ok,pressure,True,8388608,72640,4268048,4268048,64,5750784,27246592,linux_proc_vmhwm_approximate,167102943,43204463,134242304,139264,2048,2048,2048,32,2016,32,65,2015,0,1902,127501222,5127130.23,7340032,7758552,677439.045656,0,1048832,0 +dense_slow,copy_no_pool,0,ok,copy,False,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,181746660,79318230,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1923,0,106431291,10743611.753,12582912,12983832,1442582.012105,0,0,0 +dense_slow,threshold4,0,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,161215386,89862451,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1244,0,73999062,9530310.653,11534336,11475412,1273441.862727,0,1048832,0 +dense_slow,pressure,0,ok,pressure,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,181545716,79933333,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1794,0,105524896,10805826.457,12582912,13598424,1442046.73663,0,1048832,0 +dense_slow,threshold2,0,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,179808887,82476393,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1739,0,101034766,10695483.804,12582912,13755645,1427516.577261,0,1048832,0 +dense_slow,copy,0,ok,copy,True,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,181070273,83392187,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1923,0,101645153,10688072.436,12582912,13270480,1435135.645514,0,1048832,0 +dense_slow,borrow,0,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,182712953,81359541,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1784,0,105215055,10867028.691,12582912,13321591,1450265.685197,0,1048832,0 +dense_slow,project,0,ok,project,True,8388608,90560,8350112,8350112,126,9469952,27246592,linux_proc_vmhwm_approximate,187716865,74974355,134242304,100671488,2048,2048,0,0,0,0,126,1922,1885,0,116741671,11188037.068,12582912,13610887,1492582.952313,0,1048832,0 +dense_slow,threshold8,0,ok,threshold,True,8388608,90560,8350112,8350112,126,9469952,27246592,linux_proc_vmhwm_approximate,177819652,80765616,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1732,0,100790270,10569593.51,12582912,12515889,1410709.704111,0,1048832,0 +dense_slow,copy_no_pool,1,ok,copy,False,8388608,90560,8350112,8350112,125,9469952,27246592,linux_proc_vmhwm_approximate,183713376,79010560,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1923,0,108825186,10852089.214,12582912,12891240,1457141.599471,0,0,0 +dense_slow,threshold8,1,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,181690178,80219667,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1787,0,105238489,10811191.05,12582912,13211998,1442791.441175,0,1048832,0 +dense_slow,project,1,ok,project,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,187516958,75386397,134242304,100671488,2048,2048,0,0,0,0,126,1922,1881,0,116242907,11180449.479,12582912,13778256,1491587.457419,0,1048832,0 +dense_slow,pressure,1,ok,pressure,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,181604364,78951924,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1764,0,106543831,10804561.506,12582912,13821367,1441898.713593,0,1048832,0 +dense_slow,copy,1,ok,copy,True,8388608,90560,8350112,8350112,125,9469952,27246592,linux_proc_vmhwm_approximate,162629252,88543490,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1913,0,77289755,9526763.21,12582912,13455924,1279274.820821,0,1048832,0 +dense_slow,threshold2,1,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,177629041,82100973,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1640,0,98770708,10557136.903,12582912,13384591,1409274.391678,0,1048832,0 +dense_slow,borrow,1,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,176146657,82398748,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1605,0,97290225,10456864.841,12582912,12598464,1395956.019109,0,1048832,0 +dense_slow,threshold4,1,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,177871837,82048383,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1692,0,99804043,10550904.139,12582912,13302146,1408308.914356,0,1048832,0 +dense_slow,copy,2,ok,copy,True,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,178298984,83177513,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1922,0,99077474,10508907.261,12582912,13207943,1411086.299499,0,1048832,0 +dense_slow,project,2,ok,project,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,181467845,82821089,134242304,100671488,2048,2048,0,0,0,0,126,1922,1672,0,102626015,10774249.202,12582912,12846870,1438045.689091,0,1048832,0 +dense_slow,threshold2,2,ok,threshold,True,8388608,90560,8350112,8350112,126,9469952,27246592,linux_proc_vmhwm_approximate,133633132,106591389,134242304,100671488,2048,2048,0,0,2048,0,126,1922,396,0,28102711,7778565.752,11534336,12128873,1042347.024547,0,1048832,0 +dense_slow,threshold4,2,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,177934893,82638851,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1719,0,98938213,10577573.546,12582912,12061188,1411845.015325,0,1048832,0 +dense_slow,pressure,2,ok,pressure,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,180208516,83532190,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1755,0,100354141,10704952.958,11534336,11578059,1428781.613196,0,1048832,0 +dense_slow,copy_no_pool,2,ok,copy,False,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,185672333,80542923,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1923,0,109123678,10977524.849,12582912,13368332,1473966.510893,0,0,0 +dense_slow,threshold8,2,ok,threshold,True,8388608,90560,8350112,8350112,126,9469952,27246592,linux_proc_vmhwm_approximate,184526170,78362511,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1834,0,110126299,10988073.928,12582912,13728757,1466214.025114,0,1048832,0 +dense_slow,borrow,2,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,186194683,78514601,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1865,0,111800399,11071491.94,12582912,13571164,1477291.36261,0,1048832,0 +dense_slow,project,3,ok,project,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,181552086,80699818,134242304,100671488,2048,2048,0,0,0,0,126,1922,1690,0,104861831,10716792.58,12582912,12527594,1430323.101776,0,1048832,0 +dense_slow,threshold2,3,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,176007991,84507344,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1626,0,94771244,10450519.416,12582912,13672942,1395156.109095,0,1048832,0 +dense_slow,threshold4,3,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,152737854,99861953,134242304,100671488,2048,2048,0,0,2048,0,126,1922,890,0,54158702,8983717.571,12582912,12746037,1201774.933886,0,1048832,0 +dense_slow,copy,3,ok,copy,True,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,184530299,78669541,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1923,0,109915619,10914646.174,12582912,13287517,1465541.553301,0,1048832,0 +dense_slow,copy_no_pool,3,ok,copy,False,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,133391651,112766017,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1523,0,20003852,7733517.169,11534336,11792151,1038602.867364,0,0,0 +dense_slow,borrow,3,ok,borrow,True,8388608,90560,8350112,8350112,126,9469952,27246592,linux_proc_vmhwm_approximate,166473036,87150086,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1399,0,82606050,9857584.602,12582912,13713590,1316650.677161,0,1048832,0 +dense_slow,threshold8,3,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,154311406,92958406,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1082,0,62034758,9096496.741,11534336,11453763,1216142.805053,0,1048832,0 +dense_slow,pressure,3,ok,pressure,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,172422001,86572890,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1520,0,89162361,10222342.367,12582912,13171721,1365000.503032,0,1048832,0 +dense_slow,copy,4,ok,copy,True,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,182776175,83801034,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1923,0,102610607,10792342.655,12582912,13409628,1449128.406768,0,1048832,0 +dense_slow,project,4,ok,project,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,172321224,93701381,134242304,100671488,2048,2048,0,0,0,0,126,1922,1317,0,81313113,10121009.211,13631488,13629442,1352204.310615,0,1048832,0 +dense_slow,threshold8,4,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,161726717,90653564,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1291,0,73742733,9543033.955,12582912,13496479,1275079.33543,0,1048832,0 +dense_slow,copy_no_pool,4,ok,copy,False,8388608,90560,8350112,8350112,125,9486336,27246592,linux_proc_vmhwm_approximate,168343995,87047828,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1917,0,84775336,9903397.208,12582912,13360535,1329804.191236,0,0,0 +dense_slow,threshold4,4,ok,threshold,True,8388608,90560,8350112,8350112,126,9469952,27246592,linux_proc_vmhwm_approximate,150178547,91472405,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1089,0,61163642,8764951.531,12582912,12700278,1171761.488413,0,1048832,0 +dense_slow,borrow,4,ok,borrow,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,160544406,90636057,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1220,0,72541038,9456018.519,13631488,12662167,1263635.432461,0,1048832,0 +dense_slow,threshold2,4,ok,threshold,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,177206635,83537344,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1659,0,97328864,10529385.897,11534336,12256668,1405563.035864,0,1048832,0 +dense_slow,pressure,4,ok,pressure,True,8388608,90560,8350112,8350112,126,9486336,27246592,linux_proc_vmhwm_approximate,169205769,93894859,134242304,100671488,2048,2048,0,0,2048,0,126,1922,1358,0,77830920,9905630.789,14680064,15320344,1323477.613603,0,1048832,0 +mixed_slow,pressure,0,ok,pressure,True,8388608,90560,8388560,8388448,351,11059200,27246592,linux_proc_vmhwm_approximate,157820358,72816171,177631232,53735632,2048,2048,27916032,1699,349,1699,2118,1629,983,0,87435519,20627967.734,27262976,27848623,1145134.388634,0,522496,0 +mixed_slow,project,0,ok,project,True,8388608,90560,8388608,8388608,359,11059200,27246592,linux_proc_vmhwm_approximate,152663243,58871841,177631232,53735632,2048,2048,0,0,0,0,1712,336,1020,0,96564951,20645944.552,29360128,28536362,1105768.632536,0,458688,0 +mixed_slow,threshold8,0,ok,threshold,True,8388608,90560,8388480,8388432,218,10928128,27246592,linux_proc_vmhwm_approximate,154865144,68111825,177631232,53735632,2048,2048,3824848,1008,1040,0,1854,1202,1088,0,89372126,12505668.119,15728640,16314249,1173514.267706,0,1250624,0 +mixed_slow,copy_no_pool,0,ok,copy,False,8388608,90560,8388544,8388544,351,11042816,27246592,linux_proc_vmhwm_approximate,147730720,59461567,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1091,0,90844456,19373026.656,27262976,27720475,1058083.377077,0,0,0 +mixed_slow,threshold4,0,ok,threshold,True,8388608,90560,8388608,8388608,284,11042816,27246592,linux_proc_vmhwm_approximate,154278831,65116485,177631232,53735632,2048,2048,11229392,1357,691,0,1939,1466,1101,0,91754428,15979998.057,20971520,21200642,1146944.244493,0,1153216,0 +mixed_slow,copy,0,ok,copy,True,8388608,90560,8388576,8388432,351,11583488,27246592,linux_proc_vmhwm_approximate,148231627,61867221,177631232,53735632,2048,2048,53727440,2048,0,0,2282,1814,1066,0,88853774,19421431.677,27262976,27565124,1061176.084143,0,522496,0 +mixed_slow,threshold2,0,ok,threshold,True,8388608,90560,8388560,8388432,351,11583488,27246592,linux_proc_vmhwm_approximate,151434543,63073539,177631232,53735632,2048,2048,28479696,1740,308,0,2181,1607,1055,0,90878850,19880247.948,27262976,27687586,1085460.729712,0,522496,0 +mixed_slow,borrow,0,ok,borrow,True,8388608,90560,8386736,8386688,112,11042816,27246592,linux_proc_vmhwm_approximate,155306143,76020257,177631232,53735632,2048,2048,0,0,2048,0,1309,739,1057,0,81695551,6922951.601,9437184,8980622,1223077.166921,0,667904,0 +mixed_slow,copy,1,ok,copy,True,8388608,90560,8388560,8388432,350,11059200,27246592,linux_proc_vmhwm_approximate,160379239,76853903,177631232,53735632,2048,2048,53727440,2048,0,0,2142,1954,945,0,86445280,20559126.612,31457280,31399761,1126649.918907,0,519424,0 +mixed_slow,threshold2,1,ok,threshold,True,8388608,90560,8388560,8388432,351,11059200,27246592,linux_proc_vmhwm_approximate,154875385,62824900,177631232,53735632,2048,2048,28479696,1740,308,0,2184,1604,1064,0,94824069,20387628.756,29360128,28330122,1114766.522227,0,522496,0 +mixed_slow,borrow,1,ok,borrow,True,8388608,90560,8386736,8386688,112,11059200,27246592,linux_proc_vmhwm_approximate,160560980,82185381,177631232,53735632,2048,2048,0,0,2048,0,1298,750,1014,0,81045212,7165375.219,9437184,9128899,1265871.620641,0,667904,0 +mixed_slow,threshold8,1,ok,threshold,True,8388608,90560,8388480,8388432,218,10895360,27246592,linux_proc_vmhwm_approximate,158376930,70010099,177631232,53735632,2048,2048,3824848,1008,1040,0,1840,1216,1069,0,91117649,12818618.589,16777216,16509452,1202455.090015,0,1250624,0 +mixed_slow,threshold4,1,ok,threshold,True,8388608,90560,8388608,8388608,284,11059200,27246592,linux_proc_vmhwm_approximate,158364004,67436217,177631232,53735632,2048,2048,11229392,1357,691,0,1915,1490,1082,0,93660619,16427087.508,23068672,21701826,1180098.881536,0,1153216,0 +mixed_slow,pressure,1,ok,pressure,True,8388608,90560,8388560,8388432,351,11059200,27246592,linux_proc_vmhwm_approximate,150598212,62794080,177631232,53735632,2048,2048,27916032,1699,349,1699,2194,1553,1045,0,90472858,19666383.09,27262976,27539921,1091553.931133,0,522496,0 +mixed_slow,copy_no_pool,1,ok,copy,False,8388608,90560,8388432,8388432,351,11042816,27246592,linux_proc_vmhwm_approximate,147750665,60136047,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1079,0,90065234,19383039.199,27262976,27513476,1059302.637881,0,0,0 +mixed_slow,project,1,ok,project,True,8388608,90560,8388608,8388608,359,11583488,27246592,linux_proc_vmhwm_approximate,161035903,71350453,177631232,53735632,2048,2048,0,0,0,0,1684,364,940,0,92840414,21900800.478,31457280,29845877,1171929.531923,0,458688,0 +mixed_slow,copy_no_pool,2,ok,copy,False,8388608,90560,8388432,8388432,344,11059200,27246592,linux_proc_vmhwm_approximate,156784527,75333533,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,936,0,83839404,20060454.119,27262976,27533828,1101855.462406,0,0,0 +mixed_slow,project,2,ok,project,True,8388608,90560,8388608,8388608,359,11042816,27246592,linux_proc_vmhwm_approximate,155323458,61447221,177631232,53735632,2048,2048,0,0,0,0,1698,350,1001,0,96575508,21013565.399,29360128,28307510,1125180.461018,0,458688,0 +mixed_slow,copy,2,ok,copy,True,8388608,90560,8388576,8388544,351,11583488,27246592,linux_proc_vmhwm_approximate,148908587,62691660,177631232,53735632,2048,2048,53727440,2048,0,0,2295,1801,1060,0,88867276,19542842.845,27262976,27366569,1068483.42084,0,522496,0 +mixed_slow,threshold8,2,ok,threshold,True,8388608,90560,8388480,8388432,218,10895360,27246592,linux_proc_vmhwm_approximate,155250569,68193969,177631232,53735632,2048,2048,3824848,1008,1040,0,1856,1200,1091,0,89649032,12540132.329,15728640,16196379,1176609.476886,0,1250624,0 +mixed_slow,threshold4,2,ok,threshold,True,8388608,90560,8388608,8388608,284,11059200,27246592,linux_proc_vmhwm_approximate,153993443,64368954,177631232,53735632,2048,2048,11229392,1357,691,0,1951,1454,1113,0,92271541,15971202.74,20971520,20927051,1146423.83916,0,1153216,0 +mixed_slow,borrow,2,ok,borrow,True,8388608,90560,8386736,8386688,112,11059200,27246592,linux_proc_vmhwm_approximate,156530676,80398481,177631232,53735632,2048,2048,0,0,2048,0,1289,759,1012,0,78480840,6972909.934,9437184,9059956,1232378.631527,0,667904,0 +mixed_slow,threshold2,2,ok,threshold,True,8388608,90560,8388560,8388432,351,11010048,27246592,linux_proc_vmhwm_approximate,150924192,62137633,177631232,53735632,2048,2048,28479696,1740,308,0,2185,1603,1066,0,91396144,19908205.041,27262976,27188866,1087150.034862,0,522496,0 +mixed_slow,pressure,2,ok,pressure,True,8388608,90560,8388560,8388432,351,11042816,27246592,linux_proc_vmhwm_approximate,151817449,63923421,177631232,53735632,2048,2048,27916032,1699,349,1699,2188,1559,1043,0,90636475,19811286.8,27262976,27547513,1100686.98981,0,522496,0 +mixed_slow,pressure,3,ok,pressure,True,8388608,90560,8388560,8388432,351,11059200,27246592,linux_proc_vmhwm_approximate,150346490,61910200,177631232,53735632,2048,2048,27916032,1699,349,1699,2191,1556,1052,0,90957629,19620014.043,27262976,27598753,1088594.710179,0,522496,0 +mixed_slow,borrow,3,ok,borrow,True,8388608,90560,8386736,8386736,112,11059200,27246592,linux_proc_vmhwm_approximate,154664385,75260445,177631232,53735632,2048,2048,0,0,2048,0,1309,739,1060,0,81727723,6891592.543,9437184,9042474,1217311.080951,0,667904,0 +mixed_slow,copy_no_pool,3,ok,copy,False,8388608,90560,8388448,8388448,351,11583488,27246592,linux_proc_vmhwm_approximate,149074586,62457772,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1070,0,89108781,19570212.309,27262976,27735827,1068598.834234,0,0,0 +mixed_slow,threshold2,3,ok,threshold,True,8388608,90560,8388560,8388448,351,11010048,27246592,linux_proc_vmhwm_approximate,151416673,62409227,177631232,53735632,2048,2048,28479696,1740,308,0,2196,1592,1072,0,91608903,19945957.829,27262976,27499217,1089269.648492,0,522496,0 +mixed_slow,threshold8,3,ok,threshold,True,8388608,90560,8388480,8388432,218,10928128,27246592,linux_proc_vmhwm_approximate,154832681,68708379,177631232,53735632,2048,2048,3824848,1008,1040,0,1844,1212,1080,0,88746131,12500210.727,15728640,16133805,1173410.180784,0,1250624,0 +mixed_slow,threshold4,3,ok,threshold,True,8388608,90560,8388608,8388608,284,11059200,27246592,linux_proc_vmhwm_approximate,153954221,64939588,177631232,53735632,2048,2048,11229392,1357,691,0,1938,1467,1106,0,91640646,15956645.429,20971520,20876513,1145843.306835,0,1153216,0 +mixed_slow,copy,3,ok,copy,True,8388608,90560,8388576,8388432,348,11026432,27246592,linux_proc_vmhwm_approximate,154316312,71405240,177631232,53735632,2048,2048,53727440,2048,0,0,2185,1911,984,0,85159635,20042799.471,27262976,27766253,1098260.452799,0,522496,0 +mixed_slow,project,3,ok,project,True,8388608,90560,8388608,8388608,359,11567104,27246592,linux_proc_vmhwm_approximate,153157278,59550541,177631232,53735632,2048,2048,0,0,0,0,1715,333,1025,0,96249111,20737608.082,29360128,28456288,1110823.213775,0,458688,0 +mixed_slow,copy,4,ok,copy,True,8388608,90560,8388576,8388432,351,11583488,27246592,linux_proc_vmhwm_approximate,148559421,60872675,177631232,53735632,2048,2048,53727440,2048,0,0,2291,1805,1082,0,90209021,19484771.246,27262976,27464198,1064901.546602,0,522496,0 +mixed_slow,threshold2,4,ok,threshold,True,8388608,90560,8388560,8388432,351,11042816,27246592,linux_proc_vmhwm_approximate,152463225,62941260,177631232,53735632,2048,2048,28479696,1740,308,0,2190,1598,1076,0,92180791,20063222.989,27262976,27720123,1096182.913762,0,522496,0 +mixed_slow,project,4,ok,project,True,8388608,90560,8388608,8388608,359,11567104,27246592,linux_proc_vmhwm_approximate,156226158,60922941,177631232,53735632,2048,2048,0,0,0,0,1712,336,1009,0,98129027,21028903.054,31457280,30198524,1124769.099854,0,458688,0 +mixed_slow,threshold8,4,ok,threshold,True,8388608,90560,8388480,8388432,218,10534912,27246592,linux_proc_vmhwm_approximate,159080169,72479922,177631232,53735632,2048,2048,3824848,1008,1040,0,1823,1233,1052,0,89038311,12767575.616,16777216,17017117,1198559.835478,0,1250624,0 +mixed_slow,borrow,4,ok,borrow,True,8388608,90560,8386736,8386688,112,11059200,27246592,linux_proc_vmhwm_approximate,160859182,83693169,177631232,53735632,2048,2048,0,0,2048,0,1286,762,998,0,79719697,7168367.113,9437184,9348362,1265838.726396,0,667904,0 +mixed_slow,copy_no_pool,4,ok,copy,False,8388608,90560,8388432,8388432,350,11059200,27246592,linux_proc_vmhwm_approximate,153665517,63556619,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1061,0,92811834,20225367.343,29360128,27663216,1106709.142867,0,0,0 +mixed_slow,threshold4,4,ok,threshold,True,8388608,90560,8388608,8388608,284,11059200,27246592,linux_proc_vmhwm_approximate,154918866,67062426,177631232,53735632,2048,2048,11229392,1357,691,0,1917,1488,1084,0,90513202,16065937.041,20971520,20956347,1153471.713074,0,1153216,0 +mixed_slow,pressure,4,ok,pressure,True,8388608,90560,8388560,8388432,351,11026432,27246592,linux_proc_vmhwm_approximate,150607823,62564984,177631232,53735632,2048,2048,27916032,1699,349,1699,2185,1562,1048,0,90639036,19672453.154,27262976,27555809,1091824.795529,0,522496,0 +mixed_tight,pressure,0,ok,pressure,True,2097152,90560,2097136,2097136,113,4243456,27246592,linux_proc_vmhwm_approximate,190186189,62042049,177631232,53735632,2048,2048,28385168,1727,321,1727,2373,1402,1378,0,130967488,6127305.056,8388608,9614324,346183.559002,0,523712,0 +mixed_tight,project,0,ok,project,True,2097152,90560,2097120,2097120,118,4227072,27246592,linux_proc_vmhwm_approximate,175306603,59313331,177631232,53735632,2048,2048,0,0,0,0,1720,328,1300,0,118791611,6273925.584,8388608,8853437,326279.178906,0,406848,0 +mixed_tight,threshold4,0,ok,threshold,True,2097152,90560,2097136,2097104,88,4227072,27246592,linux_proc_vmhwm_approximate,225451606,64792167,177631232,53735632,2048,2048,11229392,1357,691,0,2134,1271,1457,0,163697658,5851904.688,7864320,10247748,413690.587373,0,435392,0 +mixed_tight,copy,0,ok,copy,True,2097152,90560,2097152,2097136,115,4227072,27246592,linux_proc_vmhwm_approximate,191774166,62795054,177631232,53735632,2048,2048,53727440,2048,0,0,2467,1629,1361,0,131825729,6045168.869,8388608,8688826,346452.547175,0,523712,0 +mixed_tight,copy_no_pool,0,ok,copy,False,2097152,90560,2097136,2097136,107,4243456,27246592,linux_proc_vmhwm_approximate,195726691,69455356,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1284,0,128917157,6157981.172,9437184,9013123,353204.287515,0,0,0 +mixed_tight,threshold8,0,ok,threshold,True,2097152,90560,2097152,2097136,73,4227072,27246592,linux_proc_vmhwm_approximate,284917692,71405280,177631232,53735632,2048,2048,3824848,1008,1040,0,1986,1070,1377,0,216762211,5735627.926,7864320,9131844,525336.273255,0,386176,0 +mixed_tight,borrow,0,ok,borrow,True,2097152,90560,2094128,2094128,40,3702784,27246592,linux_proc_vmhwm_approximate,493631884,81275705,177631232,53735632,2048,2048,0,0,2048,0,1338,710,1381,0,416164894,5487868.994,8388608,10310562,930117.776585,0,495872,0 +mixed_tight,threshold2,0,ok,threshold,True,2097152,90560,2097136,2097136,115,4227072,27246592,linux_proc_vmhwm_approximate,188818619,63133028,177631232,53735632,2048,2048,28479696,1740,308,0,2370,1418,1385,0,128526332,6127166.122,8388608,8910974,342945.952067,0,523712,0 +mixed_tight,project,1,ok,project,True,2097152,90560,2097120,2097120,118,4243456,27246592,linux_proc_vmhwm_approximate,179280258,63072197,177631232,53735632,2048,2048,0,0,0,0,1706,342,1292,0,119263134,6405571.191,9437184,9266621,333023.673744,0,406848,0 +mixed_tight,threshold8,1,ok,threshold,True,2097152,90560,2097136,2097136,72,4358144,27246592,linux_proc_vmhwm_approximate,289852215,73110582,177631232,53735632,2048,2048,3824848,1008,1040,0,1968,1088,1366,0,216619289,5811531.61,10485760,14653198,533913.89253,0,386176,0 +mixed_tight,copy_no_pool,1,ok,copy,False,2097152,90560,2097136,2097136,117,4243456,27246592,linux_proc_vmhwm_approximate,192676756,64089681,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1353,0,131367991,6074626.45,9437184,9595565,347823.24917,0,0,0 +mixed_tight,pressure,1,ok,pressure,True,2097152,90560,2097136,2097136,115,4456448,27246592,linux_proc_vmhwm_approximate,190841576,65297407,177631232,53735632,2048,2048,28385168,1727,321,1727,2364,1411,1367,0,128364836,6138653.731,8388608,9614713,347120.008423,0,523712,0 +mixed_tight,copy,1,ok,copy,True,2097152,90560,2097152,2097136,114,4407296,27246592,linux_proc_vmhwm_approximate,191186483,62157319,177631232,53735632,2048,2048,53727440,2048,0,0,2461,1635,1369,0,131673734,6044094.394,8388608,8856067,346056.920887,0,523712,0 +mixed_tight,threshold2,1,ok,threshold,True,2097152,90560,2097136,2097136,116,4227072,27246592,linux_proc_vmhwm_approximate,189607302,65204064,177631232,53735632,2048,2048,28479696,1740,308,0,2354,1434,1374,0,127228274,6129531.969,8388608,8917400,344084.726971,0,523712,0 +mixed_tight,threshold4,1,ok,threshold,True,2097152,90560,2097136,2097104,88,4227072,27246592,linux_proc_vmhwm_approximate,225029662,63936677,177631232,53735632,2048,2048,11229392,1357,691,0,2132,1273,1459,0,164083415,5845044.948,7864320,10255656,413025.125756,0,435392,0 +mixed_tight,borrow,1,ok,borrow,True,2097152,90560,2094128,2094128,40,3719168,27246592,linux_proc_vmhwm_approximate,493081274,77876795,177631232,53735632,2048,2048,0,0,2048,0,1342,706,1396,0,417981885,5480641.02,8388608,9914990,929020.44715,0,495872,0 +mixed_tight,borrow,2,ok,borrow,True,2097152,90560,2094128,2094128,40,4243456,27246592,linux_proc_vmhwm_approximate,491778453,73168533,177631232,53735632,2048,2048,0,0,2048,0,1352,696,1408,0,421825716,5473786.015,8388608,9925323,926994.460577,0,495872,0 +mixed_tight,threshold2,2,ok,threshold,True,2097152,90560,2097136,2097136,114,4243456,27246592,linux_proc_vmhwm_approximate,188770790,62771264,177631232,53735632,2048,2048,28479696,1740,308,0,2380,1408,1395,0,128898177,6126984.319,8388608,8906435,343018.464235,0,523712,0 +mixed_tight,copy_no_pool,2,ok,copy,False,2097152,90560,2097136,2097136,116,4227072,27246592,linux_proc_vmhwm_approximate,189986910,58086718,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1383,0,134499200,6008153.507,8388608,8893917,343741.458999,0,0,0 +mixed_tight,project,2,ok,project,True,2097152,90560,2097120,2097120,118,4243456,27246592,linux_proc_vmhwm_approximate,174670169,55539698,177631232,53735632,2048,2048,0,0,0,0,1727,321,1312,0,121848710,6258533.417,8388608,8834323,325246.46456,0,406848,0 +mixed_tight,pressure,2,ok,pressure,True,2097152,90560,2097136,2097136,117,4227072,27246592,linux_proc_vmhwm_approximate,193714972,64286995,177631232,53735632,2048,2048,28385168,1727,321,1727,2353,1422,1371,0,132496502,6233962.195,10485760,11493286,352390.15124,0,523712,0 +mixed_tight,copy,2,ok,copy,True,2097152,90560,2097136,2097136,115,4243456,27246592,linux_proc_vmhwm_approximate,191684197,62544845,177631232,53735632,2048,2048,53727440,2048,0,0,2479,1617,1373,0,131979930,6064467.489,8388608,8905416,346663.433689,0,523712,0 +mixed_tight,threshold4,2,ok,threshold,True,2097152,90560,2097136,2097104,88,4227072,27246592,linux_proc_vmhwm_approximate,229047558,67894565,177631232,53735632,2048,2048,11229392,1357,691,0,2094,1311,1425,0,164358967,5921381.751,7864320,10252296,418976.246455,0,435392,0 +mixed_tight,threshold8,2,ok,threshold,True,2097152,90560,2097136,2097136,72,4243456,27246592,linux_proc_vmhwm_approximate,283681970,69888728,177631232,53735632,2048,2048,3824848,1008,1040,0,1987,1069,1378,0,217008850,5688342.625,7340032,9247808,522809.985389,0,386176,0 +mixed_tight,threshold2,3,ok,threshold,True,2097152,90560,2097152,2097136,102,4325376,27246592,linux_proc_vmhwm_approximate,201965490,80116546,177631232,53735632,2048,2048,28479696,1740,308,0,2291,1497,1275,0,123310392,6483685.515,10485760,11156636,364031.028759,0,493440,0 +mixed_tight,borrow,3,ok,borrow,True,2097152,90560,2094128,2094128,40,3719168,27246592,linux_proc_vmhwm_approximate,494160071,76904068,177631232,53735632,2048,2048,0,0,2048,0,1346,702,1397,0,420872435,5490372.664,8388608,9952646,930733.63783,0,495872,0 +mixed_tight,copy,3,ok,copy,True,2097152,90560,2097152,2097136,117,4243456,27246592,linux_proc_vmhwm_approximate,191245856,61096231,177631232,53735632,2048,2048,53727440,2048,0,0,2481,1615,1387,0,132860200,6056702.267,8388608,8733304,346194.049558,0,523712,0 +mixed_tight,project,3,ok,project,True,2097152,90560,2097120,2097120,118,4407296,27246592,linux_proc_vmhwm_approximate,174872810,56429902,177631232,53735632,2048,2048,0,0,0,0,1726,322,1310,0,121198578,6260365.932,8388608,8926676,325635.507331,0,406848,0 +mixed_tight,threshold4,3,ok,threshold,True,2097152,90560,2097136,2097104,88,4243456,27246592,linux_proc_vmhwm_approximate,224725167,63560757,177631232,53735632,2048,2048,11229392,1357,691,0,2132,1273,1456,0,164191839,5841515.207,7864320,10276240,412794.10328,0,435392,0 +mixed_tight,threshold8,3,ok,threshold,True,2097152,90560,2097136,2097136,73,4227072,27246592,linux_proc_vmhwm_approximate,283036048,73067755,177631232,53735632,2048,2048,3824848,1008,1040,0,1972,1084,1379,0,213650376,5678436.735,7864320,9220899,521434.266403,0,386176,0 +mixed_tight,pressure,3,ok,pressure,True,2097152,90560,2097136,2097136,113,4456448,27246592,linux_proc_vmhwm_approximate,189790379,60625611,177631232,53735632,2048,2048,28385168,1727,321,1727,2369,1406,1393,0,131820061,6110038.652,8388608,9522938,345327.900203,0,523712,0 +mixed_tight,copy_no_pool,3,ok,copy,False,2097152,90560,2097136,2097136,115,4243456,27246592,linux_proc_vmhwm_approximate,192179115,71826579,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1293,0,123142369,6044858.508,8388608,8899841,346584.591442,0,0,0 +mixed_tight,project,4,ok,project,True,2097152,90560,2097120,2097120,118,4227072,27246592,linux_proc_vmhwm_approximate,175527737,57919540,177631232,53735632,2048,2048,0,0,0,0,1728,320,1310,0,120470904,6282412.787,9437184,8759229,326547.492623,0,406848,0 +mixed_tight,threshold2,4,ok,threshold,True,2097152,90560,2097136,2097136,113,4243456,27246592,linux_proc_vmhwm_approximate,188314755,61699247,177631232,53735632,2048,2048,28479696,1740,308,0,2377,1411,1394,0,129399417,6110229.914,8388608,8949046,342240.604823,0,523712,0 +mixed_tight,threshold8,4,ok,threshold,True,2097152,90560,2097136,2097136,72,4243456,27246592,linux_proc_vmhwm_approximate,282255055,67344177,177631232,53735632,2048,2048,3824848,1008,1040,0,1990,1066,1394,0,217945110,5670280.355,7340032,9286807,520513.491017,0,386176,0 +mixed_tight,copy,4,ok,copy,True,2097152,90560,2097136,2097136,116,4243456,27246592,linux_proc_vmhwm_approximate,191037345,60906478,177631232,53735632,2048,2048,53727440,2048,0,0,2491,1605,1378,0,132768233,6050681.287,8388608,8878730,345866.556263,0,523712,0 +mixed_tight,threshold4,4,ok,threshold,True,2097152,90560,2097136,2097104,88,4227072,27246592,linux_proc_vmhwm_approximate,226976916,67053797,177631232,53735632,2048,2048,11229392,1357,691,0,2113,1292,1429,0,163248630,5888077.344,8388608,10246777,416261.40193,0,435392,0 +mixed_tight,borrow,4,ok,borrow,True,2097152,90560,2094128,2094128,40,3719168,27246592,linux_proc_vmhwm_approximate,495149938,79564522,177631232,53735632,2048,2048,0,0,2048,0,1338,710,1391,0,419504678,5510109.537,8388608,10578039,933361.552539,0,495872,0 +mixed_tight,pressure,4,ok,pressure,True,2097152,90560,2097152,2097136,114,4243456,27246592,linux_proc_vmhwm_approximate,190688005,63157202,177631232,53735632,2048,2048,28385168,1727,321,1727,2371,1404,1387,0,130503094,6140018.745,8388608,9550771,347232.508173,0,523712,0 +mixed_tight,copy_no_pool,4,ok,copy,False,2097152,90560,2097136,2097136,115,4227072,27246592,linux_proc_vmhwm_approximate,195072389,63921931,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1348,0,134032681,6160078.753,9437184,9678087,352079.814928,0,0,0 +sparse_fast,borrow,0,ok,borrow,True,8388608,90560,156112,156112,1,1605632,27246592,linux_proc_vmhwm_approximate,28472110,28306962,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,13730.123,30720,65982,1788.400939,0,65552,0 +sparse_fast,copy_no_pool,0,ok,copy,False,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,28620499,28277537,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,13795.514,40960,86816,1561.617327,0,0,0 +sparse_fast,threshold4,0,ok,threshold,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,20774647,20729258,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9997.106,14336,72204,1105.947559,0,65632,0 +sparse_fast,threshold2,0,ok,threshold,True,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,20434497,20435000,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9831.374,13312,45186,1087.202091,0,65632,0 +sparse_fast,project,0,ok,project,True,8388608,90560,90640,90640,1,1540096,27246592,linux_proc_vmhwm_approximate,22218843,22096574,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10699.094,18432,59093,1.68791,0,80,0 +sparse_fast,threshold8,0,ok,threshold,True,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,20787962,20732148,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10004.317,14336,63834,1109.185459,0,65632,0 +sparse_fast,copy,0,ok,copy,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,21540986,21477686,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10352.839,18432,54334,1151.439237,0,65632,0 +sparse_fast,pressure,0,ok,pressure,True,8388608,90560,156112,156112,1,1589248,27246592,linux_proc_vmhwm_approximate,20305145,20305963,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9760.602,13312,58019,1256.330694,0,65552,0 +sparse_fast,borrow,1,ok,borrow,True,8388608,90560,156112,156112,1,1605632,27246592,linux_proc_vmhwm_approximate,20183773,20183889,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9700.917,13312,50334,1250.328097,0,65552,0 +sparse_fast,threshold4,1,ok,threshold,True,8388608,90560,156192,156192,1,1572864,27246592,linux_proc_vmhwm_approximate,20604128,20604259,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9906.217,13312,47686,1090.792499,0,65632,0 +sparse_fast,copy,1,ok,copy,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,20493516,20493889,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9859.747,14336,52407,1088.217176,0,65632,0 +sparse_fast,project,1,ok,project,True,8388608,90560,90640,90640,1,1540096,27246592,linux_proc_vmhwm_approximate,21524893,21525074,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10363.91,13312,54889,1.632854,0,80,0 +sparse_fast,threshold8,1,ok,threshold,True,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,20381497,20382185,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9805.745,12288,47353,1087.257309,0,65632,0 +sparse_fast,pressure,1,ok,pressure,True,8388608,90560,156112,156112,1,1589248,27246592,linux_proc_vmhwm_approximate,20244385,20244778,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9731.208,13312,41426,1251.232387,0,65552,0 +sparse_fast,threshold2,1,ok,threshold,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,22684901,22652390,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10925.772,22528,69779,1228.197157,0,65632,0 +sparse_fast,copy_no_pool,1,ok,copy,False,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,22993108,22893763,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,11062.46,18432,74149,1228.067562,0,0,0 +sparse_fast,threshold4,2,ok,threshold,True,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,20874129,20775314,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10044.57,16384,55556,1112.362141,0,65632,0 +sparse_fast,borrow,2,ok,borrow,True,8388608,90560,156112,156112,1,1572864,27246592,linux_proc_vmhwm_approximate,20176198,20176500,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9697.062,12288,39001,1246.966591,0,65552,0 +sparse_fast,copy,2,ok,copy,True,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,20382034,20382277,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9806.967,12288,48779,1084.755978,0,65632,0 +sparse_fast,threshold8,2,ok,threshold,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,20453663,20454074,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9841.173,13312,53241,1090.324497,0,65632,0 +sparse_fast,copy_no_pool,2,ok,copy,False,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,20636628,20636871,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,9920.915,13312,40464,1084.995845,0,0,0 +sparse_fast,pressure,2,ok,pressure,True,8388608,90560,156112,156112,1,1605632,27246592,linux_proc_vmhwm_approximate,20413515,20355722,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9812.253,14336,64112,1264.219091,0,65552,0 +sparse_fast,threshold2,2,ok,threshold,True,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,20421089,20421796,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9825.86,13312,53519,1088.001718,0,65632,0 +sparse_fast,project,2,ok,project,True,8388608,90560,90640,90640,1,1540096,27246592,linux_proc_vmhwm_approximate,21525764,21526111,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10364.601,13312,65686,1.632264,0,80,0 +sparse_fast,threshold2,3,ok,threshold,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,20829074,20798316,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10025.307,15360,65907,1111.338342,0,65632,0 +sparse_fast,pressure,3,ok,pressure,True,8388608,90560,156112,156112,1,1572864,27246592,linux_proc_vmhwm_approximate,20018290,20018407,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9620.129,12288,34982,1238.904613,0,65552,0 +sparse_fast,borrow,3,ok,borrow,True,8388608,90560,156112,156112,1,1589248,27246592,linux_proc_vmhwm_approximate,19968383,19952888,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9596.16,11264,52612,1236.323437,0,65552,0 +sparse_fast,threshold4,3,ok,threshold,True,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,21098686,21002369,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10153.021,16384,71204,1130.040808,0,65632,0 +sparse_fast,project,3,ok,project,True,8388608,90560,90640,90640,1,1540096,27246592,linux_proc_vmhwm_approximate,21459134,21459111,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10332.156,13312,41834,1.628379,0,80,0 +sparse_fast,threshold8,3,ok,threshold,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,21122947,21019219,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10159.663,16384,63556,1125.541597,0,65632,0 +sparse_fast,copy,3,ok,copy,True,8388608,90560,156192,156192,1,1572864,27246592,linux_proc_vmhwm_approximate,20421330,20421426,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9825.919,13312,56575,1089.501083,0,65632,0 +sparse_fast,copy_no_pool,3,ok,copy,False,8388608,90560,156192,156192,1,1589248,27246592,linux_proc_vmhwm_approximate,20561183,20561592,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,9883.337,12288,45185,1085.229592,0,0,0 +sparse_fast,borrow,4,ok,borrow,True,8388608,90560,156112,156112,1,1589248,27246592,linux_proc_vmhwm_approximate,20082994,20072296,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9647.624,12288,51259,1243.235174,0,65552,0 +sparse_fast,pressure,4,ok,pressure,True,8388608,90560,156112,156112,1,1589248,27246592,linux_proc_vmhwm_approximate,23280610,23231480,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,11211.571,24576,54778,1448.645185,0,65552,0 +sparse_fast,threshold2,4,ok,threshold,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,21989119,21857259,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10590.063,18432,79983,1185.989886,0,65632,0 +sparse_fast,copy_no_pool,4,ok,copy,False,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,21265947,21072353,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,10215.53,16384,91668,1125.003597,0,0,0 +sparse_fast,project,4,ok,project,True,8388608,90560,90640,90640,1,1507328,27246592,linux_proc_vmhwm_approximate,21358726,21358833,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10284.012,13312,47648,1.620534,0,80,0 +sparse_fast,threshold8,4,ok,threshold,True,8388608,90560,156192,156192,1,1572864,27246592,linux_proc_vmhwm_approximate,20441422,20426148,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9835.821,13312,50945,1089.704272,0,65632,0 +sparse_fast,copy,4,ok,copy,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,20717721,20717834,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9969.584,13312,40148,1106.215747,0,65632,0 +sparse_fast,threshold4,4,ok,threshold,True,8388608,90560,156192,156192,1,1605632,27246592,linux_proc_vmhwm_approximate,20693813,20694148,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9958.775,13312,66019,1106.254829,0,65632,0 diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.json new file mode 100644 index 0000000..6fc0da0 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.json @@ -0,0 +1,9122 @@ +[ + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1687552, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 160143537, + "cpu_ns": 26459012, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1524, + "sleep_ns": 137390253, + "latency_mean_ns": 34696998.27, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 41171715, + "active_byte_seconds": 6863.022101, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 157308101, + "cpu_ns": 31229598, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1480, + "sleep_ns": 130450222, + "latency_mean_ns": 33900498.29, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 39440182, + "active_byte_seconds": 6941.796558, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1720320, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 155496866, + "cpu_ns": 24536501, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1532, + "sleep_ns": 134789830, + "latency_mean_ns": 33617642.818, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38582611, + "active_byte_seconds": 6573.377656, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 155097886, + "cpu_ns": 24003453, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 135038766, + "latency_mean_ns": 33344720.225, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38228742, + "active_byte_seconds": 6474.089644, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154147870, + "cpu_ns": 24607892, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 133284576, + "latency_mean_ns": 33266783.578, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37610633, + "active_byte_seconds": 5450.353596, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 159785538, + "cpu_ns": 24425471, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 139188295, + "latency_mean_ns": 34401146.148, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 40673920, + "active_byte_seconds": 6694.694537, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 107974911, + "cpu_ns": 48356230, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1100, + "slot_waits": 0, + "sleep_ns": 61689899, + "latency_mean_ns": 6299431.416, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9257676, + "active_byte_seconds": 842052.147088, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4318208, + "peak_queue": 512, + "rss_peak_bytes": 5832704, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 153730223, + "cpu_ns": 27269441, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113984, + "copied_records": 1781, + "borrowed_records": 267, + "pressure_copies": 1781, + "allocations": 514, + "reuses": 3315, + "budget_waits": 0, + "slot_waits": 1480, + "sleep_ns": 130232809, + "latency_mean_ns": 32945208.971, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38336797, + "active_byte_seconds": 525465.223488, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 106374934, + "cpu_ns": 48221069, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1095, + "slot_waits": 0, + "sleep_ns": 60129493, + "latency_mean_ns": 6201710.718, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9283991, + "active_byte_seconds": 829040.966005, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1687552, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 160320037, + "cpu_ns": 25896167, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1525, + "sleep_ns": 138363559, + "latency_mean_ns": 34524425.147, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 40334217, + "active_byte_seconds": 6779.619735, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 160914091, + "cpu_ns": 25835642, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1519, + "sleep_ns": 138817024, + "latency_mean_ns": 34925615.31, + "latency_p99_upper_ns": 46137344, + "latency_max_ns": 42672599, + "active_byte_seconds": 6869.973543, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1687552, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154184073, + "cpu_ns": 23590708, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134309537, + "latency_mean_ns": 33299901.746, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37858502, + "active_byte_seconds": 6477.65682, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1687552, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154103999, + "cpu_ns": 23187579, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134573427, + "latency_mean_ns": 33283383.04, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37891279, + "active_byte_seconds": 6458.021349, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 157860470, + "cpu_ns": 28682548, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1516, + "sleep_ns": 133519836, + "latency_mean_ns": 34200243.611, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 39812385, + "active_byte_seconds": 5603.286251, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4316848, + "peak_queue": 512, + "rss_peak_bytes": 5849088, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 157264009, + "cpu_ns": 27287611, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113408, + "copied_records": 1772, + "borrowed_records": 276, + "pressure_copies": 1772, + "allocations": 514, + "reuses": 3306, + "budget_waits": 0, + "slot_waits": 1470, + "sleep_ns": 133795355, + "latency_mean_ns": 32665634.037, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 42380378, + "active_byte_seconds": 542285.660244, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 656000, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 159054504, + "cpu_ns": 24707642, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 138332046, + "latency_mean_ns": 34466956.647, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 41828861, + "active_byte_seconds": 6705.336146, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 155128904, + "cpu_ns": 25543272, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1516, + "sleep_ns": 133541383, + "latency_mean_ns": 33533790.057, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38416722, + "active_byte_seconds": 6610.674553, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154376239, + "cpu_ns": 24141589, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134070369, + "latency_mean_ns": 33348471.497, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37918872, + "active_byte_seconds": 6490.973039, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1720320, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154320869, + "cpu_ns": 23690719, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134338369, + "latency_mean_ns": 33321815.102, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37742483, + "active_byte_seconds": 6490.329618, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4318768, + "peak_queue": 512, + "rss_peak_bytes": 5832704, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 151916969, + "cpu_ns": 26295422, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 114176, + "copied_records": 1784, + "borrowed_records": 264, + "pressure_copies": 1784, + "allocations": 514, + "reuses": 3318, + "budget_waits": 0, + "slot_waits": 1489, + "sleep_ns": 129297978, + "latency_mean_ns": 32537118.75, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37291707, + "active_byte_seconds": 513114.693547, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154140610, + "cpu_ns": 23189876, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134662671, + "latency_mean_ns": 33298003.958, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37866187, + "active_byte_seconds": 6456.771667, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154094443, + "cpu_ns": 24230112, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 133556453, + "latency_mean_ns": 33259314.794, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37585114, + "active_byte_seconds": 5449.134094, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154652646, + "cpu_ns": 24092189, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 134423307, + "latency_mean_ns": 33402890.832, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37866335, + "active_byte_seconds": 6511.54798, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 129845439, + "cpu_ns": 45497318, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1501, + "slot_waits": 0, + "sleep_ns": 87228642, + "latency_mean_ns": 7656609.5, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9287806, + "active_byte_seconds": 1022546.229446, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154249202, + "cpu_ns": 24700764, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 133261937, + "latency_mean_ns": 33273652.65, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37535317, + "active_byte_seconds": 5451.483102, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 137924379, + "cpu_ns": 44501920, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1704, + "slot_waits": 0, + "sleep_ns": 96547980, + "latency_mean_ns": 8155969.084, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9259085, + "active_byte_seconds": 1088953.248436, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4318128, + "peak_queue": 512, + "rss_peak_bytes": 5849088, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 152102968, + "cpu_ns": 26353180, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113984, + "copied_records": 1781, + "borrowed_records": 267, + "pressure_copies": 1781, + "allocations": 514, + "reuses": 3315, + "budget_waits": 0, + "slot_waits": 1489, + "sleep_ns": 129366758, + "latency_mean_ns": 32586897.428, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37391725, + "active_byte_seconds": 520107.889954, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154077370, + "cpu_ns": 23197132, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134591611, + "latency_mean_ns": 33277920.587, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37816187, + "active_byte_seconds": 6453.368258, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154022055, + "cpu_ns": 23087202, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134592710, + "latency_mean_ns": 33270464.792, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37834187, + "active_byte_seconds": 6450.425541, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1720320, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 159640187, + "cpu_ns": 27408125, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1444, + "sleep_ns": 128856855, + "latency_mean_ns": 34666958.779, + "latency_p99_upper_ns": 46137344, + "latency_max_ns": 43466300, + "active_byte_seconds": 7402.36243, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1720320, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 155595440, + "cpu_ns": 25939365, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134098060, + "latency_mean_ns": 33573592.059, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 37962687, + "active_byte_seconds": 6570.059349, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 156340011, + "cpu_ns": 29539317, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1516, + "sleep_ns": 131960968, + "latency_mean_ns": 33615642.627, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38156538, + "active_byte_seconds": 6719.71558, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 162066143, + "cpu_ns": 36838768, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1413, + "sleep_ns": 131813978, + "latency_mean_ns": 34186582.359, + "latency_p99_upper_ns": 46137344, + "latency_max_ns": 44277743, + "active_byte_seconds": 7046.191505, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1720320, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154776516, + "cpu_ns": 28160466, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1467, + "sleep_ns": 130788967, + "latency_mean_ns": 33183203.845, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 39792181, + "active_byte_seconds": 6689.95279, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1720320, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154788109, + "cpu_ns": 23986070, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134524839, + "latency_mean_ns": 33403921.35, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 37878020, + "active_byte_seconds": 6520.274636, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1720320, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154198518, + "cpu_ns": 23692568, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 134186642, + "latency_mean_ns": 33304172.097, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37802706, + "active_byte_seconds": 6492.439639, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 141446795, + "cpu_ns": 44722082, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1729, + "slot_waits": 0, + "sleep_ns": 99993943, + "latency_mean_ns": 8382742.796, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9272177, + "active_byte_seconds": 1119170.099864, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 1703936, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154113537, + "cpu_ns": 24093342, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 133716023, + "latency_mean_ns": 33269305.586, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37632576, + "active_byte_seconds": 6504.166448, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4318688, + "peak_queue": 512, + "rss_peak_bytes": 5849088, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154850386, + "cpu_ns": 27901892, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 114112, + "copied_records": 1783, + "borrowed_records": 265, + "pressure_copies": 1783, + "allocations": 514, + "reuses": 3317, + "budget_waits": 0, + "slot_waits": 1468, + "sleep_ns": 130766867, + "latency_mean_ns": 33232147.448, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 39951922, + "active_byte_seconds": 524904.925261, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 157284342, + "cpu_ns": 26489150, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1510, + "sleep_ns": 134748166, + "latency_mean_ns": 33888730.885, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 39144776, + "active_byte_seconds": 5552.251467, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 77760, + "peak_active_bytes": 77760, + "peak_queue": 64, + "rss_peak_bytes": 1572864, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167149794, + "cpu_ns": 26582076, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1974, + "sleep_ns": 144371389, + "latency_mean_ns": 5130099.306, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8958215, + "active_byte_seconds": 840.438441, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1622016, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167663571, + "cpu_ns": 26966696, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1961, + "sleep_ns": 144092606, + "latency_mean_ns": 5142490.023, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9061177, + "active_byte_seconds": 2058.951339, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 168181366, + "cpu_ns": 26871012, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1965, + "sleep_ns": 145357926, + "latency_mean_ns": 5163779.497, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8918178, + "active_byte_seconds": 2024.80935, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4267968, + "peak_active_bytes": 4267968, + "peak_queue": 64, + "rss_peak_bytes": 5292032, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167216739, + "cpu_ns": 43926056, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1888, + "sleep_ns": 126773157, + "latency_mean_ns": 5126680.831, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 7718922, + "active_byte_seconds": 688195.924843, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4268048, + "peak_active_bytes": 4268048, + "peak_queue": 64, + "rss_peak_bytes": 5767168, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167185425, + "cpu_ns": 45108627, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 2048, + "copied_records": 32, + "borrowed_records": 2016, + "pressure_copies": 32, + "allocations": 65, + "reuses": 2015, + "budget_waits": 0, + "slot_waits": 1714, + "sleep_ns": 125584463, + "latency_mean_ns": 5128742.545, + "latency_p99_upper_ns": 6291456, + "latency_max_ns": 6639537, + "active_byte_seconds": 677659.57207, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1622016, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 168358292, + "cpu_ns": 24933808, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1982, + "sleep_ns": 147266280, + "latency_mean_ns": 5168010.066, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9057715, + "active_byte_seconds": 1951.688735, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166820573, + "cpu_ns": 24630999, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1982, + "sleep_ns": 145973315, + "latency_mean_ns": 5129540.053, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9056604, + "active_byte_seconds": 1927.278894, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166777389, + "cpu_ns": 24643191, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1983, + "sleep_ns": 145938953, + "latency_mean_ns": 5126620.092, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9062437, + "active_byte_seconds": 1926.54795, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4268048, + "peak_active_bytes": 4268048, + "peak_queue": 64, + "rss_peak_bytes": 5783552, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167023277, + "cpu_ns": 43214247, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 2048, + "copied_records": 32, + "borrowed_records": 2016, + "pressure_copies": 32, + "allocations": 65, + "reuses": 2015, + "budget_waits": 0, + "slot_waits": 1923, + "sleep_ns": 127480383, + "latency_mean_ns": 5125737.978, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 8171940, + "active_byte_seconds": 677260.281048, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167465182, + "cpu_ns": 25416979, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1982, + "sleep_ns": 146065308, + "latency_mean_ns": 5147482.869, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9043937, + "active_byte_seconds": 1953.686249, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 168266032, + "cpu_ns": 25648470, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1982, + "sleep_ns": 146554478, + "latency_mean_ns": 5170189.028, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8853271, + "active_byte_seconds": 1979.901257, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167352998, + "cpu_ns": 26167797, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1982, + "sleep_ns": 145161257, + "latency_mean_ns": 5140939.162, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9037974, + "active_byte_seconds": 1975.871965, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167352294, + "cpu_ns": 26149112, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1982, + "sleep_ns": 145164575, + "latency_mean_ns": 5140639.04, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9055067, + "active_byte_seconds": 1998.899707, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 77760, + "peak_active_bytes": 77760, + "peak_queue": 64, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167537794, + "cpu_ns": 26582472, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1983, + "sleep_ns": 144932270, + "latency_mean_ns": 5144362.881, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9070492, + "active_byte_seconds": 842.776507, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 177068451, + "cpu_ns": 34395657, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1964, + "sleep_ns": 148816905, + "latency_mean_ns": 5443811.067, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9497139, + "active_byte_seconds": 2287.854324, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4267968, + "peak_active_bytes": 4267968, + "peak_queue": 64, + "rss_peak_bytes": 5292032, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 169327288, + "cpu_ns": 47272489, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1559, + "sleep_ns": 125366124, + "latency_mean_ns": 5174744.393, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 7999125, + "active_byte_seconds": 694635.953387, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166910296, + "cpu_ns": 25474992, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1982, + "sleep_ns": 145193155, + "latency_mean_ns": 5128818.551, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9004900, + "active_byte_seconds": 1987.487947, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167128888, + "cpu_ns": 26220522, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1980, + "sleep_ns": 144715144, + "latency_mean_ns": 5134877.701, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9062770, + "active_byte_seconds": 2002.587942, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167148609, + "cpu_ns": 25957735, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1977, + "sleep_ns": 145051954, + "latency_mean_ns": 5131955.022, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9073752, + "active_byte_seconds": 1998.277517, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 169611954, + "cpu_ns": 32276554, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1914, + "sleep_ns": 142094763, + "latency_mean_ns": 5206380.796, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8901622, + "active_byte_seconds": 2300.08379, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 77760, + "peak_active_bytes": 77760, + "peak_queue": 64, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 171072487, + "cpu_ns": 27568676, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1973, + "sleep_ns": 147623196, + "latency_mean_ns": 5258257.061, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9512213, + "active_byte_seconds": 861.43609, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 169338770, + "cpu_ns": 27312589, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1973, + "sleep_ns": 146307420, + "latency_mean_ns": 5201400.59, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9016640, + "active_byte_seconds": 2055.277015, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4268048, + "peak_active_bytes": 4268048, + "peak_queue": 64, + "rss_peak_bytes": 5767168, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 170783358, + "cpu_ns": 46177893, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 2048, + "copied_records": 32, + "borrowed_records": 2016, + "pressure_copies": 32, + "allocations": 65, + "reuses": 2015, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 127950932, + "latency_mean_ns": 5236882.494, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 7222257, + "active_byte_seconds": 691939.11555, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4267968, + "peak_active_bytes": 4267968, + "peak_queue": 64, + "rss_peak_bytes": 5292032, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167156999, + "cpu_ns": 43876108, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1884, + "sleep_ns": 127058453, + "latency_mean_ns": 5126798.896, + "latency_p99_upper_ns": 6815744, + "latency_max_ns": 6645517, + "active_byte_seconds": 688213.378653, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166770166, + "cpu_ns": 25212214, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1984, + "sleep_ns": 145249611, + "latency_mean_ns": 5126749.497, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9015048, + "active_byte_seconds": 1964.863682, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167909533, + "cpu_ns": 27850375, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1956, + "sleep_ns": 144091763, + "latency_mean_ns": 5137672.764, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9072455, + "active_byte_seconds": 2094.171152, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 168528180, + "cpu_ns": 32536976, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1897, + "sleep_ns": 140338194, + "latency_mean_ns": 5169990.139, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8583716, + "active_byte_seconds": 2319.792062, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166715000, + "cpu_ns": 24659146, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1984, + "sleep_ns": 145786851, + "latency_mean_ns": 5125611.619, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9016234, + "active_byte_seconds": 1919.204343, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167074462, + "cpu_ns": 26663004, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1972, + "sleep_ns": 144308149, + "latency_mean_ns": 5134098.172, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9056067, + "active_byte_seconds": 2041.253864, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 77760, + "peak_active_bytes": 77760, + "peak_queue": 64, + "rss_peak_bytes": 1572864, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166784685, + "cpu_ns": 25542781, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1985, + "sleep_ns": 144995208, + "latency_mean_ns": 5128089.141, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9028215, + "active_byte_seconds": 840.114114, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4268048, + "peak_active_bytes": 4268048, + "peak_queue": 64, + "rss_peak_bytes": 5750784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167121239, + "cpu_ns": 44137216, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 2048, + "copied_records": 32, + "borrowed_records": 2016, + "pressure_copies": 32, + "allocations": 65, + "reuses": 2015, + "budget_waits": 0, + "slot_waits": 1831, + "sleep_ns": 126710351, + "latency_mean_ns": 5127462.262, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 7853292, + "active_byte_seconds": 677483.645644, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4267968, + "peak_active_bytes": 4267968, + "peak_queue": 64, + "rss_peak_bytes": 5292032, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167212479, + "cpu_ns": 44714000, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1813, + "sleep_ns": 126101064, + "latency_mean_ns": 5127098.881, + "latency_p99_upper_ns": 6291456, + "latency_max_ns": 6795851, + "active_byte_seconds": 688253.763142, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1622016, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166717963, + "cpu_ns": 24687036, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1983, + "sleep_ns": 145714382, + "latency_mean_ns": 5125971.683, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8962826, + "active_byte_seconds": 1940.992711, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166680851, + "cpu_ns": 24341624, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1985, + "sleep_ns": 146024444, + "latency_mean_ns": 5125321.995, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9060326, + "active_byte_seconds": 1923.416256, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1654784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166829203, + "cpu_ns": 24368638, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1984, + "sleep_ns": 146127572, + "latency_mean_ns": 5127589.86, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9064418, + "active_byte_seconds": 1922.285206, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 77760, + "peak_active_bytes": 77760, + "peak_queue": 64, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167505794, + "cpu_ns": 25505816, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1985, + "sleep_ns": 145758292, + "latency_mean_ns": 5149157.839, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9024826, + "active_byte_seconds": 843.565622, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167165609, + "cpu_ns": 25932182, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1976, + "sleep_ns": 145075045, + "latency_mean_ns": 5133720.043, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9029714, + "active_byte_seconds": 1986.249813, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4267968, + "peak_active_bytes": 4267968, + "peak_queue": 64, + "rss_peak_bytes": 5292032, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167597886, + "cpu_ns": 45634483, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 64, + "reuses": 1984, + "budget_waits": 0, + "slot_waits": 1768, + "sleep_ns": 125625299, + "latency_mean_ns": 5136396.266, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 8047606, + "active_byte_seconds": 689499.931113, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 143312, + "peak_active_bytes": 143312, + "peak_queue": 64, + "rss_peak_bytes": 1638400, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166724425, + "cpu_ns": 24304145, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 65, + "reuses": 4031, + "budget_waits": 0, + "slot_waits": 1984, + "sleep_ns": 146112652, + "latency_mean_ns": 5125215.218, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8987715, + "active_byte_seconds": 1915.595622, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slots64", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 72640, + "peak_reserved_bytes": 4268048, + "peak_active_bytes": 4268048, + "peak_queue": 64, + "rss_peak_bytes": 5750784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 167102943, + "cpu_ns": 43204463, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 2048, + "copied_records": 32, + "borrowed_records": 2016, + "pressure_copies": 32, + "allocations": 65, + "reuses": 2015, + "budget_waits": 0, + "slot_waits": 1902, + "sleep_ns": 127501222, + "latency_mean_ns": 5127130.23, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 7758552, + "active_byte_seconds": 677439.045656, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 181746660, + "cpu_ns": 79318230, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1923, + "slot_waits": 0, + "sleep_ns": 106431291, + "latency_mean_ns": 10743611.753, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12983832, + "active_byte_seconds": 1442582.012105, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 161215386, + "cpu_ns": 89862451, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1244, + "slot_waits": 0, + "sleep_ns": 73999062, + "latency_mean_ns": 9530310.653, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11475412, + "active_byte_seconds": 1273441.862727, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 181545716, + "cpu_ns": 79933333, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1794, + "slot_waits": 0, + "sleep_ns": 105524896, + "latency_mean_ns": 10805826.457, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13598424, + "active_byte_seconds": 1442046.73663, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 179808887, + "cpu_ns": 82476393, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1739, + "slot_waits": 0, + "sleep_ns": 101034766, + "latency_mean_ns": 10695483.804, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13755645, + "active_byte_seconds": 1427516.577261, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 181070273, + "cpu_ns": 83392187, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1923, + "slot_waits": 0, + "sleep_ns": 101645153, + "latency_mean_ns": 10688072.436, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13270480, + "active_byte_seconds": 1435135.645514, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 182712953, + "cpu_ns": 81359541, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1784, + "slot_waits": 0, + "sleep_ns": 105215055, + "latency_mean_ns": 10867028.691, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13321591, + "active_byte_seconds": 1450265.685197, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 187716865, + "cpu_ns": 74974355, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1885, + "slot_waits": 0, + "sleep_ns": 116741671, + "latency_mean_ns": 11188037.068, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13610887, + "active_byte_seconds": 1492582.952313, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 177819652, + "cpu_ns": 80765616, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1732, + "slot_waits": 0, + "sleep_ns": 100790270, + "latency_mean_ns": 10569593.51, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12515889, + "active_byte_seconds": 1410709.704111, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 183713376, + "cpu_ns": 79010560, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1923, + "slot_waits": 0, + "sleep_ns": 108825186, + "latency_mean_ns": 10852089.214, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12891240, + "active_byte_seconds": 1457141.599471, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 181690178, + "cpu_ns": 80219667, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1787, + "slot_waits": 0, + "sleep_ns": 105238489, + "latency_mean_ns": 10811191.05, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13211998, + "active_byte_seconds": 1442791.441175, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 187516958, + "cpu_ns": 75386397, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1881, + "slot_waits": 0, + "sleep_ns": 116242907, + "latency_mean_ns": 11180449.479, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13778256, + "active_byte_seconds": 1491587.457419, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 181604364, + "cpu_ns": 78951924, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1764, + "slot_waits": 0, + "sleep_ns": 106543831, + "latency_mean_ns": 10804561.506, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13821367, + "active_byte_seconds": 1441898.713593, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 162629252, + "cpu_ns": 88543490, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1913, + "slot_waits": 0, + "sleep_ns": 77289755, + "latency_mean_ns": 9526763.21, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13455924, + "active_byte_seconds": 1279274.820821, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 177629041, + "cpu_ns": 82100973, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1640, + "slot_waits": 0, + "sleep_ns": 98770708, + "latency_mean_ns": 10557136.903, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13384591, + "active_byte_seconds": 1409274.391678, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 176146657, + "cpu_ns": 82398748, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1605, + "slot_waits": 0, + "sleep_ns": 97290225, + "latency_mean_ns": 10456864.841, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12598464, + "active_byte_seconds": 1395956.019109, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 177871837, + "cpu_ns": 82048383, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1692, + "slot_waits": 0, + "sleep_ns": 99804043, + "latency_mean_ns": 10550904.139, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13302146, + "active_byte_seconds": 1408308.914356, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 178298984, + "cpu_ns": 83177513, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1922, + "slot_waits": 0, + "sleep_ns": 99077474, + "latency_mean_ns": 10508907.261, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13207943, + "active_byte_seconds": 1411086.299499, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 181467845, + "cpu_ns": 82821089, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1672, + "slot_waits": 0, + "sleep_ns": 102626015, + "latency_mean_ns": 10774249.202, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12846870, + "active_byte_seconds": 1438045.689091, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 133633132, + "cpu_ns": 106591389, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 396, + "slot_waits": 0, + "sleep_ns": 28102711, + "latency_mean_ns": 7778565.752, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12128873, + "active_byte_seconds": 1042347.024547, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 177934893, + "cpu_ns": 82638851, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1719, + "slot_waits": 0, + "sleep_ns": 98938213, + "latency_mean_ns": 10577573.546, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12061188, + "active_byte_seconds": 1411845.015325, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 180208516, + "cpu_ns": 83532190, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1755, + "slot_waits": 0, + "sleep_ns": 100354141, + "latency_mean_ns": 10704952.958, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11578059, + "active_byte_seconds": 1428781.613196, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 185672333, + "cpu_ns": 80542923, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1923, + "slot_waits": 0, + "sleep_ns": 109123678, + "latency_mean_ns": 10977524.849, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13368332, + "active_byte_seconds": 1473966.510893, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 184526170, + "cpu_ns": 78362511, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1834, + "slot_waits": 0, + "sleep_ns": 110126299, + "latency_mean_ns": 10988073.928, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13728757, + "active_byte_seconds": 1466214.025114, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 186194683, + "cpu_ns": 78514601, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1865, + "slot_waits": 0, + "sleep_ns": 111800399, + "latency_mean_ns": 11071491.94, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13571164, + "active_byte_seconds": 1477291.36261, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 181552086, + "cpu_ns": 80699818, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1690, + "slot_waits": 0, + "sleep_ns": 104861831, + "latency_mean_ns": 10716792.58, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12527594, + "active_byte_seconds": 1430323.101776, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 176007991, + "cpu_ns": 84507344, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1626, + "slot_waits": 0, + "sleep_ns": 94771244, + "latency_mean_ns": 10450519.416, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13672942, + "active_byte_seconds": 1395156.109095, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 152737854, + "cpu_ns": 99861953, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 890, + "slot_waits": 0, + "sleep_ns": 54158702, + "latency_mean_ns": 8983717.571, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12746037, + "active_byte_seconds": 1201774.933886, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 184530299, + "cpu_ns": 78669541, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1923, + "slot_waits": 0, + "sleep_ns": 109915619, + "latency_mean_ns": 10914646.174, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13287517, + "active_byte_seconds": 1465541.553301, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 133391651, + "cpu_ns": 112766017, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1523, + "slot_waits": 0, + "sleep_ns": 20003852, + "latency_mean_ns": 7733517.169, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11792151, + "active_byte_seconds": 1038602.867364, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 166473036, + "cpu_ns": 87150086, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1399, + "slot_waits": 0, + "sleep_ns": 82606050, + "latency_mean_ns": 9857584.602, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13713590, + "active_byte_seconds": 1316650.677161, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154311406, + "cpu_ns": 92958406, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1082, + "slot_waits": 0, + "sleep_ns": 62034758, + "latency_mean_ns": 9096496.741, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11453763, + "active_byte_seconds": 1216142.805053, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 172422001, + "cpu_ns": 86572890, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1520, + "slot_waits": 0, + "sleep_ns": 89162361, + "latency_mean_ns": 10222342.367, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13171721, + "active_byte_seconds": 1365000.503032, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 182776175, + "cpu_ns": 83801034, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1923, + "slot_waits": 0, + "sleep_ns": 102610607, + "latency_mean_ns": 10792342.655, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13409628, + "active_byte_seconds": 1449128.406768, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 172321224, + "cpu_ns": 93701381, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1317, + "slot_waits": 0, + "sleep_ns": 81313113, + "latency_mean_ns": 10121009.211, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 13629442, + "active_byte_seconds": 1352204.310615, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 161726717, + "cpu_ns": 90653564, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1291, + "slot_waits": 0, + "sleep_ns": 73742733, + "latency_mean_ns": 9543033.955, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13496479, + "active_byte_seconds": 1275079.33543, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 168343995, + "cpu_ns": 87047828, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1917, + "slot_waits": 0, + "sleep_ns": 84775336, + "latency_mean_ns": 9903397.208, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13360535, + "active_byte_seconds": 1329804.191236, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9469952, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 150178547, + "cpu_ns": 91472405, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1089, + "slot_waits": 0, + "sleep_ns": 61163642, + "latency_mean_ns": 8764951.531, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12700278, + "active_byte_seconds": 1171761.488413, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 160544406, + "cpu_ns": 90636057, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1220, + "slot_waits": 0, + "sleep_ns": 72541038, + "latency_mean_ns": 9456018.519, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 12662167, + "active_byte_seconds": 1263635.432461, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 177206635, + "cpu_ns": 83537344, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1659, + "slot_waits": 0, + "sleep_ns": 97328864, + "latency_mean_ns": 10529385.897, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12256668, + "active_byte_seconds": 1405563.035864, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 9486336, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 169205769, + "cpu_ns": 93894859, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1358, + "slot_waits": 0, + "sleep_ns": 77830920, + "latency_mean_ns": 9905630.789, + "latency_p99_upper_ns": 14680064, + "latency_max_ns": 15320344, + "active_byte_seconds": 1323477.613603, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388448, + "peak_queue": 351, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 157820358, + "cpu_ns": 72816171, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 2118, + "reuses": 1629, + "budget_waits": 983, + "slot_waits": 0, + "sleep_ns": 87435519, + "latency_mean_ns": 20627967.734, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27848623, + "active_byte_seconds": 1145134.388634, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 359, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 152663243, + "cpu_ns": 58871841, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1712, + "reuses": 336, + "budget_waits": 1020, + "slot_waits": 0, + "sleep_ns": 96564951, + "latency_mean_ns": 20645944.552, + "latency_p99_upper_ns": 29360128, + "latency_max_ns": 28536362, + "active_byte_seconds": 1105768.632536, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388480, + "peak_active_bytes": 8388432, + "peak_queue": 218, + "rss_peak_bytes": 10928128, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154865144, + "cpu_ns": 68111825, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1854, + "reuses": 1202, + "budget_waits": 1088, + "slot_waits": 0, + "sleep_ns": 89372126, + "latency_mean_ns": 12505668.119, + "latency_p99_upper_ns": 15728640, + "latency_max_ns": 16314249, + "active_byte_seconds": 1173514.267706, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388544, + "peak_active_bytes": 8388544, + "peak_queue": 351, + "rss_peak_bytes": 11042816, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 147730720, + "cpu_ns": 59461567, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1091, + "slot_waits": 0, + "sleep_ns": 90844456, + "latency_mean_ns": 19373026.656, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27720475, + "active_byte_seconds": 1058083.377077, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 284, + "rss_peak_bytes": 11042816, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154278831, + "cpu_ns": 65116485, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1939, + "reuses": 1466, + "budget_waits": 1101, + "slot_waits": 0, + "sleep_ns": 91754428, + "latency_mean_ns": 15979998.057, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 21200642, + "active_byte_seconds": 1146944.244493, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11583488, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 148231627, + "cpu_ns": 61867221, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2282, + "reuses": 1814, + "budget_waits": 1066, + "slot_waits": 0, + "sleep_ns": 88853774, + "latency_mean_ns": 19421431.677, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27565124, + "active_byte_seconds": 1061176.084143, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11583488, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 151434543, + "cpu_ns": 63073539, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2181, + "reuses": 1607, + "budget_waits": 1055, + "slot_waits": 0, + "sleep_ns": 90878850, + "latency_mean_ns": 19880247.948, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27687586, + "active_byte_seconds": 1085460.729712, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386688, + "peak_queue": 112, + "rss_peak_bytes": 11042816, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 155306143, + "cpu_ns": 76020257, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1309, + "reuses": 739, + "budget_waits": 1057, + "slot_waits": 0, + "sleep_ns": 81695551, + "latency_mean_ns": 6922951.601, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 8980622, + "active_byte_seconds": 1223077.166921, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 350, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 160379239, + "cpu_ns": 76853903, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2142, + "reuses": 1954, + "budget_waits": 945, + "slot_waits": 0, + "sleep_ns": 86445280, + "latency_mean_ns": 20559126.612, + "latency_p99_upper_ns": 31457280, + "latency_max_ns": 31399761, + "active_byte_seconds": 1126649.918907, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154875385, + "cpu_ns": 62824900, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2184, + "reuses": 1604, + "budget_waits": 1064, + "slot_waits": 0, + "sleep_ns": 94824069, + "latency_mean_ns": 20387628.756, + "latency_p99_upper_ns": 29360128, + "latency_max_ns": 28330122, + "active_byte_seconds": 1114766.522227, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386688, + "peak_queue": 112, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 160560980, + "cpu_ns": 82185381, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1298, + "reuses": 750, + "budget_waits": 1014, + "slot_waits": 0, + "sleep_ns": 81045212, + "latency_mean_ns": 7165375.219, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9128899, + "active_byte_seconds": 1265871.620641, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388480, + "peak_active_bytes": 8388432, + "peak_queue": 218, + "rss_peak_bytes": 10895360, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 158376930, + "cpu_ns": 70010099, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1840, + "reuses": 1216, + "budget_waits": 1069, + "slot_waits": 0, + "sleep_ns": 91117649, + "latency_mean_ns": 12818618.589, + "latency_p99_upper_ns": 16777216, + "latency_max_ns": 16509452, + "active_byte_seconds": 1202455.090015, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 284, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 158364004, + "cpu_ns": 67436217, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1915, + "reuses": 1490, + "budget_waits": 1082, + "slot_waits": 0, + "sleep_ns": 93660619, + "latency_mean_ns": 16427087.508, + "latency_p99_upper_ns": 23068672, + "latency_max_ns": 21701826, + "active_byte_seconds": 1180098.881536, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 150598212, + "cpu_ns": 62794080, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 2194, + "reuses": 1553, + "budget_waits": 1045, + "slot_waits": 0, + "sleep_ns": 90472858, + "latency_mean_ns": 19666383.09, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27539921, + "active_byte_seconds": 1091553.931133, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11042816, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 147750665, + "cpu_ns": 60136047, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1079, + "slot_waits": 0, + "sleep_ns": 90065234, + "latency_mean_ns": 19383039.199, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27513476, + "active_byte_seconds": 1059302.637881, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 359, + "rss_peak_bytes": 11583488, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 161035903, + "cpu_ns": 71350453, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1684, + "reuses": 364, + "budget_waits": 940, + "slot_waits": 0, + "sleep_ns": 92840414, + "latency_mean_ns": 21900800.478, + "latency_p99_upper_ns": 31457280, + "latency_max_ns": 29845877, + "active_byte_seconds": 1171929.531923, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 344, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 156784527, + "cpu_ns": 75333533, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 936, + "slot_waits": 0, + "sleep_ns": 83839404, + "latency_mean_ns": 20060454.119, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27533828, + "active_byte_seconds": 1101855.462406, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 359, + "rss_peak_bytes": 11042816, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 155323458, + "cpu_ns": 61447221, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1698, + "reuses": 350, + "budget_waits": 1001, + "slot_waits": 0, + "sleep_ns": 96575508, + "latency_mean_ns": 21013565.399, + "latency_p99_upper_ns": 29360128, + "latency_max_ns": 28307510, + "active_byte_seconds": 1125180.461018, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388544, + "peak_queue": 351, + "rss_peak_bytes": 11583488, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 148908587, + "cpu_ns": 62691660, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2295, + "reuses": 1801, + "budget_waits": 1060, + "slot_waits": 0, + "sleep_ns": 88867276, + "latency_mean_ns": 19542842.845, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27366569, + "active_byte_seconds": 1068483.42084, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388480, + "peak_active_bytes": 8388432, + "peak_queue": 218, + "rss_peak_bytes": 10895360, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 155250569, + "cpu_ns": 68193969, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1856, + "reuses": 1200, + "budget_waits": 1091, + "slot_waits": 0, + "sleep_ns": 89649032, + "latency_mean_ns": 12540132.329, + "latency_p99_upper_ns": 15728640, + "latency_max_ns": 16196379, + "active_byte_seconds": 1176609.476886, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 284, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 153993443, + "cpu_ns": 64368954, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1951, + "reuses": 1454, + "budget_waits": 1113, + "slot_waits": 0, + "sleep_ns": 92271541, + "latency_mean_ns": 15971202.74, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 20927051, + "active_byte_seconds": 1146423.83916, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386688, + "peak_queue": 112, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 156530676, + "cpu_ns": 80398481, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1289, + "reuses": 759, + "budget_waits": 1012, + "slot_waits": 0, + "sleep_ns": 78480840, + "latency_mean_ns": 6972909.934, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9059956, + "active_byte_seconds": 1232378.631527, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11010048, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 150924192, + "cpu_ns": 62137633, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2185, + "reuses": 1603, + "budget_waits": 1066, + "slot_waits": 0, + "sleep_ns": 91396144, + "latency_mean_ns": 19908205.041, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27188866, + "active_byte_seconds": 1087150.034862, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11042816, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 151817449, + "cpu_ns": 63923421, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 2188, + "reuses": 1559, + "budget_waits": 1043, + "slot_waits": 0, + "sleep_ns": 90636475, + "latency_mean_ns": 19811286.8, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27547513, + "active_byte_seconds": 1100686.98981, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 150346490, + "cpu_ns": 61910200, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 2191, + "reuses": 1556, + "budget_waits": 1052, + "slot_waits": 0, + "sleep_ns": 90957629, + "latency_mean_ns": 19620014.043, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27598753, + "active_byte_seconds": 1088594.710179, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386736, + "peak_queue": 112, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154664385, + "cpu_ns": 75260445, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1309, + "reuses": 739, + "budget_waits": 1060, + "slot_waits": 0, + "sleep_ns": 81727723, + "latency_mean_ns": 6891592.543, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9042474, + "active_byte_seconds": 1217311.080951, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388448, + "peak_active_bytes": 8388448, + "peak_queue": 351, + "rss_peak_bytes": 11583488, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 149074586, + "cpu_ns": 62457772, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1070, + "slot_waits": 0, + "sleep_ns": 89108781, + "latency_mean_ns": 19570212.309, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27735827, + "active_byte_seconds": 1068598.834234, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388448, + "peak_queue": 351, + "rss_peak_bytes": 11010048, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 151416673, + "cpu_ns": 62409227, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2196, + "reuses": 1592, + "budget_waits": 1072, + "slot_waits": 0, + "sleep_ns": 91608903, + "latency_mean_ns": 19945957.829, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27499217, + "active_byte_seconds": 1089269.648492, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388480, + "peak_active_bytes": 8388432, + "peak_queue": 218, + "rss_peak_bytes": 10928128, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154832681, + "cpu_ns": 68708379, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1844, + "reuses": 1212, + "budget_waits": 1080, + "slot_waits": 0, + "sleep_ns": 88746131, + "latency_mean_ns": 12500210.727, + "latency_p99_upper_ns": 15728640, + "latency_max_ns": 16133805, + "active_byte_seconds": 1173410.180784, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 284, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 153954221, + "cpu_ns": 64939588, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1938, + "reuses": 1467, + "budget_waits": 1106, + "slot_waits": 0, + "sleep_ns": 91640646, + "latency_mean_ns": 15956645.429, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 20876513, + "active_byte_seconds": 1145843.306835, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388432, + "peak_queue": 348, + "rss_peak_bytes": 11026432, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154316312, + "cpu_ns": 71405240, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2185, + "reuses": 1911, + "budget_waits": 984, + "slot_waits": 0, + "sleep_ns": 85159635, + "latency_mean_ns": 20042799.471, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27766253, + "active_byte_seconds": 1098260.452799, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 359, + "rss_peak_bytes": 11567104, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 153157278, + "cpu_ns": 59550541, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1715, + "reuses": 333, + "budget_waits": 1025, + "slot_waits": 0, + "sleep_ns": 96249111, + "latency_mean_ns": 20737608.082, + "latency_p99_upper_ns": 29360128, + "latency_max_ns": 28456288, + "active_byte_seconds": 1110823.213775, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11583488, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 148559421, + "cpu_ns": 60872675, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2291, + "reuses": 1805, + "budget_waits": 1082, + "slot_waits": 0, + "sleep_ns": 90209021, + "latency_mean_ns": 19484771.246, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27464198, + "active_byte_seconds": 1064901.546602, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11042816, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 152463225, + "cpu_ns": 62941260, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2190, + "reuses": 1598, + "budget_waits": 1076, + "slot_waits": 0, + "sleep_ns": 92180791, + "latency_mean_ns": 20063222.989, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27720123, + "active_byte_seconds": 1096182.913762, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 359, + "rss_peak_bytes": 11567104, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 156226158, + "cpu_ns": 60922941, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1712, + "reuses": 336, + "budget_waits": 1009, + "slot_waits": 0, + "sleep_ns": 98129027, + "latency_mean_ns": 21028903.054, + "latency_p99_upper_ns": 31457280, + "latency_max_ns": 30198524, + "active_byte_seconds": 1124769.099854, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388480, + "peak_active_bytes": 8388432, + "peak_queue": 218, + "rss_peak_bytes": 10534912, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 159080169, + "cpu_ns": 72479922, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1823, + "reuses": 1233, + "budget_waits": 1052, + "slot_waits": 0, + "sleep_ns": 89038311, + "latency_mean_ns": 12767575.616, + "latency_p99_upper_ns": 16777216, + "latency_max_ns": 17017117, + "active_byte_seconds": 1198559.835478, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386688, + "peak_queue": 112, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 160859182, + "cpu_ns": 83693169, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1286, + "reuses": 762, + "budget_waits": 998, + "slot_waits": 0, + "sleep_ns": 79719697, + "latency_mean_ns": 7168367.113, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9348362, + "active_byte_seconds": 1265838.726396, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 350, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 153665517, + "cpu_ns": 63556619, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1061, + "slot_waits": 0, + "sleep_ns": 92811834, + "latency_mean_ns": 20225367.343, + "latency_p99_upper_ns": 29360128, + "latency_max_ns": 27663216, + "active_byte_seconds": 1106709.142867, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 284, + "rss_peak_bytes": 11059200, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 154918866, + "cpu_ns": 67062426, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1917, + "reuses": 1488, + "budget_waits": 1084, + "slot_waits": 0, + "sleep_ns": 90513202, + "latency_mean_ns": 16065937.041, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 20956347, + "active_byte_seconds": 1153471.713074, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 11026432, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 150607823, + "cpu_ns": 62564984, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 2185, + "reuses": 1562, + "budget_waits": 1048, + "slot_waits": 0, + "sleep_ns": 90639036, + "latency_mean_ns": 19672453.154, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 27555809, + "active_byte_seconds": 1091824.795529, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 113, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 190186189, + "cpu_ns": 62042049, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2373, + "reuses": 1402, + "budget_waits": 1378, + "slot_waits": 0, + "sleep_ns": 130967488, + "latency_mean_ns": 6127305.056, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9614324, + "active_byte_seconds": 346183.559002, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 175306603, + "cpu_ns": 59313331, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1720, + "reuses": 328, + "budget_waits": 1300, + "slot_waits": 0, + "sleep_ns": 118791611, + "latency_mean_ns": 6273925.584, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8853437, + "active_byte_seconds": 326279.178906, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 225451606, + "cpu_ns": 64792167, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2134, + "reuses": 1271, + "budget_waits": 1457, + "slot_waits": 0, + "sleep_ns": 163697658, + "latency_mean_ns": 5851904.688, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 10247748, + "active_byte_seconds": 413690.587373, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 191774166, + "cpu_ns": 62795054, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2467, + "reuses": 1629, + "budget_waits": 1361, + "slot_waits": 0, + "sleep_ns": 131825729, + "latency_mean_ns": 6045168.869, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8688826, + "active_byte_seconds": 346452.547175, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 107, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 195726691, + "cpu_ns": 69455356, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1284, + "slot_waits": 0, + "sleep_ns": 128917157, + "latency_mean_ns": 6157981.172, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9013123, + "active_byte_seconds": 353204.287515, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 73, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 284917692, + "cpu_ns": 71405280, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1986, + "reuses": 1070, + "budget_waits": 1377, + "slot_waits": 0, + "sleep_ns": 216762211, + "latency_mean_ns": 5735627.926, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9131844, + "active_byte_seconds": 525336.273255, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 3702784, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 493631884, + "cpu_ns": 81275705, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1338, + "reuses": 710, + "budget_waits": 1381, + "slot_waits": 0, + "sleep_ns": 416164894, + "latency_mean_ns": 5487868.994, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 10310562, + "active_byte_seconds": 930117.776585, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 188818619, + "cpu_ns": 63133028, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2370, + "reuses": 1418, + "budget_waits": 1385, + "slot_waits": 0, + "sleep_ns": 128526332, + "latency_mean_ns": 6127166.122, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8910974, + "active_byte_seconds": 342945.952067, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 179280258, + "cpu_ns": 63072197, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1706, + "reuses": 342, + "budget_waits": 1292, + "slot_waits": 0, + "sleep_ns": 119263134, + "latency_mean_ns": 6405571.191, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9266621, + "active_byte_seconds": 333023.673744, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 72, + "rss_peak_bytes": 4358144, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 289852215, + "cpu_ns": 73110582, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1968, + "reuses": 1088, + "budget_waits": 1366, + "slot_waits": 0, + "sleep_ns": 216619289, + "latency_mean_ns": 5811531.61, + "latency_p99_upper_ns": 10485760, + "latency_max_ns": 14653198, + "active_byte_seconds": 533913.89253, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 117, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 192676756, + "cpu_ns": 64089681, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1353, + "slot_waits": 0, + "sleep_ns": 131367991, + "latency_mean_ns": 6074626.45, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9595565, + "active_byte_seconds": 347823.24917, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 4456448, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 190841576, + "cpu_ns": 65297407, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2364, + "reuses": 1411, + "budget_waits": 1367, + "slot_waits": 0, + "sleep_ns": 128364836, + "latency_mean_ns": 6138653.731, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9614713, + "active_byte_seconds": 347120.008423, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 114, + "rss_peak_bytes": 4407296, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 191186483, + "cpu_ns": 62157319, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2461, + "reuses": 1635, + "budget_waits": 1369, + "slot_waits": 0, + "sleep_ns": 131673734, + "latency_mean_ns": 6044094.394, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8856067, + "active_byte_seconds": 346056.920887, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 116, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 189607302, + "cpu_ns": 65204064, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2354, + "reuses": 1434, + "budget_waits": 1374, + "slot_waits": 0, + "sleep_ns": 127228274, + "latency_mean_ns": 6129531.969, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8917400, + "active_byte_seconds": 344084.726971, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 225029662, + "cpu_ns": 63936677, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2132, + "reuses": 1273, + "budget_waits": 1459, + "slot_waits": 0, + "sleep_ns": 164083415, + "latency_mean_ns": 5845044.948, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 10255656, + "active_byte_seconds": 413025.125756, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 3719168, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 493081274, + "cpu_ns": 77876795, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1342, + "reuses": 706, + "budget_waits": 1396, + "slot_waits": 0, + "sleep_ns": 417981885, + "latency_mean_ns": 5480641.02, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9914990, + "active_byte_seconds": 929020.44715, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 491778453, + "cpu_ns": 73168533, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1352, + "reuses": 696, + "budget_waits": 1408, + "slot_waits": 0, + "sleep_ns": 421825716, + "latency_mean_ns": 5473786.015, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9925323, + "active_byte_seconds": 926994.460577, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 114, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 188770790, + "cpu_ns": 62771264, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2380, + "reuses": 1408, + "budget_waits": 1395, + "slot_waits": 0, + "sleep_ns": 128898177, + "latency_mean_ns": 6126984.319, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8906435, + "active_byte_seconds": 343018.464235, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 116, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 189986910, + "cpu_ns": 58086718, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1383, + "slot_waits": 0, + "sleep_ns": 134499200, + "latency_mean_ns": 6008153.507, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8893917, + "active_byte_seconds": 343741.458999, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 174670169, + "cpu_ns": 55539698, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1727, + "reuses": 321, + "budget_waits": 1312, + "slot_waits": 0, + "sleep_ns": 121848710, + "latency_mean_ns": 6258533.417, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8834323, + "active_byte_seconds": 325246.46456, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 117, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 193714972, + "cpu_ns": 64286995, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2353, + "reuses": 1422, + "budget_waits": 1371, + "slot_waits": 0, + "sleep_ns": 132496502, + "latency_mean_ns": 6233962.195, + "latency_p99_upper_ns": 10485760, + "latency_max_ns": 11493286, + "active_byte_seconds": 352390.15124, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 191684197, + "cpu_ns": 62544845, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2479, + "reuses": 1617, + "budget_waits": 1373, + "slot_waits": 0, + "sleep_ns": 131979930, + "latency_mean_ns": 6064467.489, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8905416, + "active_byte_seconds": 346663.433689, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 229047558, + "cpu_ns": 67894565, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2094, + "reuses": 1311, + "budget_waits": 1425, + "slot_waits": 0, + "sleep_ns": 164358967, + "latency_mean_ns": 5921381.751, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 10252296, + "active_byte_seconds": 418976.246455, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 72, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 283681970, + "cpu_ns": 69888728, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1987, + "reuses": 1069, + "budget_waits": 1378, + "slot_waits": 0, + "sleep_ns": 217008850, + "latency_mean_ns": 5688342.625, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 9247808, + "active_byte_seconds": 522809.985389, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 102, + "rss_peak_bytes": 4325376, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 201965490, + "cpu_ns": 80116546, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2291, + "reuses": 1497, + "budget_waits": 1275, + "slot_waits": 0, + "sleep_ns": 123310392, + "latency_mean_ns": 6483685.515, + "latency_p99_upper_ns": 10485760, + "latency_max_ns": 11156636, + "active_byte_seconds": 364031.028759, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 493440, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 3719168, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 494160071, + "cpu_ns": 76904068, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1346, + "reuses": 702, + "budget_waits": 1397, + "slot_waits": 0, + "sleep_ns": 420872435, + "latency_mean_ns": 5490372.664, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9952646, + "active_byte_seconds": 930733.63783, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 117, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 191245856, + "cpu_ns": 61096231, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2481, + "reuses": 1615, + "budget_waits": 1387, + "slot_waits": 0, + "sleep_ns": 132860200, + "latency_mean_ns": 6056702.267, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8733304, + "active_byte_seconds": 346194.049558, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 4407296, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 174872810, + "cpu_ns": 56429902, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1726, + "reuses": 322, + "budget_waits": 1310, + "slot_waits": 0, + "sleep_ns": 121198578, + "latency_mean_ns": 6260365.932, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8926676, + "active_byte_seconds": 325635.507331, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 224725167, + "cpu_ns": 63560757, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2132, + "reuses": 1273, + "budget_waits": 1456, + "slot_waits": 0, + "sleep_ns": 164191839, + "latency_mean_ns": 5841515.207, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 10276240, + "active_byte_seconds": 412794.10328, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 73, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 283036048, + "cpu_ns": 73067755, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1972, + "reuses": 1084, + "budget_waits": 1379, + "slot_waits": 0, + "sleep_ns": 213650376, + "latency_mean_ns": 5678436.735, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 9220899, + "active_byte_seconds": 521434.266403, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 113, + "rss_peak_bytes": 4456448, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 189790379, + "cpu_ns": 60625611, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2369, + "reuses": 1406, + "budget_waits": 1393, + "slot_waits": 0, + "sleep_ns": 131820061, + "latency_mean_ns": 6110038.652, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9522938, + "active_byte_seconds": 345327.900203, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 192179115, + "cpu_ns": 71826579, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1293, + "slot_waits": 0, + "sleep_ns": 123142369, + "latency_mean_ns": 6044858.508, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8899841, + "active_byte_seconds": 346584.591442, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 175527737, + "cpu_ns": 57919540, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1728, + "reuses": 320, + "budget_waits": 1310, + "slot_waits": 0, + "sleep_ns": 120470904, + "latency_mean_ns": 6282412.787, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 8759229, + "active_byte_seconds": 326547.492623, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 113, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 188314755, + "cpu_ns": 61699247, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2377, + "reuses": 1411, + "budget_waits": 1394, + "slot_waits": 0, + "sleep_ns": 129399417, + "latency_mean_ns": 6110229.914, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8949046, + "active_byte_seconds": 342240.604823, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 72, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 282255055, + "cpu_ns": 67344177, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1990, + "reuses": 1066, + "budget_waits": 1394, + "slot_waits": 0, + "sleep_ns": 217945110, + "latency_mean_ns": 5670280.355, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 9286807, + "active_byte_seconds": 520513.491017, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 116, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 191037345, + "cpu_ns": 60906478, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2491, + "reuses": 1605, + "budget_waits": 1378, + "slot_waits": 0, + "sleep_ns": 132768233, + "latency_mean_ns": 6050681.287, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8878730, + "active_byte_seconds": 345866.556263, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 226976916, + "cpu_ns": 67053797, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2113, + "reuses": 1292, + "budget_waits": 1429, + "slot_waits": 0, + "sleep_ns": 163248630, + "latency_mean_ns": 5888077.344, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 10246777, + "active_byte_seconds": 416261.40193, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 3719168, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 495149938, + "cpu_ns": 79564522, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1338, + "reuses": 710, + "budget_waits": 1391, + "slot_waits": 0, + "sleep_ns": 419504678, + "latency_mean_ns": 5510109.537, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 10578039, + "active_byte_seconds": 933361.552539, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 114, + "rss_peak_bytes": 4243456, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 190688005, + "cpu_ns": 63157202, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2371, + "reuses": 1404, + "budget_waits": 1387, + "slot_waits": 0, + "sleep_ns": 130503094, + "latency_mean_ns": 6140018.745, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9550771, + "active_byte_seconds": 347232.508173, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 4227072, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 195072389, + "cpu_ns": 63921931, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1348, + "slot_waits": 0, + "sleep_ns": 134032681, + "latency_mean_ns": 6160078.753, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9678087, + "active_byte_seconds": 352079.814928, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 28472110, + "cpu_ns": 28306962, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 13730.123, + "latency_p99_upper_ns": 30720, + "latency_max_ns": 65982, + "active_byte_seconds": 1788.400939, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 28620499, + "cpu_ns": 28277537, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 13795.514, + "latency_p99_upper_ns": 40960, + "latency_max_ns": 86816, + "active_byte_seconds": 1561.617327, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20774647, + "cpu_ns": 20729258, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9997.106, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 72204, + "active_byte_seconds": 1105.947559, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20434497, + "cpu_ns": 20435000, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9831.374, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 45186, + "active_byte_seconds": 1087.202091, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 1540096, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 22218843, + "cpu_ns": 22096574, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10699.094, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 59093, + "active_byte_seconds": 1.68791, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20787962, + "cpu_ns": 20732148, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10004.317, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 63834, + "active_byte_seconds": 1109.185459, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21540986, + "cpu_ns": 21477686, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10352.839, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 54334, + "active_byte_seconds": 1151.439237, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20305145, + "cpu_ns": 20305963, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9760.602, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 58019, + "active_byte_seconds": 1256.330694, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20183773, + "cpu_ns": 20183889, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9700.917, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 50334, + "active_byte_seconds": 1250.328097, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1572864, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20604128, + "cpu_ns": 20604259, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9906.217, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 47686, + "active_byte_seconds": 1090.792499, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20493516, + "cpu_ns": 20493889, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9859.747, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 52407, + "active_byte_seconds": 1088.217176, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 1540096, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21524893, + "cpu_ns": 21525074, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10363.91, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 54889, + "active_byte_seconds": 1.632854, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20381497, + "cpu_ns": 20382185, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9805.745, + "latency_p99_upper_ns": 12288, + "latency_max_ns": 47353, + "active_byte_seconds": 1087.257309, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20244385, + "cpu_ns": 20244778, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9731.208, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 41426, + "active_byte_seconds": 1251.232387, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 22684901, + "cpu_ns": 22652390, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10925.772, + "latency_p99_upper_ns": 22528, + "latency_max_ns": 69779, + "active_byte_seconds": 1228.197157, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 22993108, + "cpu_ns": 22893763, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 11062.46, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 74149, + "active_byte_seconds": 1228.067562, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20874129, + "cpu_ns": 20775314, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10044.57, + "latency_p99_upper_ns": 16384, + "latency_max_ns": 55556, + "active_byte_seconds": 1112.362141, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1572864, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20176198, + "cpu_ns": 20176500, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9697.062, + "latency_p99_upper_ns": 12288, + "latency_max_ns": 39001, + "active_byte_seconds": 1246.966591, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20382034, + "cpu_ns": 20382277, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9806.967, + "latency_p99_upper_ns": 12288, + "latency_max_ns": 48779, + "active_byte_seconds": 1084.755978, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20453663, + "cpu_ns": 20454074, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9841.173, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 53241, + "active_byte_seconds": 1090.324497, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20636628, + "cpu_ns": 20636871, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9920.915, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 40464, + "active_byte_seconds": 1084.995845, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20413515, + "cpu_ns": 20355722, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9812.253, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 64112, + "active_byte_seconds": 1264.219091, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20421089, + "cpu_ns": 20421796, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9825.86, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 53519, + "active_byte_seconds": 1088.001718, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 1540096, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21525764, + "cpu_ns": 21526111, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10364.601, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 65686, + "active_byte_seconds": 1.632264, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20829074, + "cpu_ns": 20798316, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10025.307, + "latency_p99_upper_ns": 15360, + "latency_max_ns": 65907, + "active_byte_seconds": 1111.338342, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1572864, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20018290, + "cpu_ns": 20018407, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9620.129, + "latency_p99_upper_ns": 12288, + "latency_max_ns": 34982, + "active_byte_seconds": 1238.904613, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 19968383, + "cpu_ns": 19952888, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9596.16, + "latency_p99_upper_ns": 11264, + "latency_max_ns": 52612, + "active_byte_seconds": 1236.323437, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21098686, + "cpu_ns": 21002369, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10153.021, + "latency_p99_upper_ns": 16384, + "latency_max_ns": 71204, + "active_byte_seconds": 1130.040808, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 1540096, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21459134, + "cpu_ns": 21459111, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10332.156, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 41834, + "active_byte_seconds": 1.628379, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21122947, + "cpu_ns": 21019219, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10159.663, + "latency_p99_upper_ns": 16384, + "latency_max_ns": 63556, + "active_byte_seconds": 1125.541597, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1572864, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20421330, + "cpu_ns": 20421426, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9825.919, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 56575, + "active_byte_seconds": 1089.501083, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20561183, + "cpu_ns": 20561592, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9883.337, + "latency_p99_upper_ns": 12288, + "latency_max_ns": 45185, + "active_byte_seconds": 1085.229592, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20082994, + "cpu_ns": 20072296, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9647.624, + "latency_p99_upper_ns": 12288, + "latency_max_ns": 51259, + "active_byte_seconds": 1243.235174, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 1589248, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 23280610, + "cpu_ns": 23231480, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 11211.571, + "latency_p99_upper_ns": 24576, + "latency_max_ns": 54778, + "active_byte_seconds": 1448.645185, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21989119, + "cpu_ns": 21857259, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10590.063, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 79983, + "active_byte_seconds": 1185.989886, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21265947, + "cpu_ns": 21072353, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10215.53, + "latency_p99_upper_ns": 16384, + "latency_max_ns": 91668, + "active_byte_seconds": 1125.003597, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 1507328, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 21358726, + "cpu_ns": 21358833, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10284.012, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 47648, + "active_byte_seconds": 1.620534, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1572864, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20441422, + "cpu_ns": 20426148, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9835.821, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 50945, + "active_byte_seconds": 1089.704272, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20717721, + "cpu_ns": 20717834, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9969.584, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 40148, + "active_byte_seconds": 1106.215747, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 1605632, + "rusage_maxrss_bytes": 27246592, + "rss_peak_source": "linux_proc_vmhwm_approximate", + "elapsed_ns": 20693813, + "cpu_ns": 20694148, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9958.775, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 66019, + "active_byte_seconds": 1106.254829, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + } +] diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/benchmark.c b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/benchmark.c new file mode 100644 index 0000000..c9e8120 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/benchmark.c @@ -0,0 +1,338 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* A measurement apparatus, not the Pure runtime or a general document parser. */ +enum { CACHE_SLOTS = 16, SCRATCH = 65536, HIST_SIZE = 512, MAX_FRAME = 8*1024*1024 }; +typedef struct Block { size_t capacity; uint64_t since; unsigned char bytes[]; } Block; +typedef struct Item { + Block *block; + size_t offset, length; + uint64_t start, ready; +} Item; +typedef struct Engine { + Item *queue; + Block *cache[CACHE_SLOTS]; + unsigned char scratch[SCRATCH]; + uint64_t histogram[HIST_SIZE]; + size_t slots, head, count, fixed, budget, reserved, active, cached; + size_t peak_reserved, peak_active, peak_queue; + uint64_t allocations, reuses, copies, read_bytes, written_bytes; + uint64_t accepted, emitted, copied_records, borrowed_records, pressure_copies; + uint64_t budget_waits, slot_waits, sleep_ns, hold_ns, service_ns, last_emit; + uint64_t elapsed_ns, cpu_ns, latency_sum, latency_max, start, cancel_after; + long double active_byte_ns; + unsigned ratio; + bool pool; + const char *policy, *status; +} Engine; +static volatile sig_atomic_t interrupted; +static void stop(int sig) { (void)sig; interrupted = 1; } +static uint64_t ns(clockid_t clock) { + struct timespec t; + if (clock_gettime(clock, &t)) { perror("clock_gettime"); exit(2); } + return (uint64_t)t.tv_sec * UINT64_C(1000000000) + (uint64_t)t.tv_nsec; +} +static size_t charge(const Block *b) { return sizeof(*b) + b->capacity; } +static size_t rounded(size_t n) { + size_t r = 64; + while (r < n) r *= 2; + return r; +} +static void peaks(Engine *e) { + assert(e->active + e->cached == e->reserved); + assert(e->fixed + e->reserved <= e->budget); + if (e->reserved > e->peak_reserved) e->peak_reserved = e->reserved; + if (e->active > e->peak_active) e->peak_active = e->active; +} +static void evict(Engine *e, size_t i) { + Block *b = e->cache[i]; + if (!b) return; + e->cached -= charge(b); e->reserved -= charge(b); + free(b); e->cache[i] = NULL; +} +static Block *take(Engine *e, size_t n) { + size_t capacity = rounded(n), need = sizeof(Block) + capacity; + if (e->pool) { + for (size_t i = 0; i < CACHE_SLOTS; ++i) { + Block *b = e->cache[i]; + if (b && b->capacity == capacity) { + e->cache[i] = NULL; e->cached -= need; e->active += need; + ++e->reuses; b->since = ns(CLOCK_MONOTONIC); peaks(e); return b; + } + } + } + for (size_t i = 0; i < CACHE_SLOTS && need > e->budget-e->fixed-e->reserved; ++i) + evict(e, i); + if (need > e->budget-e->fixed-e->reserved) return NULL; + Block *b = malloc(need); + if (!b) { e->status = "allocation_failed"; return NULL; } + b->capacity = capacity; b->since = ns(CLOCK_MONOTONIC); + e->reserved += need; e->active += need; ++e->allocations; peaks(e); return b; +} +static void give(Engine *e, Block *b) { + if (!b) return; + size_t bytes = charge(b); + e->active_byte_ns += (long double)bytes * (ns(CLOCK_MONOTONIC)-b->since); + e->active -= bytes; + if (e->pool && bytes <= e->budget/4-e->cached) { + for (size_t i = 0; i < CACHE_SLOTS; ++i) if (!e->cache[i]) { + e->cache[i] = b; e->cached += bytes; peaks(e); return; + } + } + e->reserved -= bytes; free(b); peaks(e); +} +static bool sleep_until(Engine *e, uint64_t deadline) { + uint64_t t = ns(CLOCK_MONOTONIC), start = t; + while (t < deadline && !interrupted) { + uint64_t delta = deadline-t; + struct timespec request = {(time_t)(delta/1000000000), (long)(delta%1000000000)}; + if (nanosleep(&request, NULL) && errno != EINTR) { e->status="sleep_failed"; return false; } + t = ns(CLOCK_MONOTONIC); + } + e->sleep_ns += t-start; + return !interrupted; +} +/* floor(log2(ns)) with eight subdivisions: upper bound error <= 12.5%. */ +static unsigned bucket(uint64_t n) { + unsigned exponent = 0; + for (uint64_t x = n; x > 1; x >>= 1) ++exponent; + if (exponent < 3) return (unsigned)n; + return exponent*8 + (unsigned)((n-(UINT64_C(1)<>(exponent-3)); +} +static uint64_t upper(unsigned b) { + if (b < 24) return b+1; + unsigned exponent = b/8; + if (exponent >= 63) return UINT64_MAX; + return (UINT64_C(1)<status = "output_failed"; return false; } + p += written; n -= (size_t)written; e->written_bytes += (uint64_t)written; + } + return !interrupted; +} +static bool emit(Engine *e) { + assert(e->count); + Item *item = &e->queue[e->head]; + uint64_t ready = item->ready; + if (e->last_emit + e->service_ns > ready) ready = e->last_emit + e->service_ns; + if (!sleep_until(e, ready)) return false; + uint32_t len = (uint32_t)item->length; + unsigned char header[4] = {(unsigned char)len, (unsigned char)(len>>8), + (unsigned char)(len>>16), (unsigned char)(len>>24)}; + if (!write_all(e, header, sizeof header) || + !write_all(e, item->block->bytes+item->offset, item->length)) return false; + uint64_t now = ns(CLOCK_MONOTONIC), latency = now-item->start; + ++e->histogram[bucket(latency)]; e->latency_sum += latency; + if (latency > e->latency_max) e->latency_max = latency; + e->last_emit = now; ++e->emitted; + give(e, item->block); item->block = NULL; + e->head = (e->head+1)%e->slots; --e->count; + if (e->cancel_after && e->emitted >= e->cancel_after) { + e->status = "cancelled"; return false; + } + return true; +} +static Block *acquire(Engine *e, size_t n) { + for (;;) { + Block *b = take(e, n); + if (b) return b; + if (strcmp(e->status, "ok")) return NULL; + if (!e->count) { e->status = "budget_too_small"; return NULL; } + ++e->budget_waits; + if (!emit(e)) return NULL; + } +} +/* Returns 1 on complete read, 0 on clean EOF before any byte, -1 on failure. */ +static int read_exact(Engine *e, void *dst, size_t n) { + unsigned char *p = dst; size_t done = 0; + while (done < n && !interrupted) { + ssize_t got = read(STDIN_FILENO, p+done, n-done); + if (got < 0 && errno == EINTR) continue; + if (got < 0) { e->status="input_failed"; return -1; } + if (!got) { if (!done) return 0; e->status="truncated_input"; return -1; } + done += (size_t)got; e->read_bytes += (uint64_t)got; + } + return interrupted ? -1 : 1; +} +static bool discard(Engine *e, size_t n) { + while (n) { + size_t chunk = n < SCRATCH ? n : SCRATCH; + if (read_exact(e, e->scratch, chunk) != 1) { + if (!strcmp(e->status,"ok")) e->status="truncated_input"; + return false; + } + n -= chunk; + } + return true; +} +static uint32_t le32(const unsigned char *p) { + return (uint32_t)p[0] | (uint32_t)p[1]<<8 | (uint32_t)p[2]<<16 | (uint32_t)p[3]<<24; +} +/* Linux's rusage high-water mark survives exec. Read the current mm's HWM + * separately; proc RSS accounting is approximate, not allocator accounting. */ +static uint64_t image_peak_rss(void) { +#if defined(__linux__) + FILE *status = fopen("/proc/self/status", "r"); + if (!status) return 0; + char line[256]; unsigned long long kib = 0; + while (fgets(line, sizeof line, status)) + if (sscanf(line, "VmHWM: %llu kB", &kib) == 1) break; + fclose(status); + return (uint64_t)kib * 1024; +#else + return 0; /* No current-image peak implemented for other OSes. */ +#endif +} +static void run(Engine *e) { + for (;;) { + if (interrupted) break; + if (e->count) { + Item *head = &e->queue[e->head]; + uint64_t now = ns(CLOCK_MONOTONIC); + if (now >= head->ready && now >= e->last_emit+e->service_ns) { + if (!emit(e)) break; + continue; + } + } + if (e->count == e->slots) { ++e->slot_waits; if (!emit(e)) break; } + unsigned char header[12]; + uint64_t start = ns(CLOCK_MONOTONIC); + int got = read_exact(e, header, sizeof header); + if (got != 1) { + if (!got) while (e->count && !interrupted) if (!emit(e)) break; + break; + } + size_t payload = le32(header), offset = le32(header+4), length = le32(header+8); + if (!payload || payload > MAX_FRAME || !length || offset > payload || length > payload-offset) { + e->status="invalid_frame"; break; + } + bool project = !strcmp(e->policy,"project"); + Block *source = acquire(e, project ? length : payload); + if (!source) break; + bool valid; + if (project) { + valid = discard(e, offset) && read_exact(e, source->bytes, length)==1 && + discard(e, payload-offset-length); + offset = 0; + } else valid = read_exact(e, source->bytes, payload)==1; + if (!valid) { + if (!strcmp(e->status,"ok")) e->status="truncated_input"; + give(e, source); break; + } + bool copy = !strcmp(e->policy,"copy") || + (!strcmp(e->policy,"threshold") && rounded(payload)/rounded(length) >= e->ratio); + if (!strcmp(e->policy,"pressure")) { + /* Online rule using current charged live buffers; no future lifetime. */ + copy = rounded(length) < source->capacity && + e->active > (e->budget-e->fixed)/2; + if (copy) ++e->pressure_copies; + } + if (!project && copy) { + Block *small = acquire(e, length); + if (!small) { give(e, source); break; } + memcpy(small->bytes, source->bytes+offset, length); + e->copies += length; ++e->copied_records; + give(e, source); source = small; offset = 0; + } else if (!project) ++e->borrowed_records; + size_t tail = (e->head+e->count)%e->slots; + e->queue[tail] = (Item){source, offset, length, start, ns(CLOCK_MONOTONIC)+e->hold_ns}; + ++e->count; ++e->accepted; + if (e->count > e->peak_queue) e->peak_queue=e->count; + } + if (interrupted) e->status="cancelled"; +} +static uint64_t number(const char *s, uint64_t maximum) { + char *end; errno=0; + unsigned long long n = strtoull(s, &end, 10); + if (!*s || *s=='-' || *end || errno || n>maximum) { fprintf(stderr,"invalid number\n"); exit(2); } + return n; +} +int main(int argc, char **argv) { + Engine *e = calloc(1, sizeof *e); + if (!e) return 2; + e->policy="borrow"; e->status="ok"; e->budget=8*1024*1024; e->slots=512; + e->ratio=4; e->pool=true; + for (int i=1; ipolicy=value; + else if (!strcmp(key,"--budget")) e->budget=(size_t)number(value,1073741824); + else if (!strcmp(key,"--slots")) e->slots=(size_t)number(value,4096); + else if (!strcmp(key,"--hold-us")) e->hold_ns=number(value,10000000)*1000; + else if (!strcmp(key,"--service-us")) e->service_ns=number(value,10000000)*1000; + else if (!strcmp(key,"--ratio")) e->ratio=(unsigned)number(value,UINT_MAX); + else if (!strcmp(key,"--pool")) e->pool=number(value,1)!=0; + else if (!strcmp(key,"--cancel-after")) e->cancel_after=number(value,UINT64_MAX); + else { fprintf(stderr,"unknown option\n"); free(e); return 2; } + } + bool known = !strcmp(e->policy,"borrow") || !strcmp(e->policy,"copy") || + !strcmp(e->policy,"threshold") || !strcmp(e->policy,"pressure") || !strcmp(e->policy,"project"); + if (!known || !e->slots || e->slots>4096 || e->ratio<2 || e->budget>UINT64_C(1073741824) || + e->hold_ns>UINT64_C(10000000000) || e->service_ns>UINT64_C(10000000000)) { + fprintf(stderr,"invalid configuration\n"); free(e); return 2; + } + e->fixed=sizeof(*e)+e->slots*sizeof(Item); + if (e->budget < e->fixed) { fprintf(stderr,"budget below fixed metadata\n"); free(e); return 2; } + e->queue=calloc(e->slots,sizeof(Item)); + if (!e->queue) { free(e); return 2; } + signal(SIGINT,stop); signal(SIGTERM,stop); signal(SIGPIPE,SIG_IGN); + e->start=ns(CLOCK_MONOTONIC); uint64_t cpu=ns(CLOCK_PROCESS_CPUTIME_ID); + run(e); + size_t live_at_stop=e->active, cached_at_stop=e->cached; + while (e->count) { + give(e,e->queue[e->head].block); e->head=(e->head+1)%e->slots; --e->count; + } + for (size_t i=0;iactive && !e->reserved && !e->cached); + e->elapsed_ns=ns(CLOCK_MONOTONIC)-e->start; e->cpu_ns=ns(CLOCK_PROCESS_CPUTIME_ID)-cpu; + uint64_t target=e->emitted-e->emitted/100, sum=0, p99=0; + for (unsigned b=0; e->emitted && bhistogram[b]; if (sum>=target) { p99=upper(b); break; } + } + struct rusage usage; long rss=0; + if (!getrusage(RUSAGE_SELF,&usage)) rss=usage.ru_maxrss; +#if defined(__APPLE__) + uint64_t rusage_bytes=(uint64_t)rss; +#else + uint64_t rusage_bytes=(uint64_t)rss*1024; +#endif + uint64_t rss_bytes=image_peak_rss(); + fprintf(stderr,"{\"status\":\"%s\",\"policy\":\"%s\",\"pool\":%s," + "\"budget_bytes\":%zu,\"fixed_bytes\":%zu,\"peak_reserved_bytes\":%zu," + "\"peak_active_bytes\":%zu,\"peak_queue\":%zu,\"rss_peak_bytes\":%" PRIu64 "," + "\"rusage_maxrss_bytes\":%" PRIu64 ",\"rss_peak_source\":\"%s\"," + "\"elapsed_ns\":%" PRIu64 ",\"cpu_ns\":%" PRIu64 ",\"input_bytes\":%" PRIu64 "," + "\"output_bytes\":%" PRIu64 ",\"accepted\":%" PRIu64 ",\"emitted\":%" PRIu64 "," + "\"copy_bytes\":%" PRIu64 ",\"copied_records\":%" PRIu64 ",\"borrowed_records\":%" PRIu64 "," + "\"pressure_copies\":%" PRIu64 ",\"allocations\":%" PRIu64 ",\"reuses\":%" PRIu64 "," + "\"budget_waits\":%" PRIu64 ",\"slot_waits\":%" PRIu64 ",\"sleep_ns\":%" PRIu64 "," + "\"latency_mean_ns\":%.3f,\"latency_p99_upper_ns\":%" PRIu64 ",\"latency_max_ns\":%" PRIu64 "," + "\"active_byte_seconds\":%.6Lf,\"live_at_stop_bytes\":%zu,\"cached_at_stop_bytes\":%zu," + "\"unreleased_buffer_bytes\":%zu}\n", + e->status,e->policy,e->pool?"true":"false",e->budget,e->fixed,e->fixed+e->peak_reserved, + e->fixed+e->peak_active,e->peak_queue,rss_bytes,rusage_bytes, + rss_bytes?"linux_proc_vmhwm_approximate":"unavailable",e->elapsed_ns,e->cpu_ns,e->read_bytes, + e->written_bytes,e->accepted,e->emitted,e->copies,e->copied_records,e->borrowed_records, + e->pressure_copies,e->allocations,e->reuses,e->budget_waits,e->slot_waits,e->sleep_ns, + e->emitted?(double)e->latency_sum/(double)e->emitted:0,p99,e->latency_max, + e->active_byte_ns/1e9L,live_at_stop,cached_at_stop,e->reserved); + int result=!strcmp(e->status,"ok")?0:!strcmp(e->status,"cancelled")?3:2; + free(e->queue); free(e); return result; +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/plan.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/plan.json new file mode 100644 index 0000000..1737f45 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/plan.json @@ -0,0 +1,35 @@ +{ + "version": 2, + "written_before_measurement": true, + "revision_note": "The v1 run is preserved. Before this v2 run, RSS instrumentation was corrected and a 64-slot sparse sensitivity case was added after observing different queue occupancy. This is an exploratory follow-up, not a pre-registered confirmation.", + "seed": 20260922, + "repeats": 5, + "slots": 512, + "policies": [ + {"name": "borrow", "policy": "borrow", "pool": 1}, + {"name": "copy", "policy": "copy", "pool": 1}, + {"name": "threshold2", "policy": "threshold", "ratio": 2, "pool": 1}, + {"name": "threshold4", "policy": "threshold", "ratio": 4, "pool": 1}, + {"name": "threshold8", "policy": "threshold", "ratio": 8, "pool": 1}, + {"name": "pressure", "policy": "pressure", "pool": 1}, + {"name": "project", "policy": "project", "pool": 1}, + {"name": "copy_no_pool", "policy": "copy", "pool": 0} + ], + "cases": [ + {"name": "sparse_slow", "shape": "sparse", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "sparse_slots64", "shape": "sparse", "records": 2048, "budget": 8388608, "slots": 64, "hold_us": 5000, "service_us": 20}, + {"name": "dense_slow", "shape": "dense", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "mixed_slow", "shape": "mixed", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "mixed_tight", "shape": "mixed", "records": 2048, "budget": 2097152, "hold_us": 5000, "service_us": 20}, + {"name": "sparse_fast", "shape": "sparse", "records": 2048, "budget": 8388608, "hold_us": 0, "service_us": 0} + ], + "expectations": [ + "Borrow retains full source allocations for small selections.", + "Copy can add CPU and transient source plus destination memory for dense selections.", + "A current-pressure rule need not outperform a static size rule or early projection.", + "Pool reuse can reduce allocation calls while increasing reserved idle memory.", + "No Pure-specific advantage, application SLO, or statistical significance is presumed." + ], + "comparison_rule": "Report all presets and repetitions; no post-hoc winner is called an out-of-sample tuned policy.", + "limits": "Single FIFO local pipeline with synthetic delayed sink. No Sonic/Node/.NET benchmark, fan-out, persistent cache, generic JSON parser, external open-loop arrivals, or production SLO." +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/run.py b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/run.py new file mode 100644 index 0000000..49a693c --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/sources/run.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +"""Build, independently verify, and measure the retention experiment.""" +import argparse +import csv +import hashlib +import json +import os +from pathlib import Path +import platform +import random +import shlex +import signal +import statistics +import struct +import subprocess +import tempfile +import time + +ROOT = Path(__file__).resolve().parent +FLAGS = ["-std=c17", "-Wall", "-Wextra", "-Wpedantic", "-Werror", "-O2", "-g"] + + +def digest(path): + with open(path, "rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def dump(path, value): + path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n") + + +def build(cc, output, sanitizer=False): + command = [*cc, *FLAGS] + if sanitizer: + command += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] + command += [str(ROOT / "benchmark.c"), "-o", str(output)] + result = subprocess.run(command, capture_output=True, text=True, check=True) + return {"command": command, "stderr": result.stderr, "sha256": digest(output), + "size_bytes": output.stat().st_size} + + +def shape_for(shape, rng, i): + if shape == "sparse": + return 65536, 64 + if shape == "dense": + return 65536, 49152 + if shape == "mixed": + source = rng.choice([4096, 16384, 65536, 262144]) + keep = max(16, source // rng.choice([1, 2, 4, 8, 64, 1024])) + return source, keep + if shape == "test": + return [(1, 1), (63, 17), (4097, 12), (65536, 49152), (127, 127)][i % 5] + raise ValueError(shape) + + +def fixture(path, shape, count, seed): + rng = random.Random(seed) + expected = hashlib.sha256() + total = 0 + with path.open("wb") as stream: + for i in range(count): + size, keep = shape_for(shape, rng, i) + offset = rng.randrange(size - keep + 1) + # All input bytes exist on disk; no sparse files or seeking past data. + tile = rng.randbytes(256) + payload = (tile * ((size + 255) // 256))[:size] + stream.write(struct.pack(" size - keep: + raise ValueError("invalid frame") + if len(data) - at < size: + raise ValueError("short payload") + result.extend(struct.pack(" 0 + values[policy["policy"]] = m["rss_peak_bytes"] + # Only a sanity check for this deliberately wide memory gap, not exact RSS arithmetic. + assert values["borrow"] > values["project"], values + checks.append("current-image VmHWM distinguishes large retained sources from small projections") + return checks + + +def sample(binary, source, output, case, policy): + with source.open("rb") as inp, output.open("wb") as out: + result = subprocess.run(command(binary, case, policy), stdin=inp, stdout=out, + stderr=subprocess.PIPE, timeout=30) + m = metrics(result.stderr) + assert result.returncode == 0 and m["status"] == "ok", result.stderr + return m + + +def summarize(rows): + result = [] + names = ["elapsed_ns", "cpu_ns", "peak_reserved_bytes", "peak_active_bytes", "rss_peak_bytes", + "copy_bytes", "active_byte_seconds", "latency_mean_ns", "latency_p99_upper_ns", + "budget_waits", "allocations", "reuses"] + groups = sorted({(r["case"], r["variant"]) for r in rows}) + for case, variant in groups: + group = [r for r in rows if (r["case"], r["variant"]) == (case, variant)] + value = {"case": case, "variant": variant, "samples": len(group)} + for name in names: + values = [r[name] for r in group] + value[name] = {"median": statistics.median(values), "min": min(values), "max": max(values)} + result.append(value) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check-only", action="store_true") + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if not args.check_only and args.output is None: + parser.error("provide --check-only or --output (new directory)") + if args.output: + args.output.mkdir(parents=True, exist_ok=False) + cc = shlex.split(os.environ.get("CC", "cc")) + plan = json.loads((ROOT / "plan.json").read_text()) + affinity = None + if hasattr(os, "sched_getaffinity"): + affinity = min(os.sched_getaffinity(0)) + os.sched_setaffinity(0, {affinity}) + with tempfile.TemporaryDirectory(prefix="pure-retention-") as folder: + tmp = Path(folder) + native, sanitized = tmp / "bench", tmp / "bench-sanitized" + builds = [build(cc, native), build(cc, sanitized, True)] + verification = {"native": check(native, tmp, plan["policies"]), + "asan_ubsan": check(sanitized, tmp, plan["policies"])} + print(f'Verified {sum(map(len, verification.values()))} scenario groups (native + ASan/UBSan).', flush=True) + if args.check_only: + return + sources = {name: digest(ROOT / name) for name in ["benchmark.c", "run.py", "plan.json"]} + environment = {"platform": platform.platform(), "machine": platform.machine(), + "python": platform.python_version(), "cpu_affinity": affinity, + "compiler": subprocess.check_output([*cc, "--version"], text=True), + "builds": builds, "source_sha256": sources, + "clock": "CLOCK_MONOTONIC; CPU = CLOCK_PROCESS_CPUTIME_ID", + "timing": "includes file IO, scheduling, deliberate sleep, metrics, buffer cleanup; excludes startup/build/fixture/verification", + "limitations": "shared host; unregulated thermals; cached files; output page cache, no fsync; synthetic closed-loop arrivals", + "started_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())} + if Path("/proc/cpuinfo").exists(): + environment["cpuinfo"] = Path("/proc/cpuinfo").read_text() + dump(args.output / "environment.json", environment) + dump(args.output / "verification.json", verification) + dump(args.output / "plan.json", plan) + (args.output / "sources").mkdir() + for name in sources: + (args.output / "sources" / name).write_bytes((ROOT / name).read_bytes()) + fixtures, rows = [], [] + rng = random.Random(plan["seed"]) + for case in plan["cases"]: + source, output = tmp / "input.frames", tmp / "output.frames" + info = fixture(source, case["shape"], case["records"], plan["seed"]) + info["case"] = case["name"] + fixtures.append(info) + # Warm each strategy once. The following measurements use fresh processes. + for policy in plan["policies"]: + sample(native, source, output, case, policy) + assert digest(output) == info["expected_output_sha256"] + for repeat in range(plan["repeats"]): + order = list(plan["policies"]) + rng.shuffle(order) + for policy in order: + m = sample(native, source, output, case, policy) + assert m["accepted"] == m["emitted"] == case["records"] + assert m["input_bytes"] == info["input_bytes"] + assert m["output_bytes"] == info["expected_output_bytes"] + assert digest(output) == info["expected_output_sha256"] + rows.append({"case": case["name"], "variant": policy["name"], "repeat": repeat, **m}) + dump(args.output / "raw.json", rows) + dump(args.output / "fixtures.json", fixtures) + print(f'Completed {case["name"]}: {len(plan["policies"])} variants x {plan["repeats"]} trials.', flush=True) + dump(args.output / "summary.json", summarize(rows)) + with (args.output / "raw.csv").open("w") as out: + writer = csv.DictWriter(out, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + # Detect accidental source edits during a measurement run. + assert sources == {name: digest(ROOT / name) for name in sources} + dump(args.output / "complete.json", {"samples": len(rows), "all_output_hashes_match": True, + "finished_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}) + print(args.output.resolve()) + + +if __name__ == "__main__": + main() diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/summary.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/summary.json new file mode 100644 index 0000000..fceebba --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/summary.json @@ -0,0 +1,3122 @@ +[ + { + "case": "dense_slow", + "variant": "borrow", + "samples": 5, + "elapsed_ns": { + "median": 176146657, + "min": 160544406, + "max": 186194683 + }, + "cpu_ns": { + "median": 82398748, + "min": 78514601, + "max": 90636057 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1395956.019109, + "min": 1263635.432461, + "max": 1477291.36261 + }, + "latency_mean_ns": { + "median": 10456864.841, + "min": 9456018.519, + "max": 11071491.94 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 12582912, + "max": 13631488 + }, + "budget_waits": { + "median": 1605, + "min": 1220, + "max": 1865 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 1922, + "min": 1922, + "max": 1922 + } + }, + { + "case": "dense_slow", + "variant": "copy", + "samples": 5, + "elapsed_ns": { + "median": 181070273, + "min": 162629252, + "max": 184530299 + }, + "cpu_ns": { + "median": 83392187, + "min": 78669541, + "max": 88543490 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 100663296, + "min": 100663296, + "max": 100663296 + }, + "active_byte_seconds": { + "median": 1435135.645514, + "min": 1279274.820821, + "max": 1465541.553301 + }, + "latency_mean_ns": { + "median": 10688072.436, + "min": 9526763.21, + "max": 10914646.174 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 12582912, + "max": 12582912 + }, + "budget_waits": { + "median": 1923, + "min": 1913, + "max": 1923 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 3970, + "min": 3970, + "max": 3970 + } + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "samples": 5, + "elapsed_ns": { + "median": 181746660, + "min": 133391651, + "max": 185672333 + }, + "cpu_ns": { + "median": 80542923, + "min": 79010560, + "max": 112766017 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 100663296, + "min": 100663296, + "max": 100663296 + }, + "active_byte_seconds": { + "median": 1442582.012105, + "min": 1038602.867364, + "max": 1473966.510893 + }, + "latency_mean_ns": { + "median": 10743611.753, + "min": 7733517.169, + "max": 10977524.849 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 11534336, + "max": 12582912 + }, + "budget_waits": { + "median": 1923, + "min": 1523, + "max": 1923 + }, + "allocations": { + "median": 4096, + "min": 4096, + "max": 4096 + }, + "reuses": { + "median": 0, + "min": 0, + "max": 0 + } + }, + { + "case": "dense_slow", + "variant": "pressure", + "samples": 5, + "elapsed_ns": { + "median": 180208516, + "min": 169205769, + "max": 181604364 + }, + "cpu_ns": { + "median": 83532190, + "min": 78951924, + "max": 93894859 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9486336, + "max": 9486336 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1428781.613196, + "min": 1323477.613603, + "max": 1442046.73663 + }, + "latency_mean_ns": { + "median": 10704952.958, + "min": 9905630.789, + "max": 10805826.457 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 11534336, + "max": 14680064 + }, + "budget_waits": { + "median": 1755, + "min": 1358, + "max": 1794 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 1922, + "min": 1922, + "max": 1922 + } + }, + { + "case": "dense_slow", + "variant": "project", + "samples": 5, + "elapsed_ns": { + "median": 181552086, + "min": 172321224, + "max": 187716865 + }, + "cpu_ns": { + "median": 80699818, + "min": 74974355, + "max": 93701381 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1438045.689091, + "min": 1352204.310615, + "max": 1492582.952313 + }, + "latency_mean_ns": { + "median": 10774249.202, + "min": 10121009.211, + "max": 11188037.068 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 12582912, + "max": 13631488 + }, + "budget_waits": { + "median": 1690, + "min": 1317, + "max": 1885 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 1922, + "min": 1922, + "max": 1922 + } + }, + { + "case": "dense_slow", + "variant": "threshold2", + "samples": 5, + "elapsed_ns": { + "median": 177206635, + "min": 133633132, + "max": 179808887 + }, + "cpu_ns": { + "median": 83537344, + "min": 82100973, + "max": 106591389 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1405563.035864, + "min": 1042347.024547, + "max": 1427516.577261 + }, + "latency_mean_ns": { + "median": 10529385.897, + "min": 7778565.752, + "max": 10695483.804 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 11534336, + "max": 12582912 + }, + "budget_waits": { + "median": 1640, + "min": 396, + "max": 1739 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 1922, + "min": 1922, + "max": 1922 + } + }, + { + "case": "dense_slow", + "variant": "threshold4", + "samples": 5, + "elapsed_ns": { + "median": 161215386, + "min": 150178547, + "max": 177934893 + }, + "cpu_ns": { + "median": 89862451, + "min": 82048383, + "max": 99861953 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1273441.862727, + "min": 1171761.488413, + "max": 1411845.015325 + }, + "latency_mean_ns": { + "median": 9530310.653, + "min": 8764951.531, + "max": 10577573.546 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 11534336, + "max": 12582912 + }, + "budget_waits": { + "median": 1244, + "min": 890, + "max": 1719 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 1922, + "min": 1922, + "max": 1922 + } + }, + { + "case": "dense_slow", + "variant": "threshold8", + "samples": 5, + "elapsed_ns": { + "median": 177819652, + "min": 154311406, + "max": 184526170 + }, + "cpu_ns": { + "median": 80765616, + "min": 78362511, + "max": 92958406 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1410709.704111, + "min": 1216142.805053, + "max": 1466214.025114 + }, + "latency_mean_ns": { + "median": 10569593.51, + "min": 9096496.741, + "max": 10988073.928 + }, + "latency_p99_upper_ns": { + "median": 12582912, + "min": 11534336, + "max": 12582912 + }, + "budget_waits": { + "median": 1732, + "min": 1082, + "max": 1834 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 1922, + "min": 1922, + "max": 1922 + } + }, + { + "case": "mixed_slow", + "variant": "borrow", + "samples": 5, + "elapsed_ns": { + "median": 156530676, + "min": 154664385, + "max": 160859182 + }, + "cpu_ns": { + "median": 80398481, + "min": 75260445, + "max": 83693169 + }, + "peak_reserved_bytes": { + "median": 8386736, + "min": 8386736, + "max": 8386736 + }, + "peak_active_bytes": { + "median": 8386688, + "min": 8386688, + "max": 8386736 + }, + "rss_peak_bytes": { + "median": 11059200, + "min": 11042816, + "max": 11059200 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1232378.631527, + "min": 1217311.080951, + "max": 1265871.620641 + }, + "latency_mean_ns": { + "median": 6972909.934, + "min": 6891592.543, + "max": 7168367.113 + }, + "latency_p99_upper_ns": { + "median": 9437184, + "min": 9437184, + "max": 9437184 + }, + "budget_waits": { + "median": 1014, + "min": 998, + "max": 1060 + }, + "allocations": { + "median": 1298, + "min": 1286, + "max": 1309 + }, + "reuses": { + "median": 750, + "min": 739, + "max": 762 + } + }, + { + "case": "mixed_slow", + "variant": "copy", + "samples": 5, + "elapsed_ns": { + "median": 148908587, + "min": 148231627, + "max": 160379239 + }, + "cpu_ns": { + "median": 62691660, + "min": 60872675, + "max": 76853903 + }, + "peak_reserved_bytes": { + "median": 8388576, + "min": 8388560, + "max": 8388576 + }, + "peak_active_bytes": { + "median": 8388432, + "min": 8388432, + "max": 8388544 + }, + "rss_peak_bytes": { + "median": 11583488, + "min": 11026432, + "max": 11583488 + }, + "copy_bytes": { + "median": 53727440, + "min": 53727440, + "max": 53727440 + }, + "active_byte_seconds": { + "median": 1068483.42084, + "min": 1061176.084143, + "max": 1126649.918907 + }, + "latency_mean_ns": { + "median": 19542842.845, + "min": 19421431.677, + "max": 20559126.612 + }, + "latency_p99_upper_ns": { + "median": 27262976, + "min": 27262976, + "max": 31457280 + }, + "budget_waits": { + "median": 1060, + "min": 945, + "max": 1082 + }, + "allocations": { + "median": 2282, + "min": 2142, + "max": 2295 + }, + "reuses": { + "median": 1814, + "min": 1801, + "max": 1954 + } + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "samples": 5, + "elapsed_ns": { + "median": 149074586, + "min": 147730720, + "max": 156784527 + }, + "cpu_ns": { + "median": 62457772, + "min": 59461567, + "max": 75333533 + }, + "peak_reserved_bytes": { + "median": 8388432, + "min": 8388432, + "max": 8388544 + }, + "peak_active_bytes": { + "median": 8388432, + "min": 8388432, + "max": 8388544 + }, + "rss_peak_bytes": { + "median": 11059200, + "min": 11042816, + "max": 11583488 + }, + "copy_bytes": { + "median": 53727440, + "min": 53727440, + "max": 53727440 + }, + "active_byte_seconds": { + "median": 1068598.834234, + "min": 1058083.377077, + "max": 1106709.142867 + }, + "latency_mean_ns": { + "median": 19570212.309, + "min": 19373026.656, + "max": 20225367.343 + }, + "latency_p99_upper_ns": { + "median": 27262976, + "min": 27262976, + "max": 29360128 + }, + "budget_waits": { + "median": 1070, + "min": 936, + "max": 1091 + }, + "allocations": { + "median": 4096, + "min": 4096, + "max": 4096 + }, + "reuses": { + "median": 0, + "min": 0, + "max": 0 + } + }, + { + "case": "mixed_slow", + "variant": "pressure", + "samples": 5, + "elapsed_ns": { + "median": 150607823, + "min": 150346490, + "max": 157820358 + }, + "cpu_ns": { + "median": 62794080, + "min": 61910200, + "max": 72816171 + }, + "peak_reserved_bytes": { + "median": 8388560, + "min": 8388560, + "max": 8388560 + }, + "peak_active_bytes": { + "median": 8388432, + "min": 8388432, + "max": 8388448 + }, + "rss_peak_bytes": { + "median": 11059200, + "min": 11026432, + "max": 11059200 + }, + "copy_bytes": { + "median": 27916032, + "min": 27916032, + "max": 27916032 + }, + "active_byte_seconds": { + "median": 1091824.795529, + "min": 1088594.710179, + "max": 1145134.388634 + }, + "latency_mean_ns": { + "median": 19672453.154, + "min": 19620014.043, + "max": 20627967.734 + }, + "latency_p99_upper_ns": { + "median": 27262976, + "min": 27262976, + "max": 27262976 + }, + "budget_waits": { + "median": 1045, + "min": 983, + "max": 1052 + }, + "allocations": { + "median": 2188, + "min": 2118, + "max": 2194 + }, + "reuses": { + "median": 1559, + "min": 1553, + "max": 1629 + } + }, + { + "case": "mixed_slow", + "variant": "project", + "samples": 5, + "elapsed_ns": { + "median": 155323458, + "min": 152663243, + "max": 161035903 + }, + "cpu_ns": { + "median": 60922941, + "min": 58871841, + "max": 71350453 + }, + "peak_reserved_bytes": { + "median": 8388608, + "min": 8388608, + "max": 8388608 + }, + "peak_active_bytes": { + "median": 8388608, + "min": 8388608, + "max": 8388608 + }, + "rss_peak_bytes": { + "median": 11567104, + "min": 11042816, + "max": 11583488 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1124769.099854, + "min": 1105768.632536, + "max": 1171929.531923 + }, + "latency_mean_ns": { + "median": 21013565.399, + "min": 20645944.552, + "max": 21900800.478 + }, + "latency_p99_upper_ns": { + "median": 29360128, + "min": 29360128, + "max": 31457280 + }, + "budget_waits": { + "median": 1009, + "min": 940, + "max": 1025 + }, + "allocations": { + "median": 1712, + "min": 1684, + "max": 1715 + }, + "reuses": { + "median": 336, + "min": 333, + "max": 364 + } + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "samples": 5, + "elapsed_ns": { + "median": 151434543, + "min": 150924192, + "max": 154875385 + }, + "cpu_ns": { + "median": 62824900, + "min": 62137633, + "max": 63073539 + }, + "peak_reserved_bytes": { + "median": 8388560, + "min": 8388560, + "max": 8388560 + }, + "peak_active_bytes": { + "median": 8388432, + "min": 8388432, + "max": 8388448 + }, + "rss_peak_bytes": { + "median": 11042816, + "min": 11010048, + "max": 11583488 + }, + "copy_bytes": { + "median": 28479696, + "min": 28479696, + "max": 28479696 + }, + "active_byte_seconds": { + "median": 1089269.648492, + "min": 1085460.729712, + "max": 1114766.522227 + }, + "latency_mean_ns": { + "median": 19945957.829, + "min": 19880247.948, + "max": 20387628.756 + }, + "latency_p99_upper_ns": { + "median": 27262976, + "min": 27262976, + "max": 29360128 + }, + "budget_waits": { + "median": 1066, + "min": 1055, + "max": 1076 + }, + "allocations": { + "median": 2185, + "min": 2181, + "max": 2196 + }, + "reuses": { + "median": 1603, + "min": 1592, + "max": 1607 + } + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "samples": 5, + "elapsed_ns": { + "median": 154278831, + "min": 153954221, + "max": 158364004 + }, + "cpu_ns": { + "median": 65116485, + "min": 64368954, + "max": 67436217 + }, + "peak_reserved_bytes": { + "median": 8388608, + "min": 8388608, + "max": 8388608 + }, + "peak_active_bytes": { + "median": 8388608, + "min": 8388608, + "max": 8388608 + }, + "rss_peak_bytes": { + "median": 11059200, + "min": 11042816, + "max": 11059200 + }, + "copy_bytes": { + "median": 11229392, + "min": 11229392, + "max": 11229392 + }, + "active_byte_seconds": { + "median": 1146944.244493, + "min": 1145843.306835, + "max": 1180098.881536 + }, + "latency_mean_ns": { + "median": 15979998.057, + "min": 15956645.429, + "max": 16427087.508 + }, + "latency_p99_upper_ns": { + "median": 20971520, + "min": 20971520, + "max": 23068672 + }, + "budget_waits": { + "median": 1101, + "min": 1082, + "max": 1113 + }, + "allocations": { + "median": 1938, + "min": 1915, + "max": 1951 + }, + "reuses": { + "median": 1467, + "min": 1454, + "max": 1490 + } + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "samples": 5, + "elapsed_ns": { + "median": 155250569, + "min": 154832681, + "max": 159080169 + }, + "cpu_ns": { + "median": 68708379, + "min": 68111825, + "max": 72479922 + }, + "peak_reserved_bytes": { + "median": 8388480, + "min": 8388480, + "max": 8388480 + }, + "peak_active_bytes": { + "median": 8388432, + "min": 8388432, + "max": 8388432 + }, + "rss_peak_bytes": { + "median": 10895360, + "min": 10534912, + "max": 10928128 + }, + "copy_bytes": { + "median": 3824848, + "min": 3824848, + "max": 3824848 + }, + "active_byte_seconds": { + "median": 1176609.476886, + "min": 1173410.180784, + "max": 1202455.090015 + }, + "latency_mean_ns": { + "median": 12540132.329, + "min": 12500210.727, + "max": 12818618.589 + }, + "latency_p99_upper_ns": { + "median": 15728640, + "min": 15728640, + "max": 16777216 + }, + "budget_waits": { + "median": 1080, + "min": 1052, + "max": 1091 + }, + "allocations": { + "median": 1844, + "min": 1823, + "max": 1856 + }, + "reuses": { + "median": 1212, + "min": 1200, + "max": 1233 + } + }, + { + "case": "mixed_tight", + "variant": "borrow", + "samples": 5, + "elapsed_ns": { + "median": 493631884, + "min": 491778453, + "max": 495149938 + }, + "cpu_ns": { + "median": 77876795, + "min": 73168533, + "max": 81275705 + }, + "peak_reserved_bytes": { + "median": 2094128, + "min": 2094128, + "max": 2094128 + }, + "peak_active_bytes": { + "median": 2094128, + "min": 2094128, + "max": 2094128 + }, + "rss_peak_bytes": { + "median": 3719168, + "min": 3702784, + "max": 4243456 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 930117.776585, + "min": 926994.460577, + "max": 933361.552539 + }, + "latency_mean_ns": { + "median": 5487868.994, + "min": 5473786.015, + "max": 5510109.537 + }, + "latency_p99_upper_ns": { + "median": 8388608, + "min": 8388608, + "max": 8388608 + }, + "budget_waits": { + "median": 1396, + "min": 1381, + "max": 1408 + }, + "allocations": { + "median": 1342, + "min": 1338, + "max": 1352 + }, + "reuses": { + "median": 706, + "min": 696, + "max": 710 + } + }, + { + "case": "mixed_tight", + "variant": "copy", + "samples": 5, + "elapsed_ns": { + "median": 191245856, + "min": 191037345, + "max": 191774166 + }, + "cpu_ns": { + "median": 62157319, + "min": 60906478, + "max": 62795054 + }, + "peak_reserved_bytes": { + "median": 2097152, + "min": 2097136, + "max": 2097152 + }, + "peak_active_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097136 + }, + "rss_peak_bytes": { + "median": 4243456, + "min": 4227072, + "max": 4407296 + }, + "copy_bytes": { + "median": 53727440, + "min": 53727440, + "max": 53727440 + }, + "active_byte_seconds": { + "median": 346194.049558, + "min": 345866.556263, + "max": 346663.433689 + }, + "latency_mean_ns": { + "median": 6050681.287, + "min": 6044094.394, + "max": 6064467.489 + }, + "latency_p99_upper_ns": { + "median": 8388608, + "min": 8388608, + "max": 8388608 + }, + "budget_waits": { + "median": 1373, + "min": 1361, + "max": 1387 + }, + "allocations": { + "median": 2479, + "min": 2461, + "max": 2491 + }, + "reuses": { + "median": 1617, + "min": 1605, + "max": 1635 + } + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "samples": 5, + "elapsed_ns": { + "median": 192676756, + "min": 189986910, + "max": 195726691 + }, + "cpu_ns": { + "median": 64089681, + "min": 58086718, + "max": 71826579 + }, + "peak_reserved_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097136 + }, + "peak_active_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097136 + }, + "rss_peak_bytes": { + "median": 4243456, + "min": 4227072, + "max": 4243456 + }, + "copy_bytes": { + "median": 53727440, + "min": 53727440, + "max": 53727440 + }, + "active_byte_seconds": { + "median": 347823.24917, + "min": 343741.458999, + "max": 353204.287515 + }, + "latency_mean_ns": { + "median": 6074626.45, + "min": 6008153.507, + "max": 6160078.753 + }, + "latency_p99_upper_ns": { + "median": 9437184, + "min": 8388608, + "max": 9437184 + }, + "budget_waits": { + "median": 1348, + "min": 1284, + "max": 1383 + }, + "allocations": { + "median": 4096, + "min": 4096, + "max": 4096 + }, + "reuses": { + "median": 0, + "min": 0, + "max": 0 + } + }, + { + "case": "mixed_tight", + "variant": "pressure", + "samples": 5, + "elapsed_ns": { + "median": 190688005, + "min": 189790379, + "max": 193714972 + }, + "cpu_ns": { + "median": 63157202, + "min": 60625611, + "max": 65297407 + }, + "peak_reserved_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097152 + }, + "peak_active_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097136 + }, + "rss_peak_bytes": { + "median": 4243456, + "min": 4227072, + "max": 4456448 + }, + "copy_bytes": { + "median": 28385168, + "min": 28385168, + "max": 28385168 + }, + "active_byte_seconds": { + "median": 347120.008423, + "min": 345327.900203, + "max": 352390.15124 + }, + "latency_mean_ns": { + "median": 6138653.731, + "min": 6110038.652, + "max": 6233962.195 + }, + "latency_p99_upper_ns": { + "median": 8388608, + "min": 8388608, + "max": 10485760 + }, + "budget_waits": { + "median": 1378, + "min": 1367, + "max": 1393 + }, + "allocations": { + "median": 2369, + "min": 2353, + "max": 2373 + }, + "reuses": { + "median": 1406, + "min": 1402, + "max": 1422 + } + }, + { + "case": "mixed_tight", + "variant": "project", + "samples": 5, + "elapsed_ns": { + "median": 175306603, + "min": 174670169, + "max": 179280258 + }, + "cpu_ns": { + "median": 57919540, + "min": 55539698, + "max": 63072197 + }, + "peak_reserved_bytes": { + "median": 2097120, + "min": 2097120, + "max": 2097120 + }, + "peak_active_bytes": { + "median": 2097120, + "min": 2097120, + "max": 2097120 + }, + "rss_peak_bytes": { + "median": 4243456, + "min": 4227072, + "max": 4407296 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 326279.178906, + "min": 325246.46456, + "max": 333023.673744 + }, + "latency_mean_ns": { + "median": 6273925.584, + "min": 6258533.417, + "max": 6405571.191 + }, + "latency_p99_upper_ns": { + "median": 8388608, + "min": 8388608, + "max": 9437184 + }, + "budget_waits": { + "median": 1310, + "min": 1292, + "max": 1312 + }, + "allocations": { + "median": 1726, + "min": 1706, + "max": 1728 + }, + "reuses": { + "median": 322, + "min": 320, + "max": 342 + } + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "samples": 5, + "elapsed_ns": { + "median": 188818619, + "min": 188314755, + "max": 201965490 + }, + "cpu_ns": { + "median": 63133028, + "min": 61699247, + "max": 80116546 + }, + "peak_reserved_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097152 + }, + "peak_active_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097136 + }, + "rss_peak_bytes": { + "median": 4243456, + "min": 4227072, + "max": 4325376 + }, + "copy_bytes": { + "median": 28479696, + "min": 28479696, + "max": 28479696 + }, + "active_byte_seconds": { + "median": 343018.464235, + "min": 342240.604823, + "max": 364031.028759 + }, + "latency_mean_ns": { + "median": 6127166.122, + "min": 6110229.914, + "max": 6483685.515 + }, + "latency_p99_upper_ns": { + "median": 8388608, + "min": 8388608, + "max": 10485760 + }, + "budget_waits": { + "median": 1385, + "min": 1275, + "max": 1395 + }, + "allocations": { + "median": 2370, + "min": 2291, + "max": 2380 + }, + "reuses": { + "median": 1418, + "min": 1408, + "max": 1497 + } + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "samples": 5, + "elapsed_ns": { + "median": 225451606, + "min": 224725167, + "max": 229047558 + }, + "cpu_ns": { + "median": 64792167, + "min": 63560757, + "max": 67894565 + }, + "peak_reserved_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097136 + }, + "peak_active_bytes": { + "median": 2097104, + "min": 2097104, + "max": 2097104 + }, + "rss_peak_bytes": { + "median": 4227072, + "min": 4227072, + "max": 4243456 + }, + "copy_bytes": { + "median": 11229392, + "min": 11229392, + "max": 11229392 + }, + "active_byte_seconds": { + "median": 413690.587373, + "min": 412794.10328, + "max": 418976.246455 + }, + "latency_mean_ns": { + "median": 5851904.688, + "min": 5841515.207, + "max": 5921381.751 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7864320, + "max": 8388608 + }, + "budget_waits": { + "median": 1456, + "min": 1425, + "max": 1459 + }, + "allocations": { + "median": 2132, + "min": 2094, + "max": 2134 + }, + "reuses": { + "median": 1273, + "min": 1271, + "max": 1311 + } + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "samples": 5, + "elapsed_ns": { + "median": 283681970, + "min": 282255055, + "max": 289852215 + }, + "cpu_ns": { + "median": 71405280, + "min": 67344177, + "max": 73110582 + }, + "peak_reserved_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097152 + }, + "peak_active_bytes": { + "median": 2097136, + "min": 2097136, + "max": 2097136 + }, + "rss_peak_bytes": { + "median": 4243456, + "min": 4227072, + "max": 4358144 + }, + "copy_bytes": { + "median": 3824848, + "min": 3824848, + "max": 3824848 + }, + "active_byte_seconds": { + "median": 522809.985389, + "min": 520513.491017, + "max": 533913.89253 + }, + "latency_mean_ns": { + "median": 5688342.625, + "min": 5670280.355, + "max": 5811531.61 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7340032, + "max": 10485760 + }, + "budget_waits": { + "median": 1378, + "min": 1366, + "max": 1394 + }, + "allocations": { + "median": 1986, + "min": 1968, + "max": 1990 + }, + "reuses": { + "median": 1070, + "min": 1066, + "max": 1088 + } + }, + { + "case": "sparse_fast", + "variant": "borrow", + "samples": 5, + "elapsed_ns": { + "median": 20176198, + "min": 19968383, + "max": 28472110 + }, + "cpu_ns": { + "median": 20176500, + "min": 19952888, + "max": 28306962 + }, + "peak_reserved_bytes": { + "median": 156112, + "min": 156112, + "max": 156112 + }, + "peak_active_bytes": { + "median": 156112, + "min": 156112, + "max": 156112 + }, + "rss_peak_bytes": { + "median": 1589248, + "min": 1572864, + "max": 1605632 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1246.966591, + "min": 1236.323437, + "max": 1788.400939 + }, + "latency_mean_ns": { + "median": 9697.062, + "min": 9596.16, + "max": 13730.123 + }, + "latency_p99_upper_ns": { + "median": 12288, + "min": 11264, + "max": 30720 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 1, + "min": 1, + "max": 1 + }, + "reuses": { + "median": 2047, + "min": 2047, + "max": 2047 + } + }, + { + "case": "sparse_fast", + "variant": "copy", + "samples": 5, + "elapsed_ns": { + "median": 20493516, + "min": 20382034, + "max": 21540986 + }, + "cpu_ns": { + "median": 20493889, + "min": 20382277, + "max": 21477686 + }, + "peak_reserved_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "peak_active_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "rss_peak_bytes": { + "median": 1605632, + "min": 1572864, + "max": 1605632 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1089.501083, + "min": 1084.755978, + "max": 1151.439237 + }, + "latency_mean_ns": { + "median": 9859.747, + "min": 9806.967, + "max": 10352.839 + }, + "latency_p99_upper_ns": { + "median": 13312, + "min": 12288, + "max": 18432 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 2, + "min": 2, + "max": 2 + }, + "reuses": { + "median": 4094, + "min": 4094, + "max": 4094 + } + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "samples": 5, + "elapsed_ns": { + "median": 21265947, + "min": 20561183, + "max": 28620499 + }, + "cpu_ns": { + "median": 21072353, + "min": 20561592, + "max": 28277537 + }, + "peak_reserved_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "peak_active_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "rss_peak_bytes": { + "median": 1605632, + "min": 1589248, + "max": 1605632 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1125.003597, + "min": 1084.995845, + "max": 1561.617327 + }, + "latency_mean_ns": { + "median": 10215.53, + "min": 9883.337, + "max": 13795.514 + }, + "latency_p99_upper_ns": { + "median": 16384, + "min": 12288, + "max": 40960 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 4096, + "min": 4096, + "max": 4096 + }, + "reuses": { + "median": 0, + "min": 0, + "max": 0 + } + }, + { + "case": "sparse_fast", + "variant": "pressure", + "samples": 5, + "elapsed_ns": { + "median": 20305145, + "min": 20018290, + "max": 23280610 + }, + "cpu_ns": { + "median": 20305963, + "min": 20018407, + "max": 23231480 + }, + "peak_reserved_bytes": { + "median": 156112, + "min": 156112, + "max": 156112 + }, + "peak_active_bytes": { + "median": 156112, + "min": 156112, + "max": 156112 + }, + "rss_peak_bytes": { + "median": 1589248, + "min": 1572864, + "max": 1605632 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1256.330694, + "min": 1238.904613, + "max": 1448.645185 + }, + "latency_mean_ns": { + "median": 9760.602, + "min": 9620.129, + "max": 11211.571 + }, + "latency_p99_upper_ns": { + "median": 13312, + "min": 12288, + "max": 24576 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 1, + "min": 1, + "max": 1 + }, + "reuses": { + "median": 2047, + "min": 2047, + "max": 2047 + } + }, + { + "case": "sparse_fast", + "variant": "project", + "samples": 5, + "elapsed_ns": { + "median": 21524893, + "min": 21358726, + "max": 22218843 + }, + "cpu_ns": { + "median": 21525074, + "min": 21358833, + "max": 22096574 + }, + "peak_reserved_bytes": { + "median": 90640, + "min": 90640, + "max": 90640 + }, + "peak_active_bytes": { + "median": 90640, + "min": 90640, + "max": 90640 + }, + "rss_peak_bytes": { + "median": 1540096, + "min": 1507328, + "max": 1540096 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1.632264, + "min": 1.620534, + "max": 1.68791 + }, + "latency_mean_ns": { + "median": 10363.91, + "min": 10284.012, + "max": 10699.094 + }, + "latency_p99_upper_ns": { + "median": 13312, + "min": 13312, + "max": 18432 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 1, + "min": 1, + "max": 1 + }, + "reuses": { + "median": 2047, + "min": 2047, + "max": 2047 + } + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "samples": 5, + "elapsed_ns": { + "median": 20829074, + "min": 20421089, + "max": 22684901 + }, + "cpu_ns": { + "median": 20798316, + "min": 20421796, + "max": 22652390 + }, + "peak_reserved_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "peak_active_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "rss_peak_bytes": { + "median": 1605632, + "min": 1589248, + "max": 1605632 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1111.338342, + "min": 1087.202091, + "max": 1228.197157 + }, + "latency_mean_ns": { + "median": 10025.307, + "min": 9825.86, + "max": 10925.772 + }, + "latency_p99_upper_ns": { + "median": 15360, + "min": 13312, + "max": 22528 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 2, + "min": 2, + "max": 2 + }, + "reuses": { + "median": 4094, + "min": 4094, + "max": 4094 + } + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "samples": 5, + "elapsed_ns": { + "median": 20774647, + "min": 20604128, + "max": 21098686 + }, + "cpu_ns": { + "median": 20729258, + "min": 20604259, + "max": 21002369 + }, + "peak_reserved_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "peak_active_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "rss_peak_bytes": { + "median": 1589248, + "min": 1572864, + "max": 1605632 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1106.254829, + "min": 1090.792499, + "max": 1130.040808 + }, + "latency_mean_ns": { + "median": 9997.106, + "min": 9906.217, + "max": 10153.021 + }, + "latency_p99_upper_ns": { + "median": 14336, + "min": 13312, + "max": 16384 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 2, + "min": 2, + "max": 2 + }, + "reuses": { + "median": 4094, + "min": 4094, + "max": 4094 + } + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "samples": 5, + "elapsed_ns": { + "median": 20453663, + "min": 20381497, + "max": 21122947 + }, + "cpu_ns": { + "median": 20454074, + "min": 20382185, + "max": 21019219 + }, + "peak_reserved_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "peak_active_bytes": { + "median": 156192, + "min": 156192, + "max": 156192 + }, + "rss_peak_bytes": { + "median": 1589248, + "min": 1572864, + "max": 1605632 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1090.324497, + "min": 1087.257309, + "max": 1125.541597 + }, + "latency_mean_ns": { + "median": 9841.173, + "min": 9805.745, + "max": 10159.663 + }, + "latency_p99_upper_ns": { + "median": 13312, + "min": 12288, + "max": 16384 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 2, + "min": 2, + "max": 2 + }, + "reuses": { + "median": 4094, + "min": 4094, + "max": 4094 + } + }, + { + "case": "sparse_slots64", + "variant": "borrow", + "samples": 5, + "elapsed_ns": { + "median": 167216739, + "min": 167156999, + "max": 169327288 + }, + "cpu_ns": { + "median": 44714000, + "min": 43876108, + "max": 47272489 + }, + "peak_reserved_bytes": { + "median": 4267968, + "min": 4267968, + "max": 4267968 + }, + "peak_active_bytes": { + "median": 4267968, + "min": 4267968, + "max": 4267968 + }, + "rss_peak_bytes": { + "median": 5292032, + "min": 5292032, + "max": 5292032 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 688253.763142, + "min": 688195.924843, + "max": 694635.953387 + }, + "latency_mean_ns": { + "median": 5127098.881, + "min": 5126680.831, + "max": 5174744.393 + }, + "latency_p99_upper_ns": { + "median": 7340032, + "min": 6291456, + "max": 7864320 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 64, + "min": 64, + "max": 64 + }, + "reuses": { + "median": 1984, + "min": 1984, + "max": 1984 + } + }, + { + "case": "sparse_slots64", + "variant": "copy", + "samples": 5, + "elapsed_ns": { + "median": 168358292, + "min": 166680851, + "max": 177068451 + }, + "cpu_ns": { + "median": 27312589, + "min": 24341624, + "max": 34395657 + }, + "peak_reserved_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "peak_active_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "rss_peak_bytes": { + "median": 1638400, + "min": 1622016, + "max": 1654784 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 2055.277015, + "min": 1923.416256, + "max": 2287.854324 + }, + "latency_mean_ns": { + "median": 5168010.066, + "min": 5125321.995, + "max": 5443811.067 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7864320, + "max": 9437184 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 65, + "min": 65, + "max": 65 + }, + "reuses": { + "median": 4031, + "min": 4031, + "max": 4031 + } + }, + { + "case": "sparse_slots64", + "variant": "copy_no_pool", + "samples": 5, + "elapsed_ns": { + "median": 167165609, + "min": 166715000, + "max": 168181366 + }, + "cpu_ns": { + "median": 26167797, + "min": 24659146, + "max": 26871012 + }, + "peak_reserved_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "peak_active_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "rss_peak_bytes": { + "median": 1654784, + "min": 1638400, + "max": 1654784 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1986.249813, + "min": 1919.204343, + "max": 2024.80935 + }, + "latency_mean_ns": { + "median": 5134877.701, + "min": 5125611.619, + "max": 5163779.497 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7864320, + "max": 7864320 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 4096, + "min": 4096, + "max": 4096 + }, + "reuses": { + "median": 0, + "min": 0, + "max": 0 + } + }, + { + "case": "sparse_slots64", + "variant": "pressure", + "samples": 5, + "elapsed_ns": { + "median": 167121239, + "min": 167023277, + "max": 170783358 + }, + "cpu_ns": { + "median": 44137216, + "min": 43204463, + "max": 46177893 + }, + "peak_reserved_bytes": { + "median": 4268048, + "min": 4268048, + "max": 4268048 + }, + "peak_active_bytes": { + "median": 4268048, + "min": 4268048, + "max": 4268048 + }, + "rss_peak_bytes": { + "median": 5767168, + "min": 5750784, + "max": 5783552 + }, + "copy_bytes": { + "median": 2048, + "min": 2048, + "max": 2048 + }, + "active_byte_seconds": { + "median": 677483.645644, + "min": 677260.281048, + "max": 691939.11555 + }, + "latency_mean_ns": { + "median": 5127462.262, + "min": 5125737.978, + "max": 5236882.494 + }, + "latency_p99_upper_ns": { + "median": 7340032, + "min": 6291456, + "max": 7340032 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 65, + "min": 65, + "max": 65 + }, + "reuses": { + "median": 2015, + "min": 2015, + "max": 2015 + } + }, + { + "case": "sparse_slots64", + "variant": "project", + "samples": 5, + "elapsed_ns": { + "median": 167505794, + "min": 166784685, + "max": 171072487 + }, + "cpu_ns": { + "median": 26582076, + "min": 25505816, + "max": 27568676 + }, + "peak_reserved_bytes": { + "median": 77760, + "min": 77760, + "max": 77760 + }, + "peak_active_bytes": { + "median": 77760, + "min": 77760, + "max": 77760 + }, + "rss_peak_bytes": { + "median": 1589248, + "min": 1572864, + "max": 1589248 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 842.776507, + "min": 840.114114, + "max": 861.43609 + }, + "latency_mean_ns": { + "median": 5144362.881, + "min": 5128089.141, + "max": 5258257.061 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7864320, + "max": 7864320 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 64, + "min": 64, + "max": 64 + }, + "reuses": { + "median": 1984, + "min": 1984, + "max": 1984 + } + }, + { + "case": "sparse_slots64", + "variant": "threshold2", + "samples": 5, + "elapsed_ns": { + "median": 167148609, + "min": 166724425, + "max": 167663571 + }, + "cpu_ns": { + "median": 25957735, + "min": 24304145, + "max": 26966696 + }, + "peak_reserved_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "peak_active_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "rss_peak_bytes": { + "median": 1638400, + "min": 1622016, + "max": 1654784 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1998.277517, + "min": 1915.595622, + "max": 2058.951339 + }, + "latency_mean_ns": { + "median": 5134098.172, + "min": 5125215.218, + "max": 5147482.869 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7864320, + "max": 7864320 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 65, + "min": 65, + "max": 65 + }, + "reuses": { + "median": 4031, + "min": 4031, + "max": 4031 + } + }, + { + "case": "sparse_slots64", + "variant": "threshold4", + "samples": 5, + "elapsed_ns": { + "median": 166829203, + "min": 166770166, + "max": 168266032 + }, + "cpu_ns": { + "median": 25212214, + "min": 24368638, + "max": 25648470 + }, + "peak_reserved_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "peak_active_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "rss_peak_bytes": { + "median": 1638400, + "min": 1638400, + "max": 1654784 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1964.863682, + "min": 1922.285206, + "max": 1987.487947 + }, + "latency_mean_ns": { + "median": 5128818.551, + "min": 5126749.497, + "max": 5170189.028 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7864320, + "max": 7864320 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 65, + "min": 65, + "max": 65 + }, + "reuses": { + "median": 4031, + "min": 4031, + "max": 4031 + } + }, + { + "case": "sparse_slots64", + "variant": "threshold8", + "samples": 5, + "elapsed_ns": { + "median": 167352294, + "min": 166717963, + "max": 169611954 + }, + "cpu_ns": { + "median": 26149112, + "min": 24643191, + "max": 32536976 + }, + "peak_reserved_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "peak_active_bytes": { + "median": 143312, + "min": 143312, + "max": 143312 + }, + "rss_peak_bytes": { + "median": 1638400, + "min": 1622016, + "max": 1654784 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 1998.899707, + "min": 1926.54795, + "max": 2319.792062 + }, + "latency_mean_ns": { + "median": 5140639.04, + "min": 5125971.683, + "max": 5206380.796 + }, + "latency_p99_upper_ns": { + "median": 7864320, + "min": 7864320, + "max": 7864320 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 65, + "min": 65, + "max": 65 + }, + "reuses": { + "median": 4031, + "min": 4031, + "max": 4031 + } + }, + { + "case": "sparse_slow", + "variant": "borrow", + "samples": 5, + "elapsed_ns": { + "median": 129845439, + "min": 106374934, + "max": 141446795 + }, + "cpu_ns": { + "median": 45497318, + "min": 44501920, + "max": 48356230 + }, + "peak_reserved_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "peak_active_bytes": { + "median": 8350112, + "min": 8350112, + "max": 8350112 + }, + "rss_peak_bytes": { + "median": 9486336, + "min": 9469952, + "max": 9486336 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 1022546.229446, + "min": 829040.966005, + "max": 1119170.099864 + }, + "latency_mean_ns": { + "median": 7656609.5, + "min": 6201710.718, + "max": 8382742.796 + }, + "latency_p99_upper_ns": { + "median": 9437184, + "min": 9437184, + "max": 9437184 + }, + "budget_waits": { + "median": 1501, + "min": 1095, + "max": 1729 + }, + "allocations": { + "median": 126, + "min": 126, + "max": 126 + }, + "reuses": { + "median": 1922, + "min": 1922, + "max": 1922 + } + }, + { + "case": "sparse_slow", + "variant": "copy", + "samples": 5, + "elapsed_ns": { + "median": 159054504, + "min": 154140610, + "max": 160143537 + }, + "cpu_ns": { + "median": 26459012, + "min": 23189876, + "max": 28160466 + }, + "peak_reserved_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "peak_active_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "rss_peak_bytes": { + "median": 1703936, + "min": 1687552, + "max": 1720320 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 6705.336146, + "min": 6456.771667, + "max": 7402.36243 + }, + "latency_mean_ns": { + "median": 34466956.647, + "min": 33183203.845, + "max": 34696998.27 + }, + "latency_p99_upper_ns": { + "median": 41943040, + "min": 37748736, + "max": 46137344 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 513, + "min": 513, + "max": 513 + }, + "reuses": { + "median": 3583, + "min": 3583, + "max": 3583 + } + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "samples": 5, + "elapsed_ns": { + "median": 155595440, + "min": 154376239, + "max": 162066143 + }, + "cpu_ns": { + "median": 25896167, + "min": 24003453, + "max": 36838768 + }, + "peak_reserved_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "peak_active_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "rss_peak_bytes": { + "median": 1703936, + "min": 1687552, + "max": 1720320 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 6570.059349, + "min": 6474.089644, + "max": 7046.191505 + }, + "latency_mean_ns": { + "median": 33573592.059, + "min": 33344720.225, + "max": 34524425.147 + }, + "latency_p99_upper_ns": { + "median": 41943040, + "min": 37748736, + "max": 46137344 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 4096, + "min": 4096, + "max": 4096 + }, + "reuses": { + "median": 0, + "min": 0, + "max": 0 + } + }, + { + "case": "sparse_slow", + "variant": "pressure", + "samples": 5, + "elapsed_ns": { + "median": 153730223, + "min": 151916969, + "max": 157264009 + }, + "cpu_ns": { + "median": 27269441, + "min": 26295422, + "max": 27901892 + }, + "peak_reserved_bytes": { + "median": 4321888, + "min": 4321888, + "max": 4321888 + }, + "peak_active_bytes": { + "median": 4318208, + "min": 4316848, + "max": 4318768 + }, + "rss_peak_bytes": { + "median": 5849088, + "min": 5832704, + "max": 5849088 + }, + "copy_bytes": { + "median": 113984, + "min": 113408, + "max": 114176 + }, + "active_byte_seconds": { + "median": 524904.925261, + "min": 513114.693547, + "max": 542285.660244 + }, + "latency_mean_ns": { + "median": 32665634.037, + "min": 32537118.75, + "max": 33232147.448 + }, + "latency_p99_upper_ns": { + "median": 41943040, + "min": 37748736, + "max": 41943040 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 514, + "min": 514, + "max": 514 + }, + "reuses": { + "median": 3315, + "min": 3306, + "max": 3318 + } + }, + { + "case": "sparse_slow", + "variant": "project", + "samples": 5, + "elapsed_ns": { + "median": 154249202, + "min": 154094443, + "max": 157860470 + }, + "cpu_ns": { + "median": 24700764, + "min": 24230112, + "max": 28682548 + }, + "peak_reserved_bytes": { + "median": 131520, + "min": 131520, + "max": 131520 + }, + "peak_active_bytes": { + "median": 131520, + "min": 131520, + "max": 131520 + }, + "rss_peak_bytes": { + "median": 1638400, + "min": 1638400, + "max": 1654784 + }, + "copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "active_byte_seconds": { + "median": 5451.483102, + "min": 5449.134094, + "max": 5603.286251 + }, + "latency_mean_ns": { + "median": 33273652.65, + "min": 33259314.794, + "max": 34200243.611 + }, + "latency_p99_upper_ns": { + "median": 37748736, + "min": 37748736, + "max": 41943040 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 512, + "min": 512, + "max": 512 + }, + "reuses": { + "median": 1536, + "min": 1536, + "max": 1536 + } + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "samples": 5, + "elapsed_ns": { + "median": 154320869, + "min": 154022055, + "max": 160914091 + }, + "cpu_ns": { + "median": 24093342, + "min": 23087202, + "max": 25835642 + }, + "peak_reserved_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "peak_active_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "rss_peak_bytes": { + "median": 1703936, + "min": 1703936, + "max": 1720320 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 6504.166448, + "min": 6450.425541, + "max": 6869.973543 + }, + "latency_mean_ns": { + "median": 33321815.102, + "min": 33269305.586, + "max": 34925615.31 + }, + "latency_p99_upper_ns": { + "median": 37748736, + "min": 37748736, + "max": 46137344 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 513, + "min": 513, + "max": 513 + }, + "reuses": { + "median": 3583, + "min": 3583, + "max": 3583 + } + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "samples": 5, + "elapsed_ns": { + "median": 155128904, + "min": 154184073, + "max": 157308101 + }, + "cpu_ns": { + "median": 25543272, + "min": 23590708, + "max": 31229598 + }, + "peak_reserved_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "peak_active_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "rss_peak_bytes": { + "median": 1703936, + "min": 1687552, + "max": 1720320 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 6610.674553, + "min": 6477.65682, + "max": 6941.796558 + }, + "latency_mean_ns": { + "median": 33533790.057, + "min": 33299901.746, + "max": 33900498.29 + }, + "latency_p99_upper_ns": { + "median": 41943040, + "min": 37748736, + "max": 41943040 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 513, + "min": 513, + "max": 513 + }, + "reuses": { + "median": 3583, + "min": 3583, + "max": 3583 + } + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "samples": 5, + "elapsed_ns": { + "median": 154198518, + "min": 154077370, + "max": 159785538 + }, + "cpu_ns": { + "median": 23692568, + "min": 23187579, + "max": 24425471 + }, + "peak_reserved_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "peak_active_bytes": { + "median": 197072, + "min": 197072, + "max": 197072 + }, + "rss_peak_bytes": { + "median": 1703936, + "min": 1687552, + "max": 1720320 + }, + "copy_bytes": { + "median": 131072, + "min": 131072, + "max": 131072 + }, + "active_byte_seconds": { + "median": 6492.439639, + "min": 6453.368258, + "max": 6694.694537 + }, + "latency_mean_ns": { + "median": 33304172.097, + "min": 33277920.587, + "max": 34401146.148 + }, + "latency_p99_upper_ns": { + "median": 37748736, + "min": 37748736, + "max": 41943040 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "allocations": { + "median": 513, + "min": 513, + "max": 513 + }, + "reuses": { + "median": 3583, + "min": 3583, + "max": 3583 + } + } +] diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/verification.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/verification.json new file mode 100644 index 0000000..539be67 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/verification.json @@ -0,0 +1,94 @@ +{ + "native": [ + "borrow: byte equality, accounting, cleanup, slow=False", + "borrow: byte equality, accounting, cleanup, slow=True", + "borrow: six malformed inputs with pending buffer cleanup", + "borrow: cancellation preserves completed output prefix and releases pending buffers", + "borrow: empty stream", + "copy: byte equality, accounting, cleanup, slow=False", + "copy: byte equality, accounting, cleanup, slow=True", + "copy: six malformed inputs with pending buffer cleanup", + "copy: cancellation preserves completed output prefix and releases pending buffers", + "copy: empty stream", + "threshold2: byte equality, accounting, cleanup, slow=False", + "threshold2: byte equality, accounting, cleanup, slow=True", + "threshold2: six malformed inputs with pending buffer cleanup", + "threshold2: cancellation preserves completed output prefix and releases pending buffers", + "threshold2: empty stream", + "threshold4: byte equality, accounting, cleanup, slow=False", + "threshold4: byte equality, accounting, cleanup, slow=True", + "threshold4: six malformed inputs with pending buffer cleanup", + "threshold4: cancellation preserves completed output prefix and releases pending buffers", + "threshold4: empty stream", + "threshold8: byte equality, accounting, cleanup, slow=False", + "threshold8: byte equality, accounting, cleanup, slow=True", + "threshold8: six malformed inputs with pending buffer cleanup", + "threshold8: cancellation preserves completed output prefix and releases pending buffers", + "threshold8: empty stream", + "pressure: byte equality, accounting, cleanup, slow=False", + "pressure: byte equality, accounting, cleanup, slow=True", + "pressure: six malformed inputs with pending buffer cleanup", + "pressure: cancellation preserves completed output prefix and releases pending buffers", + "pressure: empty stream", + "project: byte equality, accounting, cleanup, slow=False", + "project: byte equality, accounting, cleanup, slow=True", + "project: six malformed inputs with pending buffer cleanup", + "project: cancellation preserves completed output prefix and releases pending buffers", + "project: empty stream", + "copy_no_pool: byte equality, accounting, cleanup, slow=False", + "copy_no_pool: byte equality, accounting, cleanup, slow=True", + "copy_no_pool: six malformed inputs with pending buffer cleanup", + "copy_no_pool: cancellation preserves completed output prefix and releases pending buffers", + "copy_no_pool: empty stream", + "frame larger than buffer budget: bounded refusal versus projection", + "real output failure through /dev/full, pending buffers released", + "SIGTERM interrupts delayed sink and releases buffers", + "current-image VmHWM distinguishes large retained sources from small projections" + ], + "asan_ubsan": [ + "borrow: byte equality, accounting, cleanup, slow=False", + "borrow: byte equality, accounting, cleanup, slow=True", + "borrow: six malformed inputs with pending buffer cleanup", + "borrow: cancellation preserves completed output prefix and releases pending buffers", + "borrow: empty stream", + "copy: byte equality, accounting, cleanup, slow=False", + "copy: byte equality, accounting, cleanup, slow=True", + "copy: six malformed inputs with pending buffer cleanup", + "copy: cancellation preserves completed output prefix and releases pending buffers", + "copy: empty stream", + "threshold2: byte equality, accounting, cleanup, slow=False", + "threshold2: byte equality, accounting, cleanup, slow=True", + "threshold2: six malformed inputs with pending buffer cleanup", + "threshold2: cancellation preserves completed output prefix and releases pending buffers", + "threshold2: empty stream", + "threshold4: byte equality, accounting, cleanup, slow=False", + "threshold4: byte equality, accounting, cleanup, slow=True", + "threshold4: six malformed inputs with pending buffer cleanup", + "threshold4: cancellation preserves completed output prefix and releases pending buffers", + "threshold4: empty stream", + "threshold8: byte equality, accounting, cleanup, slow=False", + "threshold8: byte equality, accounting, cleanup, slow=True", + "threshold8: six malformed inputs with pending buffer cleanup", + "threshold8: cancellation preserves completed output prefix and releases pending buffers", + "threshold8: empty stream", + "pressure: byte equality, accounting, cleanup, slow=False", + "pressure: byte equality, accounting, cleanup, slow=True", + "pressure: six malformed inputs with pending buffer cleanup", + "pressure: cancellation preserves completed output prefix and releases pending buffers", + "pressure: empty stream", + "project: byte equality, accounting, cleanup, slow=False", + "project: byte equality, accounting, cleanup, slow=True", + "project: six malformed inputs with pending buffer cleanup", + "project: cancellation preserves completed output prefix and releases pending buffers", + "project: empty stream", + "copy_no_pool: byte equality, accounting, cleanup, slow=False", + "copy_no_pool: byte equality, accounting, cleanup, slow=True", + "copy_no_pool: six malformed inputs with pending buffer cleanup", + "copy_no_pool: cancellation preserves completed output prefix and releases pending buffers", + "copy_no_pool: empty stream", + "frame larger than buffer budget: bounded refusal versus projection", + "real output failure through /dev/full, pending buffers released", + "SIGTERM interrupts delayed sink and releases buffers", + "current-image VmHWM distinguishes large retained sources from small projections" + ] +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/audit.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/audit.json new file mode 100644 index 0000000..54a4546 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/audit.json @@ -0,0 +1,6 @@ +{ + "status": "superseded_by_corrected_measurement", + "reason": "All rusage RSS samples were identical at 28753920 bytes. Linux resource usage survives exec; this measure includes pre-exec history and is unsuitable for child image RSS comparison. The corrected run records proc/self/status VmHWM as a separate approximate metric and retains raw rusage. A 64-slot sparse case is added to examine the latency/queue confound. Other first-run observations remain exploratory.", + "next_run": "2026-09-22-linux-aarch64-v2", + "sources_preserved": true +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/complete.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/complete.json new file mode 100644 index 0000000..0980f35 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/complete.json @@ -0,0 +1,5 @@ +{ + "samples": 200, + "all_output_hashes_match": true, + "finished_utc": "2026-09-22T12:20:38Z" +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/environment.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/environment.json new file mode 100644 index 0000000..f5173ae --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/environment.json @@ -0,0 +1,57 @@ +{ + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "python": "3.14.7", + "cpu_affinity": 0, + "compiler": "cc (Debian 14.2.0-19) 14.2.0\nCopyright (C) 2024 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n", + "builds": [ + { + "command": [ + "cc", + "-std=c17", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-O2", + "-g", + "/home/eunhhu/work/pure/research/experiments/002-buffer-retention/benchmark.c", + "-o", + "/tmp/pure-retention-ezjbfrb1/bench" + ], + "stderr": "", + "sha256": "c38e4f6e9e09c77368c1ba188333bc43a8b8a4a1c4500fbdf218ee74545bd2d2", + "size_bytes": 99936 + }, + { + "command": [ + "cc", + "-std=c17", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-O2", + "-g", + "-fsanitize=address,undefined", + "-fno-omit-frame-pointer", + "/home/eunhhu/work/pure/research/experiments/002-buffer-retention/benchmark.c", + "-o", + "/tmp/pure-retention-ezjbfrb1/bench-sanitized" + ], + "stderr": "", + "sha256": "4810904c7507cc9d8d5550eb6444560f3ed5658a49e7e4a6d753cde23dc32124", + "size_bytes": 256136 + } + ], + "source_sha256": { + "benchmark.c": "68256a441907d944b4f5b3f50d32bb06b349cc18db49e03f5dbd6158c79e2623", + "run.py": "d337cd069b63f50f5810875c5fa59e9771ab69333805ab450d182c19e07b814d", + "plan.json": "12daa51463df2a4a288adaf85c631f439993e50c9bf3f21fc248c92859ae5214" + }, + "clock": "CLOCK_MONOTONIC; CPU = CLOCK_PROCESS_CPUTIME_ID", + "timing": "includes file IO, scheduling, deliberate sleep, metrics, buffer cleanup; excludes startup/build/fixture/verification", + "limitations": "shared host; unregulated thermals; cached files; output page cache, no fsync; synthetic closed-loop arrivals", + "started_utc": "2026-09-22T12:19:50Z", + "cpuinfo": "processor\t: 0\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 1\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 2\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 3\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nRevision\t: d04171\nSerial\t\t: d3ba8e625dbe42ac\nModel\t\t: Raspberry Pi 5 Model B Rev 1.1\n" +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/fixtures.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/fixtures.json new file mode 100644 index 0000000..3a68713 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/fixtures.json @@ -0,0 +1,57 @@ +[ + { + "path": "input.frames", + "shape": "sparse", + "records": 2048, + "seed": 20260922, + "input_bytes": 134242304, + "input_sha256": "161377ac76f4b7d35b13db7edf3b93bb279ddd83f6514051020e8b49e743d1c0", + "expected_output_bytes": 139264, + "expected_output_sha256": "a01c7bff45f73512f3bc0dbd02d7c3a1f4ceb09f574d7c25b926cd3d30bb1bfa", + "case": "sparse_slow" + }, + { + "path": "input.frames", + "shape": "dense", + "records": 2048, + "seed": 20260922, + "input_bytes": 134242304, + "input_sha256": "fb28c2da0af227079bd6888c3f549fa5a48abd0b790c3939dc330111e80c0b7f", + "expected_output_bytes": 100671488, + "expected_output_sha256": "d96dd7f80d696895e66521630fd30f13adabf63bea3b84b0a709fbc8c4095aa5", + "case": "dense_slow" + }, + { + "path": "input.frames", + "shape": "mixed", + "records": 2048, + "seed": 20260922, + "input_bytes": 177631232, + "input_sha256": "5cfcb8e74d5f30c3a611f373ddf53fd5bc843841d83de42c763e1d931403521d", + "expected_output_bytes": 53735632, + "expected_output_sha256": "f6b4eb83e7ca9aa4b5b2e0675d34b656815c82c992700ee9cbd7e0a15594cb0b", + "case": "mixed_slow" + }, + { + "path": "input.frames", + "shape": "mixed", + "records": 2048, + "seed": 20260922, + "input_bytes": 177631232, + "input_sha256": "5cfcb8e74d5f30c3a611f373ddf53fd5bc843841d83de42c763e1d931403521d", + "expected_output_bytes": 53735632, + "expected_output_sha256": "f6b4eb83e7ca9aa4b5b2e0675d34b656815c82c992700ee9cbd7e0a15594cb0b", + "case": "mixed_tight" + }, + { + "path": "input.frames", + "shape": "sparse", + "records": 2048, + "seed": 20260922, + "input_bytes": 134242304, + "input_sha256": "161377ac76f4b7d35b13db7edf3b93bb279ddd83f6514051020e8b49e743d1c0", + "expected_output_bytes": 139264, + "expected_output_sha256": "a01c7bff45f73512f3bc0dbd02d7c3a1f4ceb09f574d7c25b926cd3d30bb1bfa", + "case": "sparse_fast" + } +] diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/plan.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/plan.json new file mode 100644 index 0000000..349e5e2 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/plan.json @@ -0,0 +1,103 @@ +{ + "version": 1, + "written_before_measurement": true, + "seed": 20260922, + "repeats": 5, + "slots": 512, + "policies": [ + { + "name": "borrow", + "policy": "borrow", + "pool": 1 + }, + { + "name": "copy", + "policy": "copy", + "pool": 1 + }, + { + "name": "threshold2", + "policy": "threshold", + "ratio": 2, + "pool": 1 + }, + { + "name": "threshold4", + "policy": "threshold", + "ratio": 4, + "pool": 1 + }, + { + "name": "threshold8", + "policy": "threshold", + "ratio": 8, + "pool": 1 + }, + { + "name": "pressure", + "policy": "pressure", + "pool": 1 + }, + { + "name": "project", + "policy": "project", + "pool": 1 + }, + { + "name": "copy_no_pool", + "policy": "copy", + "pool": 0 + } + ], + "cases": [ + { + "name": "sparse_slow", + "shape": "sparse", + "records": 2048, + "budget": 8388608, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "dense_slow", + "shape": "dense", + "records": 2048, + "budget": 8388608, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "mixed_slow", + "shape": "mixed", + "records": 2048, + "budget": 8388608, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "mixed_tight", + "shape": "mixed", + "records": 2048, + "budget": 2097152, + "hold_us": 5000, + "service_us": 20 + }, + { + "name": "sparse_fast", + "shape": "sparse", + "records": 2048, + "budget": 8388608, + "hold_us": 0, + "service_us": 0 + } + ], + "expectations": [ + "Borrow retains full source allocations for small selections.", + "Copy can add CPU and transient source plus destination memory for dense selections.", + "A current-pressure rule need not outperform a static size rule or early projection.", + "Pool reuse can reduce allocation calls while increasing reserved idle memory.", + "No Pure-specific advantage, application SLO, or statistical significance is presumed." + ], + "comparison_rule": "Report all presets and repetitions; no post-hoc winner is called an out-of-sample tuned policy.", + "limits": "Single FIFO local pipeline with synthetic delayed sink. No Sonic/Node/.NET benchmark, fan-out, persistent cache, generic JSON parser, external open-loop arrivals, or production SLO." +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.csv b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.csv new file mode 100644 index 0000000..bf0f08d --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.csv @@ -0,0 +1,201 @@ +case,variant,repeat,status,policy,pool,budget_bytes,fixed_bytes,peak_reserved_bytes,peak_active_bytes,peak_queue,rss_peak_bytes,elapsed_ns,cpu_ns,input_bytes,output_bytes,accepted,emitted,copy_bytes,copied_records,borrowed_records,pressure_copies,allocations,reuses,budget_waits,slot_waits,sleep_ns,latency_mean_ns,latency_p99_upper_ns,latency_max_ns,active_byte_seconds,live_at_stop_bytes,cached_at_stop_bytes,unreleased_buffer_bytes +sparse_slow,copy,0,ok,copy,True,8388608,90560,197072,197072,512,28753920,156440711,48121065,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,878,109587045,32718961.288,50331648,47686700,7879.414446,0,66752,0 +sparse_slow,threshold4,0,ok,threshold,True,8388608,90560,197072,197072,512,28753920,152733814,52980104,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,718,102189918,30324683.181,54525952,52327335,7602.229051,0,66752,0 +sparse_slow,threshold2,0,ok,threshold,True,8388608,90560,197072,197072,512,28753920,155247640,47779812,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,809,108519282,31414916.006,54525952,51100228,7369.340036,0,66752,0 +sparse_slow,copy_no_pool,0,ok,copy,False,8388608,90560,197072,197072,512,28753920,164046264,46944898,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,881,113764083,35133900.267,46137344,46051556,8459.440246,0,0,0 +sparse_slow,project,0,ok,project,True,8388608,90560,131520,131520,512,28753920,157789596,25739163,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1536,136058109,34150955.425,41943040,40528961,5595.217111,0,1280,0 +sparse_slow,threshold8,0,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154250903,24028565,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,133827808,33265597.392,37748736,37531339,6504.707846,0,66752,0 +sparse_slow,borrow,0,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,101576920,50248602,134242304,139264,2048,2048,0,0,2048,0,126,1922,825,0,53200392,5868549.98,10485760,10885229,784884.708981,0,1048832,0 +sparse_slow,pressure,0,ok,pressure,True,8388608,90560,4321888,4316448,512,28753920,153086035,29353156,134242304,139264,2048,2048,113152,1768,280,1768,514,3302,0,1424,127637166,32763957.697,41943040,38592411,549197.237404,0,590528,0 +sparse_slow,borrow,1,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,132106520,45409725,134242304,139264,2048,2048,0,0,2048,0,126,1922,1532,0,89610425,7801228.044,9437184,9303567,1041796.843202,0,1048832,0 +sparse_slow,copy_no_pool,1,ok,copy,False,8388608,90560,197072,197072,512,28753920,154376717,24625261,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1534,133550825,33304515.117,37748736,37563136,6521.304714,0,0,0 +sparse_slow,threshold2,1,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154790882,24092354,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134531052,33433846.048,37748736,37877431,6519.648368,0,66752,0 +sparse_slow,threshold4,1,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154495679,24603184,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1536,133731727,33331969.71,37748736,37527099,6535.057316,0,66752,0 +sparse_slow,threshold8,1,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154242976,24135969,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1535,133865874,33299370.61,37748736,37484173,6509.096995,0,66752,0 +sparse_slow,project,1,ok,project,True,8388608,90560,131520,131520,512,28753920,154024996,25025451,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1534,132769732,33212736.419,37748736,37402803,5441.502377,0,1280,0 +sparse_slow,pressure,1,ok,pressure,True,8388608,90560,4321888,4316928,512,28753920,151466095,27009927,134242304,139264,2048,2048,113280,1770,278,1770,514,3304,0,1473,128099292,32391902.953,37748736,37408099,539531.143577,0,590528,0 +sparse_slow,copy,1,ok,copy,True,8388608,90560,197072,197072,512,28753920,155007270,24608283,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1535,134124966,33382726.714,37748736,37593062,6546.135984,0,66752,0 +sparse_slow,threshold4,2,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154312902,23956224,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1535,134085359,33321820.453,37748736,37660191,6502.811837,0,66752,0 +sparse_slow,copy_no_pool,2,ok,copy,False,8388608,90560,197072,197072,512,28753920,154586382,24703970,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1534,133705501,33392210.559,41943040,37987523,6536.248232,0,0,0 +sparse_slow,threshold2,2,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154368809,24191625,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1534,133926227,33288663.865,37748736,37544080,6508.096109,0,66752,0 +sparse_slow,pressure,2,ok,pressure,True,8388608,90560,4321888,4316208,512,28753920,151522021,27174295,134242304,139264,2048,2048,113088,1767,281,1767,514,3301,0,1465,128053394,32398179.535,37748736,37585173,545315.69968,0,590528,0 +sparse_slow,copy,2,ok,copy,True,8388608,90560,197072,197072,512,28753920,154139254,24156681,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1535,133742452,33274854.653,37748736,37580636,6504.184345,0,66752,0 +sparse_slow,project,2,ok,project,True,8388608,90560,131520,131520,512,28753920,154504494,25538018,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1533,132803019,33348679.346,37748736,37696024,5463.772808,0,1280,0 +sparse_slow,threshold8,2,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154268606,24017037,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134039266,33296530.878,37748736,37600117,6499.765006,0,66752,0 +sparse_slow,borrow,2,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,116617303,46471284,134242304,139264,2048,2048,0,0,2048,0,126,1922,1258,0,72494973,6836360.134,9437184,9280234,913414.105279,0,1048832,0 +sparse_slow,project,3,ok,project,True,8388608,90560,131520,131520,512,28753920,155002696,25290252,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1537,133467827,33443160.686,41943040,38061801,5479.253547,0,1280,0 +sparse_slow,borrow,3,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,122707360,46832624,134242304,139264,2048,2048,0,0,2048,0,126,1922,1334,0,78581274,7210177.487,9437184,9450215,963195.592926,0,1048832,0 +sparse_slow,pressure,3,ok,pressure,True,8388608,90560,4321888,4316128,512,28753920,170735523,36085789,134242304,139264,2048,2048,113024,1766,282,1766,514,3300,0,1403,140741412,37128525.741,50331648,49207437,624641.105995,0,590528,0 +sparse_slow,threshold8,3,ok,threshold,True,8388608,90560,197072,197072,512,28753920,161984455,35308455,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1418,132544941,34734342.366,41943040,41927513,7196.657649,0,66752,0 +sparse_slow,threshold2,3,ok,threshold,True,8388608,90560,197072,197072,512,28753920,183186784,39562601,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1377,149944607,39799236.839,50331648,49954231,8175.636446,0,66752,0 +sparse_slow,copy,3,ok,copy,True,8388608,90560,197072,197072,512,28753920,160896124,25740442,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1519,138959714,34942676.828,41943040,40635979,6866.098036,0,66752,0 +sparse_slow,copy_no_pool,3,ok,copy,False,8388608,90560,197072,197072,512,28753920,155242417,24840078,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1534,134375289,33387730.27,41943040,38123912,6526.934401,0,0,0 +sparse_slow,threshold4,3,ok,threshold,True,8388608,90560,197072,197072,512,28753920,153980365,24665615,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1523,133098283,33153737.672,37748736,37518117,6514.197106,0,66752,0 +sparse_slow,copy_no_pool,4,ok,copy,False,8388608,90560,197072,197072,512,28753920,154143199,24004931,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,1536,133901751,33287028.642,37748736,37626617,6483.552028,0,0,0 +sparse_slow,copy,4,ok,copy,True,8388608,90560,197072,197072,512,28753920,153170646,26299681,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1505,130663631,33019408.27,37748736,37369469,6585.869234,0,66752,0 +sparse_slow,threshold4,4,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154103699,23694419,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1537,134088060,33271036.464,37748736,37623950,6485.074292,0,66752,0 +sparse_slow,threshold8,4,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154585216,23936885,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1536,134389609,33393120.598,37748736,38036616,6515.371593,0,66752,0 +sparse_slow,borrow,4,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,124475504,45943956,134242304,139264,2048,2048,0,0,2048,0,126,1922,1468,0,81119761,7320646.162,9437184,9274900,977840.583339,0,1048832,0 +sparse_slow,threshold2,4,ok,threshold,True,8388608,90560,197072,197072,512,28753920,154125217,24321563,134242304,139264,2048,2048,131072,2048,0,0,513,3583,0,1536,133545099,33270620.015,37748736,37515988,6516.994726,0,66752,0 +sparse_slow,pressure,4,ok,pressure,True,8388608,90560,4321888,4317248,512,28753920,151398447,26713792,134242304,139264,2048,2048,113408,1772,276,1772,514,3306,0,1473,128307648,32397554.332,37748736,37324395,535707.02646,0,590528,0 +sparse_slow,project,4,ok,project,True,8388608,90560,131520,131520,512,28753920,154766567,24892483,134242304,139264,2048,2048,0,0,0,0,512,1536,0,1537,133703008,33388895.003,41943040,37874598,5470.365986,0,1280,0 +dense_slow,project,0,ok,project,True,8388608,90560,8350112,8350112,126,28753920,153699255,97036844,134242304,100671488,2048,2048,0,0,0,0,126,1922,890,0,59162056,8908081.106,15728640,16113048,1191532.621299,0,1048832,0 +dense_slow,threshold2,0,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,130854245,109226645,134242304,100671488,2048,2048,0,0,2048,0,126,1922,318,0,22611916,7607977.365,11534336,12586113,1019874.721927,0,1048832,0 +dense_slow,copy_no_pool,0,ok,copy,False,8388608,90560,8350112,8350112,125,28753920,130227024,112627422,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1363,0,17672583,7496902.226,12582912,12386151,1006822.239807,0,0,0 +dense_slow,borrow,0,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,132968553,110199520,134242304,100671488,2048,2048,0,0,2048,0,126,1922,230,0,24087338,7516016.308,16777216,17649580,1007870.242312,0,1048832,0 +dense_slow,pressure,0,ok,pressure,True,8388608,90560,8350112,8350112,126,28753920,139106888,110625767,134242304,100671488,2048,2048,0,0,2048,0,126,1922,217,0,29387892,8069611.928,16777216,17083878,1081569.371059,0,1048832,0 +dense_slow,copy,0,ok,copy,True,8388608,90560,8350112,8350112,125,28753920,139677276,98850013,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1911,0,41099434,8122260.495,11534336,11425690,1090782.263887,0,1048832,0 +dense_slow,threshold4,0,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,152388963,133280290,134242304,100671488,2048,2048,0,0,2048,0,126,1922,263,0,19097885,8858624.865,13631488,13575925,1188061.088853,0,1048832,0 +dense_slow,threshold8,0,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,158487631,104607294,134242304,100671488,2048,2048,0,0,2048,0,126,1922,881,0,55726830,9142910.493,16777216,16508139,1223330.140557,0,1048832,0 +dense_slow,pressure,1,ok,pressure,True,8388608,90560,8350112,8350112,126,28753920,148054808,118930635,134242304,100671488,2048,2048,0,0,2048,0,126,1922,243,0,23339314,8545660.143,12582912,13488795,1145837.182993,0,1048832,0 +dense_slow,threshold2,1,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,127102070,98389110,134242304,100671488,2048,2048,0,0,2048,0,126,1922,548,0,29835471,7376714.273,11534336,11721430,988062.578411,0,1048832,0 +dense_slow,threshold4,1,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,137097783,105658521,134242304,100671488,2048,2048,0,0,2048,0,126,1922,319,0,33507567,7492151.426,25165824,25521632,1004322.345588,0,1048832,0 +dense_slow,copy_no_pool,1,ok,copy,False,8388608,90560,8350112,8350112,125,28753920,129205398,106642340,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1837,0,23366005,7161052.395,20971520,19672000,961708.896247,0,0,0 +dense_slow,threshold8,1,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,149831970,119769747,134242304,100671488,2048,2048,0,0,2048,0,126,1922,207,0,22835752,8570181.832,25165824,24456487,1149620.580849,0,1048832,0 +dense_slow,project,1,ok,project,True,8388608,90560,8350112,8350112,126,28753920,138012595,104050639,134242304,100671488,2048,2048,0,0,0,0,126,1922,573,0,35587895,8024328.08,11534336,11967226,1074555.345603,0,1048832,0 +dense_slow,copy,1,ok,copy,True,8388608,90560,8350112,8350112,125,28753920,139897423,116916607,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1245,0,21827476,7467830.775,11534336,12057799,1002967.933698,0,1048832,0 +dense_slow,borrow,1,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,124760040,105059769,134242304,100671488,2048,2048,0,0,2048,0,126,1922,279,0,20851427,7121146.062,13631488,13454333,954812.49966,0,1048832,0 +dense_slow,threshold4,2,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,128197678,100339777,134242304,100671488,2048,2048,0,0,2048,0,126,1922,483,0,29286062,7442559.441,11534336,11489301,997040.290853,0,1048832,0 +dense_slow,copy_no_pool,2,ok,copy,False,8388608,90560,8350112,8350112,125,28753920,205943127,116032671,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1454,0,23092169,12031760.945,20971520,20696239,1615356.534078,0,0,0 +dense_slow,threshold2,2,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,126630183,111840417,134242304,100671488,2048,2048,0,0,2048,0,126,1922,123,0,14544050,7317376.727,11534336,11766189,981675.539219,0,1048832,0 +dense_slow,threshold8,2,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,130400357,115426208,134242304,100671488,2048,2048,0,0,2048,0,126,1922,33,0,15522516,7311488.307,13631488,16432805,981225.27374,0,1048832,0 +dense_slow,project,2,ok,project,True,8388608,90560,8350112,8350112,126,28753920,158617537,105530265,134242304,100671488,2048,2048,0,0,0,0,126,1922,630,0,53505513,8986415.696,20971520,20150036,1202933.657783,0,1048832,0 +dense_slow,copy,2,ok,copy,True,8388608,90560,8350112,8350112,125,28753920,136167489,109997334,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1562,0,27532835,7639923.617,13631488,16520028,1026043.651851,0,1048832,0 +dense_slow,pressure,2,ok,pressure,True,8388608,90560,8350112,8350112,126,28753920,124401411,111783663,134242304,100671488,2048,2048,0,0,2048,0,126,1922,108,0,12978960,7175143.561,11534336,11871096,962716.975355,0,1048832,0 +dense_slow,borrow,2,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,140192626,129302907,134242304,100671488,2048,2048,0,0,2048,0,126,1922,32,0,11304553,8151791.4,12582912,11985985,1094054.601176,0,1048832,0 +dense_slow,threshold4,3,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,127500625,109051339,134242304,100671488,2048,2048,0,0,2048,0,126,1922,86,0,15655621,7370372.812,13631488,13648518,988934.033123,0,1048832,0 +dense_slow,copy,3,ok,copy,True,8388608,90560,8350112,8350112,125,28753920,139629443,112148433,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1923,0,28146396,7999954.838,16777216,16468658,1074362.142096,0,1048832,0 +dense_slow,threshold8,3,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,129397045,108222327,134242304,100671488,2048,2048,0,0,2048,0,126,1922,210,0,19281611,7449478.358,13631488,13544240,999089.777214,0,1048832,0 +dense_slow,copy_no_pool,3,ok,copy,False,8388608,90560,8350112,8350112,125,28753920,125060409,101408203,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1720,0,24842157,7224036.272,11534336,11678097,970120.753206,0,0,0 +dense_slow,threshold2,3,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,121944973,98926473,134242304,100671488,2048,2048,0,0,2048,0,126,1922,410,0,24262193,7027416.376,11534336,12229040,941745.792822,0,1048832,0 +dense_slow,project,3,ok,project,True,8388608,90560,8350112,8350112,126,28753920,140664495,101124724,134242304,100671488,2048,2048,0,0,0,0,126,1922,716,0,41804911,8134234.887,12582912,13075389,1088709.015188,0,1048832,0 +dense_slow,pressure,3,ok,pressure,True,8388608,90560,8350112,8350112,126,28753920,121667696,101165480,134242304,100671488,2048,2048,0,0,2048,0,126,1922,330,0,21242064,7047783.954,11534336,11408672,944768.902586,0,1048832,0 +dense_slow,borrow,3,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,122887693,108048811,134242304,100671488,2048,2048,0,0,2048,0,126,1922,100,0,15309844,6960585.889,14680064,14962365,933965.439249,0,1048832,0 +dense_slow,threshold8,4,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,127832493,107011535,134242304,100671488,2048,2048,0,0,2048,0,126,1922,312,0,21568032,7387611.189,12582912,12693631,990437.837956,0,1048832,0 +dense_slow,copy,4,ok,copy,True,8388608,90560,8350112,8350112,125,28753920,132323981,104579167,134242304,100671488,2048,2048,100663296,2048,0,0,126,3970,1868,0,28280826,7629563.252,11534336,12006726,1024670.67878,0,1048832,0 +dense_slow,threshold4,4,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,117467153,100913544,134242304,100671488,2048,2048,0,0,2048,0,126,1922,211,0,17302742,6773721.904,11534336,11750486,908428.389283,0,1048832,0 +dense_slow,project,4,ok,project,True,8388608,90560,8350112,8350112,126,28753920,177547354,98087776,134242304,100671488,2048,2048,0,0,0,0,126,1922,1077,0,66269238,10478829.253,27262976,26359796,1401395.334755,0,1048832,0 +dense_slow,copy_no_pool,4,ok,copy,False,8388608,90560,8350112,8350112,125,28753920,127352551,95896509,134242304,100671488,2048,2048,100663296,2048,0,0,4096,0,1854,0,33140024,7371263.893,11534336,11482598,989890.745684,0,0,0 +dense_slow,borrow,4,ok,borrow,True,8388608,90560,8350112,8350112,126,28753920,156176896,135527318,134242304,100671488,2048,2048,0,0,2048,0,126,1922,8,0,19302646,8983882.317,23068672,21705495,1205798.201258,0,1048832,0 +dense_slow,threshold2,4,ok,threshold,True,8388608,90560,8350112,8350112,126,28753920,150967021,112103512,134242304,100671488,2048,2048,0,0,2048,0,126,1922,343,0,27798231,8607200.207,14680064,15553678,1153842.867633,0,1048832,0 +dense_slow,pressure,4,ok,pressure,True,8388608,90560,8350112,8350112,126,28753920,131031651,108678412,134242304,100671488,2048,2048,0,0,2048,0,126,1922,194,0,17318187,7550902.647,12582912,12787335,1012712.39438,0,1048832,0 +mixed_slow,copy_no_pool,0,ok,copy,False,8388608,90560,8388432,8388432,349,28753920,196227931,90207409,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,899,0,106212702,25497204.119,41943040,38861965,1403800.480768,0,0,0 +mixed_slow,threshold4,0,ok,threshold,True,8388608,90560,8388608,8388608,282,28753920,182099749,86444975,177631232,53735632,2048,2048,11229392,1357,691,0,1834,1571,951,0,99448742,18702737.958,27262976,26735888,1347766.35285,0,1153216,0 +mixed_slow,pressure,0,ok,pressure,True,8388608,90560,8388480,8388432,345,28753920,170729984,95177478,177631232,53735632,2048,2048,27916032,1699,349,1699,1996,1751,848,0,79228418,22318118.532,33554432,33636461,1237184.456112,0,522496,0 +mixed_slow,threshold2,0,ok,threshold,True,8388608,90560,8388560,8388432,344,28753920,248649117,108815021,177631232,53735632,2048,2048,28479696,1740,308,0,1911,1877,806,0,141754620,32835308.438,50331648,52109020,1817663.170181,0,519424,0 +mixed_slow,copy,0,ok,copy,True,8388608,90560,8388576,8388432,343,28753920,188497435,95408242,177631232,53735632,2048,2048,53727440,2048,0,0,2001,2095,825,0,93045012,24119999.997,37748736,36830878,1331578.296683,0,519424,0 +mixed_slow,borrow,0,ok,borrow,True,8388608,90560,8386736,8386640,112,28753920,202207266,112741417,177631232,53735632,2048,2048,0,0,2048,0,1260,788,922,0,93158576,9030450.097,15728640,16897915,1584952.748115,0,667904,0 +mixed_slow,project,0,ok,project,True,8388608,90560,8388608,8388544,358,28753920,196788319,85794578,177631232,53735632,2048,2048,0,0,0,0,1594,454,835,0,111694672,25911259.331,41943040,39632482,1386620.278442,0,458688,0 +mixed_slow,threshold8,0,ok,threshold,True,8388608,90560,8388608,8388432,216,28753920,177004559,88488220,177631232,53735632,2048,2048,3824848,1008,1040,0,1769,1287,956,0,91999803,13961564.399,20971520,20644424,1308343.909911,0,1250624,0 +mixed_slow,copy_no_pool,1,ok,copy,False,8388608,90560,8388432,8388432,349,28753920,209526727,99009959,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,852,0,113158134,27706362.205,50331648,49857323,1504182.816335,0,0,0 +mixed_slow,threshold8,1,ok,threshold,True,8388608,90560,8388608,8388432,216,28753920,178695721,88024506,177631232,53735632,2048,2048,3824848,1008,1040,0,1752,1304,957,0,93665641,14283495.48,25165824,25314392,1350685.597943,0,1250624,0 +mixed_slow,project,1,ok,project,True,8388608,90560,8388608,8388544,358,28753920,188062454,85805781,177631232,53735632,2048,2048,0,0,0,0,1613,435,876,0,106943146,25106963.876,41943040,40388905,1338315.607594,0,463808,0 +mixed_slow,pressure,1,ok,pressure,True,8388608,90560,8388560,8388432,349,28753920,180476475,82814346,177631232,53735632,2048,2048,27916032,1699,349,1699,2034,1713,906,0,101386971,23117968.235,37748736,34624643,1287754.655314,0,522496,0 +mixed_slow,copy,1,ok,copy,True,8388608,90560,8388576,8388432,345,28753920,179916143,88224841,177631232,53735632,2048,2048,53727440,2048,0,0,2035,2061,868,0,95038979,23134688.68,37748736,38482948,1259295.778761,0,519424,0 +mixed_slow,threshold2,1,ok,threshold,True,8388608,90560,8388560,8388432,345,28753920,172458424,95876297,177631232,53735632,2048,2048,28479696,1740,308,0,1950,1838,838,0,80637955,22201893.822,29360128,29750009,1218383.798869,0,522496,0 +mixed_slow,borrow,1,ok,borrow,True,8388608,90560,8386736,8386688,112,28753920,202718412,105879101,177631232,53735632,2048,2048,0,0,2048,0,1270,778,933,0,101496664,8968645.74,15728640,15509271,1593585.502544,0,667904,0 +mixed_slow,threshold4,1,ok,threshold,True,8388608,90560,8388608,8388608,268,28753920,178050667,95042355,177631232,53735632,2048,2048,11229392,1357,691,0,1767,1638,919,0,87305079,17737601.933,27262976,26498834,1276748.071158,0,1154176,0 +mixed_slow,copy,2,ok,copy,True,8388608,90560,8388560,8388432,343,28753920,166505070,87773371,177631232,53735632,2048,2048,53727440,2048,0,0,2029,2067,855,0,82039937,21365903.831,31457280,30499822,1169734.902412,0,522496,0 +mixed_slow,project,2,ok,project,True,8388608,90560,8388576,8388576,358,28753920,184834889,77869217,177631232,53735632,2048,2048,0,0,0,0,1654,394,900,0,108638540,24466594.426,46137344,45971408,1295645.073976,0,458688,0 +mixed_slow,threshold2,2,ok,threshold,True,8388608,90560,8388432,8388432,346,28753920,191170037,94174606,177631232,53735632,2048,2048,28479696,1740,308,0,1933,1855,839,0,99297240,24916604.797,37748736,37194081,1364006.158543,0,519424,0 +mixed_slow,threshold4,2,ok,threshold,True,8388608,90560,8388608,8388608,267,28753920,221114880,108320175,177631232,53735632,2048,2048,11229392,1357,691,0,1725,1680,867,0,112272606,22302398.136,31457280,31418968,1620151.723755,0,1153216,0 +mixed_slow,pressure,2,ok,pressure,True,8388608,90560,8388560,8388432,343,28753920,200423808,95868665,177631232,53735632,2048,2048,27916032,1699,349,1699,1950,1797,826,0,104684245,26149827.952,37748736,37286562,1462564.435239,0,519424,0 +mixed_slow,copy_no_pool,2,ok,copy,False,8388608,90560,8388432,8388432,348,28753920,185563183,99669005,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,824,0,88520259,24158310.672,37748736,37578913,1315287.672243,0,0,0 +mixed_slow,threshold8,2,ok,threshold,True,8388608,90560,8388576,8388352,216,28753920,285706068,123773735,177631232,53735632,2048,2048,3824848,1008,1040,0,1639,1417,811,0,151915315,22809999.729,41943040,39308704,2141933.275175,0,1250624,0 +mixed_slow,borrow,2,ok,borrow,True,8388608,90560,8386736,8386640,112,28753920,277725554,130508378,177631232,53735632,2048,2048,0,0,2048,0,1246,802,882,0,145190817,12364716.181,25165824,24717468,2188978.814479,0,667904,0 +mixed_slow,project,3,ok,project,True,8388608,90560,8388576,8388544,350,28753920,266408067,111663429,177631232,53735632,2048,2048,0,0,0,0,1498,550,732,0,138942208,34638344.422,54525952,52949259,1878408.17389,0,458688,0 +mixed_slow,threshold2,3,ok,threshold,True,8388608,90560,8388560,8388432,341,28753920,204623815,104506177,177631232,53735632,2048,2048,28479696,1740,308,0,1822,1966,758,0,97171540,25691162.972,41943040,40286443,1429280.943963,0,522496,0 +mixed_slow,threshold4,3,ok,threshold,True,8388608,90560,8388608,8388608,277,28753920,162195656,80564634,177631232,53735632,2048,2048,11229392,1357,691,0,1810,1595,983,0,84519072,16670151.746,23068672,21537625,1200241.584576,0,1153216,0 +mixed_slow,copy,3,ok,copy,True,8388608,90560,8388560,8388432,350,28753920,155808970,73686530,177631232,53735632,2048,2048,53727440,2048,0,0,2181,1915,970,0,84997837,20210703.898,29360128,29034418,1105823.092919,0,522496,0 +mixed_slow,copy_no_pool,3,ok,copy,False,8388608,90560,8388432,8388432,343,28753920,175012120,86574113,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,847,0,86153992,21577383.531,37748736,35170512,1189109.090661,0,0,0 +mixed_slow,borrow,3,ok,borrow,True,8388608,90560,8386736,8386640,112,28753920,192464571,109214745,177631232,53735632,2048,2048,0,0,2048,0,1270,778,921,0,85288420,8548822.691,13631488,14178942,1513389.1547,0,667904,0 +mixed_slow,threshold8,3,ok,threshold,True,8388608,90560,8388496,8388432,216,28753920,180744548,92447570,177631232,53735632,2048,2048,3824848,1008,1040,0,1772,1284,987,0,91033265,14634490.266,20971520,21334847,1375000.84592,0,1250624,0 +mixed_slow,pressure,3,ok,pressure,True,8388608,90560,8388480,8388432,347,28753920,190539057,89751173,177631232,53735632,2048,2048,27916032,1699,349,1699,1983,1764,862,0,102577548,24120880.139,46137344,43388027,1333423.649457,0,519424,0 +mixed_slow,copy,4,ok,copy,True,8388608,90560,8388432,8388432,339,28753920,221349934,109576369,177631232,53735632,2048,2048,53727440,2048,0,0,1871,2225,733,0,114046592,28163326.597,46137344,46133518,1540447.287587,0,519424,0 +mixed_slow,project,4,ok,project,True,8388608,90560,8388576,8388544,351,28753920,182031656,81559603,177631232,53735632,2048,2048,0,0,0,0,1543,505,829,0,98044537,23787174.92,33554432,33075333,1288001.912824,0,458688,0 +mixed_slow,threshold8,4,ok,threshold,True,8388608,90560,8388608,8388432,216,28753920,190119929,98659849,177631232,53735632,2048,2048,3824848,1008,1040,0,1748,1308,937,0,91519644,15332315.134,27262976,28453087,1432190.421467,0,1262912,0 +mixed_slow,copy_no_pool,4,ok,copy,False,8388608,90560,8388432,8388432,348,28753920,181246528,87821814,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,854,0,96465580,23009953.597,37748736,35350753,1263667.037898,0,0,0 +mixed_slow,threshold4,4,ok,threshold,True,8388608,90560,8388608,8388608,268,28753920,202902060,94178139,177631232,53735632,2048,2048,11229392,1357,691,0,1778,1627,948,0,111627333,20292318.825,41943040,42672491,1456891.057432,0,1153216,0 +mixed_slow,borrow,4,ok,borrow,True,8388608,90560,8386736,8386640,112,28753920,187355085,105102970,177631232,53735632,2048,2048,0,0,2048,0,1268,780,942,0,86194520,8306217.014,16777216,16560027,1467132.987688,0,667904,0 +mixed_slow,threshold2,4,ok,threshold,True,8388608,90560,8388560,8388432,349,28753920,185932830,88442274,177631232,53735632,2048,2048,28479696,1740,308,0,1998,1790,873,0,100159229,24415772.17,37748736,35463160,1333943.667435,0,519424,0 +mixed_slow,pressure,4,ok,pressure,True,8388608,90560,8388560,8388432,351,28753920,175115990,85590028,177631232,53735632,2048,2048,27916032,1699,349,1699,2029,1718,897,0,90980114,22901875.606,31457280,30636340,1271349.314418,0,519424,0 +mixed_tight,pressure,0,ok,pressure,True,2097152,90560,2097152,2097136,106,28753920,194998211,80964449,177631232,53735632,2048,2048,28385168,1727,321,1727,2265,1510,1251,0,117554960,6209271.27,9437184,9610047,352937.551048,0,523712,0 +mixed_tight,project,0,ok,project,True,2097152,90560,2097120,2097120,118,28753920,191006927,80024226,177631232,53735632,2048,2048,0,0,0,0,1659,389,1164,0,113283506,6749781.49,10485760,10754747,354343.266259,0,406848,0 +mixed_tight,threshold8,0,ok,threshold,True,2097152,90560,2097120,2097120,72,28753920,315263040,90750328,177631232,53735632,2048,2048,3824848,1008,1040,0,1890,1166,1248,0,228816753,6291155.426,13631488,13216574,579371.042178,0,386176,0 +mixed_tight,copy_no_pool,0,ok,copy,False,2097152,90560,2097136,2097136,116,28753920,215534802,92135979,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1179,0,127073694,6714221.641,10485760,12545668,387556.898349,0,0,0 +mixed_tight,threshold4,0,ok,threshold,True,2097152,90560,2097136,2097104,91,28753920,240539157,82888986,177631232,53735632,2048,2048,11229392,1357,691,0,2037,1368,1333,0,157978818,6115847.212,9437184,11204691,437508.612236,0,435392,0 +mixed_tight,copy,0,ok,copy,True,2097152,90560,2097136,2097136,111,28753920,205803459,87522857,177631232,53735632,2048,2048,53727440,2048,0,0,2361,1735,1210,0,121961836,6356735.332,11534336,12149188,363428.263552,0,523712,0 +mixed_tight,threshold2,0,ok,threshold,True,2097152,90560,2097136,2097136,103,28753920,218109869,94383237,177631232,53735632,2048,2048,28479696,1740,308,0,2188,1600,1166,0,128480946,7013746.965,14680064,15108236,394831.364885,0,523712,0 +mixed_tight,borrow,0,ok,borrow,True,2097152,90560,2094128,2094032,40,28753920,544543692,90900399,177631232,53735632,2048,2048,0,0,2048,0,1332,716,1286,0,453134113,6077465.417,11534336,12176243,1028555.825239,0,495872,0 +mixed_tight,copy,1,ok,copy,True,2097152,90560,2097136,2097136,117,28753920,191836517,62332245,177631232,53735632,2048,2048,53727440,2048,0,0,2460,1636,1362,0,131898458,6069082.115,8388608,8770216,346911.480275,0,523712,0 +mixed_tight,threshold2,1,ok,threshold,True,2097152,90560,2097136,2097136,117,28753920,188885136,63904326,177631232,53735632,2048,2048,28479696,1740,308,0,2356,1432,1390,0,127844964,6121880.805,8388608,8708698,342822.067006,0,523712,0 +mixed_tight,borrow,1,ok,borrow,True,2097152,90560,2094128,2094128,40,28753920,492789763,76140523,177631232,53735632,2048,2048,0,0,2048,0,1347,701,1406,0,420428963,5485700.477,8388608,9955175,928718.610429,0,495872,0 +mixed_tight,threshold8,1,ok,threshold,True,2097152,90560,2097136,2097136,72,28753920,282084485,66066433,177631232,53735632,2048,2048,3824848,1008,1040,0,1992,1064,1398,0,219096863,5671193.431,7340032,9274955,520410.956771,0,386176,0 +mixed_tight,threshold4,1,ok,threshold,True,2097152,90560,2097136,2097104,88,28753920,224500368,61534673,177631232,53735632,2048,2048,11229392,1357,691,0,2145,1260,1465,0,165914339,5837746.733,7864320,10210564,412262.960044,0,435392,0 +mixed_tight,pressure,1,ok,pressure,True,2097152,90560,2097136,2097136,115,28753920,189684559,60774942,177631232,53735632,2048,2048,28385168,1727,321,1727,2383,1392,1399,0,131755127,6110993.322,8388608,9608714,345607.344111,0,523712,0 +mixed_tight,copy_no_pool,1,ok,copy,False,2097152,90560,2097136,2097136,117,28753920,190441538,59274120,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1383,0,133844636,6031707.432,8388608,8881216,344699.365648,0,0,0 +mixed_tight,project,1,ok,project,True,2097152,90560,2097120,2097120,118,28753920,175373266,57346839,177631232,53735632,2048,2048,0,0,0,0,1727,321,1312,0,120854151,6287663.374,8388608,8829253,326669.995028,0,406848,0 +mixed_tight,copy_no_pool,2,ok,copy,False,2097152,90560,2097136,2097136,115,28753920,190230224,58883103,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1379,0,133923360,6020977.936,8388608,8904123,344486.353015,0,0,0 +mixed_tight,project,2,ok,project,True,2097152,90560,2097120,2097120,118,28753920,176712466,57142470,177631232,53735632,2048,2048,0,0,0,0,1727,321,1308,0,122435178,6344282.023,9437184,9173733,329327.109037,0,406848,0 +mixed_tight,copy,2,ok,copy,True,2097152,90560,2097152,2097136,115,28753920,192640551,62036091,177631232,53735632,2048,2048,53727440,2048,0,0,2463,1633,1367,0,133360267,6075663.601,8388608,8830642,347845.920578,0,523712,0 +mixed_tight,threshold8,2,ok,threshold,True,2097152,90560,2097136,2097136,73,28753920,284454034,68644342,177631232,53735632,2048,2048,3824848,1008,1040,0,1983,1073,1386,0,218577789,5702048.39,7340032,9254474,523314.413618,0,386176,0 +mixed_tight,threshold4,2,ok,threshold,True,2097152,90560,2097136,2097104,88,28753920,226008956,64169718,177631232,53735632,2048,2048,11229392,1357,691,0,2125,1280,1458,0,164889565,5866464.096,8388608,10318805,414911.228869,0,435392,0 +mixed_tight,borrow,2,ok,borrow,True,2097152,90560,2094128,2094128,40,28753920,494360387,73512176,177631232,53735632,2048,2048,0,0,2048,0,1345,703,1395,0,424305645,5500812.732,8388608,9972009,932412.777581,0,495872,0 +mixed_tight,threshold2,2,ok,threshold,True,2097152,90560,2097136,2097136,115,28753920,188224878,60744558,177631232,53735632,2048,2048,28479696,1740,308,0,2384,1404,1411,0,130279846,6114092.392,8388608,8874864,342235.388035,0,523712,0 +mixed_tight,pressure,2,ok,pressure,True,2097152,90560,2097136,2097136,115,28753920,189816540,60109883,177631232,53735632,2048,2048,28385168,1727,321,1727,2377,1398,1401,0,132388467,6115537.712,8388608,9634918,345575.615482,0,523712,0 +mixed_tight,pressure,3,ok,pressure,True,2097152,90560,2097136,2097136,115,28753920,190593723,62104383,177631232,53735632,2048,2048,28385168,1727,321,1727,2377,1398,1387,0,131246281,6133228.412,8388608,9617713,346774.138471,0,523712,0 +mixed_tight,borrow,3,ok,borrow,True,2097152,90560,2094128,2094128,40,28753920,491528840,70906509,177631232,53735632,2048,2048,0,0,2048,0,1353,695,1421,0,423844868,5472000.565,8388608,9945028,926418.118409,0,495872,0 +mixed_tight,copy_no_pool,3,ok,copy,False,2097152,90560,2097136,2097136,116,28753920,190661223,60180004,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1376,0,133160918,6036141.159,8388608,8916068,345094.680154,0,0,0 +mixed_tight,threshold2,3,ok,threshold,True,2097152,90560,2097136,2097136,116,28753920,193175809,69008296,177631232,53735632,2048,2048,28479696,1740,308,0,2336,1452,1351,0,125127054,6239198.13,9437184,9276566,350903.059901,0,523712,0 +mixed_tight,threshold8,3,ok,threshold,True,2097152,90560,2097120,2097120,73,28753920,286766304,73096130,177631232,53735632,2048,2048,3824848,1008,1040,0,1974,1082,1367,0,215532386,5744366.208,7864320,8864197,528100.049771,0,386176,0 +mixed_tight,threshold4,3,ok,threshold,True,2097152,90560,2097136,2097104,88,28753920,225938124,65488664,177631232,53735632,2048,2048,11229392,1357,691,0,2121,1284,1445,0,163452164,5856203.824,8388608,10380804,414648.202609,0,435392,0 +mixed_tight,copy,3,ok,copy,True,2097152,90560,2097152,2097136,108,28753920,191114147,63369902,177631232,53735632,2048,2048,53727440,2048,0,0,2453,1643,1341,0,130459979,6032302.559,8388608,8833531,345755.184019,0,523712,0 +mixed_tight,project,3,ok,project,True,2097152,90560,2097120,2097120,118,28753920,177942277,63483663,177631232,53735632,2048,2048,0,0,0,0,1711,337,1279,0,117556310,6348716.251,8388608,8883105,331194.459386,0,406848,0 +mixed_tight,copy,4,ok,copy,True,2097152,90560,2097152,2097136,117,28753920,191747145,60306782,177631232,53735632,2048,2048,53727440,2048,0,0,2470,1626,1379,0,134097654,6065392.323,8388608,8895161,347142.858576,0,523712,0 +mixed_tight,threshold2,4,ok,threshold,True,2097152,90560,2097136,2097136,115,28753920,187864582,60112693,177631232,53735632,2048,2048,28479696,1740,308,0,2380,1408,1409,0,130504092,6102214.292,8388608,8884715,341567.414808,0,523712,0 +mixed_tight,project,4,ok,project,True,2097152,90560,2097120,2097120,118,28753920,175293210,56768485,177631232,53735632,2048,2048,0,0,0,0,1722,326,1313,0,121359184,6283898.527,8388608,8849623,326483.546647,0,406848,0 +mixed_tight,threshold8,4,ok,threshold,True,2097152,90560,2097136,2097136,73,28753920,283727960,68131361,177631232,53735632,2048,2048,3824848,1008,1040,0,1988,1068,1394,0,218912386,5707951.51,7340032,9272677,523909.294385,0,386176,0 +mixed_tight,borrow,4,ok,borrow,True,2097152,90560,2094128,2094128,40,28753920,495666215,81002646,177631232,53735632,2048,2048,0,0,2048,0,1336,712,1382,0,418460319,5519510.972,8388608,9930268,935139.458784,0,495872,0 +mixed_tight,copy_no_pool,4,ok,copy,False,2097152,90560,2097136,2097136,116,28753920,190841888,60356559,177631232,53735632,2048,2048,53727440,2048,0,0,4096,0,1382,0,133224289,6038284.196,8388608,8947753,345496.101548,0,0,0 +mixed_tight,threshold4,4,ok,threshold,True,2097152,90560,2097136,2097104,88,28753920,226098419,63538828,177631232,53735632,2048,2048,11229392,1357,691,0,2129,1276,1452,0,165691682,5878789.657,7864320,10264360,415107.899759,0,435392,0 +mixed_tight,pressure,4,ok,pressure,True,2097152,90560,2097136,2097136,115,28753920,192204126,62496629,177631232,53735632,2048,2048,28385168,1727,321,1727,2367,1408,1381,0,132616450,6161110.138,9437184,9782879,349554.881495,0,523712,0 +sparse_fast,pressure,0,ok,pressure,True,8388608,90560,156112,156112,1,28753920,20554720,20527722,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9881.978,13312,60759,1273.955005,0,65552,0 +sparse_fast,project,0,ok,project,True,8388608,90560,90640,90640,1,28753920,22216122,22101834,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10698.669,16384,58241,1.68633,0,80,0 +sparse_fast,threshold4,0,ok,threshold,True,8388608,90560,156192,156192,1,28753920,22839176,22457056,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10958.209,20480,165851,1227.172318,0,65632,0 +sparse_fast,copy,0,ok,copy,True,8388608,90560,156192,156192,1,28753920,21009107,20955370,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10112.231,14336,66129,1120.781895,0,65632,0 +sparse_fast,copy_no_pool,0,ok,copy,False,8388608,90560,156192,156192,1,28753920,21132162,21048817,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,10163.675,16384,68685,1121.783606,0,0,0 +sparse_fast,threshold8,0,ok,threshold,True,8388608,90560,156192,156192,1,28753920,20540535,20499501,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9884.486,13312,54093,1100.863361,0,65632,0 +sparse_fast,borrow,0,ok,borrow,True,8388608,90560,156112,156112,1,28753920,21727605,21682906,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,10449.435,20480,65999,1348.749968,0,65552,0 +sparse_fast,threshold2,0,ok,threshold,True,8388608,90560,156192,156192,1,28753920,22373067,22308741,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10776.724,18432,66703,1207.809872,0,65632,0 +sparse_fast,project,1,ok,project,True,8388608,90560,90640,90640,1,28753920,25768428,25607575,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,12427.336,22528,135518,1.971383,0,80,0 +sparse_fast,threshold8,1,ok,threshold,True,8388608,90560,156192,156192,1,28753920,22601085,22218814,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10886.611,28672,70518,1220.513584,0,65632,0 +sparse_fast,copy_no_pool,1,ok,copy,False,8388608,90560,156192,156192,1,28753920,22194827,21633689,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,10673.54,36864,207444,1178.059751,0,0,0 +sparse_fast,pressure,1,ok,pressure,True,8388608,90560,156112,156112,1,28753920,23241749,23003312,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,11135.177,22528,79185,1441.524009,0,65552,0 +sparse_fast,copy,1,ok,copy,True,8388608,90560,156192,156192,1,28753920,24103618,23832019,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,11617.114,22528,88389,1310.460688,0,65632,0 +sparse_fast,threshold2,1,ok,threshold,True,8388608,90560,156192,156192,1,28753920,21191681,21131537,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10199.927,14336,68426,1132.085862,0,65632,0 +sparse_fast,threshold4,1,ok,threshold,True,8388608,90560,156192,156192,1,28753920,21871975,21613127,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10530.066,18432,78704,1171.334175,0,65632,0 +sparse_fast,borrow,1,ok,borrow,True,8388608,90560,156112,156112,1,28753920,21555643,21467723,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,10366.948,18432,62648,1339.691796,0,65552,0 +sparse_fast,borrow,2,ok,borrow,True,8388608,90560,156112,156112,1,28753920,20704016,20633572,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9954.482,14336,84963,1283.092823,0,65552,0 +sparse_fast,threshold2,2,ok,threshold,True,8388608,90560,156192,156192,1,28753920,20854404,20822149,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10030.324,14336,62445,1108.309428,0,65632,0 +sparse_fast,copy_no_pool,2,ok,copy,False,8388608,90560,156192,156192,1,28753920,21286218,21228201,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,10237.021,15360,67889,1129.223222,0,0,0 +sparse_fast,project,2,ok,project,True,8388608,90560,90640,90640,1,28753920,22649251,22567445,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10910.76,15360,61499,1.717551,0,80,0 +sparse_fast,pressure,2,ok,pressure,True,8388608,90560,156112,156112,1,28753920,20767942,20708872,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9985.038,15360,65425,1286.595987,0,65552,0 +sparse_fast,copy,2,ok,copy,True,8388608,90560,156192,156192,1,28753920,21065681,21006279,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10139.793,15360,67018,1124.320165,0,65632,0 +sparse_fast,threshold4,2,ok,threshold,True,8388608,90560,156192,156192,1,28753920,20961885,20931648,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10087.398,13312,60962,1117.500277,0,65632,0 +sparse_fast,threshold8,2,ok,threshold,True,8388608,90560,156192,156192,1,28753920,20908700,20878832,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10062.625,14336,70648,1116.46627,0,65632,0 +sparse_fast,threshold2,3,ok,threshold,True,8388608,90560,156192,156192,1,28753920,21328570,21275351,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,10266.949,15360,61241,1144.526904,0,65632,0 +sparse_fast,borrow,3,ok,borrow,True,8388608,90560,156112,156112,1,28753920,20408294,20408999,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,9812.079,13312,48982,1265.057763,0,65552,0 +sparse_fast,copy,3,ok,copy,True,8388608,90560,156192,156192,1,28753920,20744256,20744722,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,9979.793,13312,42518,1107.901257,0,65632,0 +sparse_fast,project,3,ok,project,True,8388608,90560,90640,90640,1,28753920,21766957,21732111,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10483.601,13312,74593,1.653919,0,80,0 +sparse_fast,threshold4,3,ok,threshold,True,8388608,90560,156192,156192,1,28753920,23474730,23418353,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,11311.146,24576,72445,1275.148887,0,65632,0 +sparse_fast,threshold8,3,ok,threshold,True,8388608,90560,156192,156192,1,28753920,25891223,25855036,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,12490.423,32768,62019,1433.084986,0,65632,0 +sparse_fast,pressure,3,ok,pressure,True,8388608,90560,156112,156112,1,28753920,25124448,24854663,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,12097.683,24576,83351,1569.932052,0,65552,0 +sparse_fast,copy_no_pool,3,ok,copy,False,8388608,90560,156192,156192,1,28753920,21725958,21582704,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,10452.028,16384,197777,1159.292471,0,0,0 +sparse_fast,project,4,ok,project,True,8388608,90560,90640,90640,1,28753920,22202622,22202889,134242304,139264,2048,2048,0,0,0,0,1,2047,0,0,0,10695.292,18432,48444,1.688434,0,80,0 +sparse_fast,threshold2,4,ok,threshold,True,8388608,90560,156192,156192,1,28753920,27294793,27138353,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,13171.363,28672,78185,1515.562825,0,65632,0 +sparse_fast,threshold8,4,ok,threshold,True,8388608,90560,156192,156192,1,28753920,33207148,33180296,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,16048.501,36864,74648,1873.802277,0,65632,0 +sparse_fast,copy,4,ok,copy,True,8388608,90560,156192,156192,1,28753920,40643311,40589351,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,19667.254,49152,147388,2334.403772,0,65632,0 +sparse_fast,threshold4,4,ok,threshold,True,8388608,90560,156192,156192,1,28753920,28395383,28350296,134242304,139264,2048,2048,131072,2048,0,0,2,4094,0,0,0,13707.471,26624,87370,1583.182193,0,65632,0 +sparse_fast,borrow,4,ok,borrow,True,8388608,90560,156112,156112,1,28753920,25754576,25674260,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,12408.192,20480,98925,1611.048102,0,65552,0 +sparse_fast,pressure,4,ok,pressure,True,8388608,90560,156112,156112,1,28753920,28270087,28259741,134242304,139264,2048,2048,0,0,2048,0,1,2047,0,0,0,13635.652,20480,75407,1772.014643,0,65552,0 +sparse_fast,copy_no_pool,4,ok,copy,False,8388608,90560,156192,156192,1,28753920,30675302,30605147,134242304,139264,2048,2048,131072,2048,0,0,4096,0,0,0,0,14792.559,22528,144222,1690.623657,0,0,0 diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.json new file mode 100644 index 0000000..78400eb --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/raw.json @@ -0,0 +1,7202 @@ +[ + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 156440711, + "cpu_ns": 48121065, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 878, + "sleep_ns": 109587045, + "latency_mean_ns": 32718961.288, + "latency_p99_upper_ns": 50331648, + "latency_max_ns": 47686700, + "active_byte_seconds": 7879.414446, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 152733814, + "cpu_ns": 52980104, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 718, + "sleep_ns": 102189918, + "latency_mean_ns": 30324683.181, + "latency_p99_upper_ns": 54525952, + "latency_max_ns": 52327335, + "active_byte_seconds": 7602.229051, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 155247640, + "cpu_ns": 47779812, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 809, + "sleep_ns": 108519282, + "latency_mean_ns": 31414916.006, + "latency_p99_upper_ns": 54525952, + "latency_max_ns": 51100228, + "active_byte_seconds": 7369.340036, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 164046264, + "cpu_ns": 46944898, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 881, + "sleep_ns": 113764083, + "latency_mean_ns": 35133900.267, + "latency_p99_upper_ns": 46137344, + "latency_max_ns": 46051556, + "active_byte_seconds": 8459.440246, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 157789596, + "cpu_ns": 25739163, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 136058109, + "latency_mean_ns": 34150955.425, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 40528961, + "active_byte_seconds": 5595.217111, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154250903, + "cpu_ns": 24028565, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 133827808, + "latency_mean_ns": 33265597.392, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37531339, + "active_byte_seconds": 6504.707846, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 101576920, + "cpu_ns": 50248602, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 825, + "slot_waits": 0, + "sleep_ns": 53200392, + "latency_mean_ns": 5868549.98, + "latency_p99_upper_ns": 10485760, + "latency_max_ns": 10885229, + "active_byte_seconds": 784884.708981, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4316448, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 153086035, + "cpu_ns": 29353156, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113152, + "copied_records": 1768, + "borrowed_records": 280, + "pressure_copies": 1768, + "allocations": 514, + "reuses": 3302, + "budget_waits": 0, + "slot_waits": 1424, + "sleep_ns": 127637166, + "latency_mean_ns": 32763957.697, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38592411, + "active_byte_seconds": 549197.237404, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 132106520, + "cpu_ns": 45409725, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1532, + "slot_waits": 0, + "sleep_ns": 89610425, + "latency_mean_ns": 7801228.044, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9303567, + "active_byte_seconds": 1041796.843202, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154376717, + "cpu_ns": 24625261, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 133550825, + "latency_mean_ns": 33304515.117, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37563136, + "active_byte_seconds": 6521.304714, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154790882, + "cpu_ns": 24092354, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134531052, + "latency_mean_ns": 33433846.048, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37877431, + "active_byte_seconds": 6519.648368, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154495679, + "cpu_ns": 24603184, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 133731727, + "latency_mean_ns": 33331969.71, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37527099, + "active_byte_seconds": 6535.057316, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154242976, + "cpu_ns": 24135969, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1535, + "sleep_ns": 133865874, + "latency_mean_ns": 33299370.61, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37484173, + "active_byte_seconds": 6509.096995, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154024996, + "cpu_ns": 25025451, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 132769732, + "latency_mean_ns": 33212736.419, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37402803, + "active_byte_seconds": 5441.502377, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4316928, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 151466095, + "cpu_ns": 27009927, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113280, + "copied_records": 1770, + "borrowed_records": 278, + "pressure_copies": 1770, + "allocations": 514, + "reuses": 3304, + "budget_waits": 0, + "slot_waits": 1473, + "sleep_ns": 128099292, + "latency_mean_ns": 32391902.953, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37408099, + "active_byte_seconds": 539531.143577, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 155007270, + "cpu_ns": 24608283, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1535, + "sleep_ns": 134124966, + "latency_mean_ns": 33382726.714, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37593062, + "active_byte_seconds": 6546.135984, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154312902, + "cpu_ns": 23956224, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1535, + "sleep_ns": 134085359, + "latency_mean_ns": 33321820.453, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37660191, + "active_byte_seconds": 6502.811837, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154586382, + "cpu_ns": 24703970, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 133705501, + "latency_mean_ns": 33392210.559, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 37987523, + "active_byte_seconds": 6536.248232, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154368809, + "cpu_ns": 24191625, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 133926227, + "latency_mean_ns": 33288663.865, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37544080, + "active_byte_seconds": 6508.096109, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4316208, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 151522021, + "cpu_ns": 27174295, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113088, + "copied_records": 1767, + "borrowed_records": 281, + "pressure_copies": 1767, + "allocations": 514, + "reuses": 3301, + "budget_waits": 0, + "slot_waits": 1465, + "sleep_ns": 128053394, + "latency_mean_ns": 32398179.535, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37585173, + "active_byte_seconds": 545315.69968, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154139254, + "cpu_ns": 24156681, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1535, + "sleep_ns": 133742452, + "latency_mean_ns": 33274854.653, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37580636, + "active_byte_seconds": 6504.184345, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154504494, + "cpu_ns": 25538018, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1533, + "sleep_ns": 132803019, + "latency_mean_ns": 33348679.346, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37696024, + "active_byte_seconds": 5463.772808, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154268606, + "cpu_ns": 24017037, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134039266, + "latency_mean_ns": 33296530.878, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37600117, + "active_byte_seconds": 6499.765006, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 116617303, + "cpu_ns": 46471284, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1258, + "slot_waits": 0, + "sleep_ns": 72494973, + "latency_mean_ns": 6836360.134, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9280234, + "active_byte_seconds": 913414.105279, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 155002696, + "cpu_ns": 25290252, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 133467827, + "latency_mean_ns": 33443160.686, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38061801, + "active_byte_seconds": 5479.253547, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 122707360, + "cpu_ns": 46832624, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1334, + "slot_waits": 0, + "sleep_ns": 78581274, + "latency_mean_ns": 7210177.487, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9450215, + "active_byte_seconds": 963195.592926, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4316128, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 170735523, + "cpu_ns": 36085789, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113024, + "copied_records": 1766, + "borrowed_records": 282, + "pressure_copies": 1766, + "allocations": 514, + "reuses": 3300, + "budget_waits": 0, + "slot_waits": 1403, + "sleep_ns": 140741412, + "latency_mean_ns": 37128525.741, + "latency_p99_upper_ns": 50331648, + "latency_max_ns": 49207437, + "active_byte_seconds": 624641.105995, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 161984455, + "cpu_ns": 35308455, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1418, + "sleep_ns": 132544941, + "latency_mean_ns": 34734342.366, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 41927513, + "active_byte_seconds": 7196.657649, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 183186784, + "cpu_ns": 39562601, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1377, + "sleep_ns": 149944607, + "latency_mean_ns": 39799236.839, + "latency_p99_upper_ns": 50331648, + "latency_max_ns": 49954231, + "active_byte_seconds": 8175.636446, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 160896124, + "cpu_ns": 25740442, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1519, + "sleep_ns": 138959714, + "latency_mean_ns": 34942676.828, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 40635979, + "active_byte_seconds": 6866.098036, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 155242417, + "cpu_ns": 24840078, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1534, + "sleep_ns": 134375289, + "latency_mean_ns": 33387730.27, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38123912, + "active_byte_seconds": 6526.934401, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 153980365, + "cpu_ns": 24665615, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1523, + "sleep_ns": 133098283, + "latency_mean_ns": 33153737.672, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37518117, + "active_byte_seconds": 6514.197106, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154143199, + "cpu_ns": 24004931, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 133901751, + "latency_mean_ns": 33287028.642, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37626617, + "active_byte_seconds": 6483.552028, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 153170646, + "cpu_ns": 26299681, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1505, + "sleep_ns": 130663631, + "latency_mean_ns": 33019408.27, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37369469, + "active_byte_seconds": 6585.869234, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154103699, + "cpu_ns": 23694419, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 134088060, + "latency_mean_ns": 33271036.464, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37623950, + "active_byte_seconds": 6485.074292, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154585216, + "cpu_ns": 23936885, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 134389609, + "latency_mean_ns": 33393120.598, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 38036616, + "active_byte_seconds": 6515.371593, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 124475504, + "cpu_ns": 45943956, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1468, + "slot_waits": 0, + "sleep_ns": 81119761, + "latency_mean_ns": 7320646.162, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9274900, + "active_byte_seconds": 977840.583339, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 197072, + "peak_active_bytes": 197072, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154125217, + "cpu_ns": 24321563, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 513, + "reuses": 3583, + "budget_waits": 0, + "slot_waits": 1536, + "sleep_ns": 133545099, + "latency_mean_ns": 33270620.015, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37515988, + "active_byte_seconds": 6516.994726, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 66752, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 4321888, + "peak_active_bytes": 4317248, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 151398447, + "cpu_ns": 26713792, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 113408, + "copied_records": 1772, + "borrowed_records": 276, + "pressure_copies": 1772, + "allocations": 514, + "reuses": 3306, + "budget_waits": 0, + "slot_waits": 1473, + "sleep_ns": 128307648, + "latency_mean_ns": 32397554.332, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37324395, + "active_byte_seconds": 535707.02646, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 590528, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_slow", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 131520, + "peak_active_bytes": 131520, + "peak_queue": 512, + "rss_peak_bytes": 28753920, + "elapsed_ns": 154766567, + "cpu_ns": 24892483, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 512, + "reuses": 1536, + "budget_waits": 0, + "slot_waits": 1537, + "sleep_ns": 133703008, + "latency_mean_ns": 33388895.003, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 37874598, + "active_byte_seconds": 5470.365986, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1280, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 153699255, + "cpu_ns": 97036844, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 890, + "slot_waits": 0, + "sleep_ns": 59162056, + "latency_mean_ns": 8908081.106, + "latency_p99_upper_ns": 15728640, + "latency_max_ns": 16113048, + "active_byte_seconds": 1191532.621299, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 130854245, + "cpu_ns": 109226645, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 318, + "slot_waits": 0, + "sleep_ns": 22611916, + "latency_mean_ns": 7607977.365, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12586113, + "active_byte_seconds": 1019874.721927, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 130227024, + "cpu_ns": 112627422, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1363, + "slot_waits": 0, + "sleep_ns": 17672583, + "latency_mean_ns": 7496902.226, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12386151, + "active_byte_seconds": 1006822.239807, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 132968553, + "cpu_ns": 110199520, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 230, + "slot_waits": 0, + "sleep_ns": 24087338, + "latency_mean_ns": 7516016.308, + "latency_p99_upper_ns": 16777216, + "latency_max_ns": 17649580, + "active_byte_seconds": 1007870.242312, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 139106888, + "cpu_ns": 110625767, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 217, + "slot_waits": 0, + "sleep_ns": 29387892, + "latency_mean_ns": 8069611.928, + "latency_p99_upper_ns": 16777216, + "latency_max_ns": 17083878, + "active_byte_seconds": 1081569.371059, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 139677276, + "cpu_ns": 98850013, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1911, + "slot_waits": 0, + "sleep_ns": 41099434, + "latency_mean_ns": 8122260.495, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11425690, + "active_byte_seconds": 1090782.263887, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 152388963, + "cpu_ns": 133280290, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 263, + "slot_waits": 0, + "sleep_ns": 19097885, + "latency_mean_ns": 8858624.865, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 13575925, + "active_byte_seconds": 1188061.088853, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 158487631, + "cpu_ns": 104607294, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 881, + "slot_waits": 0, + "sleep_ns": 55726830, + "latency_mean_ns": 9142910.493, + "latency_p99_upper_ns": 16777216, + "latency_max_ns": 16508139, + "active_byte_seconds": 1223330.140557, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 148054808, + "cpu_ns": 118930635, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 243, + "slot_waits": 0, + "sleep_ns": 23339314, + "latency_mean_ns": 8545660.143, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13488795, + "active_byte_seconds": 1145837.182993, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 127102070, + "cpu_ns": 98389110, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 548, + "slot_waits": 0, + "sleep_ns": 29835471, + "latency_mean_ns": 7376714.273, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11721430, + "active_byte_seconds": 988062.578411, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 137097783, + "cpu_ns": 105658521, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 319, + "slot_waits": 0, + "sleep_ns": 33507567, + "latency_mean_ns": 7492151.426, + "latency_p99_upper_ns": 25165824, + "latency_max_ns": 25521632, + "active_byte_seconds": 1004322.345588, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 129205398, + "cpu_ns": 106642340, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1837, + "slot_waits": 0, + "sleep_ns": 23366005, + "latency_mean_ns": 7161052.395, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 19672000, + "active_byte_seconds": 961708.896247, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 149831970, + "cpu_ns": 119769747, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 207, + "slot_waits": 0, + "sleep_ns": 22835752, + "latency_mean_ns": 8570181.832, + "latency_p99_upper_ns": 25165824, + "latency_max_ns": 24456487, + "active_byte_seconds": 1149620.580849, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 138012595, + "cpu_ns": 104050639, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 573, + "slot_waits": 0, + "sleep_ns": 35587895, + "latency_mean_ns": 8024328.08, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11967226, + "active_byte_seconds": 1074555.345603, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 139897423, + "cpu_ns": 116916607, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1245, + "slot_waits": 0, + "sleep_ns": 21827476, + "latency_mean_ns": 7467830.775, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12057799, + "active_byte_seconds": 1002967.933698, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 124760040, + "cpu_ns": 105059769, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 279, + "slot_waits": 0, + "sleep_ns": 20851427, + "latency_mean_ns": 7121146.062, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 13454333, + "active_byte_seconds": 954812.49966, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 128197678, + "cpu_ns": 100339777, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 483, + "slot_waits": 0, + "sleep_ns": 29286062, + "latency_mean_ns": 7442559.441, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11489301, + "active_byte_seconds": 997040.290853, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 205943127, + "cpu_ns": 116032671, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1454, + "slot_waits": 0, + "sleep_ns": 23092169, + "latency_mean_ns": 12031760.945, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 20696239, + "active_byte_seconds": 1615356.534078, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 126630183, + "cpu_ns": 111840417, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 123, + "slot_waits": 0, + "sleep_ns": 14544050, + "latency_mean_ns": 7317376.727, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11766189, + "active_byte_seconds": 981675.539219, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 130400357, + "cpu_ns": 115426208, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 33, + "slot_waits": 0, + "sleep_ns": 15522516, + "latency_mean_ns": 7311488.307, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 16432805, + "active_byte_seconds": 981225.27374, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 158617537, + "cpu_ns": 105530265, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 630, + "slot_waits": 0, + "sleep_ns": 53505513, + "latency_mean_ns": 8986415.696, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 20150036, + "active_byte_seconds": 1202933.657783, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 136167489, + "cpu_ns": 109997334, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1562, + "slot_waits": 0, + "sleep_ns": 27532835, + "latency_mean_ns": 7639923.617, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 16520028, + "active_byte_seconds": 1026043.651851, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 124401411, + "cpu_ns": 111783663, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 108, + "slot_waits": 0, + "sleep_ns": 12978960, + "latency_mean_ns": 7175143.561, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11871096, + "active_byte_seconds": 962716.975355, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 140192626, + "cpu_ns": 129302907, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 32, + "slot_waits": 0, + "sleep_ns": 11304553, + "latency_mean_ns": 8151791.4, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 11985985, + "active_byte_seconds": 1094054.601176, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 127500625, + "cpu_ns": 109051339, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 86, + "slot_waits": 0, + "sleep_ns": 15655621, + "latency_mean_ns": 7370372.812, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 13648518, + "active_byte_seconds": 988934.033123, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 139629443, + "cpu_ns": 112148433, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1923, + "slot_waits": 0, + "sleep_ns": 28146396, + "latency_mean_ns": 7999954.838, + "latency_p99_upper_ns": 16777216, + "latency_max_ns": 16468658, + "active_byte_seconds": 1074362.142096, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 129397045, + "cpu_ns": 108222327, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 210, + "slot_waits": 0, + "sleep_ns": 19281611, + "latency_mean_ns": 7449478.358, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 13544240, + "active_byte_seconds": 999089.777214, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 125060409, + "cpu_ns": 101408203, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1720, + "slot_waits": 0, + "sleep_ns": 24842157, + "latency_mean_ns": 7224036.272, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11678097, + "active_byte_seconds": 970120.753206, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 121944973, + "cpu_ns": 98926473, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 410, + "slot_waits": 0, + "sleep_ns": 24262193, + "latency_mean_ns": 7027416.376, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12229040, + "active_byte_seconds": 941745.792822, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 140664495, + "cpu_ns": 101124724, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 716, + "slot_waits": 0, + "sleep_ns": 41804911, + "latency_mean_ns": 8134234.887, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 13075389, + "active_byte_seconds": 1088709.015188, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 121667696, + "cpu_ns": 101165480, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 330, + "slot_waits": 0, + "sleep_ns": 21242064, + "latency_mean_ns": 7047783.954, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11408672, + "active_byte_seconds": 944768.902586, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 122887693, + "cpu_ns": 108048811, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 100, + "slot_waits": 0, + "sleep_ns": 15309844, + "latency_mean_ns": 6960585.889, + "latency_p99_upper_ns": 14680064, + "latency_max_ns": 14962365, + "active_byte_seconds": 933965.439249, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 127832493, + "cpu_ns": 107011535, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 312, + "slot_waits": 0, + "sleep_ns": 21568032, + "latency_mean_ns": 7387611.189, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12693631, + "active_byte_seconds": 990437.837956, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 132323981, + "cpu_ns": 104579167, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 3970, + "budget_waits": 1868, + "slot_waits": 0, + "sleep_ns": 28280826, + "latency_mean_ns": 7629563.252, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12006726, + "active_byte_seconds": 1024670.67878, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 117467153, + "cpu_ns": 100913544, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 211, + "slot_waits": 0, + "sleep_ns": 17302742, + "latency_mean_ns": 6773721.904, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11750486, + "active_byte_seconds": 908428.389283, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 177547354, + "cpu_ns": 98087776, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 1077, + "slot_waits": 0, + "sleep_ns": 66269238, + "latency_mean_ns": 10478829.253, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 26359796, + "active_byte_seconds": 1401395.334755, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 125, + "rss_peak_bytes": 28753920, + "elapsed_ns": 127352551, + "cpu_ns": 95896509, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 100663296, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1854, + "slot_waits": 0, + "sleep_ns": 33140024, + "latency_mean_ns": 7371263.893, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 11482598, + "active_byte_seconds": 989890.745684, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 156176896, + "cpu_ns": 135527318, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 8, + "slot_waits": 0, + "sleep_ns": 19302646, + "latency_mean_ns": 8983882.317, + "latency_p99_upper_ns": 23068672, + "latency_max_ns": 21705495, + "active_byte_seconds": 1205798.201258, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 150967021, + "cpu_ns": 112103512, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 343, + "slot_waits": 0, + "sleep_ns": 27798231, + "latency_mean_ns": 8607200.207, + "latency_p99_upper_ns": 14680064, + "latency_max_ns": 15553678, + "active_byte_seconds": 1153842.867633, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "dense_slow", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8350112, + "peak_active_bytes": 8350112, + "peak_queue": 126, + "rss_peak_bytes": 28753920, + "elapsed_ns": 131031651, + "cpu_ns": 108678412, + "input_bytes": 134242304, + "output_bytes": 100671488, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 126, + "reuses": 1922, + "budget_waits": 194, + "slot_waits": 0, + "sleep_ns": 17318187, + "latency_mean_ns": 7550902.647, + "latency_p99_upper_ns": 12582912, + "latency_max_ns": 12787335, + "active_byte_seconds": 1012712.39438, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1048832, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 349, + "rss_peak_bytes": 28753920, + "elapsed_ns": 196227931, + "cpu_ns": 90207409, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 899, + "slot_waits": 0, + "sleep_ns": 106212702, + "latency_mean_ns": 25497204.119, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 38861965, + "active_byte_seconds": 1403800.480768, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 282, + "rss_peak_bytes": 28753920, + "elapsed_ns": 182099749, + "cpu_ns": 86444975, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1834, + "reuses": 1571, + "budget_waits": 951, + "slot_waits": 0, + "sleep_ns": 99448742, + "latency_mean_ns": 18702737.958, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 26735888, + "active_byte_seconds": 1347766.35285, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388480, + "peak_active_bytes": 8388432, + "peak_queue": 345, + "rss_peak_bytes": 28753920, + "elapsed_ns": 170729984, + "cpu_ns": 95177478, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 1996, + "reuses": 1751, + "budget_waits": 848, + "slot_waits": 0, + "sleep_ns": 79228418, + "latency_mean_ns": 22318118.532, + "latency_p99_upper_ns": 33554432, + "latency_max_ns": 33636461, + "active_byte_seconds": 1237184.456112, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 344, + "rss_peak_bytes": 28753920, + "elapsed_ns": 248649117, + "cpu_ns": 108815021, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 1911, + "reuses": 1877, + "budget_waits": 806, + "slot_waits": 0, + "sleep_ns": 141754620, + "latency_mean_ns": 32835308.438, + "latency_p99_upper_ns": 50331648, + "latency_max_ns": 52109020, + "active_byte_seconds": 1817663.170181, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388432, + "peak_queue": 343, + "rss_peak_bytes": 28753920, + "elapsed_ns": 188497435, + "cpu_ns": 95408242, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2001, + "reuses": 2095, + "budget_waits": 825, + "slot_waits": 0, + "sleep_ns": 93045012, + "latency_mean_ns": 24119999.997, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 36830878, + "active_byte_seconds": 1331578.296683, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386640, + "peak_queue": 112, + "rss_peak_bytes": 28753920, + "elapsed_ns": 202207266, + "cpu_ns": 112741417, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1260, + "reuses": 788, + "budget_waits": 922, + "slot_waits": 0, + "sleep_ns": 93158576, + "latency_mean_ns": 9030450.097, + "latency_p99_upper_ns": 15728640, + "latency_max_ns": 16897915, + "active_byte_seconds": 1584952.748115, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388544, + "peak_queue": 358, + "rss_peak_bytes": 28753920, + "elapsed_ns": 196788319, + "cpu_ns": 85794578, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1594, + "reuses": 454, + "budget_waits": 835, + "slot_waits": 0, + "sleep_ns": 111694672, + "latency_mean_ns": 25911259.331, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 39632482, + "active_byte_seconds": 1386620.278442, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388432, + "peak_queue": 216, + "rss_peak_bytes": 28753920, + "elapsed_ns": 177004559, + "cpu_ns": 88488220, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1769, + "reuses": 1287, + "budget_waits": 956, + "slot_waits": 0, + "sleep_ns": 91999803, + "latency_mean_ns": 13961564.399, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 20644424, + "active_byte_seconds": 1308343.909911, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 349, + "rss_peak_bytes": 28753920, + "elapsed_ns": 209526727, + "cpu_ns": 99009959, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 852, + "slot_waits": 0, + "sleep_ns": 113158134, + "latency_mean_ns": 27706362.205, + "latency_p99_upper_ns": 50331648, + "latency_max_ns": 49857323, + "active_byte_seconds": 1504182.816335, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388432, + "peak_queue": 216, + "rss_peak_bytes": 28753920, + "elapsed_ns": 178695721, + "cpu_ns": 88024506, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1752, + "reuses": 1304, + "budget_waits": 957, + "slot_waits": 0, + "sleep_ns": 93665641, + "latency_mean_ns": 14283495.48, + "latency_p99_upper_ns": 25165824, + "latency_max_ns": 25314392, + "active_byte_seconds": 1350685.597943, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388544, + "peak_queue": 358, + "rss_peak_bytes": 28753920, + "elapsed_ns": 188062454, + "cpu_ns": 85805781, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1613, + "reuses": 435, + "budget_waits": 876, + "slot_waits": 0, + "sleep_ns": 106943146, + "latency_mean_ns": 25106963.876, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 40388905, + "active_byte_seconds": 1338315.607594, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 463808, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 349, + "rss_peak_bytes": 28753920, + "elapsed_ns": 180476475, + "cpu_ns": 82814346, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 2034, + "reuses": 1713, + "budget_waits": 906, + "slot_waits": 0, + "sleep_ns": 101386971, + "latency_mean_ns": 23117968.235, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 34624643, + "active_byte_seconds": 1287754.655314, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388432, + "peak_queue": 345, + "rss_peak_bytes": 28753920, + "elapsed_ns": 179916143, + "cpu_ns": 88224841, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2035, + "reuses": 2061, + "budget_waits": 868, + "slot_waits": 0, + "sleep_ns": 95038979, + "latency_mean_ns": 23134688.68, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 38482948, + "active_byte_seconds": 1259295.778761, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 345, + "rss_peak_bytes": 28753920, + "elapsed_ns": 172458424, + "cpu_ns": 95876297, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 1950, + "reuses": 1838, + "budget_waits": 838, + "slot_waits": 0, + "sleep_ns": 80637955, + "latency_mean_ns": 22201893.822, + "latency_p99_upper_ns": 29360128, + "latency_max_ns": 29750009, + "active_byte_seconds": 1218383.798869, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386688, + "peak_queue": 112, + "rss_peak_bytes": 28753920, + "elapsed_ns": 202718412, + "cpu_ns": 105879101, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1270, + "reuses": 778, + "budget_waits": 933, + "slot_waits": 0, + "sleep_ns": 101496664, + "latency_mean_ns": 8968645.74, + "latency_p99_upper_ns": 15728640, + "latency_max_ns": 15509271, + "active_byte_seconds": 1593585.502544, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 268, + "rss_peak_bytes": 28753920, + "elapsed_ns": 178050667, + "cpu_ns": 95042355, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1767, + "reuses": 1638, + "budget_waits": 919, + "slot_waits": 0, + "sleep_ns": 87305079, + "latency_mean_ns": 17737601.933, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 26498834, + "active_byte_seconds": 1276748.071158, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1154176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 343, + "rss_peak_bytes": 28753920, + "elapsed_ns": 166505070, + "cpu_ns": 87773371, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2029, + "reuses": 2067, + "budget_waits": 855, + "slot_waits": 0, + "sleep_ns": 82039937, + "latency_mean_ns": 21365903.831, + "latency_p99_upper_ns": 31457280, + "latency_max_ns": 30499822, + "active_byte_seconds": 1169734.902412, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388576, + "peak_queue": 358, + "rss_peak_bytes": 28753920, + "elapsed_ns": 184834889, + "cpu_ns": 77869217, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1654, + "reuses": 394, + "budget_waits": 900, + "slot_waits": 0, + "sleep_ns": 108638540, + "latency_mean_ns": 24466594.426, + "latency_p99_upper_ns": 46137344, + "latency_max_ns": 45971408, + "active_byte_seconds": 1295645.073976, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 346, + "rss_peak_bytes": 28753920, + "elapsed_ns": 191170037, + "cpu_ns": 94174606, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 1933, + "reuses": 1855, + "budget_waits": 839, + "slot_waits": 0, + "sleep_ns": 99297240, + "latency_mean_ns": 24916604.797, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37194081, + "active_byte_seconds": 1364006.158543, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 267, + "rss_peak_bytes": 28753920, + "elapsed_ns": 221114880, + "cpu_ns": 108320175, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1725, + "reuses": 1680, + "budget_waits": 867, + "slot_waits": 0, + "sleep_ns": 112272606, + "latency_mean_ns": 22302398.136, + "latency_p99_upper_ns": 31457280, + "latency_max_ns": 31418968, + "active_byte_seconds": 1620151.723755, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 343, + "rss_peak_bytes": 28753920, + "elapsed_ns": 200423808, + "cpu_ns": 95868665, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 1950, + "reuses": 1797, + "budget_waits": 826, + "slot_waits": 0, + "sleep_ns": 104684245, + "latency_mean_ns": 26149827.952, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37286562, + "active_byte_seconds": 1462564.435239, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 348, + "rss_peak_bytes": 28753920, + "elapsed_ns": 185563183, + "cpu_ns": 99669005, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 824, + "slot_waits": 0, + "sleep_ns": 88520259, + "latency_mean_ns": 24158310.672, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 37578913, + "active_byte_seconds": 1315287.672243, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388352, + "peak_queue": 216, + "rss_peak_bytes": 28753920, + "elapsed_ns": 285706068, + "cpu_ns": 123773735, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1639, + "reuses": 1417, + "budget_waits": 811, + "slot_waits": 0, + "sleep_ns": 151915315, + "latency_mean_ns": 22809999.729, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 39308704, + "active_byte_seconds": 2141933.275175, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386640, + "peak_queue": 112, + "rss_peak_bytes": 28753920, + "elapsed_ns": 277725554, + "cpu_ns": 130508378, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1246, + "reuses": 802, + "budget_waits": 882, + "slot_waits": 0, + "sleep_ns": 145190817, + "latency_mean_ns": 12364716.181, + "latency_p99_upper_ns": 25165824, + "latency_max_ns": 24717468, + "active_byte_seconds": 2188978.814479, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388544, + "peak_queue": 350, + "rss_peak_bytes": 28753920, + "elapsed_ns": 266408067, + "cpu_ns": 111663429, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1498, + "reuses": 550, + "budget_waits": 732, + "slot_waits": 0, + "sleep_ns": 138942208, + "latency_mean_ns": 34638344.422, + "latency_p99_upper_ns": 54525952, + "latency_max_ns": 52949259, + "active_byte_seconds": 1878408.17389, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 341, + "rss_peak_bytes": 28753920, + "elapsed_ns": 204623815, + "cpu_ns": 104506177, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 1822, + "reuses": 1966, + "budget_waits": 758, + "slot_waits": 0, + "sleep_ns": 97171540, + "latency_mean_ns": 25691162.972, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 40286443, + "active_byte_seconds": 1429280.943963, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 277, + "rss_peak_bytes": 28753920, + "elapsed_ns": 162195656, + "cpu_ns": 80564634, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1810, + "reuses": 1595, + "budget_waits": 983, + "slot_waits": 0, + "sleep_ns": 84519072, + "latency_mean_ns": 16670151.746, + "latency_p99_upper_ns": 23068672, + "latency_max_ns": 21537625, + "active_byte_seconds": 1200241.584576, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 350, + "rss_peak_bytes": 28753920, + "elapsed_ns": 155808970, + "cpu_ns": 73686530, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2181, + "reuses": 1915, + "budget_waits": 970, + "slot_waits": 0, + "sleep_ns": 84997837, + "latency_mean_ns": 20210703.898, + "latency_p99_upper_ns": 29360128, + "latency_max_ns": 29034418, + "active_byte_seconds": 1105823.092919, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 522496, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 343, + "rss_peak_bytes": 28753920, + "elapsed_ns": 175012120, + "cpu_ns": 86574113, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 847, + "slot_waits": 0, + "sleep_ns": 86153992, + "latency_mean_ns": 21577383.531, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 35170512, + "active_byte_seconds": 1189109.090661, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386640, + "peak_queue": 112, + "rss_peak_bytes": 28753920, + "elapsed_ns": 192464571, + "cpu_ns": 109214745, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1270, + "reuses": 778, + "budget_waits": 921, + "slot_waits": 0, + "sleep_ns": 85288420, + "latency_mean_ns": 8548822.691, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 14178942, + "active_byte_seconds": 1513389.1547, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388496, + "peak_active_bytes": 8388432, + "peak_queue": 216, + "rss_peak_bytes": 28753920, + "elapsed_ns": 180744548, + "cpu_ns": 92447570, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1772, + "reuses": 1284, + "budget_waits": 987, + "slot_waits": 0, + "sleep_ns": 91033265, + "latency_mean_ns": 14634490.266, + "latency_p99_upper_ns": 20971520, + "latency_max_ns": 21334847, + "active_byte_seconds": 1375000.84592, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1250624, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388480, + "peak_active_bytes": 8388432, + "peak_queue": 347, + "rss_peak_bytes": 28753920, + "elapsed_ns": 190539057, + "cpu_ns": 89751173, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 1983, + "reuses": 1764, + "budget_waits": 862, + "slot_waits": 0, + "sleep_ns": 102577548, + "latency_mean_ns": 24120880.139, + "latency_p99_upper_ns": 46137344, + "latency_max_ns": 43388027, + "active_byte_seconds": 1333423.649457, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 339, + "rss_peak_bytes": 28753920, + "elapsed_ns": 221349934, + "cpu_ns": 109576369, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1871, + "reuses": 2225, + "budget_waits": 733, + "slot_waits": 0, + "sleep_ns": 114046592, + "latency_mean_ns": 28163326.597, + "latency_p99_upper_ns": 46137344, + "latency_max_ns": 46133518, + "active_byte_seconds": 1540447.287587, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388576, + "peak_active_bytes": 8388544, + "peak_queue": 351, + "rss_peak_bytes": 28753920, + "elapsed_ns": 182031656, + "cpu_ns": 81559603, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1543, + "reuses": 505, + "budget_waits": 829, + "slot_waits": 0, + "sleep_ns": 98044537, + "latency_mean_ns": 23787174.92, + "latency_p99_upper_ns": 33554432, + "latency_max_ns": 33075333, + "active_byte_seconds": 1288001.912824, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 458688, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388432, + "peak_queue": 216, + "rss_peak_bytes": 28753920, + "elapsed_ns": 190119929, + "cpu_ns": 98659849, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1748, + "reuses": 1308, + "budget_waits": 937, + "slot_waits": 0, + "sleep_ns": 91519644, + "latency_mean_ns": 15332315.134, + "latency_p99_upper_ns": 27262976, + "latency_max_ns": 28453087, + "active_byte_seconds": 1432190.421467, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1262912, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388432, + "peak_active_bytes": 8388432, + "peak_queue": 348, + "rss_peak_bytes": 28753920, + "elapsed_ns": 181246528, + "cpu_ns": 87821814, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 854, + "slot_waits": 0, + "sleep_ns": 96465580, + "latency_mean_ns": 23009953.597, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 35350753, + "active_byte_seconds": 1263667.037898, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388608, + "peak_active_bytes": 8388608, + "peak_queue": 268, + "rss_peak_bytes": 28753920, + "elapsed_ns": 202902060, + "cpu_ns": 94178139, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 1778, + "reuses": 1627, + "budget_waits": 948, + "slot_waits": 0, + "sleep_ns": 111627333, + "latency_mean_ns": 20292318.825, + "latency_p99_upper_ns": 41943040, + "latency_max_ns": 42672491, + "active_byte_seconds": 1456891.057432, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 1153216, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8386736, + "peak_active_bytes": 8386640, + "peak_queue": 112, + "rss_peak_bytes": 28753920, + "elapsed_ns": 187355085, + "cpu_ns": 105102970, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1268, + "reuses": 780, + "budget_waits": 942, + "slot_waits": 0, + "sleep_ns": 86194520, + "latency_mean_ns": 8306217.014, + "latency_p99_upper_ns": 16777216, + "latency_max_ns": 16560027, + "active_byte_seconds": 1467132.987688, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 667904, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 349, + "rss_peak_bytes": 28753920, + "elapsed_ns": 185932830, + "cpu_ns": 88442274, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 1998, + "reuses": 1790, + "budget_waits": 873, + "slot_waits": 0, + "sleep_ns": 100159229, + "latency_mean_ns": 24415772.17, + "latency_p99_upper_ns": 37748736, + "latency_max_ns": 35463160, + "active_byte_seconds": 1333943.667435, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_slow", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 8388560, + "peak_active_bytes": 8388432, + "peak_queue": 351, + "rss_peak_bytes": 28753920, + "elapsed_ns": 175115990, + "cpu_ns": 85590028, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 27916032, + "copied_records": 1699, + "borrowed_records": 349, + "pressure_copies": 1699, + "allocations": 2029, + "reuses": 1718, + "budget_waits": 897, + "slot_waits": 0, + "sleep_ns": 90980114, + "latency_mean_ns": 22901875.606, + "latency_p99_upper_ns": 31457280, + "latency_max_ns": 30636340, + "active_byte_seconds": 1271349.314418, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 519424, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 106, + "rss_peak_bytes": 28753920, + "elapsed_ns": 194998211, + "cpu_ns": 80964449, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2265, + "reuses": 1510, + "budget_waits": 1251, + "slot_waits": 0, + "sleep_ns": 117554960, + "latency_mean_ns": 6209271.27, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9610047, + "active_byte_seconds": 352937.551048, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 28753920, + "elapsed_ns": 191006927, + "cpu_ns": 80024226, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1659, + "reuses": 389, + "budget_waits": 1164, + "slot_waits": 0, + "sleep_ns": 113283506, + "latency_mean_ns": 6749781.49, + "latency_p99_upper_ns": 10485760, + "latency_max_ns": 10754747, + "active_byte_seconds": 354343.266259, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 72, + "rss_peak_bytes": 28753920, + "elapsed_ns": 315263040, + "cpu_ns": 90750328, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1890, + "reuses": 1166, + "budget_waits": 1248, + "slot_waits": 0, + "sleep_ns": 228816753, + "latency_mean_ns": 6291155.426, + "latency_p99_upper_ns": 13631488, + "latency_max_ns": 13216574, + "active_byte_seconds": 579371.042178, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 116, + "rss_peak_bytes": 28753920, + "elapsed_ns": 215534802, + "cpu_ns": 92135979, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1179, + "slot_waits": 0, + "sleep_ns": 127073694, + "latency_mean_ns": 6714221.641, + "latency_p99_upper_ns": 10485760, + "latency_max_ns": 12545668, + "active_byte_seconds": 387556.898349, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 91, + "rss_peak_bytes": 28753920, + "elapsed_ns": 240539157, + "cpu_ns": 82888986, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2037, + "reuses": 1368, + "budget_waits": 1333, + "slot_waits": 0, + "sleep_ns": 157978818, + "latency_mean_ns": 6115847.212, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 11204691, + "active_byte_seconds": 437508.612236, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 111, + "rss_peak_bytes": 28753920, + "elapsed_ns": 205803459, + "cpu_ns": 87522857, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2361, + "reuses": 1735, + "budget_waits": 1210, + "slot_waits": 0, + "sleep_ns": 121961836, + "latency_mean_ns": 6356735.332, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12149188, + "active_byte_seconds": 363428.263552, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 103, + "rss_peak_bytes": 28753920, + "elapsed_ns": 218109869, + "cpu_ns": 94383237, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2188, + "reuses": 1600, + "budget_waits": 1166, + "slot_waits": 0, + "sleep_ns": 128480946, + "latency_mean_ns": 7013746.965, + "latency_p99_upper_ns": 14680064, + "latency_max_ns": 15108236, + "active_byte_seconds": 394831.364885, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094032, + "peak_queue": 40, + "rss_peak_bytes": 28753920, + "elapsed_ns": 544543692, + "cpu_ns": 90900399, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1332, + "reuses": 716, + "budget_waits": 1286, + "slot_waits": 0, + "sleep_ns": 453134113, + "latency_mean_ns": 6077465.417, + "latency_p99_upper_ns": 11534336, + "latency_max_ns": 12176243, + "active_byte_seconds": 1028555.825239, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 117, + "rss_peak_bytes": 28753920, + "elapsed_ns": 191836517, + "cpu_ns": 62332245, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2460, + "reuses": 1636, + "budget_waits": 1362, + "slot_waits": 0, + "sleep_ns": 131898458, + "latency_mean_ns": 6069082.115, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8770216, + "active_byte_seconds": 346911.480275, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 117, + "rss_peak_bytes": 28753920, + "elapsed_ns": 188885136, + "cpu_ns": 63904326, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2356, + "reuses": 1432, + "budget_waits": 1390, + "slot_waits": 0, + "sleep_ns": 127844964, + "latency_mean_ns": 6121880.805, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8708698, + "active_byte_seconds": 342822.067006, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 28753920, + "elapsed_ns": 492789763, + "cpu_ns": 76140523, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1347, + "reuses": 701, + "budget_waits": 1406, + "slot_waits": 0, + "sleep_ns": 420428963, + "latency_mean_ns": 5485700.477, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9955175, + "active_byte_seconds": 928718.610429, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 72, + "rss_peak_bytes": 28753920, + "elapsed_ns": 282084485, + "cpu_ns": 66066433, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1992, + "reuses": 1064, + "budget_waits": 1398, + "slot_waits": 0, + "sleep_ns": 219096863, + "latency_mean_ns": 5671193.431, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 9274955, + "active_byte_seconds": 520410.956771, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 28753920, + "elapsed_ns": 224500368, + "cpu_ns": 61534673, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2145, + "reuses": 1260, + "budget_waits": 1465, + "slot_waits": 0, + "sleep_ns": 165914339, + "latency_mean_ns": 5837746.733, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 10210564, + "active_byte_seconds": 412262.960044, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 189684559, + "cpu_ns": 60774942, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2383, + "reuses": 1392, + "budget_waits": 1399, + "slot_waits": 0, + "sleep_ns": 131755127, + "latency_mean_ns": 6110993.322, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9608714, + "active_byte_seconds": 345607.344111, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 117, + "rss_peak_bytes": 28753920, + "elapsed_ns": 190441538, + "cpu_ns": 59274120, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1383, + "slot_waits": 0, + "sleep_ns": 133844636, + "latency_mean_ns": 6031707.432, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8881216, + "active_byte_seconds": 344699.365648, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 28753920, + "elapsed_ns": 175373266, + "cpu_ns": 57346839, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1727, + "reuses": 321, + "budget_waits": 1312, + "slot_waits": 0, + "sleep_ns": 120854151, + "latency_mean_ns": 6287663.374, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8829253, + "active_byte_seconds": 326669.995028, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 190230224, + "cpu_ns": 58883103, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1379, + "slot_waits": 0, + "sleep_ns": 133923360, + "latency_mean_ns": 6020977.936, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8904123, + "active_byte_seconds": 344486.353015, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 28753920, + "elapsed_ns": 176712466, + "cpu_ns": 57142470, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1727, + "reuses": 321, + "budget_waits": 1308, + "slot_waits": 0, + "sleep_ns": 122435178, + "latency_mean_ns": 6344282.023, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9173733, + "active_byte_seconds": 329327.109037, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 192640551, + "cpu_ns": 62036091, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2463, + "reuses": 1633, + "budget_waits": 1367, + "slot_waits": 0, + "sleep_ns": 133360267, + "latency_mean_ns": 6075663.601, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8830642, + "active_byte_seconds": 347845.920578, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 73, + "rss_peak_bytes": 28753920, + "elapsed_ns": 284454034, + "cpu_ns": 68644342, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1983, + "reuses": 1073, + "budget_waits": 1386, + "slot_waits": 0, + "sleep_ns": 218577789, + "latency_mean_ns": 5702048.39, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 9254474, + "active_byte_seconds": 523314.413618, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 28753920, + "elapsed_ns": 226008956, + "cpu_ns": 64169718, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2125, + "reuses": 1280, + "budget_waits": 1458, + "slot_waits": 0, + "sleep_ns": 164889565, + "latency_mean_ns": 5866464.096, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 10318805, + "active_byte_seconds": 414911.228869, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 28753920, + "elapsed_ns": 494360387, + "cpu_ns": 73512176, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1345, + "reuses": 703, + "budget_waits": 1395, + "slot_waits": 0, + "sleep_ns": 424305645, + "latency_mean_ns": 5500812.732, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9972009, + "active_byte_seconds": 932412.777581, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 188224878, + "cpu_ns": 60744558, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2384, + "reuses": 1404, + "budget_waits": 1411, + "slot_waits": 0, + "sleep_ns": 130279846, + "latency_mean_ns": 6114092.392, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8874864, + "active_byte_seconds": 342235.388035, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 189816540, + "cpu_ns": 60109883, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2377, + "reuses": 1398, + "budget_waits": 1401, + "slot_waits": 0, + "sleep_ns": 132388467, + "latency_mean_ns": 6115537.712, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9634918, + "active_byte_seconds": 345575.615482, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 190593723, + "cpu_ns": 62104383, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2377, + "reuses": 1398, + "budget_waits": 1387, + "slot_waits": 0, + "sleep_ns": 131246281, + "latency_mean_ns": 6133228.412, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9617713, + "active_byte_seconds": 346774.138471, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 28753920, + "elapsed_ns": 491528840, + "cpu_ns": 70906509, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1353, + "reuses": 695, + "budget_waits": 1421, + "slot_waits": 0, + "sleep_ns": 423844868, + "latency_mean_ns": 5472000.565, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9945028, + "active_byte_seconds": 926418.118409, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 116, + "rss_peak_bytes": 28753920, + "elapsed_ns": 190661223, + "cpu_ns": 60180004, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1376, + "slot_waits": 0, + "sleep_ns": 133160918, + "latency_mean_ns": 6036141.159, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8916068, + "active_byte_seconds": 345094.680154, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 116, + "rss_peak_bytes": 28753920, + "elapsed_ns": 193175809, + "cpu_ns": 69008296, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2336, + "reuses": 1452, + "budget_waits": 1351, + "slot_waits": 0, + "sleep_ns": 125127054, + "latency_mean_ns": 6239198.13, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9276566, + "active_byte_seconds": 350903.059901, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 73, + "rss_peak_bytes": 28753920, + "elapsed_ns": 286766304, + "cpu_ns": 73096130, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1974, + "reuses": 1082, + "budget_waits": 1367, + "slot_waits": 0, + "sleep_ns": 215532386, + "latency_mean_ns": 5744366.208, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 8864197, + "active_byte_seconds": 528100.049771, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 28753920, + "elapsed_ns": 225938124, + "cpu_ns": 65488664, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2121, + "reuses": 1284, + "budget_waits": 1445, + "slot_waits": 0, + "sleep_ns": 163452164, + "latency_mean_ns": 5856203.824, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 10380804, + "active_byte_seconds": 414648.202609, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 108, + "rss_peak_bytes": 28753920, + "elapsed_ns": 191114147, + "cpu_ns": 63369902, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2453, + "reuses": 1643, + "budget_waits": 1341, + "slot_waits": 0, + "sleep_ns": 130459979, + "latency_mean_ns": 6032302.559, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8833531, + "active_byte_seconds": 345755.184019, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 28753920, + "elapsed_ns": 177942277, + "cpu_ns": 63483663, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1711, + "reuses": 337, + "budget_waits": 1279, + "slot_waits": 0, + "sleep_ns": 117556310, + "latency_mean_ns": 6348716.251, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8883105, + "active_byte_seconds": 331194.459386, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097152, + "peak_active_bytes": 2097136, + "peak_queue": 117, + "rss_peak_bytes": 28753920, + "elapsed_ns": 191747145, + "cpu_ns": 60306782, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2470, + "reuses": 1626, + "budget_waits": 1379, + "slot_waits": 0, + "sleep_ns": 134097654, + "latency_mean_ns": 6065392.323, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8895161, + "active_byte_seconds": 347142.858576, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 187864582, + "cpu_ns": 60112693, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28479696, + "copied_records": 1740, + "borrowed_records": 308, + "pressure_copies": 0, + "allocations": 2380, + "reuses": 1408, + "budget_waits": 1409, + "slot_waits": 0, + "sleep_ns": 130504092, + "latency_mean_ns": 6102214.292, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8884715, + "active_byte_seconds": 341567.414808, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097120, + "peak_active_bytes": 2097120, + "peak_queue": 118, + "rss_peak_bytes": 28753920, + "elapsed_ns": 175293210, + "cpu_ns": 56768485, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1722, + "reuses": 326, + "budget_waits": 1313, + "slot_waits": 0, + "sleep_ns": 121359184, + "latency_mean_ns": 6283898.527, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8849623, + "active_byte_seconds": 326483.546647, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 406848, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 73, + "rss_peak_bytes": 28753920, + "elapsed_ns": 283727960, + "cpu_ns": 68131361, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 3824848, + "copied_records": 1008, + "borrowed_records": 1040, + "pressure_copies": 0, + "allocations": 1988, + "reuses": 1068, + "budget_waits": 1394, + "slot_waits": 0, + "sleep_ns": 218912386, + "latency_mean_ns": 5707951.51, + "latency_p99_upper_ns": 7340032, + "latency_max_ns": 9272677, + "active_byte_seconds": 523909.294385, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 386176, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2094128, + "peak_active_bytes": 2094128, + "peak_queue": 40, + "rss_peak_bytes": 28753920, + "elapsed_ns": 495666215, + "cpu_ns": 81002646, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1336, + "reuses": 712, + "budget_waits": 1382, + "slot_waits": 0, + "sleep_ns": 418460319, + "latency_mean_ns": 5519510.972, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 9930268, + "active_byte_seconds": 935139.458784, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 495872, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 116, + "rss_peak_bytes": 28753920, + "elapsed_ns": 190841888, + "cpu_ns": 60356559, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 53727440, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 1382, + "slot_waits": 0, + "sleep_ns": 133224289, + "latency_mean_ns": 6038284.196, + "latency_p99_upper_ns": 8388608, + "latency_max_ns": 8947753, + "active_byte_seconds": 345496.101548, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097104, + "peak_queue": 88, + "rss_peak_bytes": 28753920, + "elapsed_ns": 226098419, + "cpu_ns": 63538828, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 11229392, + "copied_records": 1357, + "borrowed_records": 691, + "pressure_copies": 0, + "allocations": 2129, + "reuses": 1276, + "budget_waits": 1452, + "slot_waits": 0, + "sleep_ns": 165691682, + "latency_mean_ns": 5878789.657, + "latency_p99_upper_ns": 7864320, + "latency_max_ns": 10264360, + "active_byte_seconds": 415107.899759, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 435392, + "unreleased_buffer_bytes": 0 + }, + { + "case": "mixed_tight", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 2097152, + "fixed_bytes": 90560, + "peak_reserved_bytes": 2097136, + "peak_active_bytes": 2097136, + "peak_queue": 115, + "rss_peak_bytes": 28753920, + "elapsed_ns": 192204126, + "cpu_ns": 62496629, + "input_bytes": 177631232, + "output_bytes": 53735632, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 28385168, + "copied_records": 1727, + "borrowed_records": 321, + "pressure_copies": 1727, + "allocations": 2367, + "reuses": 1408, + "budget_waits": 1381, + "slot_waits": 0, + "sleep_ns": 132616450, + "latency_mean_ns": 6161110.138, + "latency_p99_upper_ns": 9437184, + "latency_max_ns": 9782879, + "active_byte_seconds": 349554.881495, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 523712, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 0, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20554720, + "cpu_ns": 20527722, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9881.978, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 60759, + "active_byte_seconds": 1273.955005, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 0, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 22216122, + "cpu_ns": 22101834, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10698.669, + "latency_p99_upper_ns": 16384, + "latency_max_ns": 58241, + "active_byte_seconds": 1.68633, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 22839176, + "cpu_ns": 22457056, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10958.209, + "latency_p99_upper_ns": 20480, + "latency_max_ns": 165851, + "active_byte_seconds": 1227.172318, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21009107, + "cpu_ns": 20955370, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10112.231, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 66129, + "active_byte_seconds": 1120.781895, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21132162, + "cpu_ns": 21048817, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10163.675, + "latency_p99_upper_ns": 16384, + "latency_max_ns": 68685, + "active_byte_seconds": 1121.783606, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20540535, + "cpu_ns": 20499501, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9884.486, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 54093, + "active_byte_seconds": 1100.863361, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21727605, + "cpu_ns": 21682906, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10449.435, + "latency_p99_upper_ns": 20480, + "latency_max_ns": 65999, + "active_byte_seconds": 1348.749968, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 22373067, + "cpu_ns": 22308741, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10776.724, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 66703, + "active_byte_seconds": 1207.809872, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 1, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 25768428, + "cpu_ns": 25607575, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 12427.336, + "latency_p99_upper_ns": 22528, + "latency_max_ns": 135518, + "active_byte_seconds": 1.971383, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 22601085, + "cpu_ns": 22218814, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10886.611, + "latency_p99_upper_ns": 28672, + "latency_max_ns": 70518, + "active_byte_seconds": 1220.513584, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 22194827, + "cpu_ns": 21633689, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10673.54, + "latency_p99_upper_ns": 36864, + "latency_max_ns": 207444, + "active_byte_seconds": 1178.059751, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 1, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 23241749, + "cpu_ns": 23003312, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 11135.177, + "latency_p99_upper_ns": 22528, + "latency_max_ns": 79185, + "active_byte_seconds": 1441.524009, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 24103618, + "cpu_ns": 23832019, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 11617.114, + "latency_p99_upper_ns": 22528, + "latency_max_ns": 88389, + "active_byte_seconds": 1310.460688, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21191681, + "cpu_ns": 21131537, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10199.927, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 68426, + "active_byte_seconds": 1132.085862, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21871975, + "cpu_ns": 21613127, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10530.066, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 78704, + "active_byte_seconds": 1171.334175, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21555643, + "cpu_ns": 21467723, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10366.948, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 62648, + "active_byte_seconds": 1339.691796, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20704016, + "cpu_ns": 20633572, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9954.482, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 84963, + "active_byte_seconds": 1283.092823, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20854404, + "cpu_ns": 20822149, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10030.324, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 62445, + "active_byte_seconds": 1108.309428, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21286218, + "cpu_ns": 21228201, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10237.021, + "latency_p99_upper_ns": 15360, + "latency_max_ns": 67889, + "active_byte_seconds": 1129.223222, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 2, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 22649251, + "cpu_ns": 22567445, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10910.76, + "latency_p99_upper_ns": 15360, + "latency_max_ns": 61499, + "active_byte_seconds": 1.717551, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 2, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20767942, + "cpu_ns": 20708872, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9985.038, + "latency_p99_upper_ns": 15360, + "latency_max_ns": 65425, + "active_byte_seconds": 1286.595987, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21065681, + "cpu_ns": 21006279, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10139.793, + "latency_p99_upper_ns": 15360, + "latency_max_ns": 67018, + "active_byte_seconds": 1124.320165, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20961885, + "cpu_ns": 20931648, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10087.398, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 60962, + "active_byte_seconds": 1117.500277, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20908700, + "cpu_ns": 20878832, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10062.625, + "latency_p99_upper_ns": 14336, + "latency_max_ns": 70648, + "active_byte_seconds": 1116.46627, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21328570, + "cpu_ns": 21275351, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10266.949, + "latency_p99_upper_ns": 15360, + "latency_max_ns": 61241, + "active_byte_seconds": 1144.526904, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20408294, + "cpu_ns": 20408999, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9812.079, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 48982, + "active_byte_seconds": 1265.057763, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 20744256, + "cpu_ns": 20744722, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 9979.793, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 42518, + "active_byte_seconds": 1107.901257, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 3, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21766957, + "cpu_ns": 21732111, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10483.601, + "latency_p99_upper_ns": 13312, + "latency_max_ns": 74593, + "active_byte_seconds": 1.653919, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 23474730, + "cpu_ns": 23418353, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 11311.146, + "latency_p99_upper_ns": 24576, + "latency_max_ns": 72445, + "active_byte_seconds": 1275.148887, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 25891223, + "cpu_ns": 25855036, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 12490.423, + "latency_p99_upper_ns": 32768, + "latency_max_ns": 62019, + "active_byte_seconds": 1433.084986, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 3, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 25124448, + "cpu_ns": 24854663, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 12097.683, + "latency_p99_upper_ns": 24576, + "latency_max_ns": 83351, + "active_byte_seconds": 1569.932052, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 21725958, + "cpu_ns": 21582704, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10452.028, + "latency_p99_upper_ns": 16384, + "latency_max_ns": 197777, + "active_byte_seconds": 1159.292471, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "project", + "repeat": 4, + "status": "ok", + "policy": "project", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 90640, + "peak_active_bytes": 90640, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 22202622, + "cpu_ns": 22202889, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 10695.292, + "latency_p99_upper_ns": 18432, + "latency_max_ns": 48444, + "active_byte_seconds": 1.688434, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 80, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 27294793, + "cpu_ns": 27138353, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 13171.363, + "latency_p99_upper_ns": 28672, + "latency_max_ns": 78185, + "active_byte_seconds": 1515.562825, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 33207148, + "cpu_ns": 33180296, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 16048.501, + "latency_p99_upper_ns": 36864, + "latency_max_ns": 74648, + "active_byte_seconds": 1873.802277, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 40643311, + "cpu_ns": 40589351, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 19667.254, + "latency_p99_upper_ns": 49152, + "latency_max_ns": 147388, + "active_byte_seconds": 2334.403772, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "threshold4", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 28395383, + "cpu_ns": 28350296, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 2, + "reuses": 4094, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 13707.471, + "latency_p99_upper_ns": 26624, + "latency_max_ns": 87370, + "active_byte_seconds": 1583.182193, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65632, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 25754576, + "cpu_ns": 25674260, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 12408.192, + "latency_p99_upper_ns": 20480, + "latency_max_ns": 98925, + "active_byte_seconds": 1611.048102, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "pressure", + "repeat": 4, + "status": "ok", + "policy": "pressure", + "pool": true, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156112, + "peak_active_bytes": 156112, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 28270087, + "cpu_ns": 28259741, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 0, + "copied_records": 0, + "borrowed_records": 2048, + "pressure_copies": 0, + "allocations": 1, + "reuses": 2047, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 13635.652, + "latency_p99_upper_ns": 20480, + "latency_max_ns": 75407, + "active_byte_seconds": 1772.014643, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 65552, + "unreleased_buffer_bytes": 0 + }, + { + "case": "sparse_fast", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "pool": false, + "budget_bytes": 8388608, + "fixed_bytes": 90560, + "peak_reserved_bytes": 156192, + "peak_active_bytes": 156192, + "peak_queue": 1, + "rss_peak_bytes": 28753920, + "elapsed_ns": 30675302, + "cpu_ns": 30605147, + "input_bytes": 134242304, + "output_bytes": 139264, + "accepted": 2048, + "emitted": 2048, + "copy_bytes": 131072, + "copied_records": 2048, + "borrowed_records": 0, + "pressure_copies": 0, + "allocations": 4096, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 0, + "sleep_ns": 0, + "latency_mean_ns": 14792.559, + "latency_p99_upper_ns": 22528, + "latency_max_ns": 144222, + "active_byte_seconds": 1690.623657, + "live_at_stop_bytes": 0, + "cached_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0 + } +] diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/benchmark.c b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/benchmark.c new file mode 100644 index 0000000..496ad5e --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/benchmark.c @@ -0,0 +1,320 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* A measurement apparatus, not the Pure runtime or a general document parser. */ +enum { CACHE_SLOTS = 16, SCRATCH = 65536, HIST_SIZE = 512, MAX_FRAME = 8*1024*1024 }; +typedef struct Block { size_t capacity; uint64_t since; unsigned char bytes[]; } Block; +typedef struct Item { + Block *block; + size_t offset, length; + uint64_t start, ready; +} Item; +typedef struct Engine { + Item *queue; + Block *cache[CACHE_SLOTS]; + unsigned char scratch[SCRATCH]; + uint64_t histogram[HIST_SIZE]; + size_t slots, head, count, fixed, budget, reserved, active, cached; + size_t peak_reserved, peak_active, peak_queue; + uint64_t allocations, reuses, copies, read_bytes, written_bytes; + uint64_t accepted, emitted, copied_records, borrowed_records, pressure_copies; + uint64_t budget_waits, slot_waits, sleep_ns, hold_ns, service_ns, last_emit; + uint64_t elapsed_ns, cpu_ns, latency_sum, latency_max, start, cancel_after; + long double active_byte_ns; + unsigned ratio; + bool pool; + const char *policy, *status; +} Engine; +static volatile sig_atomic_t interrupted; +static void stop(int sig) { (void)sig; interrupted = 1; } +static uint64_t ns(clockid_t clock) { + struct timespec t; + if (clock_gettime(clock, &t)) { perror("clock_gettime"); exit(2); } + return (uint64_t)t.tv_sec * UINT64_C(1000000000) + (uint64_t)t.tv_nsec; +} +static size_t charge(const Block *b) { return sizeof(*b) + b->capacity; } +static size_t rounded(size_t n) { + size_t r = 64; + while (r < n) r *= 2; + return r; +} +static void peaks(Engine *e) { + assert(e->active + e->cached == e->reserved); + assert(e->fixed + e->reserved <= e->budget); + if (e->reserved > e->peak_reserved) e->peak_reserved = e->reserved; + if (e->active > e->peak_active) e->peak_active = e->active; +} +static void evict(Engine *e, size_t i) { + Block *b = e->cache[i]; + if (!b) return; + e->cached -= charge(b); e->reserved -= charge(b); + free(b); e->cache[i] = NULL; +} +static Block *take(Engine *e, size_t n) { + size_t capacity = rounded(n), need = sizeof(Block) + capacity; + if (e->pool) { + for (size_t i = 0; i < CACHE_SLOTS; ++i) { + Block *b = e->cache[i]; + if (b && b->capacity == capacity) { + e->cache[i] = NULL; e->cached -= need; e->active += need; + ++e->reuses; b->since = ns(CLOCK_MONOTONIC); peaks(e); return b; + } + } + } + for (size_t i = 0; i < CACHE_SLOTS && need > e->budget-e->fixed-e->reserved; ++i) + evict(e, i); + if (need > e->budget-e->fixed-e->reserved) return NULL; + Block *b = malloc(need); + if (!b) { e->status = "allocation_failed"; return NULL; } + b->capacity = capacity; b->since = ns(CLOCK_MONOTONIC); + e->reserved += need; e->active += need; ++e->allocations; peaks(e); return b; +} +static void give(Engine *e, Block *b) { + if (!b) return; + size_t bytes = charge(b); + e->active_byte_ns += (long double)bytes * (ns(CLOCK_MONOTONIC)-b->since); + e->active -= bytes; + if (e->pool && bytes <= e->budget/4-e->cached) { + for (size_t i = 0; i < CACHE_SLOTS; ++i) if (!e->cache[i]) { + e->cache[i] = b; e->cached += bytes; peaks(e); return; + } + } + e->reserved -= bytes; free(b); peaks(e); +} +static bool sleep_until(Engine *e, uint64_t deadline) { + uint64_t t = ns(CLOCK_MONOTONIC), start = t; + while (t < deadline && !interrupted) { + uint64_t delta = deadline-t; + struct timespec request = {(time_t)(delta/1000000000), (long)(delta%1000000000)}; + if (nanosleep(&request, NULL) && errno != EINTR) { e->status="sleep_failed"; return false; } + t = ns(CLOCK_MONOTONIC); + } + e->sleep_ns += t-start; + return !interrupted; +} +/* floor(log2(ns)) with eight subdivisions: upper bound error <= 12.5%. */ +static unsigned bucket(uint64_t n) { + unsigned exponent = 0; + for (uint64_t x = n; x > 1; x >>= 1) ++exponent; + if (exponent < 3) return (unsigned)n; + return exponent*8 + (unsigned)((n-(UINT64_C(1)<>(exponent-3)); +} +static uint64_t upper(unsigned b) { + if (b < 24) return b+1; + unsigned exponent = b/8; + if (exponent >= 63) return UINT64_MAX; + return (UINT64_C(1)<status = "output_failed"; return false; } + p += written; n -= (size_t)written; e->written_bytes += (uint64_t)written; + } + return !interrupted; +} +static bool emit(Engine *e) { + assert(e->count); + Item *item = &e->queue[e->head]; + uint64_t ready = item->ready; + if (e->last_emit + e->service_ns > ready) ready = e->last_emit + e->service_ns; + if (!sleep_until(e, ready)) return false; + uint32_t len = (uint32_t)item->length; + unsigned char header[4] = {(unsigned char)len, (unsigned char)(len>>8), + (unsigned char)(len>>16), (unsigned char)(len>>24)}; + if (!write_all(e, header, sizeof header) || + !write_all(e, item->block->bytes+item->offset, item->length)) return false; + uint64_t now = ns(CLOCK_MONOTONIC), latency = now-item->start; + ++e->histogram[bucket(latency)]; e->latency_sum += latency; + if (latency > e->latency_max) e->latency_max = latency; + e->last_emit = now; ++e->emitted; + give(e, item->block); item->block = NULL; + e->head = (e->head+1)%e->slots; --e->count; + if (e->cancel_after && e->emitted >= e->cancel_after) { + e->status = "cancelled"; return false; + } + return true; +} +static Block *acquire(Engine *e, size_t n) { + for (;;) { + Block *b = take(e, n); + if (b) return b; + if (strcmp(e->status, "ok")) return NULL; + if (!e->count) { e->status = "budget_too_small"; return NULL; } + ++e->budget_waits; + if (!emit(e)) return NULL; + } +} +/* Returns 1 on complete read, 0 on clean EOF before any byte, -1 on failure. */ +static int read_exact(Engine *e, void *dst, size_t n) { + unsigned char *p = dst; size_t done = 0; + while (done < n && !interrupted) { + ssize_t got = read(STDIN_FILENO, p+done, n-done); + if (got < 0 && errno == EINTR) continue; + if (got < 0) { e->status="input_failed"; return -1; } + if (!got) { if (!done) return 0; e->status="truncated_input"; return -1; } + done += (size_t)got; e->read_bytes += (uint64_t)got; + } + return interrupted ? -1 : 1; +} +static bool discard(Engine *e, size_t n) { + while (n) { + size_t chunk = n < SCRATCH ? n : SCRATCH; + if (read_exact(e, e->scratch, chunk) != 1) { + if (!strcmp(e->status,"ok")) e->status="truncated_input"; + return false; + } + n -= chunk; + } + return true; +} +static uint32_t le32(const unsigned char *p) { + return (uint32_t)p[0] | (uint32_t)p[1]<<8 | (uint32_t)p[2]<<16 | (uint32_t)p[3]<<24; +} +static void run(Engine *e) { + for (;;) { + if (interrupted) break; + if (e->count) { + Item *head = &e->queue[e->head]; + uint64_t now = ns(CLOCK_MONOTONIC); + if (now >= head->ready && now >= e->last_emit+e->service_ns) { + if (!emit(e)) break; + continue; + } + } + if (e->count == e->slots) { ++e->slot_waits; if (!emit(e)) break; } + unsigned char header[12]; + uint64_t start = ns(CLOCK_MONOTONIC); + int got = read_exact(e, header, sizeof header); + if (got != 1) { + if (!got) while (e->count && !interrupted) if (!emit(e)) break; + break; + } + size_t payload = le32(header), offset = le32(header+4), length = le32(header+8); + if (!payload || payload > MAX_FRAME || !length || offset > payload || length > payload-offset) { + e->status="invalid_frame"; break; + } + bool project = !strcmp(e->policy,"project"); + Block *source = acquire(e, project ? length : payload); + if (!source) break; + bool valid; + if (project) { + valid = discard(e, offset) && read_exact(e, source->bytes, length)==1 && + discard(e, payload-offset-length); + offset = 0; + } else valid = read_exact(e, source->bytes, payload)==1; + if (!valid) { + if (!strcmp(e->status,"ok")) e->status="truncated_input"; + give(e, source); break; + } + bool copy = !strcmp(e->policy,"copy") || + (!strcmp(e->policy,"threshold") && rounded(payload)/rounded(length) >= e->ratio); + if (!strcmp(e->policy,"pressure")) { + /* Online rule using current charged live buffers; no future lifetime. */ + copy = rounded(length) < source->capacity && + e->active > (e->budget-e->fixed)/2; + if (copy) ++e->pressure_copies; + } + if (!project && copy) { + Block *small = acquire(e, length); + if (!small) { give(e, source); break; } + memcpy(small->bytes, source->bytes+offset, length); + e->copies += length; ++e->copied_records; + give(e, source); source = small; offset = 0; + } else if (!project) ++e->borrowed_records; + size_t tail = (e->head+e->count)%e->slots; + e->queue[tail] = (Item){source, offset, length, start, ns(CLOCK_MONOTONIC)+e->hold_ns}; + ++e->count; ++e->accepted; + if (e->count > e->peak_queue) e->peak_queue=e->count; + } + if (interrupted) e->status="cancelled"; +} +static uint64_t number(const char *s, uint64_t maximum) { + char *end; errno=0; + unsigned long long n = strtoull(s, &end, 10); + if (!*s || *s=='-' || *end || errno || n>maximum) { fprintf(stderr,"invalid number\n"); exit(2); } + return n; +} +int main(int argc, char **argv) { + Engine *e = calloc(1, sizeof *e); + if (!e) return 2; + e->policy="borrow"; e->status="ok"; e->budget=8*1024*1024; e->slots=512; + e->ratio=4; e->pool=true; + for (int i=1; ipolicy=value; + else if (!strcmp(key,"--budget")) e->budget=(size_t)number(value,1073741824); + else if (!strcmp(key,"--slots")) e->slots=(size_t)number(value,4096); + else if (!strcmp(key,"--hold-us")) e->hold_ns=number(value,10000000)*1000; + else if (!strcmp(key,"--service-us")) e->service_ns=number(value,10000000)*1000; + else if (!strcmp(key,"--ratio")) e->ratio=(unsigned)number(value,UINT_MAX); + else if (!strcmp(key,"--pool")) e->pool=number(value,1)!=0; + else if (!strcmp(key,"--cancel-after")) e->cancel_after=number(value,UINT64_MAX); + else { fprintf(stderr,"unknown option\n"); free(e); return 2; } + } + bool known = !strcmp(e->policy,"borrow") || !strcmp(e->policy,"copy") || + !strcmp(e->policy,"threshold") || !strcmp(e->policy,"pressure") || !strcmp(e->policy,"project"); + if (!known || !e->slots || e->slots>4096 || e->ratio<2 || e->budget>UINT64_C(1073741824) || + e->hold_ns>UINT64_C(10000000000) || e->service_ns>UINT64_C(10000000000)) { + fprintf(stderr,"invalid configuration\n"); free(e); return 2; + } + e->fixed=sizeof(*e)+e->slots*sizeof(Item); + if (e->budget < e->fixed) { fprintf(stderr,"budget below fixed metadata\n"); free(e); return 2; } + e->queue=calloc(e->slots,sizeof(Item)); + if (!e->queue) { free(e); return 2; } + signal(SIGINT,stop); signal(SIGTERM,stop); signal(SIGPIPE,SIG_IGN); + e->start=ns(CLOCK_MONOTONIC); uint64_t cpu=ns(CLOCK_PROCESS_CPUTIME_ID); + run(e); + size_t live_at_stop=e->active, cached_at_stop=e->cached; + while (e->count) { + give(e,e->queue[e->head].block); e->head=(e->head+1)%e->slots; --e->count; + } + for (size_t i=0;iactive && !e->reserved && !e->cached); + e->elapsed_ns=ns(CLOCK_MONOTONIC)-e->start; e->cpu_ns=ns(CLOCK_PROCESS_CPUTIME_ID)-cpu; + uint64_t target=e->emitted-e->emitted/100, sum=0, p99=0; + for (unsigned b=0; e->emitted && bhistogram[b]; if (sum>=target) { p99=upper(b); break; } + } + struct rusage usage; long rss=0; + if (!getrusage(RUSAGE_SELF,&usage)) rss=usage.ru_maxrss; +#if defined(__APPLE__) + uint64_t rss_bytes=(uint64_t)rss; +#else + uint64_t rss_bytes=(uint64_t)rss*1024; +#endif + fprintf(stderr,"{\"status\":\"%s\",\"policy\":\"%s\",\"pool\":%s," + "\"budget_bytes\":%zu,\"fixed_bytes\":%zu,\"peak_reserved_bytes\":%zu," + "\"peak_active_bytes\":%zu,\"peak_queue\":%zu,\"rss_peak_bytes\":%" PRIu64 "," + "\"elapsed_ns\":%" PRIu64 ",\"cpu_ns\":%" PRIu64 ",\"input_bytes\":%" PRIu64 "," + "\"output_bytes\":%" PRIu64 ",\"accepted\":%" PRIu64 ",\"emitted\":%" PRIu64 "," + "\"copy_bytes\":%" PRIu64 ",\"copied_records\":%" PRIu64 ",\"borrowed_records\":%" PRIu64 "," + "\"pressure_copies\":%" PRIu64 ",\"allocations\":%" PRIu64 ",\"reuses\":%" PRIu64 "," + "\"budget_waits\":%" PRIu64 ",\"slot_waits\":%" PRIu64 ",\"sleep_ns\":%" PRIu64 "," + "\"latency_mean_ns\":%.3f,\"latency_p99_upper_ns\":%" PRIu64 ",\"latency_max_ns\":%" PRIu64 "," + "\"active_byte_seconds\":%.6Lf,\"live_at_stop_bytes\":%zu,\"cached_at_stop_bytes\":%zu," + "\"unreleased_buffer_bytes\":%zu}\n", + e->status,e->policy,e->pool?"true":"false",e->budget,e->fixed,e->fixed+e->peak_reserved, + e->fixed+e->peak_active,e->peak_queue,rss_bytes,e->elapsed_ns,e->cpu_ns,e->read_bytes, + e->written_bytes,e->accepted,e->emitted,e->copies,e->copied_records,e->borrowed_records, + e->pressure_copies,e->allocations,e->reuses,e->budget_waits,e->slot_waits,e->sleep_ns, + e->emitted?(double)e->latency_sum/(double)e->emitted:0,p99,e->latency_max, + e->active_byte_ns/1e9L,live_at_stop,cached_at_stop,e->reserved); + int result=!strcmp(e->status,"ok")?0:!strcmp(e->status,"cancelled")?3:2; + free(e->queue); free(e); return result; +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/plan.json b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/plan.json new file mode 100644 index 0000000..5040006 --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/plan.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "written_before_measurement": true, + "seed": 20260922, + "repeats": 5, + "slots": 512, + "policies": [ + {"name": "borrow", "policy": "borrow", "pool": 1}, + {"name": "copy", "policy": "copy", "pool": 1}, + {"name": "threshold2", "policy": "threshold", "ratio": 2, "pool": 1}, + {"name": "threshold4", "policy": "threshold", "ratio": 4, "pool": 1}, + {"name": "threshold8", "policy": "threshold", "ratio": 8, "pool": 1}, + {"name": "pressure", "policy": "pressure", "pool": 1}, + {"name": "project", "policy": "project", "pool": 1}, + {"name": "copy_no_pool", "policy": "copy", "pool": 0} + ], + "cases": [ + {"name": "sparse_slow", "shape": "sparse", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "dense_slow", "shape": "dense", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "mixed_slow", "shape": "mixed", "records": 2048, "budget": 8388608, "hold_us": 5000, "service_us": 20}, + {"name": "mixed_tight", "shape": "mixed", "records": 2048, "budget": 2097152, "hold_us": 5000, "service_us": 20}, + {"name": "sparse_fast", "shape": "sparse", "records": 2048, "budget": 8388608, "hold_us": 0, "service_us": 0} + ], + "expectations": [ + "Borrow retains full source allocations for small selections.", + "Copy can add CPU and transient source plus destination memory for dense selections.", + "A current-pressure rule need not outperform a static size rule or early projection.", + "Pool reuse can reduce allocation calls while increasing reserved idle memory.", + "No Pure-specific advantage, application SLO, or statistical significance is presumed." + ], + "comparison_rule": "Report all presets and repetitions; no post-hoc winner is called an out-of-sample tuned policy.", + "limits": "Single FIFO local pipeline with synthetic delayed sink. No Sonic/Node/.NET benchmark, fan-out, persistent cache, generic JSON parser, external open-loop arrivals, or production SLO." +} diff --git a/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/run.py b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/run.py new file mode 100644 index 0000000..94657cc --- /dev/null +++ b/research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/sources/run.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +"""Build, independently verify, and measure the retention experiment.""" +import argparse +import csv +import hashlib +import json +import os +from pathlib import Path +import platform +import random +import shlex +import signal +import statistics +import struct +import subprocess +import tempfile +import time + +ROOT = Path(__file__).resolve().parent +FLAGS = ["-std=c17", "-Wall", "-Wextra", "-Wpedantic", "-Werror", "-O2", "-g"] + + +def digest(path): + with open(path, "rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def dump(path, value): + path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n") + + +def build(cc, output, sanitizer=False): + command = [*cc, *FLAGS] + if sanitizer: + command += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] + command += [str(ROOT / "benchmark.c"), "-o", str(output)] + result = subprocess.run(command, capture_output=True, text=True, check=True) + return {"command": command, "stderr": result.stderr, "sha256": digest(output), + "size_bytes": output.stat().st_size} + + +def shape_for(shape, rng, i): + if shape == "sparse": + return 65536, 64 + if shape == "dense": + return 65536, 49152 + if shape == "mixed": + source = rng.choice([4096, 16384, 65536, 262144]) + keep = max(16, source // rng.choice([1, 2, 4, 8, 64, 1024])) + return source, keep + if shape == "test": + return [(1, 1), (63, 17), (4097, 12), (65536, 49152), (127, 127)][i % 5] + raise ValueError(shape) + + +def fixture(path, shape, count, seed): + rng = random.Random(seed) + expected = hashlib.sha256() + total = 0 + with path.open("wb") as stream: + for i in range(count): + size, keep = shape_for(shape, rng, i) + offset = rng.randrange(size - keep + 1) + # All input bytes exist on disk; no sparse files or seeking past data. + tile = rng.randbytes(256) + payload = (tile * ((size + 255) // 256))[:size] + stream.write(struct.pack(" size - keep: + raise ValueError("invalid frame") + if len(data) - at < size: + raise ValueError("short payload") + result.extend(struct.pack(" size - keep: + raise ValueError("invalid frame") + if len(data) - at < size: + raise ValueError("short payload") + result.extend(struct.pack(" 0 + values[policy["policy"]] = m["rss_peak_bytes"] + # Only a sanity check for this deliberately wide memory gap, not exact RSS arithmetic. + assert values["borrow"] > values["project"], values + checks.append("current-image VmHWM distinguishes large retained sources from small projections") + return checks + + +def sample(binary, source, output, case, policy): + with source.open("rb") as inp, output.open("wb") as out: + result = subprocess.run(command(binary, case, policy), stdin=inp, stdout=out, + stderr=subprocess.PIPE, timeout=30) + m = metrics(result.stderr) + assert result.returncode == 0 and m["status"] == "ok", result.stderr + return m + + +def summarize(rows): + result = [] + names = ["elapsed_ns", "cpu_ns", "peak_reserved_bytes", "peak_active_bytes", "rss_peak_bytes", + "copy_bytes", "active_byte_seconds", "latency_mean_ns", "latency_p99_upper_ns", + "budget_waits", "allocations", "reuses"] + groups = sorted({(r["case"], r["variant"]) for r in rows}) + for case, variant in groups: + group = [r for r in rows if (r["case"], r["variant"]) == (case, variant)] + value = {"case": case, "variant": variant, "samples": len(group)} + for name in names: + values = [r[name] for r in group] + value[name] = {"median": statistics.median(values), "min": min(values), "max": max(values)} + result.append(value) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check-only", action="store_true") + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if not args.check_only and args.output is None: + parser.error("provide --check-only or --output (new directory)") + if args.output: + args.output.mkdir(parents=True, exist_ok=False) + cc = shlex.split(os.environ.get("CC", "cc")) + plan = json.loads((ROOT / "plan.json").read_text()) + affinity = None + if hasattr(os, "sched_getaffinity"): + affinity = min(os.sched_getaffinity(0)) + os.sched_setaffinity(0, {affinity}) + with tempfile.TemporaryDirectory(prefix="pure-retention-") as folder: + tmp = Path(folder) + native, sanitized = tmp / "bench", tmp / "bench-sanitized" + builds = [build(cc, native), build(cc, sanitized, True)] + verification = {"native": check(native, tmp, plan["policies"]), + "asan_ubsan": check(sanitized, tmp, plan["policies"])} + print(f'Verified {sum(map(len, verification.values()))} scenario groups (native + ASan/UBSan).', flush=True) + if args.check_only: + return + sources = {name: digest(ROOT / name) for name in ["benchmark.c", "run.py", "plan.json"]} + environment = {"platform": platform.platform(), "machine": platform.machine(), + "python": platform.python_version(), "cpu_affinity": affinity, + "compiler": subprocess.check_output([*cc, "--version"], text=True), + "builds": builds, "source_sha256": sources, + "clock": "CLOCK_MONOTONIC; CPU = CLOCK_PROCESS_CPUTIME_ID", + "timing": "includes file IO, scheduling, deliberate sleep, metrics, buffer cleanup; excludes startup/build/fixture/verification", + "limitations": "shared host; unregulated thermals; cached files; output page cache, no fsync; synthetic closed-loop arrivals", + "started_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())} + if Path("/proc/cpuinfo").exists(): + environment["cpuinfo"] = Path("/proc/cpuinfo").read_text() + dump(args.output / "environment.json", environment) + dump(args.output / "verification.json", verification) + dump(args.output / "plan.json", plan) + (args.output / "sources").mkdir() + for name in sources: + (args.output / "sources" / name).write_bytes((ROOT / name).read_bytes()) + fixtures, rows = [], [] + rng = random.Random(plan["seed"]) + for case in plan["cases"]: + source, output = tmp / "input.frames", tmp / "output.frames" + info = fixture(source, case["shape"], case["records"], plan["seed"]) + info["case"] = case["name"] + fixtures.append(info) + # Warm each strategy once. The following measurements use fresh processes. + for policy in plan["policies"]: + sample(native, source, output, case, policy) + assert digest(output) == info["expected_output_sha256"] + for repeat in range(plan["repeats"]): + order = list(plan["policies"]) + rng.shuffle(order) + for policy in order: + m = sample(native, source, output, case, policy) + assert m["accepted"] == m["emitted"] == case["records"] + assert m["input_bytes"] == info["input_bytes"] + assert m["output_bytes"] == info["expected_output_bytes"] + assert digest(output) == info["expected_output_sha256"] + rows.append({"case": case["name"], "variant": policy["name"], "repeat": repeat, **m}) + dump(args.output / "raw.json", rows) + dump(args.output / "fixtures.json", fixtures) + print(f'Completed {case["name"]}: {len(plan["policies"])} variants x {plan["repeats"]} trials.', flush=True) + dump(args.output / "summary.json", summarize(rows)) + with (args.output / "raw.csv").open("w") as out: + writer = csv.DictWriter(out, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + # Detect accidental source edits during a measurement run. + assert sources == {name: digest(ROOT / name) for name in sources} + dump(args.output / "complete.json", {"samples": len(rows), "all_output_hashes_match": True, + "finished_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}) + print(args.output.resolve()) + + +if __name__ == "__main__": + main() diff --git a/research/experiments/003-json-fanout/.gitignore b/research/experiments/003-json-fanout/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/research/experiments/003-json-fanout/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/research/experiments/003-json-fanout/README.md b/research/experiments/003-json-fanout/README.md new file mode 100644 index 0000000..bab7ee4 --- /dev/null +++ b/research/experiments/003-json-fanout/README.md @@ -0,0 +1,164 @@ +# Experiment 003: actual JSON parsing and shared-buffer fan-out + +This apparatus extends the [single-consumer experiment](../002-buffer-retention/README.md) +with a real JSON parser and up to three independently paced consumers. It is C17 +code for comparing ordinary ownership policies, not the Pure execution core. + +## Dependency and semantics + +The MIT-licensed **yyjson 0.12.0** source and license are vendored unchanged at +commit [`8b4a38dc994a110abaec8a400615567bd996105f`](https://github.com/ibireme/yyjson/tree/8b4a38dc994a110abaec8a400615567bd996105f). +[UPSTREAM.json](vendor/yyjson/UPSTREAM.json) records source URLs and hashes, which +the runner verifies. Reproduction requires no dependency download. + +Input is newline-delimited JSON, with a maximum of 1 MiB per record excluding its +newline. A complete last JSON object may omit the newline. Blank lines are errors. +Each object must have string-valued selected fields (`body`, `tag`, `tag` by +default). Other members are parsed and validated too. Duplicate selected names +use yyjson's first matching member; the Python reference uses the same explicit +first-member rule. This is an experiment schema, not a general JSON application. + +All variants use `YYJSON_READ_INSITU`, with required zero padding. The parser +decodes strings into the input buffer. We extract offsets and byte lengths, then +immediately free document metadata. Queued views retain their input allocation +or a compact copy. No document node outlives the document. Unicode escapes, +embedded NULs and empty strings are compared as decoded UTF-8 bytes, not C strings. +The pinned [header](vendor/yyjson/yyjson.h) and [implementation](vendor/yyjson/yyjson.c) +are the authority for this lifetime behavior; the official +[API guide](https://ibireme.github.io/yyjson/doc/doxygen/html/api.html) explains +in-situ input ownership and the allocator interface. + +Output is an experimental multiplexed binary stream. Each selection has a +16-byte little-endian header: uint32 consumer index, uint32 byte length, uint64 +input sequence number, followed by the decoded selected bytes. Interleaving can +change with scheduling; each consumer's sequence and contents must match exactly. +The verifier checks separate per-consumer digests and counts rather than requiring +an identical whole-file interleaving. + +## Compared policies + +| Name | Choice | +|---|---| +| borrow | Every consumer keeps a view of the original input allocation. | +| copy | Copy each consumer's selection independently. | +| shared_copy | Copy each distinct selected range once; consumers of that range share the copy. | +| threshold2/8 | Copy when rounded input capacity / selected capacity reaches 2/8, sharing identical copies. | +| compact | Initially borrow; after a consumer finishes, compact a smaller final remaining view if the ratio reaches 2 and byte credits allow it. | +| copy_no_pool | Independent copies with buffer caching disabled, using the same capacity classes. | + +Identical selections mean the same offset and length in one parsed record. This +does not hash or intern equal string contents across unrelated fields/records. +The compact heuristic uses current ownership only, never future consumer times. +It does not move storage still referenced by multiple consumers. No particular +heuristic is claimed to be new or to require a language/compiler change. + +Every allocation has a reference count and a list of its queued views. A buffer +is charged once, even when several consumers reference it. Completion unlinks its +view; only the final owner can return the allocation to the pool. This is a +single-threaded implementation with non-atomic counts, not a concurrency proof. + +## Memory accounting + +One shared requested-byte budget includes: + +- Engine state, input read buffer, latency histograms and all consumer queue slots. +- Input allocations and simultaneous old/new allocations during reader growth. +- Compact selection allocations, including overlap with their original input. +- Parser allocations through yyjson's custom malloc/realloc/free callbacks. +- Allocation headers and cached idle allocations. + +Capacity classes are powers of two, starting at 64 bytes. The pool caches at most +16 idle blocks and at most one quarter of the total budget. Reuse requires an +exact class match; idle blocks are evicted before an allocation is refused. Parser, +input and selection allocations can reuse the same class after their owners +release them. This is a small baseline cache, not a claim about the best allocator. + +When byte credits run out, existing queued outputs are serviced before retrying. +This can occur inside the parser allocator. If no queued consumer can release +enough space, parsing fails explicitly within the cap. Reallocation charges both +old and new storage until the copy finishes. Shrinking may retain existing capacity. + +The budget is **not process RAM**: allocator overhead, executable/library pages, +other stack frames, kernel buffering and filesystem cache are outside it. Linux +current-image `VmHWM` is reported separately as approximate RSS; zero means the +RSS metric is unavailable on another OS. Peak SOURCE/OUTPUT/PARSER counters occur +at potentially different times and must not be summed as a simultaneous peak. + +## Scheduling and metrics + +Every variant uses the same event loop and per-consumer FIFO slot limit. At each +iteration, an eligible queue head is served before another input record. If any +queue is full or the byte budget prevents allocation, the earliest eligible +consumer is serviced, sleeping until its deadline if necessary. The input waits +until all consumers have room for the next record. This is ordinary backpressure. + +The corpus and delays are synthetic. Reads, JSON parsing, copies, writes, buffer +ownership and sleeps are real. Consumers run in one thread; independent delays +do not mean concurrent CPU execution. There are no external open-loop arrivals, +network connections, durable writes, or production latency targets. + +- `peak_reserved_bytes`: peak requested apparatus storage including idle cache. +- `source_peak_bytes`, `output_peak_bytes`, `parser_peak_bytes`: active charges by + role, including headers; cache reuse changes the role for the next lease. +- `*_byte_seconds`: charged active allocation bytes times lifetime, counted once + until the last reference releases a block. Fixed storage and idle cache time + are excluded. +- `selection_copy_bytes`: explicit policy copies only. Reader assembly/growth and + parser realloc copies have separate counters. yyjson's decoding, kernel and + other hidden data movements are not included in these counters. +- `parse_call_ns`: wall time inside `yyjson_read_opts`. Because allocator callbacks + can service consumers under pressure, this includes their work and waiting. + It is not isolated parser CPU time and is a subset of elapsed time. +- `elapsed_ns`: input, parsing, policies, consumers, instrumentation and buffer + cleanup. Excludes build, process startup, fixed setup and output verification. +- `cpu_ns`: process CPU clock for the same timed region. +- Per-consumer latency starts before its input record begins to be assembled and + ends after that consumer's output write. Time waiting upstream before that + start is excluded. P99 is a histogram upper bound, with at most 12.5% normal + bin rounding. These are not end-to-end service latency measurements. + +Five fresh-process trials per case/policy follow one warm-up per policy. Variant +order is randomized within each trial; case order is fixed. All measurements are +instrumented. The host CPU affinity is recorded, but thermals and power are not +controlled. Output is a cached temporary file with no `fsync`. Medians and ranges +describe this apparatus; no significance or universal performance claim is made. + +## Reproduction and verification + +From the repository root: + +```sh +python3 research/experiments/003-json-fanout/run.py --check-only +python3 research/experiments/003-json-fanout/run.py --output /tmp/pure-json-fanout-results +``` + +Use a new result directory; the runner refuses to overwrite one. `CC` can choose +a compiler. Native and ASan/UBSan checks run before measurements. LeakSanitizer +requires an environment without ptrace tracing; its diagnostics are not hidden. +Compiler flags, source snapshots (including the parser), hashes, input rules and +digests, every measurement and verification results are saved. Binary sizes +include debug information. `complete.json` marks successful completion. +The runner disables yyjson's unused writer, utilities and incremental-reader +modules using upstream build switches. The normal reader remains enabled for all +policies. Native measurement uses `-O2`; the sanitizer check uses `-O1`. + +Checks cover decoded byte equality with Python, 1/2/3 consumers, UTF-8 and escapes, +queue-slot reuse, shared-copy final ownership, final-owner compaction, malformed +JSON/schema, parser budget refusal, long records, partial-work cancellation, +SIGTERM and real output failure where `/dev/full` exists. These are functional +tests, not an exhaustive audit of yyjson or thread/device cancellation races. + +For direct use: + +```sh +cc -std=c17 -O2 -DYYJSON_DISABLE_WRITER=1 -DYYJSON_DISABLE_UTILS=1 -DYYJSON_DISABLE_INCR_READER=1 research/experiments/003-json-fanout/benchmark.c research/experiments/003-json-fanout/vendor/yyjson/yyjson.c -o /tmp/pure-json-fanout +/tmp/pure-json-fanout --policy threshold --ratio 2 < records.ndjson > selections.bin 2> metrics.json +``` + +Exit 0 means completion, 2 means failure, 3 means cancellation. Earlier completed +selections are not rolled back. Output failure or a signal during a write may +leave a partial last output frame. Cleanup releases all apparatus-owned buffers; +it is not an atomic-output or asynchronous backend guarantee. + +Measured results and their interpretation are in +[Korean report 008](../../../docs/research/008-json-fanout-results.md). diff --git a/research/experiments/003-json-fanout/benchmark.c b/research/experiments/003-json-fanout/benchmark.c new file mode 100644 index 0000000..474bc7f --- /dev/null +++ b/research/experiments/003-json-fanout/benchmark.c @@ -0,0 +1,391 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vendor/yyjson/yyjson.h" + +enum { BRANCHES=3, CACHE=16, READ_SIZE=16384, MAX_JSON=1024*1024, HIST=512 }; +enum { SOURCE, OUTPUT, PARSER, KINDS }; +typedef struct Item Item; +typedef struct Block { + size_t capacity, refs; + uint64_t since; + unsigned kind; + Item *views; + _Alignas(max_align_t) unsigned char data[]; +} Block; +struct Item { + Block *block; + Item *prev, *next; + size_t offset, length; + uint64_t sequence, start, ready; +}; +typedef struct Queue { + Item *items; + size_t head, count, peak; + const char *key; + uint64_t hold, service, last, emitted, bytes, latency_sum, histogram[HIST]; +} Queue; +typedef struct Engine { + Queue q[BRANCHES]; + Block *cache[CACHE]; + unsigned char read_buffer[READ_SIZE]; + size_t read_at, read_end; + size_t budget, fixed, slots, fanout, reserved, active, cached, peak; + size_t by_kind[KINDS], peak_kind[KINDS]; + long double byte_ns[KINDS]; + uint64_t allocations[KINDS], reuses, accepted, emitted, input_bytes; + uint64_t selection_copies, reader_copies, parser_copies, parse_ns; + uint64_t budget_waits, slot_waits, compacted, compact_skipped, dedup_hits; + uint64_t sleep_ns, elapsed_ns, cpu_ns, cancel_after; + unsigned ratio; + int pool; + const char *policy, *status; +} Engine; +static volatile sig_atomic_t stopped; +static void stop(int sig) { (void)sig; stopped=1; } +static uint64_t ns(clockid_t clock) { + struct timespec t; + if (clock_gettime(clock,&t)) abort(); + return (uint64_t)t.tv_sec*UINT64_C(1000000000)+(uint64_t)t.tv_nsec; +} +static size_t rounded(size_t n) { size_t c=64; while(ccapacity; } +static void check(Engine *e) { + assert(e->active+e->cached==e->reserved); + assert(e->by_kind[0]+e->by_kind[1]+e->by_kind[2]==e->active); + assert(e->fixed+e->reserved<=e->budget); + if(e->reserved>e->peak)e->peak=e->reserved; + for(unsigned k=0;kby_kind[k]>e->peak_kind[k])e->peak_kind[k]=e->by_kind[k]; +} +static void evict(Engine *e,size_t i) { + Block *b=e->cache[i]; if(!b)return; + assert(!b->refs && !b->views); + e->reserved-=charge(b); e->cached-=charge(b); free(b); e->cache[i]=NULL; +} +static Block *take(Engine *e,size_t n,unsigned kind) { + size_t capacity=rounded(n),need=sizeof(Block)+capacity; + Block *b=NULL; + if(e->pool)for(size_t i=0;icache[i] && e->cache[i]->capacity==capacity) { + b=e->cache[i]; e->cache[i]=NULL; e->cached-=need; ++e->reuses; break; + } + if(!b) { + for(size_t i=0;ie->budget-e->fixed-e->reserved;++i)evict(e,i); + if(need>e->budget-e->fixed-e->reserved)return NULL; + b=malloc(need); + if(!b){e->status="allocation_failed";return NULL;} + b->capacity=capacity; e->reserved+=need; ++e->allocations[kind]; + } + b->kind=kind;b->refs=1;b->views=NULL;b->since=ns(CLOCK_MONOTONIC); + e->active+=need;e->by_kind[kind]+=need;check(e);return b; +} +static void drop(Engine *e,Block *b) { + if(!b)return; + assert(b->refs); + if(--b->refs)return; + assert(!b->views); + size_t bytes=charge(b); + e->byte_ns[b->kind]+=(long double)bytes*(ns(CLOCK_MONOTONIC)-b->since); + e->active-=bytes;e->by_kind[b->kind]-=bytes; + if(e->pool && bytes<=e->budget/4-e->cached)for(size_t i=0;icache[i]) { + e->cache[i]=b;e->cached+=bytes;check(e);return; + } + e->reserved-=bytes;free(b);check(e); +} +static void attach(Item *item) { + Block *b=item->block; + item->prev=NULL;item->next=b->views; + if(b->views)b->views->prev=item; + b->views=item; +} +static void detach(Item *item) { + if(item->prev)item->prev->next=item->next;else item->block->views=item->next; + if(item->next)item->next->prev=item->prev; + item->prev=item->next=NULL; +} +static void compact(Engine *e,Block *source) { + if(strcmp(e->policy,"compact") || source->kind!=SOURCE || source->refs!=1 || !source->views)return; + Item *item=source->views; + assert(!item->next); + if(source->capacity/rounded(item->length)ratio)return; + /* Opportunistic only: never block or recurse from a completion callback. */ + Block *small=take(e,item->length,OUTPUT); + if(!small){++e->compact_skipped;return;} + memcpy(small->data,source->data+item->offset,item->length); + e->selection_copies+=item->length;++e->compacted; + detach(item);item->block=small;item->offset=0;attach(item);drop(e,source); +} +static uint64_t deadline(Queue *q) { + uint64_t ready=q->items[q->head].ready; + return ready>q->last+q->service?ready:q->last+q->service; +} +static int next_queue(Engine *e) { + int selected=-1;uint64_t soon=UINT64_MAX; + for(size_t k=0;kfanout;++k)if(e->q[k].count && deadline(&e->q[k])q[k]); + } + return selected; +} +static int sleep_to(Engine *e,uint64_t until) { + uint64_t start=ns(CLOCK_MONOTONIC),now=start; + while(nowstatus="sleep_failed";return 0;} + now=ns(CLOCK_MONOTONIC); + } + e->sleep_ns+=now-start;return !stopped; +} +static unsigned bucket(uint64_t n) { + unsigned k=0;for(uint64_t x=n;x>1;x>>=1)++k; + return k<3?(unsigned)n:k*8+(unsigned)((n-(UINT64_C(1)<>(k-3)); +} +static uint64_t upper(unsigned b) { + unsigned k=b/8; + if(b<24)return b+1; + if(k>=63)return UINT64_MAX; + return (UINT64_C(1)<status="output_failed";return 0;} + p+=w;n-=(size_t)w; + } + return !stopped; +} +static int emit(Engine *e) { + int k=next_queue(e);assert(k>=0); + Queue *q=&e->q[k];Item *item=&q->items[q->head]; + if(!sleep_to(e,deadline(q)))return 0; + unsigned char header[16]; + for(unsigned i=0;i<4;++i){header[i]=(unsigned char)((unsigned)k>>(8*i));header[4+i]=(unsigned char)(item->length>>(8*i));} + for(unsigned i=0;i<8;++i)header[8+i]=(unsigned char)(item->sequence>>(8*i)); + if(!write_all(e,header,16) || !write_all(e,item->block->data+item->offset,item->length))return 0; + uint64_t now=ns(CLOCK_MONOTONIC),latency=now-item->start; + q->last=now;q->latency_sum+=latency;++q->histogram[bucket(latency)]; + ++q->emitted;q->bytes+=item->length;++e->emitted; + Block *b=item->block;size_t old_refs=b->refs; + detach(item);drop(e,b);item->block=NULL; + q->head=(q->head+1)%e->slots;--q->count; + if(old_refs>1)compact(e,b); + if(e->cancel_after && e->emitted>=e->cancel_after){e->status="cancelled";return 0;} + return !strcmp(e->status,"ok"); +} +static Block *acquire(Engine *e,size_t n,unsigned kind) { + for(;;) { + Block *b=take(e,n,kind);if(b)return b; + if(strcmp(e->status,"ok"))return NULL; + if(next_queue(e)<0){e->status="budget_too_small";return NULL;} + ++e->budget_waits;if(!emit(e))return NULL; + } +} +static void *parser_alloc(void *ctx,size_t n) { + Block *b=acquire(ctx,n,PARSER);return b?b->data:NULL; +} +static Block *from_data(void *p) {return (Block *)((unsigned char *)p-sizeof(Block));} +static void parser_free(void *ctx,void *p) {if(p)drop(ctx,from_data(p));} +static void *parser_realloc(void *ctx,void *p,size_t old,size_t n) { + if(!p)return parser_alloc(ctx,n); + Block *b=from_data(p);assert(b->refs==1 && b->kind==PARSER && old<=b->capacity); + if(n<=b->capacity)return p; + void *fresh=parser_alloc(ctx,n);if(!fresh)return NULL; + memcpy(fresh,p,old);((Engine *)ctx)->parser_copies+=old;parser_free(ctx,p);return fresh; +} +/* Bounded NDJSON reader, accepting a final complete JSON without a newline. */ +static Block *read_record(Engine *e,size_t *length) { + Block *b=acquire(e,256,SOURCE);if(!b)return NULL; + *length=0; + for(;;) { + if(stopped){drop(e,b);return NULL;} + if(e->read_at==e->read_end) { + ssize_t got=read(STDIN_FILENO,e->read_buffer,READ_SIZE); + if(got<0 && errno==EINTR)continue; + if(got<0){e->status="input_failed";drop(e,b);return NULL;} + if(!got){if(*length)break;drop(e,b);return NULL;} + e->read_at=0;e->read_end=(size_t)got;e->input_bytes+=(uint64_t)got; + } + unsigned char *p=e->read_buffer+e->read_at; + size_t available=e->read_end-e->read_at; + unsigned char *newline=memchr(p,'\n',available); + size_t amount=newline?(size_t)(newline-p):available; + if(amount>MAX_JSON-*length){e->status="record_too_large";drop(e,b);return NULL;} + size_t need=*length+amount+YYJSON_PADDING_SIZE; + if(need>b->capacity) { + Block *larger=acquire(e,need,SOURCE); + if(!larger){drop(e,b);return NULL;} + memcpy(larger->data,b->data,*length);e->reader_copies+=*length; + drop(e,b);b=larger; + } + memcpy(b->data+*length,p,amount);e->reader_copies+=amount; + *length+=amount;e->read_at+=amount+(newline?1:0); + if(newline)break; + } + memset(b->data+*length,0,YYJSON_PADDING_SIZE);return b; +} +static int submit(Engine *e,Block *source,size_t length,uint64_t start) { + size_t offsets[BRANCHES]={0},lengths[BRANCHES]={0}; + yyjson_alc alc={parser_alloc,parser_realloc,parser_free,e}; + yyjson_read_err error; + uint64_t before=ns(CLOCK_MONOTONIC); + yyjson_doc *doc=yyjson_read_opts((char *)source->data,length,YYJSON_READ_INSITU,&alc,&error); + e->parse_ns+=ns(CLOCK_MONOTONIC)-before; + if(!doc){if(!strcmp(e->status,"ok"))e->status="invalid_json";drop(e,source);return 0;} + yyjson_val *root=yyjson_doc_get_root(doc); + int valid=yyjson_is_obj(root); + for(size_t k=0;valid && kfanout;++k) { + yyjson_val *value=yyjson_obj_get(root,e->q[k].key); + const char *str=yyjson_get_str(value);size_t size=yyjson_get_len(value); + uintptr_t address=(uintptr_t)str,base=(uintptr_t)source->data; + if(!yyjson_is_str(value) || addresslength || size>length-(address-base))valid=0; + else{offsets[k]=(size_t)(address-base);lengths[k]=size;} + } + /* Only decoded string offsets/lengths survive. No DOM nodes escape. */ + yyjson_doc_free(doc); + if(!valid){e->status="invalid_schema";drop(e,source);return 0;} + Item pending[BRANCHES]={{0}}; + for(size_t k=0;kfanout;++k) { + int copy=!strcmp(e->policy,"copy") || !strcmp(e->policy,"shared_copy") || + (!strcmp(e->policy,"threshold") && source->capacity/rounded(lengths[k])>=e->ratio); + int reuse=copy && (!strcmp(e->policy,"shared_copy") || !strcmp(e->policy,"threshold")); + Block *b=NULL;size_t offset=offsets[k]; + if(reuse)for(size_t j=0;jrefs;offset=0;++e->dedup_hits;break; + } + if(!b && copy) { + b=acquire(e,lengths[k],OUTPUT); + if(!b) { + for(size_t j=0;jdata,source->data+offset,lengths[k]);e->selection_copies+=lengths[k];offset=0; + } else if(!b){b=source;++b->refs;} + pending[k]=(Item){.block=b,.offset=offset,.length=lengths[k],.sequence=e->accepted,.start=start}; + } + uint64_t now=ns(CLOCK_MONOTONIC); + for(size_t k=0;kfanout;++k) { + Queue *q=&e->q[k];assert(q->countslots); + Item *item=&q->items[(q->head+q->count)%e->slots]; + *item=pending[k];item->ready=now+q->hold;attach(item);++q->count; + if(q->count>q->peak)q->peak=q->count; + } + ++e->accepted;drop(e,source);return 1; +} +static void run(Engine *e) { + while(!stopped && !strcmp(e->status,"ok")) { + int k=next_queue(e); + if(k>=0 && deadline(&e->q[k])<=ns(CLOCK_MONOTONIC)){if(!emit(e))break;continue;} + int full=0;for(size_t j=0;jfanout;++j)if(e->q[j].count==e->slots)full=1; + if(full){++e->slot_waits;if(!emit(e))break;continue;} + size_t length;uint64_t start=ns(CLOCK_MONOTONIC); + Block *source=read_record(e,&length); + if(!source) { + while(!stopped && !strcmp(e->status,"ok") && next_queue(e)>=0)if(!emit(e))break; + break; + } + if(!submit(e,source,length,start))break; + } + if(stopped)e->status="cancelled"; +} +static uint64_t number(const char *s,uint64_t max) { + char *end;errno=0;unsigned long long n=strtoull(s,&end,10); + if(!*s || *s=='-' || *end || errno || n>max){fprintf(stderr,"invalid number\n");exit(2);} + return n; +} +static uint64_t rss(void) { +#if defined(__linux__) + FILE *f=fopen("/proc/self/status","r");if(!f)return 0; + char line[256];unsigned long long kib=0; + while(fgets(line,sizeof(line),f))if(sscanf(line,"VmHWM: %llu kB",&kib)==1)break; + fclose(f);return (uint64_t)kib*1024; +#else + return 0; +#endif +} +int main(int argc,char **argv) { + _Static_assert(sizeof(Block)==offsetof(Block,data),"aligned block header"); + Engine storage={0},*e=&storage; + e->policy="borrow";e->status="ok";e->slots=64;e->fanout=3;e->budget=8*1024*1024;e->ratio=2;e->pool=1; + e->q[0].key="body";e->q[1].key=e->q[2].key="tag"; + e->q[1].hold=5000000;e->q[2].hold=8000000; + for(int i=1;ipolicy=value; + else if(!strcmp(key,"--budget"))e->budget=(size_t)number(value,1073741824); + else if(!strcmp(key,"--slots"))e->slots=(size_t)number(value,4096); + else if(!strcmp(key,"--fanout"))e->fanout=(size_t)number(value,BRANCHES); + else if(!strcmp(key,"--ratio"))e->ratio=(unsigned)number(value,1024); + else if(!strcmp(key,"--pool"))e->pool=(int)number(value,1); + else if(!strcmp(key,"--cancel-after"))e->cancel_after=number(value,UINT64_MAX); + else { + int recognized=0; + for(size_t k=0;kq[k].key=value;recognized=1;} + snprintf(option,sizeof(option),"--hold%zu-us",k); + if(!strcmp(key,option)){e->q[k].hold=number(value,10000000)*1000;recognized=1;} + snprintf(option,sizeof(option),"--service%zu-us",k); + if(!strcmp(key,option)){e->q[k].service=number(value,10000000)*1000;recognized=1;} + } + if(!recognized){fprintf(stderr,"unknown option\n");return 2;} + } + } + if(!e->slots || !e->fanout || e->ratio<2 || (strcmp(e->policy,"borrow") && strcmp(e->policy,"copy") && + strcmp(e->policy,"shared_copy") && strcmp(e->policy,"threshold") && strcmp(e->policy,"compact"))) { + fprintf(stderr,"invalid configuration\n");return 2; + } + e->fixed=sizeof(*e)+e->fanout*e->slots*sizeof(Item); + if(e->fixed>e->budget){fprintf(stderr,"budget below metadata\n");return 2;} + for(size_t k=0;kfanout;++k) { + e->q[k].items=calloc(e->slots,sizeof(Item)); + if(!e->q[k].items){for(size_t j=0;jq[j].items);return 2;} + } + signal(SIGINT,stop);signal(SIGTERM,stop);signal(SIGPIPE,SIG_IGN); + uint64_t start=ns(CLOCK_MONOTONIC),cpu=ns(CLOCK_PROCESS_CPUTIME_ID); + run(e); + size_t live_at_stop=e->active; + for(size_t k=0;kfanout;++k) { + Queue *q=&e->q[k]; + while(q->count){Item *item=&q->items[q->head];detach(item);drop(e,item->block);q->head=(q->head+1)%e->slots;--q->count;} + } + for(size_t i=0;iactive && !e->reserved); + e->elapsed_ns=ns(CLOCK_MONOTONIC)-start;e->cpu_ns=ns(CLOCK_PROCESS_CPUTIME_ID)-cpu; + fprintf(stderr,"{\"status\":\"%s\",\"policy\":\"%s\",\"parser_version\":\"%s\",\"budget_bytes\":%zu," + "\"fixed_bytes\":%zu,\"peak_reserved_bytes\":%zu,\"source_peak_bytes\":%zu,\"output_peak_bytes\":%zu," + "\"parser_peak_bytes\":%zu,\"rss_peak_bytes\":%" PRIu64 ",\"elapsed_ns\":%" PRIu64 ",\"cpu_ns\":%" PRIu64 "," + "\"parse_call_ns\":%" PRIu64 ",\"accepted\":%" PRIu64 ",\"emitted\":%" PRIu64 ",\"input_bytes\":%" PRIu64 "," + "\"selection_copy_bytes\":%" PRIu64 ",\"reader_copy_bytes\":%" PRIu64 ",\"parser_realloc_copy_bytes\":%" PRIu64 "," + "\"source_byte_seconds\":%.6Lf,\"output_byte_seconds\":%.6Lf,\"parser_byte_seconds\":%.6Lf," + "\"source_allocations\":%" PRIu64 ",\"output_allocations\":%" PRIu64 ",\"parser_allocations\":%" PRIu64 "," + "\"reuses\":%" PRIu64 ",\"budget_waits\":%" PRIu64 ",\"slot_waits\":%" PRIu64 "," + "\"compacted\":%" PRIu64 ",\"compact_skipped\":%" PRIu64 ",\"dedup_hits\":%" PRIu64 ",\"sleep_ns\":%" PRIu64 "," + "\"live_at_stop_bytes\":%zu,\"unreleased_buffer_bytes\":%zu,\"queues\":[", + e->status,e->policy,YYJSON_VERSION_STRING,e->budget,e->fixed,e->fixed+e->peak,e->peak_kind[SOURCE], + e->peak_kind[OUTPUT],e->peak_kind[PARSER],rss(),e->elapsed_ns,e->cpu_ns,e->parse_ns,e->accepted,e->emitted, + e->input_bytes,e->selection_copies,e->reader_copies,e->parser_copies,e->byte_ns[SOURCE]/1e9L, + e->byte_ns[OUTPUT]/1e9L,e->byte_ns[PARSER]/1e9L,e->allocations[SOURCE],e->allocations[OUTPUT], + e->allocations[PARSER],e->reuses,e->budget_waits,e->slot_waits,e->compacted,e->compact_skipped, + e->dedup_hits,e->sleep_ns,live_at_stop,e->reserved); + for(size_t k=0;kfanout;++k) { + Queue *q=&e->q[k];uint64_t target=q->emitted-q->emitted/100,sum=0,p99=0; + for(unsigned b=0;q->emitted && bhistogram[b];if(sum>=target){p99=upper(b);break;}} + fprintf(stderr,"%s{\"emitted\":%" PRIu64 ",\"bytes\":%" PRIu64 ",\"peak_count\":%zu," + "\"latency_mean_ns\":%.3f,\"latency_p99_upper_ns\":%" PRIu64 "}",k?",":"",q->emitted,q->bytes,q->peak, + q->emitted?(double)q->latency_sum/q->emitted:0,p99); + free(q->items); + } + fprintf(stderr,"]}\n");return !strcmp(e->status,"ok")?0:!strcmp(e->status,"cancelled")?3:2; +} diff --git a/research/experiments/003-json-fanout/plan.json b/research/experiments/003-json-fanout/plan.json new file mode 100644 index 0000000..60c179b --- /dev/null +++ b/research/experiments/003-json-fanout/plan.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "written_before_measurement": true, + "seed": 20260923, + "repeats": 5, + "policies": [ + {"name": "borrow", "policy": "borrow"}, + {"name": "copy", "policy": "copy"}, + {"name": "shared_copy", "policy": "shared_copy"}, + {"name": "threshold2", "policy": "threshold", "ratio": 2}, + {"name": "threshold8", "policy": "threshold", "ratio": 8}, + {"name": "compact", "policy": "compact", "ratio": 2}, + {"name": "copy_no_pool", "policy": "copy", "pool": 0} + ], + "cases": [ + {"name": "one_slow", "shape": "large", "records": 1024, "keys": ["tag"], "hold_us": [8000], "service_us": [0], "budget": 8388608, "slots": 64}, + {"name": "three_distinct_speeds", "shape": "large", "records": 1024, "keys": ["body", "tag", "tag"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 8388608, "slots": 64}, + {"name": "same_large_field", "shape": "large", "records": 1024, "keys": ["body", "body", "body"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 8388608, "slots": 64}, + {"name": "slow_large_field", "shape": "large", "records": 1024, "keys": ["tag", "tag", "body"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 8388608, "slots": 64}, + {"name": "mixed_tight", "shape": "mixed", "records": 1024, "keys": ["body", "tag", "tag"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 2097152, "slots": 64}, + {"name": "metadata_heavy", "shape": "metadata", "records": 512, "keys": ["body", "tag", "tag"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 2097152, "slots": 64} + ], + "expectations": [ + "A slow small-field consumer can retain the input after a fast large-field consumer finishes.", + "Sharing identical compact copies can remove duplicate per-consumer copies without a new runtime.", + "Late compaction may save copied bytes or fail to beat early size-based copying; all remaining owners matter.", + "Parser temporary memory and reader growth must be charged, even when retained selections are small.", + "No novel-policy advantage or production service SLO is presumed." + ], + "comparison_rule": "Report all fixed presets; exploratory measurements, no trained/held-out policy or significance claim.", + "limits": "Real yyjson parser and actual bytes, but synthetic NDJSON corpus and sink delays, three synchronous consumers in one event loop, no open-loop arrivals, thread races, durable output, or independent parser comparison." +} diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/complete.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/complete.json new file mode 100644 index 0000000..5bde4c3 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/complete.json @@ -0,0 +1,5 @@ +{ + "samples": 210, + "all_branch_outputs_match": true, + "finished_utc": "2026-09-22T13:11:31Z" +} diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/environment.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/environment.json new file mode 100644 index 0000000..3aa2455 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/environment.json @@ -0,0 +1,96 @@ +{ + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "python": "3.14.7", + "compiler": "cc (Debian 14.2.0-19) 14.2.0\nCopyright (C) 2024 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n", + "cpu_affinity": 0, + "builds": [ + { + "command": [ + "cc", + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-DYYJSON_DISABLE_WRITER=1", + "-DYYJSON_DISABLE_UTILS=1", + "-DYYJSON_DISABLE_INCR_READER=1", + "/home/eunhhu/work/pure/research/experiments/003-json-fanout/benchmark.c", + "/home/eunhhu/work/pure/research/experiments/003-json-fanout/vendor/yyjson/yyjson.c", + "-o", + "/tmp/pure-fanout-i4xs4fhg/benchmark" + ], + "stderr": "", + "sha256": "adfd209a990abe11bfd93337c56eb2c9266d6529899fc91cd2f3844c93ef67db", + "size_bytes": 842056, + "build_wall_ns": 15110774980 + }, + { + "command": [ + "cc", + "-std=c17", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", + "-DYYJSON_DISABLE_WRITER=1", + "-DYYJSON_DISABLE_UTILS=1", + "-DYYJSON_DISABLE_INCR_READER=1", + "-O1", + "-fsanitize=address,undefined", + "-fno-omit-frame-pointer", + "/home/eunhhu/work/pure/research/experiments/003-json-fanout/benchmark.c", + "/home/eunhhu/work/pure/research/experiments/003-json-fanout/vendor/yyjson/yyjson.c", + "-o", + "/tmp/pure-fanout-i4xs4fhg/benchmark-sanitized" + ], + "stderr": "", + "sha256": "c7516cf3c1c18463c72b96a75d70cc326a0ff43873fa278e0b13fb94d6e180d2", + "size_bytes": 5441872, + "build_wall_ns": 160671462690 + } + ], + "source_sha256": { + "benchmark.c": "c9ccc4544ef296eb59a8d071ad7fb25afc4b3fe5a94a8978c415c557b8ea8d0d", + "run.py": "7ab64e454f3ce0a48c20a477d8924b06ece6ad29a442d0bb49f48b5a6ecffe49", + "plan.json": "69f1258b3afd616404c6853526a5fbb6523d406eb43db479fd4be43905bc9f5d", + "vendor/yyjson/UPSTREAM.json": "eab60a505ff59ca1f3271792c16aa57d61f596cb5c57ff04dcff15d22ba29cee", + "vendor/yyjson/yyjson.c": "ac2e9bbb2e2d9149d90878d40506a1d624fa0b33c979a11b61075c54782c6d6a", + "vendor/yyjson/yyjson.h": "175867c5493a5df648cec566717fa1c29aa2f6096f5f0cf1efad0b65e1f6d7b3", + "vendor/yyjson/LICENSE": "45e384d3d52c73cba3a64d6e6c25d47cd738cd8a55c30629e3201046eda62947" + }, + "upstream": { + "project": "yyjson", + "version": "0.12.0", + "commit": "8b4a38dc994a110abaec8a400615567bd996105f", + "license": "MIT", + "files": { + "yyjson.c": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/src/yyjson.c", + "sha256": "ac2e9bbb2e2d9149d90878d40506a1d624fa0b33c979a11b61075c54782c6d6a", + "bytes": 410813 + }, + "yyjson.h": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/src/yyjson.h", + "sha256": "175867c5493a5df648cec566717fa1c29aa2f6096f5f0cf1efad0b65e1f6d7b3", + "bytes": 322407 + }, + "LICENSE": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/LICENSE", + "sha256": "45e384d3d52c73cba3a64d6e6c25d47cd738cd8a55c30629e3201046eda62947", + "bytes": 1084 + } + } + }, + "timing": "I/O, parsing, selection, consumers, instrumentation, cleanup; excludes setup/build/verification", + "parse_call_ns": "wall duration inside yyjson_read_opts including allocator backpressure and serviced consumers", + "rss": "Linux current-image VmHWM approximate; zero means unavailable on other OSes", + "limits": "synthetic closed-loop arrivals/delays; one thread; uncontrolled thermals/power; cached file I/O; no fsync", + "started_utc": "2026-09-22T13:10:45Z", + "cpuinfo": "processor\t: 0\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 1\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 2\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nprocessor\t: 3\nBogoMIPS\t: 108.00\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\nCPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x4\nCPU part\t: 0xd0b\nCPU revision\t: 1\n\nRevision\t: d04171\nSerial\t\t: d3ba8e625dbe42ac\nModel\t\t: Raspberry Pi 5 Model B Rev 1.1\n" +} diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/fixtures.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/fixtures.json new file mode 100644 index 0000000..dbd23e2 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/fixtures.json @@ -0,0 +1,128 @@ +[ + { + "case": "one_slow", + "seed": 20260923, + "records": 1024, + "input_bytes": 42083156, + "input_sha256": "ebd22d4a12c1369ce27459e454df0a93545ca816fe13a82401160130772a3bfd", + "keys": [ + "tag" + ], + "output_bytes_by_branch": [ + 57344 + ], + "output_sha256_by_branch": [ + "1acead91fea700e5a94dc4285a37251f71876a0b505d2638cae3b9abf99915dd" + ] + }, + { + "case": "three_distinct_speeds", + "seed": 20260923, + "records": 1024, + "input_bytes": 42083156, + "input_sha256": "ebd22d4a12c1369ce27459e454df0a93545ca816fe13a82401160130772a3bfd", + "keys": [ + "body", + "tag", + "tag" + ], + "output_bytes_by_branch": [ + 33554432, + 57344, + 57344 + ], + "output_sha256_by_branch": [ + "a48ec20b430e737841289f18d08572d3630f92bac4778c7b0aa5d23acf5361bd", + "1acead91fea700e5a94dc4285a37251f71876a0b505d2638cae3b9abf99915dd", + "1acead91fea700e5a94dc4285a37251f71876a0b505d2638cae3b9abf99915dd" + ] + }, + { + "case": "same_large_field", + "seed": 20260923, + "records": 1024, + "input_bytes": 42083156, + "input_sha256": "ebd22d4a12c1369ce27459e454df0a93545ca816fe13a82401160130772a3bfd", + "keys": [ + "body", + "body", + "body" + ], + "output_bytes_by_branch": [ + 33554432, + 33554432, + 33554432 + ], + "output_sha256_by_branch": [ + "a48ec20b430e737841289f18d08572d3630f92bac4778c7b0aa5d23acf5361bd", + "a48ec20b430e737841289f18d08572d3630f92bac4778c7b0aa5d23acf5361bd", + "a48ec20b430e737841289f18d08572d3630f92bac4778c7b0aa5d23acf5361bd" + ] + }, + { + "case": "slow_large_field", + "seed": 20260923, + "records": 1024, + "input_bytes": 42083156, + "input_sha256": "ebd22d4a12c1369ce27459e454df0a93545ca816fe13a82401160130772a3bfd", + "keys": [ + "tag", + "tag", + "body" + ], + "output_bytes_by_branch": [ + 57344, + 57344, + 33554432 + ], + "output_sha256_by_branch": [ + "1acead91fea700e5a94dc4285a37251f71876a0b505d2638cae3b9abf99915dd", + "1acead91fea700e5a94dc4285a37251f71876a0b505d2638cae3b9abf99915dd", + "a48ec20b430e737841289f18d08572d3630f92bac4778c7b0aa5d23acf5361bd" + ] + }, + { + "case": "mixed_tight", + "seed": 20260923, + "records": 1024, + "input_bytes": 49877076, + "input_sha256": "3bba97318415a0c32a6eb61b04b7d73214ffcca90ae00469b45c2db6b28f600c", + "keys": [ + "body", + "tag", + "tag" + ], + "output_bytes_by_branch": [ + 26757376, + 57344, + 57344 + ], + "output_sha256_by_branch": [ + "89a5a0467e4473dd4afb7307366793ba4840463a6e9d44cd1f13090f2775bac9", + "f9bdd5932d1e9201f9dfeaf5e85d14572740d5a13dd31491654f75dd4999b5cf", + "f9bdd5932d1e9201f9dfeaf5e85d14572740d5a13dd31491654f75dd4999b5cf" + ] + }, + { + "case": "metadata_heavy", + "seed": 20260923, + "records": 512, + "input_bytes": 5259666, + "input_sha256": "b3ddc9f18b946c466f3b5819538de80e062f734fcba70d2ed578445b0bd5392f", + "keys": [ + "body", + "tag", + "tag" + ], + "output_bytes_by_branch": [ + 262144, + 28672, + 28672 + ], + "output_sha256_by_branch": [ + "9aa5e0f4c17a62255942c9d23be621e437630e5fa34ef908d7c88033e132d700", + "a65807b99ca84b43058312cfdd6892e252bb47585f37f257f7dae2dcd7ee4be2", + "a65807b99ca84b43058312cfdd6892e252bb47585f37f257f7dae2dcd7ee4be2" + ] + } +] diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/plan.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/plan.json new file mode 100644 index 0000000..3ee7b4a --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/plan.json @@ -0,0 +1,177 @@ +{ + "version": 1, + "written_before_measurement": true, + "seed": 20260923, + "repeats": 5, + "policies": [ + { + "name": "borrow", + "policy": "borrow" + }, + { + "name": "copy", + "policy": "copy" + }, + { + "name": "shared_copy", + "policy": "shared_copy" + }, + { + "name": "threshold2", + "policy": "threshold", + "ratio": 2 + }, + { + "name": "threshold8", + "policy": "threshold", + "ratio": 8 + }, + { + "name": "compact", + "policy": "compact", + "ratio": 2 + }, + { + "name": "copy_no_pool", + "policy": "copy", + "pool": 0 + } + ], + "cases": [ + { + "name": "one_slow", + "shape": "large", + "records": 1024, + "keys": [ + "tag" + ], + "hold_us": [ + 8000 + ], + "service_us": [ + 0 + ], + "budget": 8388608, + "slots": 64 + }, + { + "name": "three_distinct_speeds", + "shape": "large", + "records": 1024, + "keys": [ + "body", + "tag", + "tag" + ], + "hold_us": [ + 0, + 5000, + 8000 + ], + "service_us": [ + 0, + 0, + 0 + ], + "budget": 8388608, + "slots": 64 + }, + { + "name": "same_large_field", + "shape": "large", + "records": 1024, + "keys": [ + "body", + "body", + "body" + ], + "hold_us": [ + 0, + 5000, + 8000 + ], + "service_us": [ + 0, + 0, + 0 + ], + "budget": 8388608, + "slots": 64 + }, + { + "name": "slow_large_field", + "shape": "large", + "records": 1024, + "keys": [ + "tag", + "tag", + "body" + ], + "hold_us": [ + 0, + 5000, + 8000 + ], + "service_us": [ + 0, + 0, + 0 + ], + "budget": 8388608, + "slots": 64 + }, + { + "name": "mixed_tight", + "shape": "mixed", + "records": 1024, + "keys": [ + "body", + "tag", + "tag" + ], + "hold_us": [ + 0, + 5000, + 8000 + ], + "service_us": [ + 0, + 0, + 0 + ], + "budget": 2097152, + "slots": 64 + }, + { + "name": "metadata_heavy", + "shape": "metadata", + "records": 512, + "keys": [ + "body", + "tag", + "tag" + ], + "hold_us": [ + 0, + 5000, + 8000 + ], + "service_us": [ + 0, + 0, + 0 + ], + "budget": 2097152, + "slots": 64 + } + ], + "expectations": [ + "A slow small-field consumer can retain the input after a fast large-field consumer finishes.", + "Sharing identical compact copies can remove duplicate per-consumer copies without a new runtime.", + "Late compaction may save copied bytes or fail to beat early size-based copying; all remaining owners matter.", + "Parser temporary memory and reader growth must be charged, even when retained selections are small.", + "No novel-policy advantage or production service SLO is presumed." + ], + "comparison_rule": "Report all fixed presets; exploratory measurements, no trained/held-out policy or significance claim.", + "limits": "Real yyjson parser and actual bytes, but synthetic NDJSON corpus and sink delays, three synchronous consumers in one event loop, no open-loop arrivals, thread races, durable output, or independent parser comparison." +} diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.csv b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.csv new file mode 100644 index 0000000..d2b9a02 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.csv @@ -0,0 +1,211 @@ +case,variant,repeat,status,policy,parser_version,budget_bytes,fixed_bytes,peak_reserved_bytes,source_peak_bytes,output_peak_bytes,parser_peak_bytes,rss_peak_bytes,elapsed_ns,cpu_ns,parse_call_ns,accepted,emitted,input_bytes,selection_copy_bytes,reader_copy_bytes,parser_realloc_copy_bytes,source_byte_seconds,output_byte_seconds,parser_byte_seconds,source_allocations,output_allocations,parser_allocations,reuses,budget_waits,slot_waits,compacted,compact_skipped,dedup_hits,sleep_ns,live_at_stop_bytes,unreleased_buffer_bytes,queues +one_slow,threshold2,0,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1835008,131703634,34750648,21989760,1024,1024,42083156,57344,75711296,0,1842.615404,919.75921,2883.244752,9,64,1,6056,0,16,0,0,0,97097912,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8051720.034, ""latency_p99_upper_ns"": 8388608}]" +one_slow,compact,0,ok,compact,0.12.0,8388608,33536,4427696,4230192,0,131120,5537792,131996119,38004572,22434299,1024,1024,42083156,0,75711296,0,540791.270719,0.0,2928.256675,72,0,1,5033,0,26,0,0,0,94192606,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8057605.331, ""latency_p99_upper_ns"": 8388608}]" +one_slow,borrow,0,ok,borrow,0.12.0,8388608,33536,4427696,4230192,0,131120,5521408,132124751,37612015,22444229,1024,1024,42083156,0,75711296,0,540845.471315,0.0,2930.090388,72,0,1,5033,0,53,0,0,0,94791711,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8058299.849, ""latency_p99_upper_ns"": 8388608}]" +one_slow,threshold8,0,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131620844,34446035,21905479,1024,1024,42083156,57344,75711296,0,1832.609103,920.078119,2872.388672,9,64,1,6056,0,16,0,0,0,97293567,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054286.008, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy_no_pool,0,ok,copy,0.12.0,8388608,33536,237296,98400,7168,131120,1835008,132233158,41767998,24756316,1024,1024,42083156,57344,75711296,0,2069.543688,920.140424,2901.308237,4082,1024,1024,0,0,19,0,0,0,90619067,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8059175.662, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy,0,ok,copy,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131600270,34455850,21964978,1024,1024,42083156,57344,75711296,0,1832.537919,920.025416,2880.380829,9,64,1,6056,0,16,0,0,0,97270417,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8053796.131, ""latency_p99_upper_ns"": 8388608}]" +one_slow,shared_copy,0,ok,shared_copy,0.12.0,8388608,33536,303072,98400,7168,131120,1802240,131715584,34456703,21940218,1024,1024,42083156,57344,75711296,0,1833.007769,920.07894,2877.016945,9,64,1,6056,0,16,0,0,0,97393619,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054276.953, ""latency_p99_upper_ns"": 8388608}]" +one_slow,compact,1,ok,compact,0.12.0,8388608,33536,4427696,4230192,0,131120,5537792,132171843,38070312,22523797,1024,1024,42083156,0,75711296,0,540815.06589,0.0,2937.269732,72,0,1,5033,0,40,0,0,0,94338529,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8057936.641, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy_no_pool,1,ok,copy,0.12.0,8388608,33536,237296,98400,7168,131120,1835008,132199048,42202499,24805054,1024,1024,42083156,57344,75711296,0,2084.967873,920.132302,2905.942804,4082,1024,1024,0,0,29,0,0,0,90146770,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8059478.906, ""latency_p99_upper_ns"": 8388608}]" +one_slow,shared_copy,1,ok,shared_copy,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131708165,34804961,22044118,1024,1024,42083156,57344,75711296,0,1844.634105,919.759432,2890.857973,9,64,1,6056,0,16,0,0,0,97053198,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8051751.524, ""latency_p99_upper_ns"": 8388608}]" +one_slow,threshold8,1,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131798127,34803856,22044512,1024,1024,42083156,57344,75711296,0,1846.36305,919.612987,2891.382584,9,64,1,6056,0,16,0,0,0,97156768,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050483.774, ""latency_p99_upper_ns"": 8388608}]" +one_slow,borrow,1,ok,borrow,0.12.0,8388608,33536,4427696,4230192,0,131120,5521408,131976979,38591495,22573964,1024,1024,42083156,0,75711296,0,540860.949553,0.0,2940.513903,72,0,1,5033,0,28,0,0,0,93626665,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8058679.423, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy,1,ok,copy,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131752053,34888537,21974890,1024,1024,42083156,57344,75711296,0,1845.194241,919.599974,2880.87266,9,64,1,6056,0,17,0,0,0,97030621,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050419.036, ""latency_p99_upper_ns"": 8388608}]" +one_slow,threshold2,1,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131720609,34747849,21999728,1024,1024,42083156,57344,75711296,0,1843.805295,919.628727,2884.923744,9,64,1,6056,0,16,0,0,0,97113750,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050578.244, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy,2,ok,copy,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131735071,34877739,22030534,1024,1024,42083156,57344,75711296,0,1847.409016,919.636562,2888.89668,9,64,1,6056,0,16,0,0,0,96987832,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050770.82, ""latency_p99_upper_ns"": 8388608}]" +one_slow,compact,2,ok,compact,0.12.0,8388608,33536,4427696,4230192,0,131120,5537792,132186905,38709650,22486562,1024,1024,42083156,0,75711296,0,540826.901328,0.0,2931.839266,72,0,1,5033,0,36,0,0,0,93736446,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8058254.455, ""latency_p99_upper_ns"": 8388608}]" +one_slow,shared_copy,2,ok,shared_copy,0.12.0,8388608,33536,303072,98400,7168,131120,1835008,134003964,36820353,22141327,1024,1024,42083156,57344,75711296,0,1907.646695,924.396419,2903.071145,9,116,1,6004,0,25,0,0,0,97345374,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8093864.112, ""latency_p99_upper_ns"": 9437184}]" +one_slow,threshold2,2,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,132093280,37140317,22338927,1024,1024,42083156,57344,75711296,0,1928.638533,919.864631,2930.310014,9,68,1,6052,0,20,0,0,0,95013211,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054865.09, ""latency_p99_upper_ns"": 8388608}]" +one_slow,borrow,2,ok,borrow,0.12.0,8388608,33536,4427696,4230192,0,131120,5554176,132469040,41862764,22964342,1024,1024,42083156,0,75711296,0,541099.43903,0.0,2986.938382,73,0,1,5032,0,49,0,0,0,90878204,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8063020.332, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy_no_pool,2,ok,copy,0.12.0,8388608,33536,237296,98400,7168,131120,1802240,137450461,46306762,25720817,1024,1024,42083156,57344,75711296,0,2217.245474,934.97033,2945.941354,4082,1024,1024,0,0,23,0,0,0,91429169,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8191749.104, ""latency_p99_upper_ns"": 11534336}]" +one_slow,threshold8,2,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131976855,36997295,22056693,1024,1024,42083156,57344,75711296,0,1904.528214,920.059714,2891.295258,9,65,1,6055,0,21,0,0,0,95230128,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8056258.001, ""latency_p99_upper_ns"": 8388608}]" +one_slow,threshold2,3,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,132144262,36810738,22394390,1024,1024,42083156,57344,75711296,0,1914.610493,920.080986,2937.462741,9,71,1,6049,0,39,0,0,0,95437850,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8056316.891, ""latency_p99_upper_ns"": 8388608}]" +one_slow,threshold8,3,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131627799,34748983,22050963,1024,1024,42083156,57344,75711296,0,1845.118437,920.020651,2891.313221,9,64,1,6056,0,19,0,0,0,96996391,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054019.278, ""latency_p99_upper_ns"": 8388608}]" +one_slow,compact,3,ok,compact,0.12.0,8388608,33536,4427696,4230192,0,131120,5537792,132093080,38378509,22460168,1024,1024,42083156,0,75711296,0,540907.798619,0.0,2928.186919,72,0,1,5033,0,43,0,0,0,93965206,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8059384.654, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy_no_pool,3,ok,copy,0.12.0,8388608,33536,237296,98400,7168,131120,1802240,132246841,41768202,24673720,1024,1024,42083156,57344,75711296,0,2066.312998,920.19798,2896.434244,4082,1024,1024,0,0,16,0,0,0,90608121,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8059680.607, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy,3,ok,copy,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131689453,35243759,22012604,1024,1024,42083156,57344,75711296,0,1857.757504,919.947028,2887.51415,9,68,1,6052,0,27,0,0,0,96622803,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8053765.222, ""latency_p99_upper_ns"": 8388608}]" +one_slow,shared_copy,3,ok,shared_copy,0.12.0,8388608,33536,303072,98400,7168,131120,1802240,132103712,35248408,22004780,1024,1024,42083156,57344,75711296,0,1858.44117,919.973445,2885.070598,9,73,1,6047,0,22,0,0,0,97018510,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054006.321, ""latency_p99_upper_ns"": 8388608}]" +one_slow,borrow,3,ok,borrow,0.12.0,8388608,33536,4427696,4230192,0,131120,5537792,132436008,40869521,22656213,1024,1024,42083156,0,75711296,0,540941.338269,0.0,2953.318689,75,0,1,5030,0,52,0,0,0,91954404,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8060462.982, ""latency_p99_upper_ns"": 8388608}]" +one_slow,compact,4,ok,compact,0.12.0,8388608,33536,4427696,4230192,0,131120,5554176,132110379,38257181,22557256,1024,1024,42083156,0,75711296,0,540811.125254,0.0,2944.323595,72,0,1,5033,0,56,0,0,0,94112952,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8057929.717, ""latency_p99_upper_ns"": 8388608}]" +one_slow,threshold8,4,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1802240,131804916,34977352,22094634,1024,1024,42083156,57344,75711296,0,1853.831647,920.149467,2897.713188,9,67,1,6053,0,31,0,0,0,96937037,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8055361.395, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy_no_pool,4,ok,copy,0.12.0,8388608,33536,237296,98400,7168,131120,1818624,135161451,44330024,25503982,1024,1024,42083156,57344,75711296,0,2157.809223,924.543547,2946.975235,4082,1024,1024,0,0,35,0,0,0,91118882,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8099348.558, ""latency_p99_upper_ns"": 10485760}]" +one_slow,threshold2,4,ok,threshold,0.12.0,8388608,33536,303072,98400,7168,131120,1802240,132588310,37673671,22518430,1024,1024,42083156,57344,75711296,0,1949.11739,920.155569,2954.042078,9,78,1,6042,0,36,0,0,0,94895218,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8057756.78, ""latency_p99_upper_ns"": 8388608}]" +one_slow,copy,4,ok,copy,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,131620718,34580946,21993246,1024,1024,42083156,57344,75711296,0,1837.245494,919.925321,2884.001314,9,64,1,6056,0,16,0,0,0,97154068,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8053071.22, ""latency_p99_upper_ns"": 8388608}]" +one_slow,borrow,4,ok,borrow,0.12.0,8388608,33536,4427696,4230192,0,131120,5537792,132000791,37927424,22507290,1024,1024,42083156,0,75711296,0,540831.163899,0.0,2938.768696,72,0,1,5033,0,30,0,0,0,94241105,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8058140.749, ""latency_p99_upper_ns"": 8388608}]" +one_slow,shared_copy,4,ok,shared_copy,0.12.0,8388608,33536,303072,98400,7168,131120,1818624,132283829,37157688,22116989,1024,1024,42083156,57344,75711296,0,1917.711679,920.053817,2900.603467,9,67,1,6053,0,38,0,0,0,95415784,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8056291.708, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,borrow,0,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,5373952,132804710,49883960,22784485,1024,3072,42083156,0,75711296,0,540593.356702,0.0,2990.235132,72,0,1,5033,0,578,0,0,0,84130488,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 45505.588, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070252.164, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8055489.045, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,compact,0,ok,compact,0.12.0,8388608,41728,4436784,4230192,6944,131120,5537792,132699783,50261222,22546544,1024,3072,42083156,57344,75711296,0,340105.36302,342.277671,2958.801865,897,913,1,4319,0,550,1024,0,0,83652241,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 45463.711, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069982.729, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054576.42, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,shared_copy,0,ok,shared_copy,0.12.0,8388608,41728,311264,98400,39984,131120,1835008,132438636,46508730,22104890,1024,3072,42083156,33611776,75711296,0,1994.687521,1188.73405,2903.952009,9,64,1,7080,0,538,0,0,1024,87037859,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42492.519, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069205.345, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8052022.729, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy,0,ok,copy,0.12.0,8388608,41728,318432,98400,47152,131120,1851392,132301692,46274024,22012014,1024,3072,42083156,33669120,75711296,0,1991.776071,1766.57547,2891.89841,9,1025,1,7143,0,563,0,0,0,87083196,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42221.575, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068352.743, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8049671.308, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy_no_pool,0,ok,copy,0.12.0,8388608,41728,252544,98400,47152,131120,1933312,132221655,46432790,22094470,1024,3072,42083156,33669120,75711296,0,2002.66499,1764.18462,2896.537304,4082,3072,1024,0,0,534,0,0,0,86822233,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42461.52, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068300.921, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054065.407, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold2,0,ok,threshold,0.12.0,8388608,41728,311264,98400,39984,131120,1851392,132423215,46202689,22137673,1024,3072,42083156,33611776,75711296,0,1992.776523,1183.803646,2909.259091,9,64,1,7080,0,559,0,0,1024,87298460,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42209.067, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068504.791, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8052025.9, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold8,0,ok,threshold,0.12.0,8388608,41728,311264,98400,7168,131120,1851392,132072752,44818714,22007505,1024,3072,42083156,57344,75711296,0,2372.402719,919.579066,2891.111165,9,64,1,6056,0,531,0,0,1024,88271004,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 41081.91, ""latency_p99_upper_ns"": 49152}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067409.945, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8052229.961, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy,1,ok,copy,0.12.0,8388608,41728,318432,98400,47152,131120,1851392,132122049,45960158,22062914,1024,3072,42083156,33669120,75711296,0,1985.913742,1766.681864,2899.647733,9,1025,1,7143,0,530,0,0,0,87159643,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42080.341, ""latency_p99_upper_ns"": 53248}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068084.019, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8053834.564, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy_no_pool,1,ok,copy,0.12.0,8388608,41728,252544,98400,47152,131120,1884160,132225178,46345346,22106440,1024,3072,42083156,33669120,75711296,0,1998.568774,1764.360213,2898.609,4082,3072,1024,0,0,531,0,0,0,86929226,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42412.925, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068204.178, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8052346.592, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold2,1,ok,threshold,0.12.0,8388608,41728,311264,98400,39984,131120,1851392,132119197,45785542,22119160,1024,3072,42083156,33611776,75711296,0,1985.975023,1182.61037,2906.107753,9,64,1,7080,0,535,0,0,1024,87289352,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42063.476, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068193.573, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8051854.179, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,compact,1,ok,compact,0.12.0,8388608,41728,4436784,4230192,6944,131120,5603328,132689456,50225388,22679397,1024,3072,42083156,57344,75711296,0,340115.562513,342.313483,2976.797692,896,908,1,4325,0,548,1024,0,0,83654110,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 45534.298, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070135.917, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8055038.37, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold8,1,ok,threshold,0.12.0,8388608,41728,311264,98400,7168,131120,1851392,132687220,45610919,22143479,1024,3072,42083156,57344,75711296,0,2398.303122,919.310014,2908.551305,9,73,1,6047,0,557,0,0,1024,88180228,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 41592.506, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067924.312, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050249.576, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,shared_copy,1,ok,shared_copy,0.12.0,8388608,41728,311264,98400,39984,131120,1851392,133179387,47009567,22053622,1024,3072,42083156,33611776,75711296,0,2001.098596,1195.094634,2898.481421,9,64,1,7080,0,619,0,0,1024,87400987,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42811.697, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068344.258, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054983.926, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,borrow,1,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,5373952,132590517,49691116,22667622,1024,3072,42083156,0,75711296,0,540625.036225,0.0,2974.002082,72,0,1,5033,0,555,0,0,0,84069041,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 45356.652, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069262.154, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8055874.806, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,borrow,2,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,5373952,132595906,48936945,22622066,1024,3072,42083156,0,75711296,0,540538.226737,0.0,2969.006017,72,0,1,5033,0,550,0,0,0,84820014,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 44590.665, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069713.765, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054486.477, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold2,2,ok,threshold,0.12.0,8388608,41728,311264,98400,39984,131120,1851392,132243573,45674856,22029775,1024,3072,42083156,33611776,75711296,0,1978.146096,1181.588579,2894.51268,9,64,1,7080,0,547,0,0,1024,87607899,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 41881.888, ""latency_p99_upper_ns"": 53248}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067912.655, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8049931.179, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,compact,2,ok,compact,0.12.0,8388608,41728,4436784,4230192,6944,131120,5603328,132732463,49739534,22671992,1024,3072,42083156,57344,75711296,0,340105.112239,342.468555,2974.922545,893,887,1,4349,0,562,1024,0,0,84142850,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 44979.318, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069891.561, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8056164.197, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold8,2,ok,threshold,0.12.0,8388608,41728,311264,98400,7168,131120,1851392,132272059,45320979,22141356,1024,3072,42083156,57344,75711296,0,2389.486333,919.390876,2908.882515,9,64,1,6056,0,563,0,0,1024,88007671,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 41406.16, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068143.112, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050851.553, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy,2,ok,copy,0.12.0,8388608,41728,318432,98400,47152,131120,1867776,132351078,46122525,22082400,1024,3072,42083156,33669120,75711296,0,1991.903399,1762.771138,2900.645032,9,1026,1,7142,0,558,0,0,0,87297075,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42091.413, ""latency_p99_upper_ns"": 53248}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068444.485, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8048834.24, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,shared_copy,2,ok,shared_copy,0.12.0,8388608,41728,311264,98400,39984,131120,1851392,132284819,45823432,22066434,1024,3072,42083156,33611776,75711296,0,1983.569581,1182.323172,2898.432644,9,64,1,7080,0,542,0,0,1024,87424368,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42064.699, ""latency_p99_upper_ns"": 53248}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068229.227, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050900.843, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy_no_pool,2,ok,copy,0.12.0,8388608,41728,252544,98400,47152,131120,1900544,132459707,47692750,22170762,1024,3072,42083156,33669120,75711296,0,2023.677274,1777.322506,2907.132194,4082,3072,1024,0,0,540,0,0,0,85990890,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 43327.485, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070694.294, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8055086.674, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,shared_copy,3,ok,shared_copy,0.12.0,8388608,41728,311264,98400,39984,131120,1851392,132140503,45825210,22125197,1024,3072,42083156,33611776,75711296,0,1985.69773,1183.215978,2906.687041,9,64,1,7080,0,533,0,0,1024,87310088,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42062.03, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068036.782, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050191.375, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy_no_pool,3,ok,copy,0.12.0,8388608,41728,252544,98400,47152,131120,1916928,132297747,46669804,22175323,1024,3072,42083156,33669120,75711296,0,2009.648765,1766.146335,2907.094824,4082,3072,1024,0,0,542,0,0,0,86663675,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42678.905, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068637.263, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054142.04, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy,3,ok,copy,0.12.0,8388608,41728,318432,98400,47152,131120,1851392,132282510,46415608,22079359,1024,3072,42083156,33669120,75711296,0,1996.444688,1765.761752,2901.17633,9,1025,1,7143,0,554,0,0,0,86986908,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42277.028, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070038.574, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050279.443, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,borrow,3,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,5373952,132489898,48808681,22654163,1024,3072,42083156,0,75711296,0,540484.370521,0.0,2972.753951,72,0,1,5033,0,556,0,0,0,84863248,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 44578.165, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069410.422, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8053620.063, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold2,3,ok,threshold,0.12.0,8388608,41728,311264,98400,39984,131120,1835008,132283398,45911917,22085581,1024,3072,42083156,33611776,75711296,0,1984.871517,1183.416247,2903.347153,9,64,1,7080,0,561,0,0,1024,87433143,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42043.254, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068356.958, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8050581.195, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold8,3,ok,threshold,0.12.0,8388608,41728,311264,98400,7168,131120,1851392,132082805,44815017,22070822,1024,3072,42083156,57344,75711296,0,2374.331913,919.214871,2899.672121,9,64,1,6056,0,534,0,0,1024,88275454,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 41070.548, ""latency_p99_upper_ns"": 53248}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067369.015, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8049008.122, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,compact,3,ok,compact,0.12.0,8388608,41728,4436784,4230192,7056,131120,5488640,132597323,49981497,22749218,1024,3072,42083156,57344,75711296,0,340108.105991,342.554096,2986.081381,912,907,1,4310,0,563,1024,0,0,83819563,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 45265.223, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069991.558, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8056976.805, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy_no_pool,4,ok,copy,0.12.0,8388608,41728,252544,98400,47152,131120,1900544,132985160,49981054,23442854,1024,3072,42083156,33669120,75711296,0,2129.142526,1795.211172,3074.346252,4082,3072,1024,0,0,542,0,0,0,84089494,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 45510.754, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5098662.604, ""latency_p99_upper_ns"": 6291456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8056999.864, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,borrow,4,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,5373952,133433884,51462618,23747967,1024,3072,42083156,0,75711296,0,540814.68462,0.0,3118.422109,72,0,1,5033,0,596,0,0,0,83315856,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 46811.327, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070925.9, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8058845.673, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold2,4,ok,threshold,0.12.0,8388608,41728,311264,98400,39984,131120,1867776,132186422,45811373,22083462,1024,3072,42083156,33611776,75711296,0,1983.12097,1182.647891,2900.448352,9,64,1,7080,0,532,0,0,1024,87421696,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42006.171, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068025.061, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8049545.414, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,shared_copy,4,ok,shared_copy,0.12.0,8388608,41728,311264,98400,39984,131120,1851392,132188663,45991210,22145612,1024,3072,42083156,33611776,75711296,0,1991.371251,1184.961172,2909.800223,9,64,1,7080,0,543,0,0,1024,87186189,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42139.616, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068490.238, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8052428.242, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,compact,4,ok,compact,0.12.0,8388608,41728,4436784,4230192,7056,131120,5537792,132746292,50870250,22660664,1024,3072,42083156,57344,75711296,0,340246.674827,342.038296,2973.982939,893,895,1,4341,0,545,1024,0,0,83141858,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 45809.599, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5072116.599, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8054634.315, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,threshold8,4,ok,threshold,0.12.0,8388608,41728,311264,98400,7168,131120,1851392,132177385,44977578,22128368,1024,3072,42083156,57344,75711296,0,2381.095196,919.555429,2907.397974,9,64,1,6056,0,539,0,0,1024,88187889,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 41268.295, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067698.961, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8052209.973, ""latency_p99_upper_ns"": 8388608}]" +three_distinct_speeds,copy,4,ok,copy,0.12.0,8388608,41728,318432,98400,47152,131120,1867776,132225648,46118905,22053057,1024,3072,42083156,33669120,75711296,0,1988.501642,1767.427593,2896.491937,9,1025,1,7143,0,536,0,0,0,87128865,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 42165.024, ""latency_p99_upper_ns"": 53248}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068266.918, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8051060.116, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy_no_pool,0,ok,copy,0.12.0,8388608,41728,4373248,98400,4233264,131120,6078464,216945564,116157398,25703347,1024,3072,42083156,100663296,75711296,0,3917.681175,512250.563648,3150.964519,4082,3072,1024,0,0,383,0,0,0,48939452,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 117402.264, ""latency_p99_upper_ns"": 524288}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 6063368.282, ""latency_p99_upper_ns"": 31457280}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 9260486.827, ""latency_p99_upper_ns"": 33554432}]" +same_large_field,shared_copy,0,ok,shared_copy,0.12.0,8388608,41728,2371504,98400,2100224,131120,3178496,133534289,66640259,22303696,1024,3072,42083156,33554432,75711296,0,2044.238204,269834.895225,2930.529378,82,0,1,6047,0,500,0,0,2048,67897726,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51484.234, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078396.125, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8065897.697, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,borrow,0,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133553215,68694192,22344284,1024,3072,42083156,0,75711296,0,541236.958782,0.0,2932.875901,76,0,1,5029,0,479,0,0,0,65966946,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52115.836, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078241.217, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8065472.756, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy,0,ok,copy,0.12.0,8388608,41728,4504544,98400,4233264,131120,5292032,134319516,74842901,22151327,1024,3072,42083156,100663296,75711296,0,2277.11069,440218.115158,2909.330552,9,841,1,7327,0,490,0,0,0,60554269,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 57181.88, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5083619.048, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8071315.085, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold2,0,ok,threshold,0.12.0,8388608,41728,2371504,98400,2100224,131120,3194880,133454646,66234578,22159339,1024,3072,42083156,33554432,75711296,0,2031.818258,269861.554419,2910.266617,78,0,1,6051,0,487,0,0,2048,68332882,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51484.504, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5077341.499, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8066441.419, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,compact,0,ok,compact,0.12.0,8388608,41728,4730672,4230192,2001776,131120,6340608,134096312,74886880,22733900,1024,3072,42083156,33554432,75711296,0,341111.908821,100293.13752,2983.138393,921,779,1,4429,0,471,1024,0,0,60362834,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 54080.027, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5083405.479, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068401.165, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold8,0,ok,threshold,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133447386,68765370,22353361,1024,3072,42083156,0,75711296,0,541359.828153,0.0,2934.692831,77,0,1,5028,0,487,0,0,0,65809490,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52372.427, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078845.858, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067374.837, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,compact,1,ok,compact,0.12.0,8388608,41728,4730672,4230192,2034592,131120,6340608,134204038,74836392,22853483,1024,3072,42083156,33554432,75711296,0,341117.919471,100364.674661,2998.098529,881,767,1,4481,0,478,1024,0,0,60375968,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 54356.541, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5083649.875, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070735.206, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,shared_copy,1,ok,shared_copy,0.12.0,8388608,41728,2371504,98400,2100224,131120,3244032,133593447,66172548,22181499,1024,3072,42083156,33554432,75711296,0,2031.917671,269851.910584,2913.380586,80,0,1,6049,0,471,0,0,2048,68489180,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51688.946, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5077231.39, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8066199.086, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy,1,ok,copy,0.12.0,8388608,41728,4504544,98400,4233264,131120,5292032,134436483,75053693,22215239,1024,3072,42083156,100663296,75711296,0,2284.410516,440251.541897,2917.091676,9,832,1,7336,0,483,0,0,0,60480780,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 57382.812, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5083530.123, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8072471.683, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold2,1,ok,threshold,0.12.0,8388608,41728,2371504,98400,2100224,131120,3194880,134034428,69077852,22386881,1024,3072,42083156,33554432,75711296,0,2071.219143,269907.925264,2941.415878,82,0,1,6047,0,521,0,0,2048,66160244,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52463.206, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5079970.677, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068543.087, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold8,1,ok,threshold,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133602318,69361342,22347294,1024,3072,42083156,0,75711296,0,541336.354919,0.0,2933.602437,76,0,1,5029,0,500,0,0,0,65433839,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52034.221, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5079700.568, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067086.654, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,borrow,1,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133878082,69431646,22393309,1024,3072,42083156,0,75711296,0,541341.633608,0.0,2938.771712,78,0,1,5027,0,504,0,0,0,65629618,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52136.236, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078986.712, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067041.718, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy_no_pool,1,ok,copy,0.12.0,8388608,41728,4373248,98400,4233264,131120,5816320,134331933,75355273,22600489,1024,3072,42083156,100663296,75711296,0,2317.391148,440282.284319,2923.56271,4082,3072,1024,0,0,497,0,0,0,60100461,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 58242.85, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5084649.973, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8073924.058, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,compact,2,ok,compact,0.12.0,8388608,41728,4730672,4230192,2001776,131120,6340608,134074359,74679639,22736073,1024,3072,42083156,33554432,75711296,0,341236.488763,100267.211633,2982.994817,917,794,1,4418,0,471,1024,0,0,60515136,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 53738.105, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5085195.058, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069412.397, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy,2,ok,copy,0.12.0,8388608,41728,4504544,98400,4233264,131120,5275648,136064340,79416596,22572078,1024,3072,42083156,100663296,75711296,0,2363.378088,440586.829499,2964.218039,9,623,1,7545,0,463,0,0,0,57551670,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 60112.085, ""latency_p99_upper_ns"": 114688}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5087074.439, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8080314.649, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold2,2,ok,threshold,0.12.0,8388608,41728,2371504,98400,2100224,131120,3244032,133655494,66354625,22289900,1024,3072,42083156,33554432,75711296,0,2049.709126,269797.797722,2927.700988,78,0,1,6051,0,500,0,0,2048,68212138,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51495.073, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078118.015, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8064823.351, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,shared_copy,2,ok,shared_copy,0.12.0,8388608,41728,2371504,98400,2100224,131120,3293184,133683827,66973903,22176603,1024,3072,42083156,33554432,75711296,0,2039.054037,269818.155226,2912.084727,78,0,1,6051,0,494,0,0,2048,67816366,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51459.966, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5079235.137, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8065334.704, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold8,2,ok,threshold,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133681327,68797289,22338685,1024,3072,42083156,0,75711296,0,541289.267391,0.0,2932.086428,77,0,1,5028,0,482,0,0,0,65978872,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52563.022, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078578.492, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8066255.165, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,borrow,2,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133814643,68888479,22335355,1024,3072,42083156,0,75711296,0,541341.207084,0.0,2930.637289,79,0,1,5026,0,510,0,0,0,66149871,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51550.405, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5079607.126, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067127.786, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy_no_pool,2,ok,copy,0.12.0,8388608,41728,4373248,98400,4233264,131120,5816320,134471775,75393460,22629325,1024,3072,42083156,100663296,75711296,0,2321.181236,440258.792854,2921.16977,4082,3072,1024,0,0,508,0,0,0,60227676,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 57516.822, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5084982.783, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8073926.553, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold2,3,ok,threshold,0.12.0,8388608,41728,2371504,98400,2100224,131120,3194880,133441147,66434679,22119254,1024,3072,42083156,33554432,75711296,0,2030.079604,269825.85573,2905.779822,80,0,1,6049,0,482,0,0,2048,68150063,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51190.857, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5077733.031, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8065349.133, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy_no_pool,3,ok,copy,0.12.0,8388608,41728,4373248,98400,4233264,131120,5816320,134289332,76002725,22740343,1024,3072,42083156,100663296,75711296,0,2334.193158,440332.993703,2936.962125,4082,3072,1024,0,0,504,0,0,0,59412631,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 59077.132, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5084960.638, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8075279.097, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,shared_copy,3,ok,shared_copy,0.12.0,8388608,41728,2371504,98400,2100224,131120,3309568,133495777,66421587,22309367,1024,3072,42083156,33554432,75711296,0,2045.123956,269842.975673,2931.949276,76,0,1,6053,0,497,0,0,2048,68249668,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51216.396, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078028.333, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8066089.677, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,borrow,3,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,134337593,71105404,22575073,1024,3072,42083156,0,75711296,0,541448.462493,0.0,2962.344728,83,0,1,5022,0,495,0,0,0,64431294,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 53339.614, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5080292.879, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068765.424, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy,3,ok,copy,0.12.0,8388608,41728,4504544,98400,4233264,131120,5292032,134254523,76196602,22285338,1024,3072,42083156,100663296,75711296,0,2295.795833,440394.753415,2927.722098,9,820,1,7348,0,485,0,0,0,59133245,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 58635.959, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5084371.237, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8075247.27, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,compact,3,ok,compact,0.12.0,8388608,41728,4730672,4230192,2034592,131120,6340608,134306041,75274107,22714288,1024,3072,42083156,33554432,75711296,0,341191.973388,100297.728905,2980.354846,917,749,1,4463,0,478,1024,0,0,60108647,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 54208.683, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5084673.227, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069796.873, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold8,3,ok,threshold,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133817208,69542587,22437830,1024,3072,42083156,0,75711296,0,541408.186663,0.0,2945.936502,80,0,1,5025,0,483,0,0,0,65488224,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52016.168, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5080750.668, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068031.861, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy_no_pool,4,ok,copy,0.12.0,8388608,41728,4373248,98400,4233264,131120,5799936,134597856,77396908,22833216,1024,3072,42083156,100663296,75711296,0,2355.524795,440416.150759,2941.859195,4082,3072,1024,0,0,512,0,0,0,58493375,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 59008.762, ""latency_p99_upper_ns"": 98304}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5086107.088, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8076875.839, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,copy,4,ok,copy,0.12.0,8388608,41728,4504544,98400,4233264,131120,5292032,139508137,88332555,22864596,1024,3072,42083156,100663296,75711296,0,2528.253394,442384.852596,3002.901193,9,688,1,7480,0,372,0,0,0,51495885,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 66698.854, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5129096.339, ""latency_p99_upper_ns"": 6815744}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8093643.623, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold8,4,ok,threshold,0.12.0,8388608,41728,4435888,4230192,0,131120,4833280,134457305,70251788,22431999,1024,3072,42083156,0,75711296,0,541395.863387,0.0,2942.397573,87,0,1,5018,0,495,0,0,0,65292089,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 53169.212, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5080238.856, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068047.314, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,threshold2,4,ok,threshold,0.12.0,8388608,41728,2371504,98400,2100224,131120,3211264,133756102,67373341,22155214,1024,3072,42083156,33554432,75711296,0,2045.927873,269868.863264,2910.706656,81,0,1,6048,0,506,0,0,2048,67548515,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 51135.428, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078670.025, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8066898.388, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,shared_copy,4,ok,shared_copy,0.12.0,8388608,41728,2371504,98400,2100224,131120,3571712,135030934,71236281,22446961,1024,3072,42083156,33554432,75711296,0,2100.123381,269953.875704,2948.934561,94,0,1,6035,0,507,0,0,2048,65033726,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 53626.014, ""latency_p99_upper_ns"": 90112}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5087279.412, ""latency_p99_upper_ns"": 5767168}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070672.959, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,compact,4,ok,compact,0.12.0,8388608,41728,4730672,4230192,1936144,131120,6340608,135831514,84355786,23107923,1024,3072,42083156,33554432,75711296,0,341580.411374,100419.005421,3032.899482,853,676,1,4600,0,455,1024,0,0,52913091,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 60319.001, ""latency_p99_upper_ns"": 114688}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5091148.147, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8079928.47, ""latency_p99_upper_ns"": 8388608}]" +same_large_field,borrow,4,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133667291,68534602,22264001,1024,3072,42083156,0,75711296,0,541292.227145,0.0,2921.518156,77,0,1,5028,0,481,0,0,0,66216721,0,0,"[{""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 1, ""latency_mean_ns"": 52072.538, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 5078370.622, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8066280.939, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,copy,0,ok,copy,0.12.0,8388608,41728,2378784,98400,2107504,131120,3899392,133407209,54623208,22433685,1024,3072,42083156,33669120,75711296,0,2079.608844,270519.575773,2946.904954,118,838,1,7221,0,547,0,0,0,79664236,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 47700.066, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070062.768, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069584.641, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold2,0,ok,threshold,0.12.0,8388608,41728,2378672,98400,2107392,131120,3751936,133312135,53364925,22316231,1024,3072,42083156,33611776,75711296,0,2049.372724,270524.406644,2928.92106,137,841,1,6175,0,552,0,0,1024,80930688,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46742.189, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068879.809, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069119.599, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,shared_copy,0,ok,shared_copy,0.12.0,8388608,41728,2378672,98400,2107392,131120,3948544,154001095,65368726,23437273,1024,3072,42083156,33611776,75711296,0,2683.230182,276097.966097,3083.783614,228,746,1,6179,0,542,0,0,1024,75659422,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 64443.196, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5134851.331, ""latency_p99_upper_ns"": 7340032}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8252758.487, ""latency_p99_upper_ns"": 15728640}]" +slow_large_field,copy_no_pool,0,ok,copy,0.12.0,8388608,41728,2312896,98400,2107504,131120,3194880,133648283,61986524,25370210,1024,3072,42083156,33669120,75711296,0,2299.022458,270612.233717,2983.123314,4082,3072,1024,0,0,653,0,0,0,72630111,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 53967.42, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5080263.648, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8075763.615, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,compact,0,ok,compact,0.12.0,8388608,41728,4730672,4230192,2067408,131120,6340608,133472857,62602975,23134595,1024,3072,42083156,33554432,75711296,0,340709.17918,100724.838498,3036.377571,923,841,1,4365,0,526,1024,0,0,71679132,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 48473.289, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5073065.738, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070772.518, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold8,0,ok,threshold,0.12.0,8388608,41728,4443056,4230192,7168,131120,4751360,133297584,55094453,22718797,1024,3072,42083156,57344,75711296,0,541537.659914,577.166247,2982.289653,130,809,1,5190,0,541,0,0,1024,79050000,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 48100.386, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069482.618, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070037.129, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,borrow,0,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133477288,54372368,22466675,1024,3072,42083156,0,75711296,0,541562.393801,0.0,2949.789857,80,0,1,5025,0,554,0,0,0,80055686,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46918.51, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069158.581, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070277.888, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold2,1,ok,threshold,0.12.0,8388608,41728,2378672,98400,2107392,131120,3932160,133312509,53734268,22533891,1024,3072,42083156,33611776,75711296,0,2071.170012,270512.249274,2959.58098,159,828,1,6166,0,575,0,0,1024,80473327,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46789.037, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070040.373, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069081.323, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,copy_no_pool,1,ok,copy,0.12.0,8388608,41728,2312896,98400,2107504,131120,3194880,134281084,62190824,25213500,1024,3072,42083156,33669120,75711296,0,2300.277794,270647.838041,2975.251132,4082,3072,1024,0,0,663,0,0,0,73016473,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 54287.927, ""latency_p99_upper_ns"": 98304}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5080276.456, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8077074.059, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,copy,1,ok,copy,0.12.0,8388608,41728,2378784,98400,2107504,131120,3948544,133395362,55263103,22335683,1024,3072,42083156,33669120,75711296,0,2070.323199,270570.958879,2935.679247,123,825,1,7229,0,563,0,0,0,79157676,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 47428.456, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070071.895, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070912.004, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,shared_copy,1,ok,shared_copy,0.12.0,8388608,41728,2378672,98400,2107392,131120,3522560,133405307,53873137,22349414,1024,3072,42083156,33611776,75711296,0,2061.271642,270527.998952,2936.171471,144,811,1,6198,0,551,0,0,1024,80515381,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46564.507, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069570.9, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069438.264, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,compact,1,ok,compact,0.12.0,8388608,41728,4730672,4230192,2100224,131120,6340608,133295478,60929076,22892803,1024,3072,42083156,33554432,75711296,0,340588.424197,100730.999866,3007.852808,919,842,1,4368,0,524,1024,0,0,73271182,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 48259.733, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5071843.127, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069717.131, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,borrow,1,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,5390336,133317978,54530428,22789187,1024,3072,42083156,0,75711296,0,541551.170985,0.0,2991.654899,72,0,1,5033,0,541,0,0,0,79523636,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 47420.19, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5069779.505, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070214.071, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold8,1,ok,threshold,0.12.0,8388608,41728,4443056,4230192,7168,131120,5357568,133341496,54857945,22618201,1024,3072,42083156,57344,75711296,0,541518.37496,577.265132,2971.205031,122,835,1,5172,0,562,0,0,1024,79396532,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 47550.497, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070242.543, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069675.14, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold8,2,ok,threshold,0.12.0,8388608,41728,4443056,4230192,7168,131120,5292032,134650847,58145169,22801055,1024,3072,42083156,57344,75711296,0,542663.104532,578.298773,2995.063232,127,804,1,5198,0,553,0,0,1024,77457652,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 49310.575, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5080294.837, ""latency_p99_upper_ns"": 5767168}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8086977.866, ""latency_p99_upper_ns"": 9437184}]" +slow_large_field,copy,2,ok,copy,0.12.0,8388608,41728,2378784,98400,2107504,131120,3964928,133179940,52486508,22146823,1024,3072,42083156,33669120,75711296,0,2028.917653,270508.558846,2910.035715,119,875,1,7183,0,532,0,0,0,81744820,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46179.91, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068139.702, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068218.996, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,borrow,2,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133254368,52756869,22242096,1024,3072,42083156,0,75711296,0,541465.495701,0.0,2919.456949,79,0,1,5026,0,537,0,0,0,81620293,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 45804.026, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067325.337, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068611.8, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,compact,2,ok,compact,0.12.0,8388608,41728,4730672,4230192,2100224,131120,6340608,133273853,59503628,22579568,1024,3072,42083156,33554432,75711296,0,340521.103381,100725.988305,2964.439632,921,839,1,4369,0,529,1024,0,0,75001351,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46820.147, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5071087.53, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068767.35, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,shared_copy,2,ok,shared_copy,0.12.0,8388608,41728,2378672,98400,2107392,131120,3293184,133216315,51631438,22079426,1024,3072,42083156,33611776,75711296,0,2014.496107,270449.091989,2901.711431,152,827,1,6174,0,543,0,0,1024,82698735,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 45041.785, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068002.393, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8066161.602, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,copy_no_pool,2,ok,copy,0.12.0,8388608,41728,2312896,98400,2107504,131120,3178496,133362779,58084129,24178012,1024,3072,42083156,33669120,75711296,0,2196.167723,270535.919777,2934.024512,4082,3072,1024,0,0,605,0,0,0,76526627,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 50758.917, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5074265.021, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8071772.438, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold2,2,ok,threshold,0.12.0,8388608,41728,2378672,98400,2107392,131120,3751936,133094723,52669444,22153975,1024,3072,42083156,33611776,75711296,0,2026.350691,270487.081841,2910.446252,162,813,1,6178,0,529,0,0,1024,81515465,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46192.459, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067955.414, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067608.796, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,compact,3,ok,compact,0.12.0,8388608,41728,4730672,4230192,2100224,131120,6340608,133282278,59568109,22558632,1024,3072,42083156,33554432,75711296,0,340502.324772,100714.490169,2961.00573,923,826,1,4380,0,521,1024,0,0,74934523,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46941.425, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5070856.899, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068200.595, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,copy_no_pool,3,ok,copy,0.12.0,8388608,41728,2312896,98400,2107504,131120,3194880,133540522,58472614,24345793,1024,3072,42083156,33669120,75711296,0,2208.417867,270579.890216,2933.62499,4082,3072,1024,0,0,602,0,0,0,76325826,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 51364.197, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5074603.185, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8073207.582, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,borrow,3,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133275839,52234245,22266351,1024,3072,42083156,0,75711296,0,541367.893955,0.0,2922.935825,79,0,1,5026,0,545,0,0,0,82113024,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 45842.847, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5066801.978, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067096.868, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,shared_copy,3,ok,shared_copy,0.12.0,8388608,41728,2378672,98400,2107392,131120,3899392,133064931,52117164,22057182,1024,3072,42083156,33611776,75711296,0,2015.469116,270480.700872,2897.68644,130,842,1,6181,0,555,0,0,1024,82111301,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 45400.789, ""latency_p99_upper_ns"": 57344}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068255.587, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067189.304, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold8,3,ok,threshold,0.12.0,8388608,41728,4443056,4230192,7168,131120,5292032,133337450,53670610,22263776,1024,3072,42083156,57344,75711296,0,541459.255198,577.165832,2922.14622,137,796,1,5196,0,551,0,0,1024,80837357,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46321.397, ""latency_p99_upper_ns"": 65536}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068514.702, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8068639.91, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold2,3,ok,threshold,0.12.0,8388608,41728,2378672,98400,2107392,131120,3522560,134333542,52726291,22219255,1024,3072,42083156,33611776,75711296,0,2039.714059,270798.061653,2918.877137,140,894,1,6119,0,516,0,0,1024,82638596,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46056.021, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5218345.888, ""latency_p99_upper_ns"": 9437184}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8076530.434, ""latency_p99_upper_ns"": 9437184}]" +slow_large_field,copy,3,ok,copy,0.12.0,8388608,41728,2378784,98400,2107504,131120,3948544,133174357,52057974,22117835,1024,3072,42083156,33669120,75711296,0,2021.284206,270501.113842,2905.858494,114,849,1,7214,0,548,0,0,0,82257586,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 45428.889, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068142.38, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067808.96, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,copy_no_pool,4,ok,copy,0.12.0,8388608,41728,2312896,98400,2107504,131120,3178496,133410638,57999768,24134627,1024,3072,42083156,33669120,75711296,0,2191.645151,270510.58659,2926.663567,4082,3072,1024,0,0,603,0,0,0,76638749,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 51016.097, ""latency_p99_upper_ns"": 81920}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5074074.877, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8071032.382, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold2,4,ok,threshold,0.12.0,8388608,41728,2378672,98400,2107392,131120,3342336,133213102,52362717,22103023,1024,3072,42083156,33611776,75711296,0,2022.420003,270482.639278,2904.168619,127,855,1,6171,0,539,0,0,1024,81964913,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46044.098, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5068262.707, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067371.07, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,copy,4,ok,copy,0.12.0,8388608,41728,2378784,98400,2107504,131120,3751936,133177547,52033688,22154380,1024,3072,42083156,33669120,75711296,0,2025.354638,270478.813202,2910.934805,129,851,1,7197,0,527,0,0,0,82109654,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 45825.604, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067937.156, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067321.024, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,borrow,4,ok,borrow,0.12.0,8388608,41728,4435888,4230192,0,131120,4767744,133311566,53725285,22239925,1024,3072,42083156,0,75711296,0,541575.408384,0.0,2920.340305,80,0,1,5025,0,555,0,0,0,80727666,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46306.509, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067147.548, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8070390.562, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,threshold8,4,ok,threshold,0.12.0,8388608,41728,4443056,4230192,7168,131120,5439488,134101658,54158047,22315394,1024,3072,42083156,57344,75711296,0,541512.874989,578.716358,2931.279646,161,826,1,5142,0,533,0,0,1024,81103821,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46533.691, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5082538.041, ""latency_p99_upper_ns"": 6291456}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8069591.938, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,shared_copy,4,ok,shared_copy,0.12.0,8388608,41728,2378672,98400,2107392,131120,3948544,133134639,52075173,22128273,1024,3072,42083156,33611776,75711296,0,2026.862393,270471.476005,2908.200691,122,816,1,6215,0,512,0,0,1024,82080012,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 46182.627, ""latency_p99_upper_ns"": 61440}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5067504.389, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067072.688, ""latency_p99_upper_ns"": 8388608}]" +slow_large_field,compact,4,ok,compact,0.12.0,8388608,41728,4730672,4230192,2100224,131120,6406144,133118662,59607669,22648269,1024,3072,42083156,33554432,75711296,0,340524.867797,100684.088672,2973.798584,921,855,1,4353,0,532,1024,0,0,74562185,0,0,"[{""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 1, ""latency_mean_ns"": 47236.674, ""latency_p99_upper_ns"": 73728}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5071269.907, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 33554432, ""peak_count"": 64, ""latency_mean_ns"": 8067723.707, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,copy,0,ok,copy,0.12.0,2097152,41728,1104848,393312,79920,524336,1884160,134798098,52545277,26282225,1024,3072,49877076,26872064,114814580,0,6883.203763,1809.304759,10115.296993,367,948,374,6256,0,579,0,0,0,83482902,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47583.467, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5081460.678, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8083520.998, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,threshold8,0,ok,threshold,0.12.0,2097152,41728,1097792,393312,11312,524336,2031616,134514858,50924568,26180831,1024,3072,49877076,1059072,114814580,0,7475.627393,924.635553,10091.334952,237,96,257,5800,0,593,0,0,1024,84806597,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 46212.018, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5080764.964, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8080296.584, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,copy_no_pool,0,ok,copy,0.12.0,2097152,41728,842368,393312,79920,524336,1949696,135580728,59055276,27303180,1024,3072,49877076,26872064,114814580,0,8268.860241,1866.643736,10141.702783,3849,3072,1024,0,0,540,0,0,0,77683199,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 53763.312, ""latency_p99_upper_ns"": 163840}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5090947.59, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8093934.847, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,shared_copy,0,ok,shared_copy,0.12.0,2097152,41728,1097792,393312,72752,524336,1884160,134733970,52189389,26273407,1024,3072,49877076,26814720,114814580,0,6865.482399,1226.436074,10135.432887,365,246,368,5942,0,587,0,0,1024,83766349,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47383.279, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5081532.837, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082383.547, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,threshold2,0,ok,threshold,0.12.0,2097152,41728,1097792,393312,72752,524336,1884160,134703694,51981424,26239241,1024,3072,49877076,26814720,114814580,0,6818.11346,1220.93584,10109.238408,368,240,366,5947,0,592,0,0,1024,83962412,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47186.16, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5082027.365, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082717.212, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,compact,0,ok,compact,0.12.0,2097152,41728,2096880,2047296,3360,524336,3702784,330325402,56526552,257818693,1024,3072,49877076,57344,114814580,0,542057.64214,342.456081,10454.037412,1653,1018,587,2639,1256,0,1024,0,0,275134226,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 313916.181, ""latency_p99_upper_ns"": 4718592}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5359649.404, ""latency_p99_upper_ns"": 10485760}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 49, ""latency_mean_ns"": 8345801.06, ""latency_p99_upper_ns"": 12582912}]" +mixed_tight,borrow,0,ok,borrow,0.12.0,2097152,41728,2097024,2047296,0,524336,3702784,517444947,58385263,414175159,1024,3072,49877076,0,114814580,0,866552.611778,0.0,10646.478365,1612,0,580,2681,1250,0,0,0,0,460552293,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 496739.613, ""latency_p99_upper_ns"": 7864320}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5531099.266, ""latency_p99_upper_ns"": 12582912}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 8547759.071, ""latency_p99_upper_ns"": 15728640}]" +mixed_tight,copy_no_pool,1,ok,copy,0.12.0,2097152,41728,842368,393312,79920,524336,1916928,135818181,60130654,27699522,1024,3072,49877076,26872064,114814580,0,8398.686166,1885.555543,10206.603011,3849,3072,1024,0,0,537,0,0,0,76838088,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 54782.86, ""latency_p99_upper_ns"": 180224}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5093567.929, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8096201.349, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,threshold8,1,ok,threshold,0.12.0,2097152,41728,1097792,393312,11312,524336,2064384,134627181,51051158,26242598,1024,3072,49877076,1059072,114814580,0,7499.787736,924.734083,10090.493068,237,100,258,5795,0,587,0,0,1024,84733901,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 46365.068, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5080938.783, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8081319.231, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,shared_copy,1,ok,shared_copy,0.12.0,2097152,41728,1097792,393312,72752,524336,1900544,134660107,51849496,26235438,1024,3072,49877076,26814720,114814580,0,6785.330268,1222.453816,10103.005533,366,239,366,5950,0,579,0,0,1024,84013028,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47133.761, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5081549.099, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082113.784, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,borrow,1,ok,borrow,0.12.0,2097152,41728,2097024,2047296,0,524336,3719168,516876187,58189132,413488109,1024,3072,49877076,0,114814580,0,866155.703795,0.0,10618.392921,1617,0,581,2675,1263,0,0,0,0,460124695,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 496219.126, ""latency_p99_upper_ns"": 7864320}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5528237.536, ""latency_p99_upper_ns"": 12582912}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 8541844.312, ""latency_p99_upper_ns"": 15728640}]" +mixed_tight,threshold2,1,ok,threshold,0.12.0,2097152,41728,1097792,393312,72752,524336,1867776,134596389,51879163,26207832,1024,3072,49877076,26814720,114814580,0,6795.293198,1224.224319,10089.609534,369,233,365,5954,0,596,0,0,1024,83947198,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47146.396, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5082305.345, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082431.98, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,copy,1,ok,copy,0.12.0,2097152,41728,1104848,393312,79920,524336,1884160,134667407,52286009,26255995,1024,3072,49877076,26872064,114814580,0,6830.10163,1802.54536,10104.51946,370,952,375,6248,0,590,0,0,0,83595691,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47383.187, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5081689.522, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082200.92, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,compact,1,ok,compact,0.12.0,2097152,41728,2097104,2047296,3360,524336,3833856,330507221,57227425,258042238,1024,3072,49877076,57344,114814580,0,542403.396989,342.198165,10516.149038,1657,1018,592,2630,1262,0,1024,0,0,274671771,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 314160.033, ""latency_p99_upper_ns"": 4718592}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5361935.781, ""latency_p99_upper_ns"": 10485760}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 49, ""latency_mean_ns"": 8345834.982, ""latency_p99_upper_ns"": 12582912}]" +mixed_tight,threshold2,2,ok,threshold,0.12.0,2097152,41728,1097792,393312,72752,524336,1900544,134805557,52099602,26201196,1024,3072,49877076,26814720,114814580,0,6802.764732,1222.5673,10082.699192,363,241,368,5949,0,592,0,0,1024,83969655,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47229.56, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5083369.83, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082868.22, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,copy,2,ok,copy,0.12.0,2097152,41728,1104736,393312,79920,524336,1900544,135397301,55535720,28191073,1024,3072,49877076,26872064,114814580,0,7296.683374,1825.786254,10851.770961,364,925,376,6280,0,556,0,0,0,81078620,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 50441.328, ""latency_p99_upper_ns"": 163840}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5086253.071, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8088040.091, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,threshold8,2,ok,threshold,0.12.0,2097152,41728,1097792,393312,11312,524336,2048000,135143135,54181725,28073539,1024,3072,49877076,1059072,114814580,0,7971.142415,924.89692,10793.387694,237,89,259,5805,0,587,0,0,1024,82256234,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 49150.131, ""latency_p99_upper_ns"": 163840}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5084346.661, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8083800.547, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,borrow,2,ok,borrow,0.12.0,2097152,41728,2097024,2047296,0,524336,3719168,519737827,63401151,410873515,1024,3072,49877076,0,114814580,0,869390.688093,0.0,10874.854354,1612,0,584,2677,1267,0,0,0,0,457480540,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 498894.016, ""latency_p99_upper_ns"": 7864320}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5554388.391, ""latency_p99_upper_ns"": 12582912}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 8572436.932, ""latency_p99_upper_ns"": 15728640}]" +mixed_tight,copy_no_pool,2,ok,copy,0.12.0,2097152,41728,842368,393312,79920,524336,1933312,135808674,61310764,27409732,1024,3072,49877076,26872064,114814580,0,8492.13471,1909.35548,10184.015437,3849,3072,1024,0,0,538,0,0,0,75774039,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 55750.675, ""latency_p99_upper_ns"": 196608}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5097561.83, ""latency_p99_upper_ns"": 5767168}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8097147.871, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,compact,2,ok,compact,0.12.0,2097152,41728,2096496,2047296,3360,524336,3719168,330655613,57992572,257126466,1024,3072,49877076,57344,114814580,0,542286.835871,342.201335,10592.82654,1620,1018,589,2670,1246,0,1024,0,0,274045502,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 314214.117, ""latency_p99_upper_ns"": 4718592}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5361784.028, ""latency_p99_upper_ns"": 10485760}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 49, ""latency_mean_ns"": 8345733.162, ""latency_p99_upper_ns"": 12582912}]" +mixed_tight,shared_copy,2,ok,shared_copy,0.12.0,2097152,41728,1097792,393312,72752,524336,1900544,134725602,52125932,26256537,1024,3072,49877076,26814720,114814580,0,6831.305534,1226.062002,10092.239062,365,226,360,5970,0,576,0,0,1024,83779201,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47423.973, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5082508.901, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082650.268, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,compact,3,ok,compact,0.12.0,2097152,41728,2096656,2047296,3360,524336,3719168,332809762,64506702,255295188,1024,3072,49877076,57344,114814580,0,544031.993873,348.13532,10927.535411,1623,1008,583,2683,1227,0,1024,0,0,270135277,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 316210.616, ""latency_p99_upper_ns"": 4718592}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5373501.093, ""latency_p99_upper_ns"": 10485760}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 49, ""latency_mean_ns"": 8409239.137, ""latency_p99_upper_ns"": 12582912}]" +mixed_tight,threshold2,3,ok,threshold,0.12.0,2097152,41728,1097792,393312,72752,524336,1884160,134646087,51969989,26192074,1024,3072,49877076,26814720,114814580,0,6812.54134,1229.637184,10083.25152,369,241,365,5946,0,582,0,0,1024,83864700,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47274.193, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5082291.09, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8084035.099, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,copy,3,ok,copy,0.12.0,2097152,41728,1104848,393312,79920,524336,1900544,134732754,52218674,26241260,1024,3072,49877076,26872064,114814580,0,6810.984162,1804.529206,10104.502269,368,949,374,6254,0,584,0,0,0,83719457,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47404.955, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5082343.96, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082308.803, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,shared_copy,3,ok,shared_copy,0.12.0,2097152,41728,1097792,393312,72752,524336,1884160,134687032,52085095,26215409,1024,3072,49877076,26814720,114814580,0,6852.502663,1223.987439,10100.060122,365,235,364,5957,0,585,0,0,1024,83840818,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47354.551, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5081713.058, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082095.55, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,borrow,3,ok,borrow,0.12.0,2097152,41728,2097072,2047296,0,524336,3719168,516415971,57490634,412616864,1024,3072,49877076,0,114814580,0,865936.173669,0.0,10532.202462,1624,0,582,2667,1258,0,0,0,0,460245677,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 495746.732, ""latency_p99_upper_ns"": 7864320}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5527499.187, ""latency_p99_upper_ns"": 12582912}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 8541909.699, ""latency_p99_upper_ns"": 15728640}]" +mixed_tight,copy_no_pool,3,ok,copy,0.12.0,2097152,41728,842368,393312,79920,524336,1916928,135708499,59498427,27293452,1024,3072,49877076,26872064,114814580,0,8320.069469,1853.590142,10172.640074,3849,3072,1024,0,0,550,0,0,0,77469437,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 53679.489, ""latency_p99_upper_ns"": 180224}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5093041.554, ""latency_p99_upper_ns"": 5767168}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8093789.758, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,threshold8,3,ok,threshold,0.12.0,2097152,41728,1097792,393312,11312,524336,2048000,134580129,50901202,26138215,1024,3072,49877076,1059072,114814580,0,7468.893211,924.833553,10074.589742,237,99,260,5794,0,603,0,0,1024,84894241,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 46199.245, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5080295.214, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082153.254, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,compact,4,ok,compact,0.12.0,2097152,41728,2095936,2047296,3360,524336,3768320,330364653,56737666,257390641,1024,3072,49877076,57344,114814580,0,542129.496495,342.3922,10487.947668,1626,1018,584,2669,1234,0,1024,0,0,274961005,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 314000.227, ""latency_p99_upper_ns"": 4718592}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5359881.678, ""latency_p99_upper_ns"": 10485760}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 49, ""latency_mean_ns"": 8345470.393, ""latency_p99_upper_ns"": 12582912}]" +mixed_tight,threshold2,4,ok,threshold,0.12.0,2097152,41728,1097792,393312,72752,524336,1884160,134641407,51772899,26303805,1024,3072,49877076,26814720,114814580,0,6823.249522,1222.425123,10156.044791,363,239,367,5952,0,584,0,0,1024,83930889,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47208.702, ""latency_p99_upper_ns"": 131072}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5081715.549, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8082551.032, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,threshold8,4,ok,threshold,0.12.0,2097152,41728,1097792,393312,11312,524336,2048000,134636889,51692325,26232175,1024,3072,49877076,1059072,114814580,0,7604.090343,924.831397,10109.160375,237,97,259,5797,0,600,0,0,1024,84203974,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 46850.104, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5081772.482, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8081990.295, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,borrow,4,ok,borrow,0.12.0,2097152,41728,2097024,2047296,0,524336,3719168,516507633,56956626,413462854,1024,3072,49877076,0,114814580,0,866066.604394,0.0,10498.181654,1635,0,588,2650,1258,0,0,0,0,460890935,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 495819.875, ""latency_p99_upper_ns"": 7864320}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 5527650.658, ""latency_p99_upper_ns"": 12582912}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 30, ""latency_mean_ns"": 8542682.03, ""latency_p99_upper_ns"": 15728640}]" +mixed_tight,shared_copy,4,ok,shared_copy,0.12.0,2097152,41728,1097792,393312,72752,524336,1884160,134698503,52062419,26232455,1024,3072,49877076,26814720,114814580,0,6807.634601,1226.698884,10096.010075,358,229,363,5971,0,573,0,0,1024,83820760,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 47338.287, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5082803.919, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8081430.476, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,copy_no_pool,4,ok,copy,0.12.0,2097152,41728,842368,393312,79920,524336,1933312,135617374,59370307,27370335,1024,3072,49877076,26872064,114814580,0,8306.118105,1881.770411,10193.350967,3849,3072,1024,0,0,541,0,0,0,77447978,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 54118.737, ""latency_p99_upper_ns"": 180224}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5093193.132, ""latency_p99_upper_ns"": 5767168}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8094306.969, ""latency_p99_upper_ns"": 8388608}]" +mixed_tight,copy,4,ok,copy,0.12.0,2097152,41728,1104736,393312,79920,524336,1884160,135101765,53612089,26369173,1024,3072,49877076,26872064,114814580,0,6944.794448,1822.831558,10130.150831,348,886,368,6343,0,561,0,0,0,82695691,0,0,"[{""emitted"": 1024, ""bytes"": 26757376, ""peak_count"": 1, ""latency_mean_ns"": 48573.396, ""latency_p99_upper_ns"": 147456}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 5083912.79, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 1024, ""bytes"": 57344, ""peak_count"": 64, ""latency_mean_ns"": 8083831.809, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy_no_pool,0,ok,copy,0.12.0,2097152,41728,170672,24672,14896,98400,1835008,66381179,14937944,10634360,512,1536,5259666,319488,6312628,14086144,191.835816,748.822762,436.544508,1273,1536,1024,0,0,200,0,0,0,51858352,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26502.654, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055591.338, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8038187.436, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,borrow,0,ok,borrow,0.12.0,2097152,41728,1208192,1059888,0,98400,2785280,66395496,14949295,10748920,512,1536,5259666,0,6312628,14086144,67632.156584,0.0,441.340054,70,0,2,2225,0,210,0,0,0,51867983,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26677.738, ""latency_p99_upper_ns"": 40960}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055635.91, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8040456.355, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold8,0,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1851392,66344311,14626628,10598197,512,1536,5259666,290816,6312628,14086144,189.993439,460.282359,437.832685,6,65,2,3248,0,201,0,0,512,52108022,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26057.676, ""latency_p99_upper_ns"": 30720}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054707.516, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8038718.686, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,shared_copy,0,ok,shared_copy,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66360126,14610198,10558947,512,1536,5259666,290816,6312628,14086144,189.278595,460.358957,433.475216,6,65,2,3248,0,210,0,0,512,52155647,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26056.072, ""latency_p99_upper_ns"": 30720}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054720.746, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8039527.611, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,compact,0,ok,compact,0.12.0,2097152,41728,1209200,1059888,7168,98400,2899968,66352515,15174956,10767457,512,1536,5259666,28672,6312628,14086144,42522.512719,170.992644,444.338525,462,456,2,1889,0,203,512,0,0,51584350,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26833.023, ""latency_p99_upper_ns"": 40960}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055824.906, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8037818.045, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy,0,ok,copy,0.12.0,2097152,41728,187312,24672,14896,98400,1835008,66358774,14704746,10580140,512,1536,5259666,319488,6312628,14086144,190.401936,748.797361,434.190092,6,546,2,3279,0,198,0,0,0,52055904,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26138.373, ""latency_p99_upper_ns"": 32768}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054837.756, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8038347.354, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold2,0,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1851392,66343256,14610428,10601249,512,1536,5259666,290816,6312628,14086144,190.005404,460.293238,435.304664,6,65,2,3248,0,199,0,0,512,52113330,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26103.785, ""latency_p99_upper_ns"": 32768}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054791.455, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8038668.434, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold8,1,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66620533,15009658,10599246,512,1536,5259666,290816,6312628,14086144,190.711835,461.162485,439.194778,6,67,2,3246,0,195,0,0,512,52089340,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26514.744, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5059599.168, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8052646.494, ""latency_p99_upper_ns"": 9437184}]" +metadata_heavy,copy_no_pool,1,ok,copy,0.12.0,2097152,41728,170672,24672,14896,98400,1835008,69246534,16610834,11758115,512,1536,5259666,319488,6312628,14086144,215.365911,778.347747,481.501477,1273,1536,1024,0,0,164,0,0,0,52395416,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 29861.328, ""latency_p99_upper_ns"": 57344}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5481519.523, ""latency_p99_upper_ns"": 10485760}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8132478.682, ""latency_p99_upper_ns"": 10485760}]" +metadata_heavy,shared_copy,1,ok,shared_copy,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,67338922,15543047,10965512,512,1536,5259666,290816,6312628,14086144,198.833427,461.43294,455.786835,6,88,2,3225,0,170,0,0,512,52096250,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 27620.291, ""latency_p99_upper_ns"": 53248}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5405312.287, ""latency_p99_upper_ns"": 9437184}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8058747.33, ""latency_p99_upper_ns"": 9437184}]" +metadata_heavy,copy,1,ok,copy,0.12.0,2097152,41728,187312,24672,14896,98400,1835008,66437867,14887131,10719986,512,1536,5259666,319488,6312628,14086144,192.97609,748.927344,442.835871,6,546,2,3279,0,206,0,0,0,51911693,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26502.299, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055229.1, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8040517.863, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold2,1,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66341459,14573003,10555283,512,1536,5259666,290816,6312628,14086144,189.19959,460.262935,433.563583,6,65,2,3248,0,202,0,0,512,52150322,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26047.523, ""latency_p99_upper_ns"": 30720}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054844.742, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8037890.609, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,borrow,1,ok,borrow,0.12.0,2097152,41728,1208192,1059888,0,98400,2801664,66302663,14680433,10680579,512,1536,5259666,0,6312628,14086144,67603.216745,0.0,441.482243,70,0,2,2225,0,203,0,0,0,52022749,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26241.209, ""latency_p99_upper_ns"": 32768}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054887.895, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8036938.547, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,compact,1,ok,compact,0.12.0,2097152,41728,1209200,1059888,7168,98400,2916352,66647996,15645769,10897093,512,1536,5259666,28672,6312628,14086144,42531.184104,171.185155,448.382215,462,456,2,1889,0,208,512,0,0,51484721,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 27363.605, ""latency_p99_upper_ns"": 49152}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5056927.043, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8042303.346, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,shared_copy,2,ok,shared_copy,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66509552,15439851,10723161,512,1536,5259666,290816,6312628,14086144,194.692414,460.401472,443.814792,6,65,2,3248,0,188,0,0,512,51559999,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 27221.904, ""latency_p99_upper_ns"": 40960}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5102159.828, ""latency_p99_upper_ns"": 6291456}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8039936.652, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold2,2,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1851392,66675868,15617313,10794500,512,1536,5259666,290816,6312628,14086144,196.368374,460.582208,453.168708,6,72,2,3241,0,206,0,0,512,51576007,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 27391.873, ""latency_p99_upper_ns"": 45056}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5063401.098, ""latency_p99_upper_ns"": 5767168}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8043009.266, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold8,2,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66346091,14749114,10663838,512,1536,5259666,290816,6312628,14086144,191.566293,460.387978,441.272197,6,65,2,3248,0,198,0,0,512,51956135,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26444.111, ""latency_p99_upper_ns"": 40960}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055009.982, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8040053.549, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,borrow,2,ok,borrow,0.12.0,2097152,41728,1208192,1059888,0,98400,2768896,66340906,14840713,10710339,512,1536,5259666,0,6312628,14086144,67628.848076,0.0,441.911742,70,0,2,2225,0,214,0,0,0,51926596,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26533.881, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055556.934, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8040046.408, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy_no_pool,2,ok,copy,0.12.0,2097152,41728,170672,24672,14896,98400,1851392,66367572,14844612,10626817,512,1536,5259666,319488,6312628,14086144,191.787957,748.742987,439.039763,1273,1536,1024,0,0,202,0,0,0,51918865,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26443.496, ""latency_p99_upper_ns"": 32768}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055503.67, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8037164.848, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,compact,2,ok,compact,0.12.0,2097152,41728,1209200,1059888,7168,98400,2916352,66456221,15237761,10832907,512,1536,5259666,28672,6312628,14086144,42524.965388,171.106633,446.50427,462,456,2,1889,0,211,512,0,0,51628844,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26894.043, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5056115.033, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8040094.787, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy,2,ok,copy,0.12.0,2097152,41728,187312,24672,14896,98400,1818624,66358054,14786258,10588699,512,1536,5259666,319488,6312628,14086144,190.767691,748.949984,437.20655,6,546,2,3279,0,202,0,0,0,51982953,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26290.178, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055243.682, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8040476.592, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,shared_copy,3,ok,shared_copy,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66340017,14641398,10583202,512,1536,5259666,290816,6312628,14086144,189.850967,460.201756,434.46664,6,65,2,3248,0,200,0,0,512,52101474,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26126.43, ""latency_p99_upper_ns"": 32768}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054896.006, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8037126.236, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,borrow,3,ok,borrow,0.12.0,2097152,41728,1208192,1059888,0,98400,2768896,66397387,14926280,10743184,512,1536,5259666,0,6312628,14086144,67622.063537,0.0,442.942199,70,0,2,2225,0,212,0,0,0,51935519,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26540.598, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055706.689, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8039215.434, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy,3,ok,copy,0.12.0,2097152,41728,187312,24672,14896,98400,1835008,66347591,14688841,10558757,512,1536,5259666,319488,6312628,14086144,190.237303,748.906445,434.052077,6,546,2,3279,0,201,0,0,0,52048363,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26185.322, ""latency_p99_upper_ns"": 30720}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054934.725, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8039957.693, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,compact,3,ok,compact,0.12.0,2097152,41728,1209200,1059888,7168,98400,2916352,66425702,15126826,10762572,512,1536,5259666,28672,6312628,14086144,42518.234312,171.124375,442.813559,462,456,2,1889,0,207,512,0,0,51722109,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26699.592, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055311.875, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8039588.77, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold8,3,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66342183,14624958,10558072,512,1536,5259666,290816,6312628,14086144,189.596919,460.250779,436.103934,6,65,2,3248,0,199,0,0,512,52113950,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26082.711, ""latency_p99_upper_ns"": 32768}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054439.891, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8037911.916, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy_no_pool,3,ok,copy,0.12.0,2097152,41728,170672,24672,14896,98400,1835008,66421906,14924036,10733514,512,1536,5259666,319488,6312628,14086144,193.388376,748.846289,443.059571,1273,1536,1024,0,0,207,0,0,0,51871512,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26630.812, ""latency_p99_upper_ns"": 40960}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055263.076, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8039399.533, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold2,3,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66363184,14629240,10568715,512,1536,5259666,290816,6312628,14086144,189.501318,460.195707,436.121559,6,65,2,3248,0,199,0,0,512,52142767,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26129.004, ""latency_p99_upper_ns"": 30720}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054684.932, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8036606.693, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold2,4,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66354498,14608671,10552983,512,1536,5259666,290816,6312628,14086144,189.286265,460.245208,433.358809,6,65,2,3248,0,197,0,0,512,52143565,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26044.074, ""latency_p99_upper_ns"": 30720}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054765.459, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8037717.471, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy_no_pool,4,ok,copy,0.12.0,2097152,41728,170672,24672,14896,98400,1851392,66440315,15042208,10714630,512,1536,5259666,319488,6312628,14086144,193.34471,749.072578,442.776453,1273,1536,1024,0,0,203,0,0,0,51818059,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26719.596, ""latency_p99_upper_ns"": 40960}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055539.021, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8042928.75, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,shared_copy,4,ok,shared_copy,0.12.0,2097152,41728,180144,24672,7728,98400,1835008,66550686,14565611,10569760,512,1536,5259666,290816,6312628,14086144,189.196651,460.427963,435.936714,6,70,2,3243,0,199,0,0,512,52394259,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26054.17, ""latency_p99_upper_ns"": 30720}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054468.873, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8040821.889, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,compact,4,ok,compact,0.12.0,2097152,41728,1209200,1059888,7168,98400,2932736,66346982,15115580,10730381,512,1536,5259666,28672,6312628,14086144,42539.689103,170.963142,440.137993,462,456,2,1889,0,200,512,0,0,51642878,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26772.898, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5057876.094, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8039346.365, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,threshold8,4,ok,threshold,0.12.0,2097152,41728,180144,24672,7728,98400,1851392,66367685,14678765,10612300,512,1536,5259666,290816,6312628,14086144,190.199351,460.290364,438.024028,6,65,2,3248,0,200,0,0,512,52099788,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26178.92, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054797.43, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8038471.338, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,borrow,4,ok,borrow,0.12.0,2097152,41728,1208192,1059888,0,98400,2785280,66395630,14769876,10679603,512,1536,5259666,0,6312628,14086144,67615.986637,0.0,438.40001,70,0,2,2225,0,207,0,0,0,52035492,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26350.551, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5054965.338, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8038469.066, ""latency_p99_upper_ns"": 8388608}]" +metadata_heavy,copy,4,ok,copy,0.12.0,2097152,41728,187312,24672,14896,98400,1835008,66338315,14754424,10609310,512,1536,5259666,319488,6312628,14086144,190.968242,748.772024,435.574111,6,546,2,3279,0,199,0,0,0,51978025,0,0,"[{""emitted"": 512, ""bytes"": 262144, ""peak_count"": 1, ""latency_mean_ns"": 26285.051, ""latency_p99_upper_ns"": 36864}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 5055200.059, ""latency_p99_upper_ns"": 5242880}, {""emitted"": 512, ""bytes"": 28672, ""peak_count"": 64, ""latency_mean_ns"": 8037468.172, ""latency_p99_upper_ns"": 8388608}]" diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.json new file mode 100644 index 0000000..9bf47cb --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.json @@ -0,0 +1,12532 @@ +[ + { + "case": "one_slow", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1835008, + "elapsed_ns": 131703634, + "cpu_ns": 34750648, + "parse_call_ns": 21989760, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1842.615404, + "output_byte_seconds": 919.75921, + "parser_byte_seconds": 2883.244752, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97097912, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8051720.034, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "compact", + "repeat": 0, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 131996119, + "cpu_ns": 38004572, + "parse_call_ns": 22434299, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540791.270719, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2928.256675, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 26, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 94192606, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8057605.331, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5521408, + "elapsed_ns": 132124751, + "cpu_ns": 37612015, + "parse_call_ns": 22444229, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540845.471315, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2930.090388, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 53, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 94791711, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8058299.849, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131620844, + "cpu_ns": 34446035, + "parse_call_ns": 21905479, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1832.609103, + "output_byte_seconds": 920.078119, + "parser_byte_seconds": 2872.388672, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97293567, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054286.008, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 237296, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1835008, + "elapsed_ns": 132233158, + "cpu_ns": 41767998, + "parse_call_ns": 24756316, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2069.543688, + "output_byte_seconds": 920.140424, + "parser_byte_seconds": 2901.308237, + "source_allocations": 4082, + "output_allocations": 1024, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 19, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 90619067, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8059175.662, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131600270, + "cpu_ns": 34455850, + "parse_call_ns": 21964978, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1832.537919, + "output_byte_seconds": 920.025416, + "parser_byte_seconds": 2880.380829, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97270417, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8053796.131, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "shared_copy", + "repeat": 0, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1802240, + "elapsed_ns": 131715584, + "cpu_ns": 34456703, + "parse_call_ns": 21940218, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1833.007769, + "output_byte_seconds": 920.07894, + "parser_byte_seconds": 2877.016945, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97393619, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054276.953, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "compact", + "repeat": 1, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 132171843, + "cpu_ns": 38070312, + "parse_call_ns": 22523797, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540815.06589, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2937.269732, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 40, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 94338529, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8057936.641, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 237296, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1835008, + "elapsed_ns": 132199048, + "cpu_ns": 42202499, + "parse_call_ns": 24805054, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2084.967873, + "output_byte_seconds": 920.132302, + "parser_byte_seconds": 2905.942804, + "source_allocations": 4082, + "output_allocations": 1024, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 29, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 90146770, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8059478.906, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "shared_copy", + "repeat": 1, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131708165, + "cpu_ns": 34804961, + "parse_call_ns": 22044118, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1844.634105, + "output_byte_seconds": 919.759432, + "parser_byte_seconds": 2890.857973, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97053198, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8051751.524, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131798127, + "cpu_ns": 34803856, + "parse_call_ns": 22044512, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1846.36305, + "output_byte_seconds": 919.612987, + "parser_byte_seconds": 2891.382584, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97156768, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050483.774, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5521408, + "elapsed_ns": 131976979, + "cpu_ns": 38591495, + "parse_call_ns": 22573964, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540860.949553, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2940.513903, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 28, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 93626665, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8058679.423, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131752053, + "cpu_ns": 34888537, + "parse_call_ns": 21974890, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1845.194241, + "output_byte_seconds": 919.599974, + "parser_byte_seconds": 2880.87266, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 17, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97030621, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050419.036, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131720609, + "cpu_ns": 34747849, + "parse_call_ns": 21999728, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1843.805295, + "output_byte_seconds": 919.628727, + "parser_byte_seconds": 2884.923744, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97113750, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050578.244, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131735071, + "cpu_ns": 34877739, + "parse_call_ns": 22030534, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1847.409016, + "output_byte_seconds": 919.636562, + "parser_byte_seconds": 2888.89668, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 96987832, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050770.82, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "compact", + "repeat": 2, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 132186905, + "cpu_ns": 38709650, + "parse_call_ns": 22486562, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540826.901328, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2931.839266, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 36, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 93736446, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8058254.455, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "shared_copy", + "repeat": 2, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1835008, + "elapsed_ns": 134003964, + "cpu_ns": 36820353, + "parse_call_ns": 22141327, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1907.646695, + "output_byte_seconds": 924.396419, + "parser_byte_seconds": 2903.071145, + "source_allocations": 9, + "output_allocations": 116, + "parser_allocations": 1, + "reuses": 6004, + "budget_waits": 0, + "slot_waits": 25, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97345374, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8093864.112, + "latency_p99_upper_ns": 9437184 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 132093280, + "cpu_ns": 37140317, + "parse_call_ns": 22338927, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1928.638533, + "output_byte_seconds": 919.864631, + "parser_byte_seconds": 2930.310014, + "source_allocations": 9, + "output_allocations": 68, + "parser_allocations": 1, + "reuses": 6052, + "budget_waits": 0, + "slot_waits": 20, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 95013211, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054865.09, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5554176, + "elapsed_ns": 132469040, + "cpu_ns": 41862764, + "parse_call_ns": 22964342, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541099.43903, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2986.938382, + "source_allocations": 73, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5032, + "budget_waits": 0, + "slot_waits": 49, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 90878204, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8063020.332, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 237296, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1802240, + "elapsed_ns": 137450461, + "cpu_ns": 46306762, + "parse_call_ns": 25720817, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2217.245474, + "output_byte_seconds": 934.97033, + "parser_byte_seconds": 2945.941354, + "source_allocations": 4082, + "output_allocations": 1024, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 23, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 91429169, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8191749.104, + "latency_p99_upper_ns": 11534336 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131976855, + "cpu_ns": 36997295, + "parse_call_ns": 22056693, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1904.528214, + "output_byte_seconds": 920.059714, + "parser_byte_seconds": 2891.295258, + "source_allocations": 9, + "output_allocations": 65, + "parser_allocations": 1, + "reuses": 6055, + "budget_waits": 0, + "slot_waits": 21, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 95230128, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8056258.001, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 132144262, + "cpu_ns": 36810738, + "parse_call_ns": 22394390, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1914.610493, + "output_byte_seconds": 920.080986, + "parser_byte_seconds": 2937.462741, + "source_allocations": 9, + "output_allocations": 71, + "parser_allocations": 1, + "reuses": 6049, + "budget_waits": 0, + "slot_waits": 39, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 95437850, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8056316.891, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131627799, + "cpu_ns": 34748983, + "parse_call_ns": 22050963, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1845.118437, + "output_byte_seconds": 920.020651, + "parser_byte_seconds": 2891.313221, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 19, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 96996391, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054019.278, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "compact", + "repeat": 3, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 132093080, + "cpu_ns": 38378509, + "parse_call_ns": 22460168, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540907.798619, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2928.186919, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 43, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 93965206, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8059384.654, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 237296, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1802240, + "elapsed_ns": 132246841, + "cpu_ns": 41768202, + "parse_call_ns": 24673720, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2066.312998, + "output_byte_seconds": 920.19798, + "parser_byte_seconds": 2896.434244, + "source_allocations": 4082, + "output_allocations": 1024, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 90608121, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8059680.607, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131689453, + "cpu_ns": 35243759, + "parse_call_ns": 22012604, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1857.757504, + "output_byte_seconds": 919.947028, + "parser_byte_seconds": 2887.51415, + "source_allocations": 9, + "output_allocations": 68, + "parser_allocations": 1, + "reuses": 6052, + "budget_waits": 0, + "slot_waits": 27, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 96622803, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8053765.222, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "shared_copy", + "repeat": 3, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1802240, + "elapsed_ns": 132103712, + "cpu_ns": 35248408, + "parse_call_ns": 22004780, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1858.44117, + "output_byte_seconds": 919.973445, + "parser_byte_seconds": 2885.070598, + "source_allocations": 9, + "output_allocations": 73, + "parser_allocations": 1, + "reuses": 6047, + "budget_waits": 0, + "slot_waits": 22, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97018510, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054006.321, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 132436008, + "cpu_ns": 40869521, + "parse_call_ns": 22656213, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540941.338269, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2953.318689, + "source_allocations": 75, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5030, + "budget_waits": 0, + "slot_waits": 52, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 91954404, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8060462.982, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "compact", + "repeat": 4, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5554176, + "elapsed_ns": 132110379, + "cpu_ns": 38257181, + "parse_call_ns": 22557256, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540811.125254, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2944.323595, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 56, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 94112952, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8057929.717, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1802240, + "elapsed_ns": 131804916, + "cpu_ns": 34977352, + "parse_call_ns": 22094634, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1853.831647, + "output_byte_seconds": 920.149467, + "parser_byte_seconds": 2897.713188, + "source_allocations": 9, + "output_allocations": 67, + "parser_allocations": 1, + "reuses": 6053, + "budget_waits": 0, + "slot_waits": 31, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 96937037, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8055361.395, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 237296, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 135161451, + "cpu_ns": 44330024, + "parse_call_ns": 25503982, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2157.809223, + "output_byte_seconds": 924.543547, + "parser_byte_seconds": 2946.975235, + "source_allocations": 4082, + "output_allocations": 1024, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 35, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 91118882, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8099348.558, + "latency_p99_upper_ns": 10485760 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1802240, + "elapsed_ns": 132588310, + "cpu_ns": 37673671, + "parse_call_ns": 22518430, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1949.11739, + "output_byte_seconds": 920.155569, + "parser_byte_seconds": 2954.042078, + "source_allocations": 9, + "output_allocations": 78, + "parser_allocations": 1, + "reuses": 6042, + "budget_waits": 0, + "slot_waits": 36, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 94895218, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8057756.78, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 131620718, + "cpu_ns": 34580946, + "parse_call_ns": 21993246, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1837.245494, + "output_byte_seconds": 919.925321, + "parser_byte_seconds": 2884.001314, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 16, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 97154068, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8053071.22, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 4427696, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 132000791, + "cpu_ns": 37927424, + "parse_call_ns": 22507290, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540831.163899, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2938.768696, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 30, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 94241105, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8058140.749, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "shared_copy", + "repeat": 4, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 33536, + "peak_reserved_bytes": 303072, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1818624, + "elapsed_ns": 132283829, + "cpu_ns": 37157688, + "parse_call_ns": 22116989, + "accepted": 1024, + "emitted": 1024, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1917.711679, + "output_byte_seconds": 920.053817, + "parser_byte_seconds": 2900.603467, + "source_allocations": 9, + "output_allocations": 67, + "parser_allocations": 1, + "reuses": 6053, + "budget_waits": 0, + "slot_waits": 38, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 95415784, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8056291.708, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5373952, + "elapsed_ns": 132804710, + "cpu_ns": 49883960, + "parse_call_ns": 22784485, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540593.356702, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2990.235132, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 578, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 84130488, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 45505.588, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070252.164, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8055489.045, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "compact", + "repeat": 0, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4436784, + "source_peak_bytes": 4230192, + "output_peak_bytes": 6944, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 132699783, + "cpu_ns": 50261222, + "parse_call_ns": 22546544, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340105.36302, + "output_byte_seconds": 342.277671, + "parser_byte_seconds": 2958.801865, + "source_allocations": 897, + "output_allocations": 913, + "parser_allocations": 1, + "reuses": 4319, + "budget_waits": 0, + "slot_waits": 550, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83652241, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 45463.711, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069982.729, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054576.42, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "shared_copy", + "repeat": 0, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1835008, + "elapsed_ns": 132438636, + "cpu_ns": 46508730, + "parse_call_ns": 22104890, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1994.687521, + "output_byte_seconds": 1188.73405, + "parser_byte_seconds": 2903.952009, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 538, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87037859, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42492.519, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069205.345, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8052022.729, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 318432, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132301692, + "cpu_ns": 46274024, + "parse_call_ns": 22012014, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1991.776071, + "output_byte_seconds": 1766.57547, + "parser_byte_seconds": 2891.89841, + "source_allocations": 9, + "output_allocations": 1025, + "parser_allocations": 1, + "reuses": 7143, + "budget_waits": 0, + "slot_waits": 563, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 87083196, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42221.575, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068352.743, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8049671.308, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 252544, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1933312, + "elapsed_ns": 132221655, + "cpu_ns": 46432790, + "parse_call_ns": 22094470, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2002.66499, + "output_byte_seconds": 1764.18462, + "parser_byte_seconds": 2896.537304, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 534, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 86822233, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42461.52, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068300.921, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054065.407, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132423215, + "cpu_ns": 46202689, + "parse_call_ns": 22137673, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1992.776523, + "output_byte_seconds": 1183.803646, + "parser_byte_seconds": 2909.259091, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 559, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87298460, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42209.067, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068504.791, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8052025.9, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132072752, + "cpu_ns": 44818714, + "parse_call_ns": 22007505, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2372.402719, + "output_byte_seconds": 919.579066, + "parser_byte_seconds": 2891.111165, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 531, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 88271004, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 41081.91, + "latency_p99_upper_ns": 49152 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067409.945, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8052229.961, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 318432, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132122049, + "cpu_ns": 45960158, + "parse_call_ns": 22062914, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1985.913742, + "output_byte_seconds": 1766.681864, + "parser_byte_seconds": 2899.647733, + "source_allocations": 9, + "output_allocations": 1025, + "parser_allocations": 1, + "reuses": 7143, + "budget_waits": 0, + "slot_waits": 530, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 87159643, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42080.341, + "latency_p99_upper_ns": 53248 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068084.019, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8053834.564, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 252544, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1884160, + "elapsed_ns": 132225178, + "cpu_ns": 46345346, + "parse_call_ns": 22106440, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1998.568774, + "output_byte_seconds": 1764.360213, + "parser_byte_seconds": 2898.609, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 531, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 86929226, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42412.925, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068204.178, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8052346.592, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132119197, + "cpu_ns": 45785542, + "parse_call_ns": 22119160, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1985.975023, + "output_byte_seconds": 1182.61037, + "parser_byte_seconds": 2906.107753, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 535, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87289352, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42063.476, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068193.573, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8051854.179, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "compact", + "repeat": 1, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4436784, + "source_peak_bytes": 4230192, + "output_peak_bytes": 6944, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5603328, + "elapsed_ns": 132689456, + "cpu_ns": 50225388, + "parse_call_ns": 22679397, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340115.562513, + "output_byte_seconds": 342.313483, + "parser_byte_seconds": 2976.797692, + "source_allocations": 896, + "output_allocations": 908, + "parser_allocations": 1, + "reuses": 4325, + "budget_waits": 0, + "slot_waits": 548, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83654110, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 45534.298, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070135.917, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8055038.37, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132687220, + "cpu_ns": 45610919, + "parse_call_ns": 22143479, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2398.303122, + "output_byte_seconds": 919.310014, + "parser_byte_seconds": 2908.551305, + "source_allocations": 9, + "output_allocations": 73, + "parser_allocations": 1, + "reuses": 6047, + "budget_waits": 0, + "slot_waits": 557, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 88180228, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 41592.506, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067924.312, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050249.576, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "shared_copy", + "repeat": 1, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 133179387, + "cpu_ns": 47009567, + "parse_call_ns": 22053622, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2001.098596, + "output_byte_seconds": 1195.094634, + "parser_byte_seconds": 2898.481421, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 619, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87400987, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42811.697, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068344.258, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054983.926, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5373952, + "elapsed_ns": 132590517, + "cpu_ns": 49691116, + "parse_call_ns": 22667622, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540625.036225, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2974.002082, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 555, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 84069041, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 45356.652, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069262.154, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8055874.806, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5373952, + "elapsed_ns": 132595906, + "cpu_ns": 48936945, + "parse_call_ns": 22622066, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540538.226737, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2969.006017, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 550, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 84820014, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 44590.665, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069713.765, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054486.477, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132243573, + "cpu_ns": 45674856, + "parse_call_ns": 22029775, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1978.146096, + "output_byte_seconds": 1181.588579, + "parser_byte_seconds": 2894.51268, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 547, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87607899, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 41881.888, + "latency_p99_upper_ns": 53248 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067912.655, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8049931.179, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "compact", + "repeat": 2, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4436784, + "source_peak_bytes": 4230192, + "output_peak_bytes": 6944, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5603328, + "elapsed_ns": 132732463, + "cpu_ns": 49739534, + "parse_call_ns": 22671992, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340105.112239, + "output_byte_seconds": 342.468555, + "parser_byte_seconds": 2974.922545, + "source_allocations": 893, + "output_allocations": 887, + "parser_allocations": 1, + "reuses": 4349, + "budget_waits": 0, + "slot_waits": 562, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 84142850, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 44979.318, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069891.561, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8056164.197, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132272059, + "cpu_ns": 45320979, + "parse_call_ns": 22141356, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2389.486333, + "output_byte_seconds": 919.390876, + "parser_byte_seconds": 2908.882515, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 563, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 88007671, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 41406.16, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068143.112, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050851.553, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 318432, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1867776, + "elapsed_ns": 132351078, + "cpu_ns": 46122525, + "parse_call_ns": 22082400, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1991.903399, + "output_byte_seconds": 1762.771138, + "parser_byte_seconds": 2900.645032, + "source_allocations": 9, + "output_allocations": 1026, + "parser_allocations": 1, + "reuses": 7142, + "budget_waits": 0, + "slot_waits": 558, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 87297075, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42091.413, + "latency_p99_upper_ns": 53248 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068444.485, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8048834.24, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "shared_copy", + "repeat": 2, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132284819, + "cpu_ns": 45823432, + "parse_call_ns": 22066434, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1983.569581, + "output_byte_seconds": 1182.323172, + "parser_byte_seconds": 2898.432644, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 542, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87424368, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42064.699, + "latency_p99_upper_ns": 53248 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068229.227, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050900.843, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 252544, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1900544, + "elapsed_ns": 132459707, + "cpu_ns": 47692750, + "parse_call_ns": 22170762, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2023.677274, + "output_byte_seconds": 1777.322506, + "parser_byte_seconds": 2907.132194, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 540, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 85990890, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 43327.485, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070694.294, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8055086.674, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "shared_copy", + "repeat": 3, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132140503, + "cpu_ns": 45825210, + "parse_call_ns": 22125197, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1985.69773, + "output_byte_seconds": 1183.215978, + "parser_byte_seconds": 2906.687041, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 533, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87310088, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42062.03, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068036.782, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050191.375, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 252544, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1916928, + "elapsed_ns": 132297747, + "cpu_ns": 46669804, + "parse_call_ns": 22175323, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2009.648765, + "output_byte_seconds": 1766.146335, + "parser_byte_seconds": 2907.094824, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 542, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 86663675, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42678.905, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068637.263, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054142.04, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 318432, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132282510, + "cpu_ns": 46415608, + "parse_call_ns": 22079359, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1996.444688, + "output_byte_seconds": 1765.761752, + "parser_byte_seconds": 2901.17633, + "source_allocations": 9, + "output_allocations": 1025, + "parser_allocations": 1, + "reuses": 7143, + "budget_waits": 0, + "slot_waits": 554, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 86986908, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42277.028, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070038.574, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050279.443, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5373952, + "elapsed_ns": 132489898, + "cpu_ns": 48808681, + "parse_call_ns": 22654163, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540484.370521, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2972.753951, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 556, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 84863248, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 44578.165, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069410.422, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8053620.063, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1835008, + "elapsed_ns": 132283398, + "cpu_ns": 45911917, + "parse_call_ns": 22085581, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1984.871517, + "output_byte_seconds": 1183.416247, + "parser_byte_seconds": 2903.347153, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 561, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87433143, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42043.254, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068356.958, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8050581.195, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132082805, + "cpu_ns": 44815017, + "parse_call_ns": 22070822, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2374.331913, + "output_byte_seconds": 919.214871, + "parser_byte_seconds": 2899.672121, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 534, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 88275454, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 41070.548, + "latency_p99_upper_ns": 53248 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067369.015, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8049008.122, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "compact", + "repeat": 3, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4436784, + "source_peak_bytes": 4230192, + "output_peak_bytes": 7056, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5488640, + "elapsed_ns": 132597323, + "cpu_ns": 49981497, + "parse_call_ns": 22749218, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340108.105991, + "output_byte_seconds": 342.554096, + "parser_byte_seconds": 2986.081381, + "source_allocations": 912, + "output_allocations": 907, + "parser_allocations": 1, + "reuses": 4310, + "budget_waits": 0, + "slot_waits": 563, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83819563, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 45265.223, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069991.558, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8056976.805, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 252544, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1900544, + "elapsed_ns": 132985160, + "cpu_ns": 49981054, + "parse_call_ns": 23442854, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2129.142526, + "output_byte_seconds": 1795.211172, + "parser_byte_seconds": 3074.346252, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 542, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 84089494, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 45510.754, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5098662.604, + "latency_p99_upper_ns": 6291456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8056999.864, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5373952, + "elapsed_ns": 133433884, + "cpu_ns": 51462618, + "parse_call_ns": 23747967, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 540814.68462, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 3118.422109, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 596, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83315856, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 46811.327, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070925.9, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8058845.673, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1867776, + "elapsed_ns": 132186422, + "cpu_ns": 45811373, + "parse_call_ns": 22083462, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1983.12097, + "output_byte_seconds": 1182.647891, + "parser_byte_seconds": 2900.448352, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 532, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87421696, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42006.171, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068025.061, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8049545.414, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "shared_copy", + "repeat": 4, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 39984, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132188663, + "cpu_ns": 45991210, + "parse_call_ns": 22145612, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1991.371251, + "output_byte_seconds": 1184.961172, + "parser_byte_seconds": 2909.800223, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 7080, + "budget_waits": 0, + "slot_waits": 543, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 87186189, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42139.616, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068490.238, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8052428.242, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "compact", + "repeat": 4, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4436784, + "source_peak_bytes": 4230192, + "output_peak_bytes": 7056, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5537792, + "elapsed_ns": 132746292, + "cpu_ns": 50870250, + "parse_call_ns": 22660664, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340246.674827, + "output_byte_seconds": 342.038296, + "parser_byte_seconds": 2973.982939, + "source_allocations": 893, + "output_allocations": 895, + "parser_allocations": 1, + "reuses": 4341, + "budget_waits": 0, + "slot_waits": 545, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83141858, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 45809.599, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5072116.599, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8054634.315, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 311264, + "source_peak_bytes": 98400, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1851392, + "elapsed_ns": 132177385, + "cpu_ns": 44977578, + "parse_call_ns": 22128368, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2381.095196, + "output_byte_seconds": 919.555429, + "parser_byte_seconds": 2907.397974, + "source_allocations": 9, + "output_allocations": 64, + "parser_allocations": 1, + "reuses": 6056, + "budget_waits": 0, + "slot_waits": 539, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 88187889, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 41268.295, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067698.961, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8052209.973, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 318432, + "source_peak_bytes": 98400, + "output_peak_bytes": 47152, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 1867776, + "elapsed_ns": 132225648, + "cpu_ns": 46118905, + "parse_call_ns": 22053057, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 1988.501642, + "output_byte_seconds": 1767.427593, + "parser_byte_seconds": 2896.491937, + "source_allocations": 9, + "output_allocations": 1025, + "parser_allocations": 1, + "reuses": 7143, + "budget_waits": 0, + "slot_waits": 536, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 87128865, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 42165.024, + "latency_p99_upper_ns": 53248 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068266.918, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8051060.116, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4373248, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6078464, + "elapsed_ns": 216945564, + "cpu_ns": 116157398, + "parse_call_ns": 25703347, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 3917.681175, + "output_byte_seconds": 512250.563648, + "parser_byte_seconds": 3150.964519, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 383, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 48939452, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 117402.264, + "latency_p99_upper_ns": 524288 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 6063368.282, + "latency_p99_upper_ns": 31457280 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 9260486.827, + "latency_p99_upper_ns": 33554432 + } + ] + }, + { + "case": "same_large_field", + "variant": "shared_copy", + "repeat": 0, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3178496, + "elapsed_ns": 133534289, + "cpu_ns": 66640259, + "parse_call_ns": 22303696, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2044.238204, + "output_byte_seconds": 269834.895225, + "parser_byte_seconds": 2930.529378, + "source_allocations": 82, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6047, + "budget_waits": 0, + "slot_waits": 500, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 67897726, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51484.234, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078396.125, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8065897.697, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133553215, + "cpu_ns": 68694192, + "parse_call_ns": 22344284, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541236.958782, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2932.875901, + "source_allocations": 76, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5029, + "budget_waits": 0, + "slot_waits": 479, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 65966946, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52115.836, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078241.217, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8065472.756, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4504544, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5292032, + "elapsed_ns": 134319516, + "cpu_ns": 74842901, + "parse_call_ns": 22151327, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2277.11069, + "output_byte_seconds": 440218.115158, + "parser_byte_seconds": 2909.330552, + "source_allocations": 9, + "output_allocations": 841, + "parser_allocations": 1, + "reuses": 7327, + "budget_waits": 0, + "slot_waits": 490, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60554269, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 57181.88, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5083619.048, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8071315.085, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3194880, + "elapsed_ns": 133454646, + "cpu_ns": 66234578, + "parse_call_ns": 22159339, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2031.818258, + "output_byte_seconds": 269861.554419, + "parser_byte_seconds": 2910.266617, + "source_allocations": 78, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6051, + "budget_waits": 0, + "slot_waits": 487, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 68332882, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51484.504, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5077341.499, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8066441.419, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "compact", + "repeat": 0, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2001776, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 134096312, + "cpu_ns": 74886880, + "parse_call_ns": 22733900, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 341111.908821, + "output_byte_seconds": 100293.13752, + "parser_byte_seconds": 2983.138393, + "source_allocations": 921, + "output_allocations": 779, + "parser_allocations": 1, + "reuses": 4429, + "budget_waits": 0, + "slot_waits": 471, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60362834, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 54080.027, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5083405.479, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068401.165, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133447386, + "cpu_ns": 68765370, + "parse_call_ns": 22353361, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541359.828153, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2934.692831, + "source_allocations": 77, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5028, + "budget_waits": 0, + "slot_waits": 487, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 65809490, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52372.427, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078845.858, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067374.837, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "compact", + "repeat": 1, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2034592, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 134204038, + "cpu_ns": 74836392, + "parse_call_ns": 22853483, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 341117.919471, + "output_byte_seconds": 100364.674661, + "parser_byte_seconds": 2998.098529, + "source_allocations": 881, + "output_allocations": 767, + "parser_allocations": 1, + "reuses": 4481, + "budget_waits": 0, + "slot_waits": 478, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60375968, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 54356.541, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5083649.875, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070735.206, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "shared_copy", + "repeat": 1, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3244032, + "elapsed_ns": 133593447, + "cpu_ns": 66172548, + "parse_call_ns": 22181499, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2031.917671, + "output_byte_seconds": 269851.910584, + "parser_byte_seconds": 2913.380586, + "source_allocations": 80, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6049, + "budget_waits": 0, + "slot_waits": 471, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 68489180, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51688.946, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5077231.39, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8066199.086, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4504544, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5292032, + "elapsed_ns": 134436483, + "cpu_ns": 75053693, + "parse_call_ns": 22215239, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2284.410516, + "output_byte_seconds": 440251.541897, + "parser_byte_seconds": 2917.091676, + "source_allocations": 9, + "output_allocations": 832, + "parser_allocations": 1, + "reuses": 7336, + "budget_waits": 0, + "slot_waits": 483, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60480780, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 57382.812, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5083530.123, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8072471.683, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3194880, + "elapsed_ns": 134034428, + "cpu_ns": 69077852, + "parse_call_ns": 22386881, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2071.219143, + "output_byte_seconds": 269907.925264, + "parser_byte_seconds": 2941.415878, + "source_allocations": 82, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6047, + "budget_waits": 0, + "slot_waits": 521, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 66160244, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52463.206, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5079970.677, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068543.087, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133602318, + "cpu_ns": 69361342, + "parse_call_ns": 22347294, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541336.354919, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2933.602437, + "source_allocations": 76, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5029, + "budget_waits": 0, + "slot_waits": 500, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 65433839, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52034.221, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5079700.568, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067086.654, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133878082, + "cpu_ns": 69431646, + "parse_call_ns": 22393309, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541341.633608, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2938.771712, + "source_allocations": 78, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5027, + "budget_waits": 0, + "slot_waits": 504, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 65629618, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52136.236, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078986.712, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067041.718, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4373248, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5816320, + "elapsed_ns": 134331933, + "cpu_ns": 75355273, + "parse_call_ns": 22600489, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2317.391148, + "output_byte_seconds": 440282.284319, + "parser_byte_seconds": 2923.56271, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 497, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60100461, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 58242.85, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5084649.973, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8073924.058, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "compact", + "repeat": 2, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2001776, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 134074359, + "cpu_ns": 74679639, + "parse_call_ns": 22736073, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 341236.488763, + "output_byte_seconds": 100267.211633, + "parser_byte_seconds": 2982.994817, + "source_allocations": 917, + "output_allocations": 794, + "parser_allocations": 1, + "reuses": 4418, + "budget_waits": 0, + "slot_waits": 471, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60515136, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 53738.105, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5085195.058, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069412.397, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4504544, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5275648, + "elapsed_ns": 136064340, + "cpu_ns": 79416596, + "parse_call_ns": 22572078, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2363.378088, + "output_byte_seconds": 440586.829499, + "parser_byte_seconds": 2964.218039, + "source_allocations": 9, + "output_allocations": 623, + "parser_allocations": 1, + "reuses": 7545, + "budget_waits": 0, + "slot_waits": 463, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 57551670, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 60112.085, + "latency_p99_upper_ns": 114688 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5087074.439, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8080314.649, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3244032, + "elapsed_ns": 133655494, + "cpu_ns": 66354625, + "parse_call_ns": 22289900, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2049.709126, + "output_byte_seconds": 269797.797722, + "parser_byte_seconds": 2927.700988, + "source_allocations": 78, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6051, + "budget_waits": 0, + "slot_waits": 500, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 68212138, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51495.073, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078118.015, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8064823.351, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "shared_copy", + "repeat": 2, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3293184, + "elapsed_ns": 133683827, + "cpu_ns": 66973903, + "parse_call_ns": 22176603, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2039.054037, + "output_byte_seconds": 269818.155226, + "parser_byte_seconds": 2912.084727, + "source_allocations": 78, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6051, + "budget_waits": 0, + "slot_waits": 494, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 67816366, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51459.966, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5079235.137, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8065334.704, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133681327, + "cpu_ns": 68797289, + "parse_call_ns": 22338685, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541289.267391, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2932.086428, + "source_allocations": 77, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5028, + "budget_waits": 0, + "slot_waits": 482, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 65978872, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52563.022, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078578.492, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8066255.165, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133814643, + "cpu_ns": 68888479, + "parse_call_ns": 22335355, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541341.207084, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2930.637289, + "source_allocations": 79, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5026, + "budget_waits": 0, + "slot_waits": 510, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 66149871, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51550.405, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5079607.126, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067127.786, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4373248, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5816320, + "elapsed_ns": 134471775, + "cpu_ns": 75393460, + "parse_call_ns": 22629325, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2321.181236, + "output_byte_seconds": 440258.792854, + "parser_byte_seconds": 2921.16977, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 508, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60227676, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 57516.822, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5084982.783, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8073926.553, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3194880, + "elapsed_ns": 133441147, + "cpu_ns": 66434679, + "parse_call_ns": 22119254, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2030.079604, + "output_byte_seconds": 269825.85573, + "parser_byte_seconds": 2905.779822, + "source_allocations": 80, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6049, + "budget_waits": 0, + "slot_waits": 482, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 68150063, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51190.857, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5077733.031, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8065349.133, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4373248, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5816320, + "elapsed_ns": 134289332, + "cpu_ns": 76002725, + "parse_call_ns": 22740343, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2334.193158, + "output_byte_seconds": 440332.993703, + "parser_byte_seconds": 2936.962125, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 504, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 59412631, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 59077.132, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5084960.638, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8075279.097, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "shared_copy", + "repeat": 3, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3309568, + "elapsed_ns": 133495777, + "cpu_ns": 66421587, + "parse_call_ns": 22309367, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2045.123956, + "output_byte_seconds": 269842.975673, + "parser_byte_seconds": 2931.949276, + "source_allocations": 76, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6053, + "budget_waits": 0, + "slot_waits": 497, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 68249668, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51216.396, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078028.333, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8066089.677, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 134337593, + "cpu_ns": 71105404, + "parse_call_ns": 22575073, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541448.462493, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2962.344728, + "source_allocations": 83, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5022, + "budget_waits": 0, + "slot_waits": 495, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 64431294, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 53339.614, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5080292.879, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068765.424, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4504544, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5292032, + "elapsed_ns": 134254523, + "cpu_ns": 76196602, + "parse_call_ns": 22285338, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2295.795833, + "output_byte_seconds": 440394.753415, + "parser_byte_seconds": 2927.722098, + "source_allocations": 9, + "output_allocations": 820, + "parser_allocations": 1, + "reuses": 7348, + "budget_waits": 0, + "slot_waits": 485, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 59133245, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 58635.959, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5084371.237, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8075247.27, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "compact", + "repeat": 3, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2034592, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 134306041, + "cpu_ns": 75274107, + "parse_call_ns": 22714288, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 341191.973388, + "output_byte_seconds": 100297.728905, + "parser_byte_seconds": 2980.354846, + "source_allocations": 917, + "output_allocations": 749, + "parser_allocations": 1, + "reuses": 4463, + "budget_waits": 0, + "slot_waits": 478, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 60108647, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 54208.683, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5084673.227, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069796.873, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133817208, + "cpu_ns": 69542587, + "parse_call_ns": 22437830, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541408.186663, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2945.936502, + "source_allocations": 80, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5025, + "budget_waits": 0, + "slot_waits": 483, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 65488224, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52016.168, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5080750.668, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068031.861, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4373248, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5799936, + "elapsed_ns": 134597856, + "cpu_ns": 77396908, + "parse_call_ns": 22833216, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2355.524795, + "output_byte_seconds": 440416.150759, + "parser_byte_seconds": 2941.859195, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 512, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 58493375, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 59008.762, + "latency_p99_upper_ns": 98304 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5086107.088, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8076875.839, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4504544, + "source_peak_bytes": 98400, + "output_peak_bytes": 4233264, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5292032, + "elapsed_ns": 139508137, + "cpu_ns": 88332555, + "parse_call_ns": 22864596, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 100663296, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2528.253394, + "output_byte_seconds": 442384.852596, + "parser_byte_seconds": 3002.901193, + "source_allocations": 9, + "output_allocations": 688, + "parser_allocations": 1, + "reuses": 7480, + "budget_waits": 0, + "slot_waits": 372, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51495885, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 66698.854, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5129096.339, + "latency_p99_upper_ns": 6815744 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8093643.623, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4833280, + "elapsed_ns": 134457305, + "cpu_ns": 70251788, + "parse_call_ns": 22431999, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541395.863387, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2942.397573, + "source_allocations": 87, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5018, + "budget_waits": 0, + "slot_waits": 495, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 65292089, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 53169.212, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5080238.856, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068047.314, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3211264, + "elapsed_ns": 133756102, + "cpu_ns": 67373341, + "parse_call_ns": 22155214, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2045.927873, + "output_byte_seconds": 269868.863264, + "parser_byte_seconds": 2910.706656, + "source_allocations": 81, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6048, + "budget_waits": 0, + "slot_waits": 506, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 67548515, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 51135.428, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078670.025, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8066898.388, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "shared_copy", + "repeat": 4, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2371504, + "source_peak_bytes": 98400, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3571712, + "elapsed_ns": 135030934, + "cpu_ns": 71236281, + "parse_call_ns": 22446961, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2100.123381, + "output_byte_seconds": 269953.875704, + "parser_byte_seconds": 2948.934561, + "source_allocations": 94, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 6035, + "budget_waits": 0, + "slot_waits": 507, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 2048, + "sleep_ns": 65033726, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 53626.014, + "latency_p99_upper_ns": 90112 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5087279.412, + "latency_p99_upper_ns": 5767168 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070672.959, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "compact", + "repeat": 4, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 1936144, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 135831514, + "cpu_ns": 84355786, + "parse_call_ns": 23107923, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 341580.411374, + "output_byte_seconds": 100419.005421, + "parser_byte_seconds": 3032.899482, + "source_allocations": 853, + "output_allocations": 676, + "parser_allocations": 1, + "reuses": 4600, + "budget_waits": 0, + "slot_waits": 455, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 52913091, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 60319.001, + "latency_p99_upper_ns": 114688 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5091148.147, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8079928.47, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133667291, + "cpu_ns": 68534602, + "parse_call_ns": 22264001, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541292.227145, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2921.518156, + "source_allocations": 77, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5028, + "budget_waits": 0, + "slot_waits": 481, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 66216721, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 1, + "latency_mean_ns": 52072.538, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 5078370.622, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8066280.939, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378784, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3899392, + "elapsed_ns": 133407209, + "cpu_ns": 54623208, + "parse_call_ns": 22433685, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2079.608844, + "output_byte_seconds": 270519.575773, + "parser_byte_seconds": 2946.904954, + "source_allocations": 118, + "output_allocations": 838, + "parser_allocations": 1, + "reuses": 7221, + "budget_waits": 0, + "slot_waits": 547, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 79664236, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 47700.066, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070062.768, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069584.641, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3751936, + "elapsed_ns": 133312135, + "cpu_ns": 53364925, + "parse_call_ns": 22316231, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2049.372724, + "output_byte_seconds": 270524.406644, + "parser_byte_seconds": 2928.92106, + "source_allocations": 137, + "output_allocations": 841, + "parser_allocations": 1, + "reuses": 6175, + "budget_waits": 0, + "slot_waits": 552, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 80930688, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46742.189, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068879.809, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069119.599, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "shared_copy", + "repeat": 0, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3948544, + "elapsed_ns": 154001095, + "cpu_ns": 65368726, + "parse_call_ns": 23437273, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2683.230182, + "output_byte_seconds": 276097.966097, + "parser_byte_seconds": 3083.783614, + "source_allocations": 228, + "output_allocations": 746, + "parser_allocations": 1, + "reuses": 6179, + "budget_waits": 0, + "slot_waits": 542, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 75659422, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 64443.196, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5134851.331, + "latency_p99_upper_ns": 7340032 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8252758.487, + "latency_p99_upper_ns": 15728640 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2312896, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3194880, + "elapsed_ns": 133648283, + "cpu_ns": 61986524, + "parse_call_ns": 25370210, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2299.022458, + "output_byte_seconds": 270612.233717, + "parser_byte_seconds": 2983.123314, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 653, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 72630111, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 53967.42, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5080263.648, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8075763.615, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "compact", + "repeat": 0, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2067408, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 133472857, + "cpu_ns": 62602975, + "parse_call_ns": 23134595, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340709.17918, + "output_byte_seconds": 100724.838498, + "parser_byte_seconds": 3036.377571, + "source_allocations": 923, + "output_allocations": 841, + "parser_allocations": 1, + "reuses": 4365, + "budget_waits": 0, + "slot_waits": 526, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 71679132, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 48473.289, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5073065.738, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070772.518, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4443056, + "source_peak_bytes": 4230192, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4751360, + "elapsed_ns": 133297584, + "cpu_ns": 55094453, + "parse_call_ns": 22718797, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541537.659914, + "output_byte_seconds": 577.166247, + "parser_byte_seconds": 2982.289653, + "source_allocations": 130, + "output_allocations": 809, + "parser_allocations": 1, + "reuses": 5190, + "budget_waits": 0, + "slot_waits": 541, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 79050000, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 48100.386, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069482.618, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070037.129, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133477288, + "cpu_ns": 54372368, + "parse_call_ns": 22466675, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541562.393801, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2949.789857, + "source_allocations": 80, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5025, + "budget_waits": 0, + "slot_waits": 554, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 80055686, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46918.51, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069158.581, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070277.888, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3932160, + "elapsed_ns": 133312509, + "cpu_ns": 53734268, + "parse_call_ns": 22533891, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2071.170012, + "output_byte_seconds": 270512.249274, + "parser_byte_seconds": 2959.58098, + "source_allocations": 159, + "output_allocations": 828, + "parser_allocations": 1, + "reuses": 6166, + "budget_waits": 0, + "slot_waits": 575, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 80473327, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46789.037, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070040.373, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069081.323, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2312896, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3194880, + "elapsed_ns": 134281084, + "cpu_ns": 62190824, + "parse_call_ns": 25213500, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2300.277794, + "output_byte_seconds": 270647.838041, + "parser_byte_seconds": 2975.251132, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 663, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 73016473, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 54287.927, + "latency_p99_upper_ns": 98304 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5080276.456, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8077074.059, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378784, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3948544, + "elapsed_ns": 133395362, + "cpu_ns": 55263103, + "parse_call_ns": 22335683, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2070.323199, + "output_byte_seconds": 270570.958879, + "parser_byte_seconds": 2935.679247, + "source_allocations": 123, + "output_allocations": 825, + "parser_allocations": 1, + "reuses": 7229, + "budget_waits": 0, + "slot_waits": 563, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 79157676, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 47428.456, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070071.895, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070912.004, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "shared_copy", + "repeat": 1, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3522560, + "elapsed_ns": 133405307, + "cpu_ns": 53873137, + "parse_call_ns": 22349414, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2061.271642, + "output_byte_seconds": 270527.998952, + "parser_byte_seconds": 2936.171471, + "source_allocations": 144, + "output_allocations": 811, + "parser_allocations": 1, + "reuses": 6198, + "budget_waits": 0, + "slot_waits": 551, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 80515381, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46564.507, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069570.9, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069438.264, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "compact", + "repeat": 1, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 133295478, + "cpu_ns": 60929076, + "parse_call_ns": 22892803, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340588.424197, + "output_byte_seconds": 100730.999866, + "parser_byte_seconds": 3007.852808, + "source_allocations": 919, + "output_allocations": 842, + "parser_allocations": 1, + "reuses": 4368, + "budget_waits": 0, + "slot_waits": 524, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 73271182, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 48259.733, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5071843.127, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069717.131, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5390336, + "elapsed_ns": 133317978, + "cpu_ns": 54530428, + "parse_call_ns": 22789187, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541551.170985, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2991.654899, + "source_allocations": 72, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5033, + "budget_waits": 0, + "slot_waits": 541, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 79523636, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 47420.19, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5069779.505, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070214.071, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4443056, + "source_peak_bytes": 4230192, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5357568, + "elapsed_ns": 133341496, + "cpu_ns": 54857945, + "parse_call_ns": 22618201, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541518.37496, + "output_byte_seconds": 577.265132, + "parser_byte_seconds": 2971.205031, + "source_allocations": 122, + "output_allocations": 835, + "parser_allocations": 1, + "reuses": 5172, + "budget_waits": 0, + "slot_waits": 562, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 79396532, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 47550.497, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070242.543, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069675.14, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4443056, + "source_peak_bytes": 4230192, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5292032, + "elapsed_ns": 134650847, + "cpu_ns": 58145169, + "parse_call_ns": 22801055, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 542663.104532, + "output_byte_seconds": 578.298773, + "parser_byte_seconds": 2995.063232, + "source_allocations": 127, + "output_allocations": 804, + "parser_allocations": 1, + "reuses": 5198, + "budget_waits": 0, + "slot_waits": 553, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 77457652, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 49310.575, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5080294.837, + "latency_p99_upper_ns": 5767168 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8086977.866, + "latency_p99_upper_ns": 9437184 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378784, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3964928, + "elapsed_ns": 133179940, + "cpu_ns": 52486508, + "parse_call_ns": 22146823, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2028.917653, + "output_byte_seconds": 270508.558846, + "parser_byte_seconds": 2910.035715, + "source_allocations": 119, + "output_allocations": 875, + "parser_allocations": 1, + "reuses": 7183, + "budget_waits": 0, + "slot_waits": 532, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 81744820, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46179.91, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068139.702, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068218.996, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133254368, + "cpu_ns": 52756869, + "parse_call_ns": 22242096, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541465.495701, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2919.456949, + "source_allocations": 79, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5026, + "budget_waits": 0, + "slot_waits": 537, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 81620293, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 45804.026, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067325.337, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068611.8, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "compact", + "repeat": 2, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 133273853, + "cpu_ns": 59503628, + "parse_call_ns": 22579568, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340521.103381, + "output_byte_seconds": 100725.988305, + "parser_byte_seconds": 2964.439632, + "source_allocations": 921, + "output_allocations": 839, + "parser_allocations": 1, + "reuses": 4369, + "budget_waits": 0, + "slot_waits": 529, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 75001351, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46820.147, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5071087.53, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068767.35, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "shared_copy", + "repeat": 2, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3293184, + "elapsed_ns": 133216315, + "cpu_ns": 51631438, + "parse_call_ns": 22079426, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2014.496107, + "output_byte_seconds": 270449.091989, + "parser_byte_seconds": 2901.711431, + "source_allocations": 152, + "output_allocations": 827, + "parser_allocations": 1, + "reuses": 6174, + "budget_waits": 0, + "slot_waits": 543, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 82698735, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 45041.785, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068002.393, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8066161.602, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2312896, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3178496, + "elapsed_ns": 133362779, + "cpu_ns": 58084129, + "parse_call_ns": 24178012, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2196.167723, + "output_byte_seconds": 270535.919777, + "parser_byte_seconds": 2934.024512, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 605, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 76526627, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 50758.917, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5074265.021, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8071772.438, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3751936, + "elapsed_ns": 133094723, + "cpu_ns": 52669444, + "parse_call_ns": 22153975, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2026.350691, + "output_byte_seconds": 270487.081841, + "parser_byte_seconds": 2910.446252, + "source_allocations": 162, + "output_allocations": 813, + "parser_allocations": 1, + "reuses": 6178, + "budget_waits": 0, + "slot_waits": 529, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 81515465, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46192.459, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067955.414, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067608.796, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "compact", + "repeat": 3, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6340608, + "elapsed_ns": 133282278, + "cpu_ns": 59568109, + "parse_call_ns": 22558632, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340502.324772, + "output_byte_seconds": 100714.490169, + "parser_byte_seconds": 2961.00573, + "source_allocations": 923, + "output_allocations": 826, + "parser_allocations": 1, + "reuses": 4380, + "budget_waits": 0, + "slot_waits": 521, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 74934523, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46941.425, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5070856.899, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068200.595, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2312896, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3194880, + "elapsed_ns": 133540522, + "cpu_ns": 58472614, + "parse_call_ns": 24345793, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2208.417867, + "output_byte_seconds": 270579.890216, + "parser_byte_seconds": 2933.62499, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 602, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 76325826, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 51364.197, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5074603.185, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8073207.582, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133275839, + "cpu_ns": 52234245, + "parse_call_ns": 22266351, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541367.893955, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2922.935825, + "source_allocations": 79, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5026, + "budget_waits": 0, + "slot_waits": 545, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 82113024, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 45842.847, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5066801.978, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067096.868, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "shared_copy", + "repeat": 3, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3899392, + "elapsed_ns": 133064931, + "cpu_ns": 52117164, + "parse_call_ns": 22057182, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2015.469116, + "output_byte_seconds": 270480.700872, + "parser_byte_seconds": 2897.68644, + "source_allocations": 130, + "output_allocations": 842, + "parser_allocations": 1, + "reuses": 6181, + "budget_waits": 0, + "slot_waits": 555, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 82111301, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 45400.789, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068255.587, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067189.304, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4443056, + "source_peak_bytes": 4230192, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5292032, + "elapsed_ns": 133337450, + "cpu_ns": 53670610, + "parse_call_ns": 22263776, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541459.255198, + "output_byte_seconds": 577.165832, + "parser_byte_seconds": 2922.14622, + "source_allocations": 137, + "output_allocations": 796, + "parser_allocations": 1, + "reuses": 5196, + "budget_waits": 0, + "slot_waits": 551, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 80837357, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46321.397, + "latency_p99_upper_ns": 65536 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068514.702, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8068639.91, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3522560, + "elapsed_ns": 134333542, + "cpu_ns": 52726291, + "parse_call_ns": 22219255, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2039.714059, + "output_byte_seconds": 270798.061653, + "parser_byte_seconds": 2918.877137, + "source_allocations": 140, + "output_allocations": 894, + "parser_allocations": 1, + "reuses": 6119, + "budget_waits": 0, + "slot_waits": 516, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 82638596, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46056.021, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5218345.888, + "latency_p99_upper_ns": 9437184 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8076530.434, + "latency_p99_upper_ns": 9437184 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378784, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3948544, + "elapsed_ns": 133174357, + "cpu_ns": 52057974, + "parse_call_ns": 22117835, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2021.284206, + "output_byte_seconds": 270501.113842, + "parser_byte_seconds": 2905.858494, + "source_allocations": 114, + "output_allocations": 849, + "parser_allocations": 1, + "reuses": 7214, + "budget_waits": 0, + "slot_waits": 548, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 82257586, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 45428.889, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068142.38, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067808.96, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2312896, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3178496, + "elapsed_ns": 133410638, + "cpu_ns": 57999768, + "parse_call_ns": 24134627, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2191.645151, + "output_byte_seconds": 270510.58659, + "parser_byte_seconds": 2926.663567, + "source_allocations": 4082, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 603, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 76638749, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 51016.097, + "latency_p99_upper_ns": 81920 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5074074.877, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8071032.382, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3342336, + "elapsed_ns": 133213102, + "cpu_ns": 52362717, + "parse_call_ns": 22103023, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2022.420003, + "output_byte_seconds": 270482.639278, + "parser_byte_seconds": 2904.168619, + "source_allocations": 127, + "output_allocations": 855, + "parser_allocations": 1, + "reuses": 6171, + "budget_waits": 0, + "slot_waits": 539, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 81964913, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46044.098, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5068262.707, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067371.07, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378784, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107504, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3751936, + "elapsed_ns": 133177547, + "cpu_ns": 52033688, + "parse_call_ns": 22154380, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33669120, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2025.354638, + "output_byte_seconds": 270478.813202, + "parser_byte_seconds": 2910.934805, + "source_allocations": 129, + "output_allocations": 851, + "parser_allocations": 1, + "reuses": 7197, + "budget_waits": 0, + "slot_waits": 527, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 82109654, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 45825.604, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067937.156, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067321.024, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4435888, + "source_peak_bytes": 4230192, + "output_peak_bytes": 0, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 4767744, + "elapsed_ns": 133311566, + "cpu_ns": 53725285, + "parse_call_ns": 22239925, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 0, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541575.408384, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 2920.340305, + "source_allocations": 80, + "output_allocations": 0, + "parser_allocations": 1, + "reuses": 5025, + "budget_waits": 0, + "slot_waits": 555, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 80727666, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46306.509, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067147.548, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8070390.562, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4443056, + "source_peak_bytes": 4230192, + "output_peak_bytes": 7168, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 5439488, + "elapsed_ns": 134101658, + "cpu_ns": 54158047, + "parse_call_ns": 22315394, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 541512.874989, + "output_byte_seconds": 578.716358, + "parser_byte_seconds": 2931.279646, + "source_allocations": 161, + "output_allocations": 826, + "parser_allocations": 1, + "reuses": 5142, + "budget_waits": 0, + "slot_waits": 533, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 81103821, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46533.691, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5082538.041, + "latency_p99_upper_ns": 6291456 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8069591.938, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "shared_copy", + "repeat": 4, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2378672, + "source_peak_bytes": 98400, + "output_peak_bytes": 2107392, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 3948544, + "elapsed_ns": 133134639, + "cpu_ns": 52075173, + "parse_call_ns": 22128273, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33611776, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 2026.862393, + "output_byte_seconds": 270471.476005, + "parser_byte_seconds": 2908.200691, + "source_allocations": 122, + "output_allocations": 816, + "parser_allocations": 1, + "reuses": 6215, + "budget_waits": 0, + "slot_waits": 512, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 82080012, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 46182.627, + "latency_p99_upper_ns": 61440 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5067504.389, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067072.688, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "compact", + "repeat": 4, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 8388608, + "fixed_bytes": 41728, + "peak_reserved_bytes": 4730672, + "source_peak_bytes": 4230192, + "output_peak_bytes": 2100224, + "parser_peak_bytes": 131120, + "rss_peak_bytes": 6406144, + "elapsed_ns": 133118662, + "cpu_ns": 59607669, + "parse_call_ns": 22648269, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 42083156, + "selection_copy_bytes": 33554432, + "reader_copy_bytes": 75711296, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 340524.867797, + "output_byte_seconds": 100684.088672, + "parser_byte_seconds": 2973.798584, + "source_allocations": 921, + "output_allocations": 855, + "parser_allocations": 1, + "reuses": 4353, + "budget_waits": 0, + "slot_waits": 532, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 74562185, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 1, + "latency_mean_ns": 47236.674, + "latency_p99_upper_ns": 73728 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5071269.907, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 33554432, + "peak_count": 64, + "latency_mean_ns": 8067723.707, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1104848, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134798098, + "cpu_ns": 52545277, + "parse_call_ns": 26282225, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6883.203763, + "output_byte_seconds": 1809.304759, + "parser_byte_seconds": 10115.296993, + "source_allocations": 367, + "output_allocations": 948, + "parser_allocations": 374, + "reuses": 6256, + "budget_waits": 0, + "slot_waits": 579, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83482902, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47583.467, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5081460.678, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8083520.998, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 11312, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 2031616, + "elapsed_ns": 134514858, + "cpu_ns": 50924568, + "parse_call_ns": 26180831, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 1059072, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 7475.627393, + "output_byte_seconds": 924.635553, + "parser_byte_seconds": 10091.334952, + "source_allocations": 237, + "output_allocations": 96, + "parser_allocations": 257, + "reuses": 5800, + "budget_waits": 0, + "slot_waits": 593, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 84806597, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 46212.018, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5080764.964, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8080296.584, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 842368, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1949696, + "elapsed_ns": 135580728, + "cpu_ns": 59055276, + "parse_call_ns": 27303180, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 8268.860241, + "output_byte_seconds": 1866.643736, + "parser_byte_seconds": 10141.702783, + "source_allocations": 3849, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 540, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 77683199, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 53763.312, + "latency_p99_upper_ns": 163840 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5090947.59, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8093934.847, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "shared_copy", + "repeat": 0, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134733970, + "cpu_ns": 52189389, + "parse_call_ns": 26273407, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6865.482399, + "output_byte_seconds": 1226.436074, + "parser_byte_seconds": 10135.432887, + "source_allocations": 365, + "output_allocations": 246, + "parser_allocations": 368, + "reuses": 5942, + "budget_waits": 0, + "slot_waits": 587, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83766349, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47383.279, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5081532.837, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082383.547, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134703694, + "cpu_ns": 51981424, + "parse_call_ns": 26239241, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6818.11346, + "output_byte_seconds": 1220.93584, + "parser_byte_seconds": 10109.238408, + "source_allocations": 368, + "output_allocations": 240, + "parser_allocations": 366, + "reuses": 5947, + "budget_waits": 0, + "slot_waits": 592, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83962412, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47186.16, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5082027.365, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082717.212, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "compact", + "repeat": 0, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2096880, + "source_peak_bytes": 2047296, + "output_peak_bytes": 3360, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3702784, + "elapsed_ns": 330325402, + "cpu_ns": 56526552, + "parse_call_ns": 257818693, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 542057.64214, + "output_byte_seconds": 342.456081, + "parser_byte_seconds": 10454.037412, + "source_allocations": 1653, + "output_allocations": 1018, + "parser_allocations": 587, + "reuses": 2639, + "budget_waits": 1256, + "slot_waits": 0, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 275134226, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 313916.181, + "latency_p99_upper_ns": 4718592 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5359649.404, + "latency_p99_upper_ns": 10485760 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 49, + "latency_mean_ns": 8345801.06, + "latency_p99_upper_ns": 12582912 + } + ] + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2097024, + "source_peak_bytes": 2047296, + "output_peak_bytes": 0, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3702784, + "elapsed_ns": 517444947, + "cpu_ns": 58385263, + "parse_call_ns": 414175159, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 0, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 866552.611778, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 10646.478365, + "source_allocations": 1612, + "output_allocations": 0, + "parser_allocations": 580, + "reuses": 2681, + "budget_waits": 1250, + "slot_waits": 0, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 460552293, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 496739.613, + "latency_p99_upper_ns": 7864320 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5531099.266, + "latency_p99_upper_ns": 12582912 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 8547759.071, + "latency_p99_upper_ns": 15728640 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 842368, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1916928, + "elapsed_ns": 135818181, + "cpu_ns": 60130654, + "parse_call_ns": 27699522, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 8398.686166, + "output_byte_seconds": 1885.555543, + "parser_byte_seconds": 10206.603011, + "source_allocations": 3849, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 537, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 76838088, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 54782.86, + "latency_p99_upper_ns": 180224 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5093567.929, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8096201.349, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 11312, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 2064384, + "elapsed_ns": 134627181, + "cpu_ns": 51051158, + "parse_call_ns": 26242598, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 1059072, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 7499.787736, + "output_byte_seconds": 924.734083, + "parser_byte_seconds": 10090.493068, + "source_allocations": 237, + "output_allocations": 100, + "parser_allocations": 258, + "reuses": 5795, + "budget_waits": 0, + "slot_waits": 587, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 84733901, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 46365.068, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5080938.783, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8081319.231, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "shared_copy", + "repeat": 1, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1900544, + "elapsed_ns": 134660107, + "cpu_ns": 51849496, + "parse_call_ns": 26235438, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6785.330268, + "output_byte_seconds": 1222.453816, + "parser_byte_seconds": 10103.005533, + "source_allocations": 366, + "output_allocations": 239, + "parser_allocations": 366, + "reuses": 5950, + "budget_waits": 0, + "slot_waits": 579, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 84013028, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47133.761, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5081549.099, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082113.784, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2097024, + "source_peak_bytes": 2047296, + "output_peak_bytes": 0, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3719168, + "elapsed_ns": 516876187, + "cpu_ns": 58189132, + "parse_call_ns": 413488109, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 0, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 866155.703795, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 10618.392921, + "source_allocations": 1617, + "output_allocations": 0, + "parser_allocations": 581, + "reuses": 2675, + "budget_waits": 1263, + "slot_waits": 0, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 460124695, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 496219.126, + "latency_p99_upper_ns": 7864320 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5528237.536, + "latency_p99_upper_ns": 12582912 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 8541844.312, + "latency_p99_upper_ns": 15728640 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1867776, + "elapsed_ns": 134596389, + "cpu_ns": 51879163, + "parse_call_ns": 26207832, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6795.293198, + "output_byte_seconds": 1224.224319, + "parser_byte_seconds": 10089.609534, + "source_allocations": 369, + "output_allocations": 233, + "parser_allocations": 365, + "reuses": 5954, + "budget_waits": 0, + "slot_waits": 596, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83947198, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47146.396, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5082305.345, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082431.98, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1104848, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134667407, + "cpu_ns": 52286009, + "parse_call_ns": 26255995, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6830.10163, + "output_byte_seconds": 1802.54536, + "parser_byte_seconds": 10104.51946, + "source_allocations": 370, + "output_allocations": 952, + "parser_allocations": 375, + "reuses": 6248, + "budget_waits": 0, + "slot_waits": 590, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83595691, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47383.187, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5081689.522, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082200.92, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "compact", + "repeat": 1, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2097104, + "source_peak_bytes": 2047296, + "output_peak_bytes": 3360, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3833856, + "elapsed_ns": 330507221, + "cpu_ns": 57227425, + "parse_call_ns": 258042238, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 542403.396989, + "output_byte_seconds": 342.198165, + "parser_byte_seconds": 10516.149038, + "source_allocations": 1657, + "output_allocations": 1018, + "parser_allocations": 592, + "reuses": 2630, + "budget_waits": 1262, + "slot_waits": 0, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 274671771, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 314160.033, + "latency_p99_upper_ns": 4718592 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5361935.781, + "latency_p99_upper_ns": 10485760 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 49, + "latency_mean_ns": 8345834.982, + "latency_p99_upper_ns": 12582912 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1900544, + "elapsed_ns": 134805557, + "cpu_ns": 52099602, + "parse_call_ns": 26201196, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6802.764732, + "output_byte_seconds": 1222.5673, + "parser_byte_seconds": 10082.699192, + "source_allocations": 363, + "output_allocations": 241, + "parser_allocations": 368, + "reuses": 5949, + "budget_waits": 0, + "slot_waits": 592, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83969655, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47229.56, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5083369.83, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082868.22, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1104736, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1900544, + "elapsed_ns": 135397301, + "cpu_ns": 55535720, + "parse_call_ns": 28191073, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 7296.683374, + "output_byte_seconds": 1825.786254, + "parser_byte_seconds": 10851.770961, + "source_allocations": 364, + "output_allocations": 925, + "parser_allocations": 376, + "reuses": 6280, + "budget_waits": 0, + "slot_waits": 556, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 81078620, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 50441.328, + "latency_p99_upper_ns": 163840 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5086253.071, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8088040.091, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 11312, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 2048000, + "elapsed_ns": 135143135, + "cpu_ns": 54181725, + "parse_call_ns": 28073539, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 1059072, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 7971.142415, + "output_byte_seconds": 924.89692, + "parser_byte_seconds": 10793.387694, + "source_allocations": 237, + "output_allocations": 89, + "parser_allocations": 259, + "reuses": 5805, + "budget_waits": 0, + "slot_waits": 587, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 82256234, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 49150.131, + "latency_p99_upper_ns": 163840 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5084346.661, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8083800.547, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2097024, + "source_peak_bytes": 2047296, + "output_peak_bytes": 0, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3719168, + "elapsed_ns": 519737827, + "cpu_ns": 63401151, + "parse_call_ns": 410873515, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 0, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 869390.688093, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 10874.854354, + "source_allocations": 1612, + "output_allocations": 0, + "parser_allocations": 584, + "reuses": 2677, + "budget_waits": 1267, + "slot_waits": 0, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 457480540, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 498894.016, + "latency_p99_upper_ns": 7864320 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5554388.391, + "latency_p99_upper_ns": 12582912 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 8572436.932, + "latency_p99_upper_ns": 15728640 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 842368, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1933312, + "elapsed_ns": 135808674, + "cpu_ns": 61310764, + "parse_call_ns": 27409732, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 8492.13471, + "output_byte_seconds": 1909.35548, + "parser_byte_seconds": 10184.015437, + "source_allocations": 3849, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 538, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 75774039, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 55750.675, + "latency_p99_upper_ns": 196608 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5097561.83, + "latency_p99_upper_ns": 5767168 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8097147.871, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "compact", + "repeat": 2, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2096496, + "source_peak_bytes": 2047296, + "output_peak_bytes": 3360, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3719168, + "elapsed_ns": 330655613, + "cpu_ns": 57992572, + "parse_call_ns": 257126466, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 542286.835871, + "output_byte_seconds": 342.201335, + "parser_byte_seconds": 10592.82654, + "source_allocations": 1620, + "output_allocations": 1018, + "parser_allocations": 589, + "reuses": 2670, + "budget_waits": 1246, + "slot_waits": 0, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 274045502, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 314214.117, + "latency_p99_upper_ns": 4718592 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5361784.028, + "latency_p99_upper_ns": 10485760 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 49, + "latency_mean_ns": 8345733.162, + "latency_p99_upper_ns": 12582912 + } + ] + }, + { + "case": "mixed_tight", + "variant": "shared_copy", + "repeat": 2, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1900544, + "elapsed_ns": 134725602, + "cpu_ns": 52125932, + "parse_call_ns": 26256537, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6831.305534, + "output_byte_seconds": 1226.062002, + "parser_byte_seconds": 10092.239062, + "source_allocations": 365, + "output_allocations": 226, + "parser_allocations": 360, + "reuses": 5970, + "budget_waits": 0, + "slot_waits": 576, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83779201, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47423.973, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5082508.901, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082650.268, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "compact", + "repeat": 3, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2096656, + "source_peak_bytes": 2047296, + "output_peak_bytes": 3360, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3719168, + "elapsed_ns": 332809762, + "cpu_ns": 64506702, + "parse_call_ns": 255295188, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 544031.993873, + "output_byte_seconds": 348.13532, + "parser_byte_seconds": 10927.535411, + "source_allocations": 1623, + "output_allocations": 1008, + "parser_allocations": 583, + "reuses": 2683, + "budget_waits": 1227, + "slot_waits": 0, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 270135277, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 316210.616, + "latency_p99_upper_ns": 4718592 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5373501.093, + "latency_p99_upper_ns": 10485760 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 49, + "latency_mean_ns": 8409239.137, + "latency_p99_upper_ns": 12582912 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134646087, + "cpu_ns": 51969989, + "parse_call_ns": 26192074, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6812.54134, + "output_byte_seconds": 1229.637184, + "parser_byte_seconds": 10083.25152, + "source_allocations": 369, + "output_allocations": 241, + "parser_allocations": 365, + "reuses": 5946, + "budget_waits": 0, + "slot_waits": 582, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83864700, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47274.193, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5082291.09, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8084035.099, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1104848, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1900544, + "elapsed_ns": 134732754, + "cpu_ns": 52218674, + "parse_call_ns": 26241260, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6810.984162, + "output_byte_seconds": 1804.529206, + "parser_byte_seconds": 10104.502269, + "source_allocations": 368, + "output_allocations": 949, + "parser_allocations": 374, + "reuses": 6254, + "budget_waits": 0, + "slot_waits": 584, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 83719457, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47404.955, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5082343.96, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082308.803, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "shared_copy", + "repeat": 3, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134687032, + "cpu_ns": 52085095, + "parse_call_ns": 26215409, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6852.502663, + "output_byte_seconds": 1223.987439, + "parser_byte_seconds": 10100.060122, + "source_allocations": 365, + "output_allocations": 235, + "parser_allocations": 364, + "reuses": 5957, + "budget_waits": 0, + "slot_waits": 585, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83840818, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47354.551, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5081713.058, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082095.55, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2097072, + "source_peak_bytes": 2047296, + "output_peak_bytes": 0, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3719168, + "elapsed_ns": 516415971, + "cpu_ns": 57490634, + "parse_call_ns": 412616864, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 0, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 865936.173669, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 10532.202462, + "source_allocations": 1624, + "output_allocations": 0, + "parser_allocations": 582, + "reuses": 2667, + "budget_waits": 1258, + "slot_waits": 0, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 460245677, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 495746.732, + "latency_p99_upper_ns": 7864320 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5527499.187, + "latency_p99_upper_ns": 12582912 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 8541909.699, + "latency_p99_upper_ns": 15728640 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 842368, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1916928, + "elapsed_ns": 135708499, + "cpu_ns": 59498427, + "parse_call_ns": 27293452, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 8320.069469, + "output_byte_seconds": 1853.590142, + "parser_byte_seconds": 10172.640074, + "source_allocations": 3849, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 550, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 77469437, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 53679.489, + "latency_p99_upper_ns": 180224 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5093041.554, + "latency_p99_upper_ns": 5767168 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8093789.758, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 11312, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 2048000, + "elapsed_ns": 134580129, + "cpu_ns": 50901202, + "parse_call_ns": 26138215, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 1059072, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 7468.893211, + "output_byte_seconds": 924.833553, + "parser_byte_seconds": 10074.589742, + "source_allocations": 237, + "output_allocations": 99, + "parser_allocations": 260, + "reuses": 5794, + "budget_waits": 0, + "slot_waits": 603, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 84894241, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 46199.245, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5080295.214, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082153.254, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "compact", + "repeat": 4, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2095936, + "source_peak_bytes": 2047296, + "output_peak_bytes": 3360, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3768320, + "elapsed_ns": 330364653, + "cpu_ns": 56737666, + "parse_call_ns": 257390641, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 57344, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 542129.496495, + "output_byte_seconds": 342.3922, + "parser_byte_seconds": 10487.947668, + "source_allocations": 1626, + "output_allocations": 1018, + "parser_allocations": 584, + "reuses": 2669, + "budget_waits": 1234, + "slot_waits": 0, + "compacted": 1024, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 274961005, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 314000.227, + "latency_p99_upper_ns": 4718592 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5359881.678, + "latency_p99_upper_ns": 10485760 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 49, + "latency_mean_ns": 8345470.393, + "latency_p99_upper_ns": 12582912 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134641407, + "cpu_ns": 51772899, + "parse_call_ns": 26303805, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6823.249522, + "output_byte_seconds": 1222.425123, + "parser_byte_seconds": 10156.044791, + "source_allocations": 363, + "output_allocations": 239, + "parser_allocations": 367, + "reuses": 5952, + "budget_waits": 0, + "slot_waits": 584, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83930889, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47208.702, + "latency_p99_upper_ns": 131072 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5081715.549, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8082551.032, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 11312, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 2048000, + "elapsed_ns": 134636889, + "cpu_ns": 51692325, + "parse_call_ns": 26232175, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 1059072, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 7604.090343, + "output_byte_seconds": 924.831397, + "parser_byte_seconds": 10109.160375, + "source_allocations": 237, + "output_allocations": 97, + "parser_allocations": 259, + "reuses": 5797, + "budget_waits": 0, + "slot_waits": 600, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 84203974, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 46850.104, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5081772.482, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8081990.295, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 2097024, + "source_peak_bytes": 2047296, + "output_peak_bytes": 0, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 3719168, + "elapsed_ns": 516507633, + "cpu_ns": 56956626, + "parse_call_ns": 413462854, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 0, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 866066.604394, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 10498.181654, + "source_allocations": 1635, + "output_allocations": 0, + "parser_allocations": 588, + "reuses": 2650, + "budget_waits": 1258, + "slot_waits": 0, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 460890935, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 495819.875, + "latency_p99_upper_ns": 7864320 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 5527650.658, + "latency_p99_upper_ns": 12582912 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 30, + "latency_mean_ns": 8542682.03, + "latency_p99_upper_ns": 15728640 + } + ] + }, + { + "case": "mixed_tight", + "variant": "shared_copy", + "repeat": 4, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1097792, + "source_peak_bytes": 393312, + "output_peak_bytes": 72752, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 134698503, + "cpu_ns": 52062419, + "parse_call_ns": 26232455, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26814720, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6807.634601, + "output_byte_seconds": 1226.698884, + "parser_byte_seconds": 10096.010075, + "source_allocations": 358, + "output_allocations": 229, + "parser_allocations": 363, + "reuses": 5971, + "budget_waits": 0, + "slot_waits": 573, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 1024, + "sleep_ns": 83820760, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 47338.287, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5082803.919, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8081430.476, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 842368, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1933312, + "elapsed_ns": 135617374, + "cpu_ns": 59370307, + "parse_call_ns": 27370335, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 8306.118105, + "output_byte_seconds": 1881.770411, + "parser_byte_seconds": 10193.350967, + "source_allocations": 3849, + "output_allocations": 3072, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 541, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 77447978, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 54118.737, + "latency_p99_upper_ns": 180224 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5093193.132, + "latency_p99_upper_ns": 5767168 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8094306.969, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1104736, + "source_peak_bytes": 393312, + "output_peak_bytes": 79920, + "parser_peak_bytes": 524336, + "rss_peak_bytes": 1884160, + "elapsed_ns": 135101765, + "cpu_ns": 53612089, + "parse_call_ns": 26369173, + "accepted": 1024, + "emitted": 3072, + "input_bytes": 49877076, + "selection_copy_bytes": 26872064, + "reader_copy_bytes": 114814580, + "parser_realloc_copy_bytes": 0, + "source_byte_seconds": 6944.794448, + "output_byte_seconds": 1822.831558, + "parser_byte_seconds": 10130.150831, + "source_allocations": 348, + "output_allocations": 886, + "parser_allocations": 368, + "reuses": 6343, + "budget_waits": 0, + "slot_waits": 561, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 82695691, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 1024, + "bytes": 26757376, + "peak_count": 1, + "latency_mean_ns": 48573.396, + "latency_p99_upper_ns": 147456 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 5083912.79, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 1024, + "bytes": 57344, + "peak_count": 64, + "latency_mean_ns": 8083831.809, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy_no_pool", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 170672, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66381179, + "cpu_ns": 14937944, + "parse_call_ns": 10634360, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 191.835816, + "output_byte_seconds": 748.822762, + "parser_byte_seconds": 436.544508, + "source_allocations": 1273, + "output_allocations": 1536, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 200, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51858352, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26502.654, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055591.338, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8038187.436, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "borrow", + "repeat": 0, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1208192, + "source_peak_bytes": 1059888, + "output_peak_bytes": 0, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2785280, + "elapsed_ns": 66395496, + "cpu_ns": 14949295, + "parse_call_ns": 10748920, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 0, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 67632.156584, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 441.340054, + "source_allocations": 70, + "output_allocations": 0, + "parser_allocations": 2, + "reuses": 2225, + "budget_waits": 0, + "slot_waits": 210, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51867983, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26677.738, + "latency_p99_upper_ns": 40960 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055635.91, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8040456.355, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold8", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1851392, + "elapsed_ns": 66344311, + "cpu_ns": 14626628, + "parse_call_ns": 10598197, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.993439, + "output_byte_seconds": 460.282359, + "parser_byte_seconds": 437.832685, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 201, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52108022, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26057.676, + "latency_p99_upper_ns": 30720 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054707.516, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8038718.686, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "shared_copy", + "repeat": 0, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66360126, + "cpu_ns": 14610198, + "parse_call_ns": 10558947, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.278595, + "output_byte_seconds": 460.358957, + "parser_byte_seconds": 433.475216, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 210, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52155647, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26056.072, + "latency_p99_upper_ns": 30720 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054720.746, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8039527.611, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "compact", + "repeat": 0, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1209200, + "source_peak_bytes": 1059888, + "output_peak_bytes": 7168, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2899968, + "elapsed_ns": 66352515, + "cpu_ns": 15174956, + "parse_call_ns": 10767457, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 28672, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 42522.512719, + "output_byte_seconds": 170.992644, + "parser_byte_seconds": 444.338525, + "source_allocations": 462, + "output_allocations": 456, + "parser_allocations": 2, + "reuses": 1889, + "budget_waits": 0, + "slot_waits": 203, + "compacted": 512, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51584350, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26833.023, + "latency_p99_upper_ns": 40960 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055824.906, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8037818.045, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy", + "repeat": 0, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 187312, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66358774, + "cpu_ns": 14704746, + "parse_call_ns": 10580140, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 190.401936, + "output_byte_seconds": 748.797361, + "parser_byte_seconds": 434.190092, + "source_allocations": 6, + "output_allocations": 546, + "parser_allocations": 2, + "reuses": 3279, + "budget_waits": 0, + "slot_waits": 198, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 52055904, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26138.373, + "latency_p99_upper_ns": 32768 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054837.756, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8038347.354, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold2", + "repeat": 0, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1851392, + "elapsed_ns": 66343256, + "cpu_ns": 14610428, + "parse_call_ns": 10601249, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 190.005404, + "output_byte_seconds": 460.293238, + "parser_byte_seconds": 435.304664, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 199, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52113330, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26103.785, + "latency_p99_upper_ns": 32768 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054791.455, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8038668.434, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold8", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66620533, + "cpu_ns": 15009658, + "parse_call_ns": 10599246, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 190.711835, + "output_byte_seconds": 461.162485, + "parser_byte_seconds": 439.194778, + "source_allocations": 6, + "output_allocations": 67, + "parser_allocations": 2, + "reuses": 3246, + "budget_waits": 0, + "slot_waits": 195, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52089340, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26514.744, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5059599.168, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8052646.494, + "latency_p99_upper_ns": 9437184 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy_no_pool", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 170672, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 69246534, + "cpu_ns": 16610834, + "parse_call_ns": 11758115, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 215.365911, + "output_byte_seconds": 778.347747, + "parser_byte_seconds": 481.501477, + "source_allocations": 1273, + "output_allocations": 1536, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 164, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 52395416, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 29861.328, + "latency_p99_upper_ns": 57344 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5481519.523, + "latency_p99_upper_ns": 10485760 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8132478.682, + "latency_p99_upper_ns": 10485760 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "shared_copy", + "repeat": 1, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 67338922, + "cpu_ns": 15543047, + "parse_call_ns": 10965512, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 198.833427, + "output_byte_seconds": 461.43294, + "parser_byte_seconds": 455.786835, + "source_allocations": 6, + "output_allocations": 88, + "parser_allocations": 2, + "reuses": 3225, + "budget_waits": 0, + "slot_waits": 170, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52096250, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 27620.291, + "latency_p99_upper_ns": 53248 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5405312.287, + "latency_p99_upper_ns": 9437184 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8058747.33, + "latency_p99_upper_ns": 9437184 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy", + "repeat": 1, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 187312, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66437867, + "cpu_ns": 14887131, + "parse_call_ns": 10719986, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 192.97609, + "output_byte_seconds": 748.927344, + "parser_byte_seconds": 442.835871, + "source_allocations": 6, + "output_allocations": 546, + "parser_allocations": 2, + "reuses": 3279, + "budget_waits": 0, + "slot_waits": 206, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51911693, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26502.299, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055229.1, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8040517.863, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold2", + "repeat": 1, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66341459, + "cpu_ns": 14573003, + "parse_call_ns": 10555283, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.19959, + "output_byte_seconds": 460.262935, + "parser_byte_seconds": 433.563583, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 202, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52150322, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26047.523, + "latency_p99_upper_ns": 30720 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054844.742, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8037890.609, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "borrow", + "repeat": 1, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1208192, + "source_peak_bytes": 1059888, + "output_peak_bytes": 0, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2801664, + "elapsed_ns": 66302663, + "cpu_ns": 14680433, + "parse_call_ns": 10680579, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 0, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 67603.216745, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 441.482243, + "source_allocations": 70, + "output_allocations": 0, + "parser_allocations": 2, + "reuses": 2225, + "budget_waits": 0, + "slot_waits": 203, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 52022749, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26241.209, + "latency_p99_upper_ns": 32768 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054887.895, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8036938.547, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "compact", + "repeat": 1, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1209200, + "source_peak_bytes": 1059888, + "output_peak_bytes": 7168, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2916352, + "elapsed_ns": 66647996, + "cpu_ns": 15645769, + "parse_call_ns": 10897093, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 28672, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 42531.184104, + "output_byte_seconds": 171.185155, + "parser_byte_seconds": 448.382215, + "source_allocations": 462, + "output_allocations": 456, + "parser_allocations": 2, + "reuses": 1889, + "budget_waits": 0, + "slot_waits": 208, + "compacted": 512, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51484721, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 27363.605, + "latency_p99_upper_ns": 49152 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5056927.043, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8042303.346, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "shared_copy", + "repeat": 2, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66509552, + "cpu_ns": 15439851, + "parse_call_ns": 10723161, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 194.692414, + "output_byte_seconds": 460.401472, + "parser_byte_seconds": 443.814792, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 188, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 51559999, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 27221.904, + "latency_p99_upper_ns": 40960 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5102159.828, + "latency_p99_upper_ns": 6291456 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8039936.652, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold2", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1851392, + "elapsed_ns": 66675868, + "cpu_ns": 15617313, + "parse_call_ns": 10794500, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 196.368374, + "output_byte_seconds": 460.582208, + "parser_byte_seconds": 453.168708, + "source_allocations": 6, + "output_allocations": 72, + "parser_allocations": 2, + "reuses": 3241, + "budget_waits": 0, + "slot_waits": 206, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 51576007, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 27391.873, + "latency_p99_upper_ns": 45056 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5063401.098, + "latency_p99_upper_ns": 5767168 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8043009.266, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold8", + "repeat": 2, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66346091, + "cpu_ns": 14749114, + "parse_call_ns": 10663838, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 191.566293, + "output_byte_seconds": 460.387978, + "parser_byte_seconds": 441.272197, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 198, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 51956135, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26444.111, + "latency_p99_upper_ns": 40960 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055009.982, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8040053.549, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "borrow", + "repeat": 2, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1208192, + "source_peak_bytes": 1059888, + "output_peak_bytes": 0, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2768896, + "elapsed_ns": 66340906, + "cpu_ns": 14840713, + "parse_call_ns": 10710339, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 0, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 67628.848076, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 441.911742, + "source_allocations": 70, + "output_allocations": 0, + "parser_allocations": 2, + "reuses": 2225, + "budget_waits": 0, + "slot_waits": 214, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51926596, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26533.881, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055556.934, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8040046.408, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy_no_pool", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 170672, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1851392, + "elapsed_ns": 66367572, + "cpu_ns": 14844612, + "parse_call_ns": 10626817, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 191.787957, + "output_byte_seconds": 748.742987, + "parser_byte_seconds": 439.039763, + "source_allocations": 1273, + "output_allocations": 1536, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 202, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51918865, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26443.496, + "latency_p99_upper_ns": 32768 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055503.67, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8037164.848, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "compact", + "repeat": 2, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1209200, + "source_peak_bytes": 1059888, + "output_peak_bytes": 7168, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2916352, + "elapsed_ns": 66456221, + "cpu_ns": 15237761, + "parse_call_ns": 10832907, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 28672, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 42524.965388, + "output_byte_seconds": 171.106633, + "parser_byte_seconds": 446.50427, + "source_allocations": 462, + "output_allocations": 456, + "parser_allocations": 2, + "reuses": 1889, + "budget_waits": 0, + "slot_waits": 211, + "compacted": 512, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51628844, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26894.043, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5056115.033, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8040094.787, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy", + "repeat": 2, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 187312, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1818624, + "elapsed_ns": 66358054, + "cpu_ns": 14786258, + "parse_call_ns": 10588699, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 190.767691, + "output_byte_seconds": 748.949984, + "parser_byte_seconds": 437.20655, + "source_allocations": 6, + "output_allocations": 546, + "parser_allocations": 2, + "reuses": 3279, + "budget_waits": 0, + "slot_waits": 202, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51982953, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26290.178, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055243.682, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8040476.592, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "shared_copy", + "repeat": 3, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66340017, + "cpu_ns": 14641398, + "parse_call_ns": 10583202, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.850967, + "output_byte_seconds": 460.201756, + "parser_byte_seconds": 434.46664, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 200, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52101474, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26126.43, + "latency_p99_upper_ns": 32768 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054896.006, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8037126.236, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "borrow", + "repeat": 3, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1208192, + "source_peak_bytes": 1059888, + "output_peak_bytes": 0, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2768896, + "elapsed_ns": 66397387, + "cpu_ns": 14926280, + "parse_call_ns": 10743184, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 0, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 67622.063537, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 442.942199, + "source_allocations": 70, + "output_allocations": 0, + "parser_allocations": 2, + "reuses": 2225, + "budget_waits": 0, + "slot_waits": 212, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51935519, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26540.598, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055706.689, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8039215.434, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 187312, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66347591, + "cpu_ns": 14688841, + "parse_call_ns": 10558757, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 190.237303, + "output_byte_seconds": 748.906445, + "parser_byte_seconds": 434.052077, + "source_allocations": 6, + "output_allocations": 546, + "parser_allocations": 2, + "reuses": 3279, + "budget_waits": 0, + "slot_waits": 201, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 52048363, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26185.322, + "latency_p99_upper_ns": 30720 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054934.725, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8039957.693, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "compact", + "repeat": 3, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1209200, + "source_peak_bytes": 1059888, + "output_peak_bytes": 7168, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2916352, + "elapsed_ns": 66425702, + "cpu_ns": 15126826, + "parse_call_ns": 10762572, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 28672, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 42518.234312, + "output_byte_seconds": 171.124375, + "parser_byte_seconds": 442.813559, + "source_allocations": 462, + "output_allocations": 456, + "parser_allocations": 2, + "reuses": 1889, + "budget_waits": 0, + "slot_waits": 207, + "compacted": 512, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51722109, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26699.592, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055311.875, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8039588.77, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold8", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66342183, + "cpu_ns": 14624958, + "parse_call_ns": 10558072, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.596919, + "output_byte_seconds": 460.250779, + "parser_byte_seconds": 436.103934, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 199, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52113950, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26082.711, + "latency_p99_upper_ns": 32768 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054439.891, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8037911.916, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy_no_pool", + "repeat": 3, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 170672, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66421906, + "cpu_ns": 14924036, + "parse_call_ns": 10733514, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 193.388376, + "output_byte_seconds": 748.846289, + "parser_byte_seconds": 443.059571, + "source_allocations": 1273, + "output_allocations": 1536, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 207, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51871512, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26630.812, + "latency_p99_upper_ns": 40960 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055263.076, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8039399.533, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold2", + "repeat": 3, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66363184, + "cpu_ns": 14629240, + "parse_call_ns": 10568715, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.501318, + "output_byte_seconds": 460.195707, + "parser_byte_seconds": 436.121559, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 199, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52142767, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26129.004, + "latency_p99_upper_ns": 30720 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054684.932, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8036606.693, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold2", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66354498, + "cpu_ns": 14608671, + "parse_call_ns": 10552983, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.286265, + "output_byte_seconds": 460.245208, + "parser_byte_seconds": 433.358809, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 197, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52143565, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26044.074, + "latency_p99_upper_ns": 30720 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054765.459, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8037717.471, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy_no_pool", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 170672, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1851392, + "elapsed_ns": 66440315, + "cpu_ns": 15042208, + "parse_call_ns": 10714630, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 193.34471, + "output_byte_seconds": 749.072578, + "parser_byte_seconds": 442.776453, + "source_allocations": 1273, + "output_allocations": 1536, + "parser_allocations": 1024, + "reuses": 0, + "budget_waits": 0, + "slot_waits": 203, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51818059, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26719.596, + "latency_p99_upper_ns": 40960 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055539.021, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8042928.75, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "shared_copy", + "repeat": 4, + "status": "ok", + "policy": "shared_copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66550686, + "cpu_ns": 14565611, + "parse_call_ns": 10569760, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 189.196651, + "output_byte_seconds": 460.427963, + "parser_byte_seconds": 435.936714, + "source_allocations": 6, + "output_allocations": 70, + "parser_allocations": 2, + "reuses": 3243, + "budget_waits": 0, + "slot_waits": 199, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52394259, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26054.17, + "latency_p99_upper_ns": 30720 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054468.873, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8040821.889, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "compact", + "repeat": 4, + "status": "ok", + "policy": "compact", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1209200, + "source_peak_bytes": 1059888, + "output_peak_bytes": 7168, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2932736, + "elapsed_ns": 66346982, + "cpu_ns": 15115580, + "parse_call_ns": 10730381, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 28672, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 42539.689103, + "output_byte_seconds": 170.963142, + "parser_byte_seconds": 440.137993, + "source_allocations": 462, + "output_allocations": 456, + "parser_allocations": 2, + "reuses": 1889, + "budget_waits": 0, + "slot_waits": 200, + "compacted": 512, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51642878, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26772.898, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5057876.094, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8039346.365, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold8", + "repeat": 4, + "status": "ok", + "policy": "threshold", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 180144, + "source_peak_bytes": 24672, + "output_peak_bytes": 7728, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1851392, + "elapsed_ns": 66367685, + "cpu_ns": 14678765, + "parse_call_ns": 10612300, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 290816, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 190.199351, + "output_byte_seconds": 460.290364, + "parser_byte_seconds": 438.024028, + "source_allocations": 6, + "output_allocations": 65, + "parser_allocations": 2, + "reuses": 3248, + "budget_waits": 0, + "slot_waits": 200, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 512, + "sleep_ns": 52099788, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26178.92, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054797.43, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8038471.338, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "borrow", + "repeat": 4, + "status": "ok", + "policy": "borrow", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 1208192, + "source_peak_bytes": 1059888, + "output_peak_bytes": 0, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 2785280, + "elapsed_ns": 66395630, + "cpu_ns": 14769876, + "parse_call_ns": 10679603, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 0, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 67615.986637, + "output_byte_seconds": 0.0, + "parser_byte_seconds": 438.40001, + "source_allocations": 70, + "output_allocations": 0, + "parser_allocations": 2, + "reuses": 2225, + "budget_waits": 0, + "slot_waits": 207, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 52035492, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26350.551, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5054965.338, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8038469.066, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy", + "repeat": 4, + "status": "ok", + "policy": "copy", + "parser_version": "0.12.0", + "budget_bytes": 2097152, + "fixed_bytes": 41728, + "peak_reserved_bytes": 187312, + "source_peak_bytes": 24672, + "output_peak_bytes": 14896, + "parser_peak_bytes": 98400, + "rss_peak_bytes": 1835008, + "elapsed_ns": 66338315, + "cpu_ns": 14754424, + "parse_call_ns": 10609310, + "accepted": 512, + "emitted": 1536, + "input_bytes": 5259666, + "selection_copy_bytes": 319488, + "reader_copy_bytes": 6312628, + "parser_realloc_copy_bytes": 14086144, + "source_byte_seconds": 190.968242, + "output_byte_seconds": 748.772024, + "parser_byte_seconds": 435.574111, + "source_allocations": 6, + "output_allocations": 546, + "parser_allocations": 2, + "reuses": 3279, + "budget_waits": 0, + "slot_waits": 199, + "compacted": 0, + "compact_skipped": 0, + "dedup_hits": 0, + "sleep_ns": 51978025, + "live_at_stop_bytes": 0, + "unreleased_buffer_bytes": 0, + "queues": [ + { + "emitted": 512, + "bytes": 262144, + "peak_count": 1, + "latency_mean_ns": 26285.051, + "latency_p99_upper_ns": 36864 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 5055200.059, + "latency_p99_upper_ns": 5242880 + }, + { + "emitted": 512, + "bytes": 28672, + "peak_count": 64, + "latency_mean_ns": 8037468.172, + "latency_p99_upper_ns": 8388608 + } + ] + } +] diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/benchmark.c b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/benchmark.c new file mode 100644 index 0000000..474bc7f --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/benchmark.c @@ -0,0 +1,391 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vendor/yyjson/yyjson.h" + +enum { BRANCHES=3, CACHE=16, READ_SIZE=16384, MAX_JSON=1024*1024, HIST=512 }; +enum { SOURCE, OUTPUT, PARSER, KINDS }; +typedef struct Item Item; +typedef struct Block { + size_t capacity, refs; + uint64_t since; + unsigned kind; + Item *views; + _Alignas(max_align_t) unsigned char data[]; +} Block; +struct Item { + Block *block; + Item *prev, *next; + size_t offset, length; + uint64_t sequence, start, ready; +}; +typedef struct Queue { + Item *items; + size_t head, count, peak; + const char *key; + uint64_t hold, service, last, emitted, bytes, latency_sum, histogram[HIST]; +} Queue; +typedef struct Engine { + Queue q[BRANCHES]; + Block *cache[CACHE]; + unsigned char read_buffer[READ_SIZE]; + size_t read_at, read_end; + size_t budget, fixed, slots, fanout, reserved, active, cached, peak; + size_t by_kind[KINDS], peak_kind[KINDS]; + long double byte_ns[KINDS]; + uint64_t allocations[KINDS], reuses, accepted, emitted, input_bytes; + uint64_t selection_copies, reader_copies, parser_copies, parse_ns; + uint64_t budget_waits, slot_waits, compacted, compact_skipped, dedup_hits; + uint64_t sleep_ns, elapsed_ns, cpu_ns, cancel_after; + unsigned ratio; + int pool; + const char *policy, *status; +} Engine; +static volatile sig_atomic_t stopped; +static void stop(int sig) { (void)sig; stopped=1; } +static uint64_t ns(clockid_t clock) { + struct timespec t; + if (clock_gettime(clock,&t)) abort(); + return (uint64_t)t.tv_sec*UINT64_C(1000000000)+(uint64_t)t.tv_nsec; +} +static size_t rounded(size_t n) { size_t c=64; while(ccapacity; } +static void check(Engine *e) { + assert(e->active+e->cached==e->reserved); + assert(e->by_kind[0]+e->by_kind[1]+e->by_kind[2]==e->active); + assert(e->fixed+e->reserved<=e->budget); + if(e->reserved>e->peak)e->peak=e->reserved; + for(unsigned k=0;kby_kind[k]>e->peak_kind[k])e->peak_kind[k]=e->by_kind[k]; +} +static void evict(Engine *e,size_t i) { + Block *b=e->cache[i]; if(!b)return; + assert(!b->refs && !b->views); + e->reserved-=charge(b); e->cached-=charge(b); free(b); e->cache[i]=NULL; +} +static Block *take(Engine *e,size_t n,unsigned kind) { + size_t capacity=rounded(n),need=sizeof(Block)+capacity; + Block *b=NULL; + if(e->pool)for(size_t i=0;icache[i] && e->cache[i]->capacity==capacity) { + b=e->cache[i]; e->cache[i]=NULL; e->cached-=need; ++e->reuses; break; + } + if(!b) { + for(size_t i=0;ie->budget-e->fixed-e->reserved;++i)evict(e,i); + if(need>e->budget-e->fixed-e->reserved)return NULL; + b=malloc(need); + if(!b){e->status="allocation_failed";return NULL;} + b->capacity=capacity; e->reserved+=need; ++e->allocations[kind]; + } + b->kind=kind;b->refs=1;b->views=NULL;b->since=ns(CLOCK_MONOTONIC); + e->active+=need;e->by_kind[kind]+=need;check(e);return b; +} +static void drop(Engine *e,Block *b) { + if(!b)return; + assert(b->refs); + if(--b->refs)return; + assert(!b->views); + size_t bytes=charge(b); + e->byte_ns[b->kind]+=(long double)bytes*(ns(CLOCK_MONOTONIC)-b->since); + e->active-=bytes;e->by_kind[b->kind]-=bytes; + if(e->pool && bytes<=e->budget/4-e->cached)for(size_t i=0;icache[i]) { + e->cache[i]=b;e->cached+=bytes;check(e);return; + } + e->reserved-=bytes;free(b);check(e); +} +static void attach(Item *item) { + Block *b=item->block; + item->prev=NULL;item->next=b->views; + if(b->views)b->views->prev=item; + b->views=item; +} +static void detach(Item *item) { + if(item->prev)item->prev->next=item->next;else item->block->views=item->next; + if(item->next)item->next->prev=item->prev; + item->prev=item->next=NULL; +} +static void compact(Engine *e,Block *source) { + if(strcmp(e->policy,"compact") || source->kind!=SOURCE || source->refs!=1 || !source->views)return; + Item *item=source->views; + assert(!item->next); + if(source->capacity/rounded(item->length)ratio)return; + /* Opportunistic only: never block or recurse from a completion callback. */ + Block *small=take(e,item->length,OUTPUT); + if(!small){++e->compact_skipped;return;} + memcpy(small->data,source->data+item->offset,item->length); + e->selection_copies+=item->length;++e->compacted; + detach(item);item->block=small;item->offset=0;attach(item);drop(e,source); +} +static uint64_t deadline(Queue *q) { + uint64_t ready=q->items[q->head].ready; + return ready>q->last+q->service?ready:q->last+q->service; +} +static int next_queue(Engine *e) { + int selected=-1;uint64_t soon=UINT64_MAX; + for(size_t k=0;kfanout;++k)if(e->q[k].count && deadline(&e->q[k])q[k]); + } + return selected; +} +static int sleep_to(Engine *e,uint64_t until) { + uint64_t start=ns(CLOCK_MONOTONIC),now=start; + while(nowstatus="sleep_failed";return 0;} + now=ns(CLOCK_MONOTONIC); + } + e->sleep_ns+=now-start;return !stopped; +} +static unsigned bucket(uint64_t n) { + unsigned k=0;for(uint64_t x=n;x>1;x>>=1)++k; + return k<3?(unsigned)n:k*8+(unsigned)((n-(UINT64_C(1)<>(k-3)); +} +static uint64_t upper(unsigned b) { + unsigned k=b/8; + if(b<24)return b+1; + if(k>=63)return UINT64_MAX; + return (UINT64_C(1)<status="output_failed";return 0;} + p+=w;n-=(size_t)w; + } + return !stopped; +} +static int emit(Engine *e) { + int k=next_queue(e);assert(k>=0); + Queue *q=&e->q[k];Item *item=&q->items[q->head]; + if(!sleep_to(e,deadline(q)))return 0; + unsigned char header[16]; + for(unsigned i=0;i<4;++i){header[i]=(unsigned char)((unsigned)k>>(8*i));header[4+i]=(unsigned char)(item->length>>(8*i));} + for(unsigned i=0;i<8;++i)header[8+i]=(unsigned char)(item->sequence>>(8*i)); + if(!write_all(e,header,16) || !write_all(e,item->block->data+item->offset,item->length))return 0; + uint64_t now=ns(CLOCK_MONOTONIC),latency=now-item->start; + q->last=now;q->latency_sum+=latency;++q->histogram[bucket(latency)]; + ++q->emitted;q->bytes+=item->length;++e->emitted; + Block *b=item->block;size_t old_refs=b->refs; + detach(item);drop(e,b);item->block=NULL; + q->head=(q->head+1)%e->slots;--q->count; + if(old_refs>1)compact(e,b); + if(e->cancel_after && e->emitted>=e->cancel_after){e->status="cancelled";return 0;} + return !strcmp(e->status,"ok"); +} +static Block *acquire(Engine *e,size_t n,unsigned kind) { + for(;;) { + Block *b=take(e,n,kind);if(b)return b; + if(strcmp(e->status,"ok"))return NULL; + if(next_queue(e)<0){e->status="budget_too_small";return NULL;} + ++e->budget_waits;if(!emit(e))return NULL; + } +} +static void *parser_alloc(void *ctx,size_t n) { + Block *b=acquire(ctx,n,PARSER);return b?b->data:NULL; +} +static Block *from_data(void *p) {return (Block *)((unsigned char *)p-sizeof(Block));} +static void parser_free(void *ctx,void *p) {if(p)drop(ctx,from_data(p));} +static void *parser_realloc(void *ctx,void *p,size_t old,size_t n) { + if(!p)return parser_alloc(ctx,n); + Block *b=from_data(p);assert(b->refs==1 && b->kind==PARSER && old<=b->capacity); + if(n<=b->capacity)return p; + void *fresh=parser_alloc(ctx,n);if(!fresh)return NULL; + memcpy(fresh,p,old);((Engine *)ctx)->parser_copies+=old;parser_free(ctx,p);return fresh; +} +/* Bounded NDJSON reader, accepting a final complete JSON without a newline. */ +static Block *read_record(Engine *e,size_t *length) { + Block *b=acquire(e,256,SOURCE);if(!b)return NULL; + *length=0; + for(;;) { + if(stopped){drop(e,b);return NULL;} + if(e->read_at==e->read_end) { + ssize_t got=read(STDIN_FILENO,e->read_buffer,READ_SIZE); + if(got<0 && errno==EINTR)continue; + if(got<0){e->status="input_failed";drop(e,b);return NULL;} + if(!got){if(*length)break;drop(e,b);return NULL;} + e->read_at=0;e->read_end=(size_t)got;e->input_bytes+=(uint64_t)got; + } + unsigned char *p=e->read_buffer+e->read_at; + size_t available=e->read_end-e->read_at; + unsigned char *newline=memchr(p,'\n',available); + size_t amount=newline?(size_t)(newline-p):available; + if(amount>MAX_JSON-*length){e->status="record_too_large";drop(e,b);return NULL;} + size_t need=*length+amount+YYJSON_PADDING_SIZE; + if(need>b->capacity) { + Block *larger=acquire(e,need,SOURCE); + if(!larger){drop(e,b);return NULL;} + memcpy(larger->data,b->data,*length);e->reader_copies+=*length; + drop(e,b);b=larger; + } + memcpy(b->data+*length,p,amount);e->reader_copies+=amount; + *length+=amount;e->read_at+=amount+(newline?1:0); + if(newline)break; + } + memset(b->data+*length,0,YYJSON_PADDING_SIZE);return b; +} +static int submit(Engine *e,Block *source,size_t length,uint64_t start) { + size_t offsets[BRANCHES]={0},lengths[BRANCHES]={0}; + yyjson_alc alc={parser_alloc,parser_realloc,parser_free,e}; + yyjson_read_err error; + uint64_t before=ns(CLOCK_MONOTONIC); + yyjson_doc *doc=yyjson_read_opts((char *)source->data,length,YYJSON_READ_INSITU,&alc,&error); + e->parse_ns+=ns(CLOCK_MONOTONIC)-before; + if(!doc){if(!strcmp(e->status,"ok"))e->status="invalid_json";drop(e,source);return 0;} + yyjson_val *root=yyjson_doc_get_root(doc); + int valid=yyjson_is_obj(root); + for(size_t k=0;valid && kfanout;++k) { + yyjson_val *value=yyjson_obj_get(root,e->q[k].key); + const char *str=yyjson_get_str(value);size_t size=yyjson_get_len(value); + uintptr_t address=(uintptr_t)str,base=(uintptr_t)source->data; + if(!yyjson_is_str(value) || addresslength || size>length-(address-base))valid=0; + else{offsets[k]=(size_t)(address-base);lengths[k]=size;} + } + /* Only decoded string offsets/lengths survive. No DOM nodes escape. */ + yyjson_doc_free(doc); + if(!valid){e->status="invalid_schema";drop(e,source);return 0;} + Item pending[BRANCHES]={{0}}; + for(size_t k=0;kfanout;++k) { + int copy=!strcmp(e->policy,"copy") || !strcmp(e->policy,"shared_copy") || + (!strcmp(e->policy,"threshold") && source->capacity/rounded(lengths[k])>=e->ratio); + int reuse=copy && (!strcmp(e->policy,"shared_copy") || !strcmp(e->policy,"threshold")); + Block *b=NULL;size_t offset=offsets[k]; + if(reuse)for(size_t j=0;jrefs;offset=0;++e->dedup_hits;break; + } + if(!b && copy) { + b=acquire(e,lengths[k],OUTPUT); + if(!b) { + for(size_t j=0;jdata,source->data+offset,lengths[k]);e->selection_copies+=lengths[k];offset=0; + } else if(!b){b=source;++b->refs;} + pending[k]=(Item){.block=b,.offset=offset,.length=lengths[k],.sequence=e->accepted,.start=start}; + } + uint64_t now=ns(CLOCK_MONOTONIC); + for(size_t k=0;kfanout;++k) { + Queue *q=&e->q[k];assert(q->countslots); + Item *item=&q->items[(q->head+q->count)%e->slots]; + *item=pending[k];item->ready=now+q->hold;attach(item);++q->count; + if(q->count>q->peak)q->peak=q->count; + } + ++e->accepted;drop(e,source);return 1; +} +static void run(Engine *e) { + while(!stopped && !strcmp(e->status,"ok")) { + int k=next_queue(e); + if(k>=0 && deadline(&e->q[k])<=ns(CLOCK_MONOTONIC)){if(!emit(e))break;continue;} + int full=0;for(size_t j=0;jfanout;++j)if(e->q[j].count==e->slots)full=1; + if(full){++e->slot_waits;if(!emit(e))break;continue;} + size_t length;uint64_t start=ns(CLOCK_MONOTONIC); + Block *source=read_record(e,&length); + if(!source) { + while(!stopped && !strcmp(e->status,"ok") && next_queue(e)>=0)if(!emit(e))break; + break; + } + if(!submit(e,source,length,start))break; + } + if(stopped)e->status="cancelled"; +} +static uint64_t number(const char *s,uint64_t max) { + char *end;errno=0;unsigned long long n=strtoull(s,&end,10); + if(!*s || *s=='-' || *end || errno || n>max){fprintf(stderr,"invalid number\n");exit(2);} + return n; +} +static uint64_t rss(void) { +#if defined(__linux__) + FILE *f=fopen("/proc/self/status","r");if(!f)return 0; + char line[256];unsigned long long kib=0; + while(fgets(line,sizeof(line),f))if(sscanf(line,"VmHWM: %llu kB",&kib)==1)break; + fclose(f);return (uint64_t)kib*1024; +#else + return 0; +#endif +} +int main(int argc,char **argv) { + _Static_assert(sizeof(Block)==offsetof(Block,data),"aligned block header"); + Engine storage={0},*e=&storage; + e->policy="borrow";e->status="ok";e->slots=64;e->fanout=3;e->budget=8*1024*1024;e->ratio=2;e->pool=1; + e->q[0].key="body";e->q[1].key=e->q[2].key="tag"; + e->q[1].hold=5000000;e->q[2].hold=8000000; + for(int i=1;ipolicy=value; + else if(!strcmp(key,"--budget"))e->budget=(size_t)number(value,1073741824); + else if(!strcmp(key,"--slots"))e->slots=(size_t)number(value,4096); + else if(!strcmp(key,"--fanout"))e->fanout=(size_t)number(value,BRANCHES); + else if(!strcmp(key,"--ratio"))e->ratio=(unsigned)number(value,1024); + else if(!strcmp(key,"--pool"))e->pool=(int)number(value,1); + else if(!strcmp(key,"--cancel-after"))e->cancel_after=number(value,UINT64_MAX); + else { + int recognized=0; + for(size_t k=0;kq[k].key=value;recognized=1;} + snprintf(option,sizeof(option),"--hold%zu-us",k); + if(!strcmp(key,option)){e->q[k].hold=number(value,10000000)*1000;recognized=1;} + snprintf(option,sizeof(option),"--service%zu-us",k); + if(!strcmp(key,option)){e->q[k].service=number(value,10000000)*1000;recognized=1;} + } + if(!recognized){fprintf(stderr,"unknown option\n");return 2;} + } + } + if(!e->slots || !e->fanout || e->ratio<2 || (strcmp(e->policy,"borrow") && strcmp(e->policy,"copy") && + strcmp(e->policy,"shared_copy") && strcmp(e->policy,"threshold") && strcmp(e->policy,"compact"))) { + fprintf(stderr,"invalid configuration\n");return 2; + } + e->fixed=sizeof(*e)+e->fanout*e->slots*sizeof(Item); + if(e->fixed>e->budget){fprintf(stderr,"budget below metadata\n");return 2;} + for(size_t k=0;kfanout;++k) { + e->q[k].items=calloc(e->slots,sizeof(Item)); + if(!e->q[k].items){for(size_t j=0;jq[j].items);return 2;} + } + signal(SIGINT,stop);signal(SIGTERM,stop);signal(SIGPIPE,SIG_IGN); + uint64_t start=ns(CLOCK_MONOTONIC),cpu=ns(CLOCK_PROCESS_CPUTIME_ID); + run(e); + size_t live_at_stop=e->active; + for(size_t k=0;kfanout;++k) { + Queue *q=&e->q[k]; + while(q->count){Item *item=&q->items[q->head];detach(item);drop(e,item->block);q->head=(q->head+1)%e->slots;--q->count;} + } + for(size_t i=0;iactive && !e->reserved); + e->elapsed_ns=ns(CLOCK_MONOTONIC)-start;e->cpu_ns=ns(CLOCK_PROCESS_CPUTIME_ID)-cpu; + fprintf(stderr,"{\"status\":\"%s\",\"policy\":\"%s\",\"parser_version\":\"%s\",\"budget_bytes\":%zu," + "\"fixed_bytes\":%zu,\"peak_reserved_bytes\":%zu,\"source_peak_bytes\":%zu,\"output_peak_bytes\":%zu," + "\"parser_peak_bytes\":%zu,\"rss_peak_bytes\":%" PRIu64 ",\"elapsed_ns\":%" PRIu64 ",\"cpu_ns\":%" PRIu64 "," + "\"parse_call_ns\":%" PRIu64 ",\"accepted\":%" PRIu64 ",\"emitted\":%" PRIu64 ",\"input_bytes\":%" PRIu64 "," + "\"selection_copy_bytes\":%" PRIu64 ",\"reader_copy_bytes\":%" PRIu64 ",\"parser_realloc_copy_bytes\":%" PRIu64 "," + "\"source_byte_seconds\":%.6Lf,\"output_byte_seconds\":%.6Lf,\"parser_byte_seconds\":%.6Lf," + "\"source_allocations\":%" PRIu64 ",\"output_allocations\":%" PRIu64 ",\"parser_allocations\":%" PRIu64 "," + "\"reuses\":%" PRIu64 ",\"budget_waits\":%" PRIu64 ",\"slot_waits\":%" PRIu64 "," + "\"compacted\":%" PRIu64 ",\"compact_skipped\":%" PRIu64 ",\"dedup_hits\":%" PRIu64 ",\"sleep_ns\":%" PRIu64 "," + "\"live_at_stop_bytes\":%zu,\"unreleased_buffer_bytes\":%zu,\"queues\":[", + e->status,e->policy,YYJSON_VERSION_STRING,e->budget,e->fixed,e->fixed+e->peak,e->peak_kind[SOURCE], + e->peak_kind[OUTPUT],e->peak_kind[PARSER],rss(),e->elapsed_ns,e->cpu_ns,e->parse_ns,e->accepted,e->emitted, + e->input_bytes,e->selection_copies,e->reader_copies,e->parser_copies,e->byte_ns[SOURCE]/1e9L, + e->byte_ns[OUTPUT]/1e9L,e->byte_ns[PARSER]/1e9L,e->allocations[SOURCE],e->allocations[OUTPUT], + e->allocations[PARSER],e->reuses,e->budget_waits,e->slot_waits,e->compacted,e->compact_skipped, + e->dedup_hits,e->sleep_ns,live_at_stop,e->reserved); + for(size_t k=0;kfanout;++k) { + Queue *q=&e->q[k];uint64_t target=q->emitted-q->emitted/100,sum=0,p99=0; + for(unsigned b=0;q->emitted && bhistogram[b];if(sum>=target){p99=upper(b);break;}} + fprintf(stderr,"%s{\"emitted\":%" PRIu64 ",\"bytes\":%" PRIu64 ",\"peak_count\":%zu," + "\"latency_mean_ns\":%.3f,\"latency_p99_upper_ns\":%" PRIu64 "}",k?",":"",q->emitted,q->bytes,q->peak, + q->emitted?(double)q->latency_sum/q->emitted:0,p99); + free(q->items); + } + fprintf(stderr,"]}\n");return !strcmp(e->status,"ok")?0:!strcmp(e->status,"cancelled")?3:2; +} diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/plan.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/plan.json new file mode 100644 index 0000000..60c179b --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/plan.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "written_before_measurement": true, + "seed": 20260923, + "repeats": 5, + "policies": [ + {"name": "borrow", "policy": "borrow"}, + {"name": "copy", "policy": "copy"}, + {"name": "shared_copy", "policy": "shared_copy"}, + {"name": "threshold2", "policy": "threshold", "ratio": 2}, + {"name": "threshold8", "policy": "threshold", "ratio": 8}, + {"name": "compact", "policy": "compact", "ratio": 2}, + {"name": "copy_no_pool", "policy": "copy", "pool": 0} + ], + "cases": [ + {"name": "one_slow", "shape": "large", "records": 1024, "keys": ["tag"], "hold_us": [8000], "service_us": [0], "budget": 8388608, "slots": 64}, + {"name": "three_distinct_speeds", "shape": "large", "records": 1024, "keys": ["body", "tag", "tag"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 8388608, "slots": 64}, + {"name": "same_large_field", "shape": "large", "records": 1024, "keys": ["body", "body", "body"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 8388608, "slots": 64}, + {"name": "slow_large_field", "shape": "large", "records": 1024, "keys": ["tag", "tag", "body"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 8388608, "slots": 64}, + {"name": "mixed_tight", "shape": "mixed", "records": 1024, "keys": ["body", "tag", "tag"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 2097152, "slots": 64}, + {"name": "metadata_heavy", "shape": "metadata", "records": 512, "keys": ["body", "tag", "tag"], "hold_us": [0, 5000, 8000], "service_us": [0, 0, 0], "budget": 2097152, "slots": 64} + ], + "expectations": [ + "A slow small-field consumer can retain the input after a fast large-field consumer finishes.", + "Sharing identical compact copies can remove duplicate per-consumer copies without a new runtime.", + "Late compaction may save copied bytes or fail to beat early size-based copying; all remaining owners matter.", + "Parser temporary memory and reader growth must be charged, even when retained selections are small.", + "No novel-policy advantage or production service SLO is presumed." + ], + "comparison_rule": "Report all fixed presets; exploratory measurements, no trained/held-out policy or significance claim.", + "limits": "Real yyjson parser and actual bytes, but synthetic NDJSON corpus and sink delays, three synchronous consumers in one event loop, no open-loop arrivals, thread races, durable output, or independent parser comparison." +} diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/run.py b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/run.py new file mode 100644 index 0000000..e4848bd --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/run.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +"""Verify and measure yyjson parsing with bounded, differently paced consumers.""" +import argparse +import csv +import hashlib +import json +import os +from pathlib import Path +import platform +import random +import shlex +import signal +import statistics +import struct +import subprocess +import tempfile +import time + +ROOT = Path(__file__).resolve().parent +FLAGS = ["-std=c17", "-O2", "-g", "-Wall", "-Wextra", "-Wpedantic", "-Werror"] +FLAGS += ["-DYYJSON_DISABLE_WRITER=1", "-DYYJSON_DISABLE_UTILS=1", "-DYYJSON_DISABLE_INCR_READER=1"] +SOURCE_NAMES = ["benchmark.c", "run.py", "plan.json", "vendor/yyjson/UPSTREAM.json", + "vendor/yyjson/yyjson.c", "vendor/yyjson/yyjson.h", "vendor/yyjson/LICENSE"] + + +def digest(path): + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def dump(path, value): + path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n") + + +def first_members(pairs): + result = {} + for key, value in pairs: + result.setdefault(key, value) + return result + + +def reference(data, keys): + values = [[] for _ in keys] + for line in data.splitlines(): + row = json.loads(line, object_pairs_hook=first_members) + if not isinstance(row, dict): + raise ValueError("object required") + for k, key in enumerate(keys): + if not isinstance(row.get(key), str): + raise ValueError("string field required") + values[k].append(row[key].encode("utf-8")) + return values + + +def decode(data, keys, expected, partial=False): + at = 0 + found = [[] for _ in keys] + while at < len(data): + if len(data) - at < 16: + raise AssertionError("incomplete output header") + k, length, sequence = struct.unpack_from(" + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/UPSTREAM.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/UPSTREAM.json new file mode 100644 index 0000000..7de2396 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/UPSTREAM.json @@ -0,0 +1,23 @@ +{ + "project": "yyjson", + "version": "0.12.0", + "commit": "8b4a38dc994a110abaec8a400615567bd996105f", + "license": "MIT", + "files": { + "yyjson.c": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/src/yyjson.c", + "sha256": "ac2e9bbb2e2d9149d90878d40506a1d624fa0b33c979a11b61075c54782c6d6a", + "bytes": 410813 + }, + "yyjson.h": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/src/yyjson.h", + "sha256": "175867c5493a5df648cec566717fa1c29aa2f6096f5f0cf1efad0b65e1f6d7b3", + "bytes": 322407 + }, + "LICENSE": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/LICENSE", + "sha256": "45e384d3d52c73cba3a64d6e6c25d47cd738cd8a55c30629e3201046eda62947", + "bytes": 1084 + } + } +} diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.c b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.c new file mode 100644 index 0000000..c16d925 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.c @@ -0,0 +1,11065 @@ +/*============================================================================== + Copyright (c) 2020 YaoYuan + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + *============================================================================*/ + +#include "yyjson.h" +#include /* for `HUGE_VAL/INFINIY/NAN` macros, no libm required */ + + + +/*============================================================================== + * MARK: - Warning Suppress (Private) + *============================================================================*/ + +#if defined(__clang__) +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wunused-parameter" +# pragma clang diagnostic ignored "-Wunused-label" +# pragma clang diagnostic ignored "-Wunused-macros" +# pragma clang diagnostic ignored "-Wunused-variable" +#elif defined(__GNUC__) +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wunused-parameter" +# pragma GCC diagnostic ignored "-Wunused-label" +# pragma GCC diagnostic ignored "-Wunused-macros" +# pragma GCC diagnostic ignored "-Wunused-variable" +#elif defined(_MSC_VER) +# pragma warning(disable:4100) /* unreferenced formal parameter */ +# pragma warning(disable:4101) /* unreferenced variable */ +# pragma warning(disable:4102) /* unreferenced label */ +# pragma warning(disable:4127) /* conditional expression is constant */ +# pragma warning(disable:4706) /* assignment within conditional expression */ +#endif + + + +/*============================================================================== + * MARK: - Version (Public) + *============================================================================*/ + +uint32_t yyjson_version(void) { + return YYJSON_VERSION_HEX; +} + + + +/*============================================================================== + * MARK: - Flags (Private) + *============================================================================*/ + +/* msvc intrinsic */ +#if YYJSON_MSC_VER >= 1400 +# include +# if defined(_M_AMD64) || defined(_M_ARM64) +# define MSC_HAS_BIT_SCAN_64 1 +# pragma intrinsic(_BitScanForward64) +# pragma intrinsic(_BitScanReverse64) +# else +# define MSC_HAS_BIT_SCAN_64 0 +# endif +# if defined(_M_AMD64) || defined(_M_ARM64) || \ + defined(_M_IX86) || defined(_M_ARM) +# define MSC_HAS_BIT_SCAN 1 +# pragma intrinsic(_BitScanForward) +# pragma intrinsic(_BitScanReverse) +# else +# define MSC_HAS_BIT_SCAN 0 +# endif +# if defined(_M_AMD64) +# define MSC_HAS_UMUL128 1 +# pragma intrinsic(_umul128) +# else +# define MSC_HAS_UMUL128 0 +# endif +#else +# define MSC_HAS_BIT_SCAN_64 0 +# define MSC_HAS_BIT_SCAN 0 +# define MSC_HAS_UMUL128 0 +#endif + +/* gcc builtin */ +#if yyjson_has_builtin(__builtin_clzll) || yyjson_gcc_available(3, 4, 0) +# define GCC_HAS_CLZLL 1 +#else +# define GCC_HAS_CLZLL 0 +#endif + +#if yyjson_has_builtin(__builtin_ctzll) || yyjson_gcc_available(3, 4, 0) +# define GCC_HAS_CTZLL 1 +#else +# define GCC_HAS_CTZLL 0 +#endif + +/* int128 type */ +#if defined(__SIZEOF_INT128__) && (__SIZEOF_INT128__ == 16) && \ + (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER)) +# define YYJSON_HAS_INT128 1 +#else +# define YYJSON_HAS_INT128 0 +#endif + +/* IEEE 754 floating-point binary representation */ +#if defined(__STDC_IEC_559__) || defined(__STDC_IEC_60559_BFP__) +# define YYJSON_HAS_IEEE_754 1 +#elif FLT_RADIX == 2 && \ + FLT_MANT_DIG == 24 && FLT_DIG == 6 && \ + FLT_MIN_EXP == -125 && FLT_MAX_EXP == 128 && \ + FLT_MIN_10_EXP == -37 && FLT_MAX_10_EXP == 38 && \ + DBL_MANT_DIG == 53 && DBL_DIG == 15 && \ + DBL_MIN_EXP == -1021 && DBL_MAX_EXP == 1024 && \ + DBL_MIN_10_EXP == -307 && DBL_MAX_10_EXP == 308 +# define YYJSON_HAS_IEEE_754 1 +#else +# define YYJSON_HAS_IEEE_754 0 +# undef YYJSON_DISABLE_FAST_FP_CONV +# define YYJSON_DISABLE_FAST_FP_CONV 1 +#endif + +/* + Correct rounding in double number computations. + + On the x86 architecture, some compilers may use x87 FPU instructions for + floating-point arithmetic. The x87 FPU loads all floating point number as + 80-bit double-extended precision internally, then rounds the result to original + precision, which may produce inaccurate results. For a more detailed + explanation, see the paper: https://arxiv.org/abs/cs/0701192 + + Here are some examples of double precision calculation error: + + 2877.0 / 1e6 == 0.002877, but x87 returns 0.0028770000000000002 + 43683.0 * 1e21 == 4.3683e25, but x87 returns 4.3683000000000004e25 + + Here are some examples of compiler flags to generate x87 instructions on x86: + + clang -m32 -mno-sse + gcc/icc -m32 -mfpmath=387 + msvc /arch:SSE or /arch:IA32 + + If we are sure that there's no similar error described above, we can define the + YYJSON_DOUBLE_MATH_CORRECT as 1 to enable the fast path calculation. This is + not an accurate detection, it's just try to avoid the error at compile-time. + An accurate detection can be done at run-time: + + bool is_double_math_correct(void) { + volatile double r = 43683.0; + r *= 1e21; + return r == 4.3683e25; + } + + See also: utils.h in https://github.com/google/double-conversion/ + */ +#if !defined(FLT_EVAL_METHOD) && defined(__FLT_EVAL_METHOD__) +# define FLT_EVAL_METHOD __FLT_EVAL_METHOD__ +#endif + +#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 && FLT_EVAL_METHOD != 1 +# define YYJSON_DOUBLE_MATH_CORRECT 0 +#elif defined(i386) || defined(__i386) || defined(__i386__) || \ + defined(_X86_) || defined(__X86__) || defined(_M_IX86) || \ + defined(__I86__) || defined(__IA32__) || defined(__THW_INTEL) +# if (defined(_MSC_VER) && defined(_M_IX86_FP) && _M_IX86_FP == 2) || \ + (defined(__SSE2_MATH__) && __SSE2_MATH__) +# define YYJSON_DOUBLE_MATH_CORRECT 1 +# else +# define YYJSON_DOUBLE_MATH_CORRECT 0 +# endif +#elif defined(__mc68000__) || defined(__pnacl__) || defined(__native_client__) +# define YYJSON_DOUBLE_MATH_CORRECT 0 +#else +# define YYJSON_DOUBLE_MATH_CORRECT 1 +#endif + +/* + Detect the endianness at compile-time. + YYJSON_ENDIAN == YYJSON_BIG_ENDIAN + YYJSON_ENDIAN == YYJSON_LITTLE_ENDIAN + */ +#define YYJSON_BIG_ENDIAN 4321 +#define YYJSON_LITTLE_ENDIAN 1234 + +#if yyjson_has_include() +# include /* POSIX */ +#endif +#if yyjson_has_include() +# include /* Linux */ +#elif yyjson_has_include() +# include /* BSD, Android */ +#elif yyjson_has_include() +# include /* BSD, Darwin */ +#endif + +#if defined(BYTE_ORDER) && BYTE_ORDER +# if defined(BIG_ENDIAN) && (BYTE_ORDER == BIG_ENDIAN) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +# elif defined(LITTLE_ENDIAN) && (BYTE_ORDER == LITTLE_ENDIAN) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +# endif +#elif defined(__BYTE_ORDER) && __BYTE_ORDER +# if defined(__BIG_ENDIAN) && (__BYTE_ORDER == __BIG_ENDIAN) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +# elif defined(__LITTLE_ENDIAN) && (__BYTE_ORDER == __LITTLE_ENDIAN) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +# endif +#elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ +# if defined(__ORDER_BIG_ENDIAN__) && \ + (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +# elif defined(__ORDER_LITTLE_ENDIAN__) && \ + (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +# endif +#elif (defined(__LITTLE_ENDIAN__) && __LITTLE_ENDIAN__ == 1) || \ + defined(__i386) || defined(__i386__) || \ + defined(_X86_) || defined(__X86__) || \ + defined(_M_IX86) || defined(__THW_INTEL__) || \ + defined(__x86_64) || defined(__x86_64__) || \ + defined(__amd64) || defined(__amd64__) || \ + defined(_M_AMD64) || defined(_M_X64) || \ + defined(_M_ARM) || defined(_M_ARM64) || \ + defined(__ARMEL__) || defined(__THUMBEL__) || defined(__AARCH64EL__) || \ + defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \ + defined(__EMSCRIPTEN__) || defined(__wasm__) || \ + defined(__loongarch__) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +#elif (defined(__BIG_ENDIAN__) && __BIG_ENDIAN__ == 1) || \ + defined(__ARMEB__) || defined(__THUMBEB__) || defined(__AARCH64EB__) || \ + defined(_MIPSEB) || defined(__MIPSEB) || defined(__MIPSEB__) || \ + defined(__or1k__) || defined(__OR1K__) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +#else +# define YYJSON_ENDIAN 0 /* unknown endian, detect at run-time */ +#endif + +/* + This macro controls how yyjson handles unaligned memory accesses. + + By default, yyjson uses `memcpy()` for memory copying. This allows the compiler + to optimize the code and emit unaligned memory access instructions when + supported by the target architecture. + + However, on some older compilers or architectures where `memcpy()` is not + well-optimized and may result in unnecessary function calls, defining this + macro as 1 may help. In such cases, yyjson switches to manual byte-by-byte + access, which can potentially improve performance. + + An example of the generated assembly code for ARM can be found here: + https://godbolt.org/z/334jjhxPT + + This flag is already enabled for common architectures in the following code, + so manual configuration is usually unnecessary. If unsure, you can check the + generated assembly or run benchmarks to make an informed decision. + */ +#ifndef YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS +# if defined(__ia64) || defined(_IA64) || defined(__IA64__) || \ + defined(__ia64__) || defined(_M_IA64) || defined(__itanium__) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* Itanium */ +# elif (defined(__arm__) || defined(__arm64__) || defined(__aarch64__)) && \ + (defined(__GNUC__) || defined(__clang__)) && \ + (!defined(__ARM_FEATURE_UNALIGNED) || !__ARM_FEATURE_UNALIGNED) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* ARM */ +# elif defined(__sparc) || defined(__sparc__) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* SPARC */ +# elif defined(__mips) || defined(__mips__) || defined(__MIPS__) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* MIPS */ +# elif defined(__m68k__) || defined(M68000) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* M68K */ +# else +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 0 +# endif +#endif + +/* + Estimated initial ratio of the JSON data (data_size / value_count). + For example: + + data: {"id":12345678,"name":"Harry"} + data_size: 30 + value_count: 5 + ratio: 6 + + yyjson uses dynamic memory with a growth factor of 1.5 when reading and writing + JSON, the ratios below are used to determine the initial memory size. + + A too large ratio will waste memory, and a too small ratio will cause multiple + memory growths and degrade performance. Currently, these ratios are generated + with some commonly used JSON datasets. + */ +#define YYJSON_READER_ESTIMATED_PRETTY_RATIO 16 +#define YYJSON_READER_ESTIMATED_MINIFY_RATIO 6 +#define YYJSON_WRITER_ESTIMATED_PRETTY_RATIO 32 +#define YYJSON_WRITER_ESTIMATED_MINIFY_RATIO 18 + +/* The initial and maximum size of the memory pool's chunk in yyjson_mut_doc. */ +#define YYJSON_MUT_DOC_STR_POOL_INIT_SIZE 0x100 +#define YYJSON_MUT_DOC_STR_POOL_MAX_SIZE 0x10000000 +#define YYJSON_MUT_DOC_VAL_POOL_INIT_SIZE (0x10 * sizeof(yyjson_mut_val)) +#define YYJSON_MUT_DOC_VAL_POOL_MAX_SIZE (0x1000000 * sizeof(yyjson_mut_val)) + +/* The minimum size of the dynamic allocator's chunk. */ +#define YYJSON_ALC_DYN_MIN_SIZE 0x1000 + +/* Default value for compile-time options. */ +#ifndef YYJSON_DISABLE_READER +#define YYJSON_DISABLE_READER 0 +#endif +#ifndef YYJSON_DISABLE_WRITER +#define YYJSON_DISABLE_WRITER 0 +#endif +#ifndef YYJSON_DISABLE_INCR_READER +#define YYJSON_DISABLE_INCR_READER 0 +#endif +#ifndef YYJSON_DISABLE_UTILS +#define YYJSON_DISABLE_UTILS 0 +#endif +#ifndef YYJSON_DISABLE_FAST_FP_CONV +#define YYJSON_DISABLE_FAST_FP_CONV 0 +#endif +#ifndef YYJSON_DISABLE_NON_STANDARD +#define YYJSON_DISABLE_NON_STANDARD 0 +#endif +#ifndef YYJSON_DISABLE_UTF8_VALIDATION +#define YYJSON_DISABLE_UTF8_VALIDATION 0 +#endif + + + +/*============================================================================== + * MARK: - Macros (Private) + *============================================================================*/ + +/* Macros used for loop unrolling and other purpose. */ +#define repeat2(x) { x x } +#define repeat4(x) { x x x x } +#define repeat8(x) { x x x x x x x x } +#define repeat16(x) { x x x x x x x x x x x x x x x x } + +#define repeat2_incr(x) { x(0) x(1) } +#define repeat4_incr(x) { x(0) x(1) x(2) x(3) } +#define repeat8_incr(x) { x(0) x(1) x(2) x(3) x(4) x(5) x(6) x(7) } +#define repeat16_incr(x) { x(0) x(1) x(2) x(3) x(4) x(5) x(6) x(7) \ + x(8) x(9) x(10) x(11) x(12) x(13) x(14) x(15) } +#define repeat_in_1_18(x) { x(1) x(2) x(3) x(4) x(5) x(6) x(7) x(8) \ + x(9) x(10) x(11) x(12) x(13) x(14) x(15) x(16) \ + x(17) x(18) } + +/* Macros used to provide branch prediction information for compiler. */ +#undef likely +#define likely(x) yyjson_likely(x) +#undef unlikely +#define unlikely(x) yyjson_unlikely(x) + +/* Macros used to provide inline information for compiler. */ +#undef static_inline +#define static_inline static yyjson_inline +#undef static_noinline +#define static_noinline static yyjson_noinline + +/* Macros for min and max. */ +#undef yyjson_min +#define yyjson_min(x, y) ((x) < (y) ? (x) : (y)) +#undef yyjson_max +#define yyjson_max(x, y) ((x) > (y) ? (x) : (y)) + +/* Used to write u64 literal for C89 which doesn't support "ULL" suffix. */ +#undef U64 +#define U64(hi, lo) ((((u64)hi##UL) << 32U) + lo##UL) +#undef U32 +#define U32(hi) ((u32)(hi##UL)) + +/* Used to cast away (remove) const qualifier. */ +#define constcast(type) (type)(void *)(size_t)(const void *) + +/* + Compiler barriers for single variables. + + These macros inform GCC that a read or write access to the given memory + location will occur, preventing certain compiler optimizations or reordering + around the access to 'val'. They do not emit any actual instructions. + + This is useful when GCC's default optimization strategies are suboptimal and + precise control over memory access patterns is required. + These barriers are not needed when using Clang or MSVC. + */ +#if YYJSON_IS_REAL_GCC +# define gcc_load_barrier(val) __asm__ volatile(""::"m"(val)) +# define gcc_store_barrier(val) __asm__ volatile("":"=m"(val)) +# define gcc_full_barrier(val) __asm__ volatile("":"=m"(val):"m"(val)) +#else +# define gcc_load_barrier(val) +# define gcc_store_barrier(val) +# define gcc_full_barrier(val) +#endif + + + +/*============================================================================== + * MARK: - Constants (Private) + *============================================================================*/ + +/* Common error messages. */ +#define MSG_FOPEN "failed to open file" +#define MSG_FREAD "failed to read file" +#define MSG_FWRITE "failed to write file" +#define MSG_FCLOSE "failed to close file" +#define MSG_MALLOC "failed to allocate memory" +#define MSG_CHAR_T "invalid literal, expected 'true'" +#define MSG_CHAR_F "invalid literal, expected 'false'" +#define MSG_CHAR_N "invalid literal, expected 'null'" +#define MSG_CHAR "unexpected character, expected a JSON value" +#define MSG_ARR_END "unexpected character, expected ',' or ']'" +#define MSG_OBJ_KEY "unexpected character, expected a string key" +#define MSG_OBJ_SEP "unexpected character, expected ':' after key" +#define MSG_OBJ_END "unexpected character, expected ',' or '}'" +#define MSG_GARBAGE "unexpected content after document" +#define MSG_NOT_END "unexpected end of data" +#define MSG_COMMENT "unclosed multiline comment" +#define MSG_COMMA "trailing comma is not allowed" +#define MSG_NAN_INF "nan or inf number is not allowed" +#define MSG_ERR_TYPE "invalid JSON value type" +#define MSG_ERR_BOM "UTF-8 byte order mark (BOM) is not supported" +#define MSG_ERR_UTF8 "invalid utf-8 encoding in string" +#define MSG_ERR_UTF16 "UTF-16 encoding is not supported" +#define MSG_ERR_UTF32 "UTF-32 encoding is not supported" + +/* U64 constant values */ +#undef U64_MAX +#define U64_MAX U64(0xFFFFFFFF, 0xFFFFFFFF) +#undef I64_MAX +#define I64_MAX U64(0x7FFFFFFF, 0xFFFFFFFF) +#undef USIZE_MAX +#define USIZE_MAX ((usize)(~(usize)0)) + +/* Maximum number of digits for reading u32/u64/usize safety (not overflow). */ +#undef U32_SAFE_DIG +#define U32_SAFE_DIG 9 /* u32 max is 4294967295, 10 digits */ +#undef U64_SAFE_DIG +#define U64_SAFE_DIG 19 /* u64 max is 18446744073709551615, 20 digits */ +#undef USIZE_SAFE_DIG +#define USIZE_SAFE_DIG (sizeof(usize) == 8 ? U64_SAFE_DIG : U32_SAFE_DIG) + +/* Inf bits (positive) */ +#define F64_BITS_INF U64(0x7FF00000, 0x00000000) + +/* NaN bits (quiet NaN, no payload, no sign) */ +#if defined(__hppa__) || (defined(__mips__) && !defined(__mips_nan2008)) +#define F64_BITS_NAN U64(0x7FF7FFFF, 0xFFFFFFFF) +#else +#define F64_BITS_NAN U64(0x7FF80000, 0x00000000) +#endif + +/* maximum significant digits count in decimal when reading double number */ +#define F64_MAX_DEC_DIG 768 + +/* maximum decimal power of double number (1.7976931348623157e308) */ +#define F64_MAX_DEC_EXP 308 + +/* minimum decimal power of double number (4.9406564584124654e-324) */ +#define F64_MIN_DEC_EXP (-324) + +/* maximum binary power of double number */ +#define F64_MAX_BIN_EXP 1024 + +/* minimum binary power of double number */ +#define F64_MIN_BIN_EXP (-1021) + +/* float/double number bits */ +#define F32_BITS 32 +#define F64_BITS 64 + +/* float/double number exponent part bits */ +#define F32_EXP_BITS 8 +#define F64_EXP_BITS 11 + +/* float/double number significand part bits */ +#define F32_SIG_BITS 23 +#define F64_SIG_BITS 52 + +/* float/double number significand part bits (with 1 hidden bit) */ +#define F32_SIG_FULL_BITS 24 +#define F64_SIG_FULL_BITS 53 + +/* float/double number significand bit mask */ +#define F32_SIG_MASK U32(0x007FFFFF) +#define F64_SIG_MASK U64(0x000FFFFF, 0xFFFFFFFF) + +/* float/double number exponent bit mask */ +#define F32_EXP_MASK U32(0x7F800000) +#define F64_EXP_MASK U64(0x7FF00000, 0x00000000) + +/* float/double number exponent bias */ +#define F32_EXP_BIAS 127 +#define F64_EXP_BIAS 1023 + +/* float/double number significant digits count in decimal */ +#define F32_DEC_DIG 9 +#define F64_DEC_DIG 17 + +/* buffer length required for float/double number writer */ +#define FP_BUF_LEN 40 + +/* maximum length of a number in incremental parsing */ +#define INCR_NUM_MAX_LEN 1024 + + + +/*============================================================================== + * MARK: - Types (Private) + *============================================================================*/ + +/** Type define for primitive types. */ +typedef float f32; +typedef double f64; +typedef int8_t i8; +typedef uint8_t u8; +typedef int16_t i16; +typedef uint16_t u16; +typedef int32_t i32; +typedef uint32_t u32; +typedef int64_t i64; +typedef uint64_t u64; +typedef size_t usize; + +/** 128-bit integer, used by floating-point number reader and writer. */ +#if YYJSON_HAS_INT128 +__extension__ typedef __int128 i128; +__extension__ typedef unsigned __int128 u128; +#endif + +/** 16/32/64-bit vector */ +typedef struct v16 { char c[2]; } v16; +typedef struct v32 { char c[4]; } v32; +typedef struct v64 { char c[8]; } v64; + +/** 16/32/64-bit vector union */ +typedef union v16_uni { v16 v; u16 u; } v16_uni; +typedef union v32_uni { v32 v; u32 u; } v32_uni; +typedef union v64_uni { v64 v; u64 u; } v64_uni; + + + +/*============================================================================== + * MARK: - Load/Store Utils (Private) + *============================================================================*/ + +#define byte_move_idx(x) ((char *)dst)[x] = ((const char *)src)[x]; +#define byte_move_src(x) ((char *)tmp)[x] = ((const char *)src)[x]; +#define byte_move_dst(x) ((char *)dst)[x] = ((const char *)tmp)[x]; + +/** Same as `memcpy(dst, src, 2)`, no overlap. */ +static_inline void byte_copy_2(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 2); +#else + repeat2_incr(byte_move_idx) +#endif +} + +/** Same as `memcpy(dst, src, 4)`, no overlap. */ +static_inline void byte_copy_4(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 4); +#else + repeat4_incr(byte_move_idx) +#endif +} + +/** Same as `memcpy(dst, src, 8)`, no overlap. */ +static_inline void byte_copy_8(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 8); +#else + repeat8_incr(byte_move_idx) +#endif +} + +/** Same as `memcpy(dst, src, 16)`, no overlap. */ +static_inline void byte_copy_16(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 16); +#else + repeat16_incr(byte_move_idx) +#endif +} + +/** Same as `memmove(dst, src, 2)`, allows overlap. */ +static_inline void byte_move_2(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + u16 tmp; + memcpy(&tmp, src, 2); + memcpy(dst, &tmp, 2); +#else + char tmp[2]; + repeat2_incr(byte_move_src) + repeat2_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, 4)`, allows overlap. */ +static_inline void byte_move_4(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + u32 tmp; + memcpy(&tmp, src, 4); + memcpy(dst, &tmp, 4); +#else + char tmp[4]; + repeat4_incr(byte_move_src) + repeat4_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, 8)`, allows overlap. */ +static_inline void byte_move_8(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + u64 tmp; + memcpy(&tmp, src, 8); + memcpy(dst, &tmp, 8); +#else + char tmp[8]; + repeat8_incr(byte_move_src) + repeat8_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, 16)`, allows overlap. */ +static_inline void byte_move_16(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + char *pdst = (char *)dst; + const char *psrc = (const char *)src; + u64 tmp1, tmp2; + memcpy(&tmp1, psrc, 8); + memcpy(&tmp2, psrc + 8, 8); + memcpy(pdst, &tmp1, 8); + memcpy(pdst + 8, &tmp2, 8); +#else + char tmp[16]; + repeat16_incr(byte_move_src) + repeat16_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, n)`, but only `dst <= src` and `n <= 16`. */ +static_inline void byte_move_forward(void *dst, void *src, usize n) { + char *d = (char *)dst, *s = (char *)src; + n += (n % 2); /* round up to even */ + if (n == 16) { byte_move_16(d, s); return; } + if (n >= 8) { byte_move_8(d, s); n -= 8; d += 8; s += 8; } + if (n >= 4) { byte_move_4(d, s); n -= 4; d += 4; s += 4; } + if (n >= 2) { byte_move_2(d, s); } +} + +/** Same as `memcmp(buf, pat, 2) == 0`. */ +static_inline bool byte_match_2(void *buf, const char *pat) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + v16_uni u1, u2; + memcpy(&u1, buf, 2); + memcpy(&u2, pat, 2); + return u1.u == u2.u; +#else + return ((char *)buf)[0] == ((const char *)pat)[0] && + ((char *)buf)[1] == ((const char *)pat)[1]; +#endif +} + +/** Same as `memcmp(buf, pat, 4) == 0`. */ +static_inline bool byte_match_4(void *buf, const char *pat) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + v32_uni u1, u2; + memcpy(&u1, buf, 4); + memcpy(&u2, pat, 4); + return u1.u == u2.u; +#else + return ((char *)buf)[0] == ((const char *)pat)[0] && + ((char *)buf)[1] == ((const char *)pat)[1] && + ((char *)buf)[2] == ((const char *)pat)[2] && + ((char *)buf)[3] == ((const char *)pat)[3]; +#endif +} + +/** Loads 2 bytes from `src` as a u16 (native-endian). */ +static_inline u16 byte_load_2(const void *src) { + v16_uni uni; +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(&uni, src, 2); +#else + uni.v.c[0] = ((const char *)src)[0]; + uni.v.c[1] = ((const char *)src)[1]; +#endif + return uni.u; +} + +/** Loads 3 bytes from `src` as a u32 (native-endian). */ +static_inline u32 byte_load_3(const void *src) { + v32_uni uni; +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(&uni, src, 2); + uni.v.c[2] = ((const char *)src)[2]; + uni.v.c[3] = 0; +#else + uni.v.c[0] = ((const char *)src)[0]; + uni.v.c[1] = ((const char *)src)[1]; + uni.v.c[2] = ((const char *)src)[2]; + uni.v.c[3] = 0; +#endif + return uni.u; +} + +/** Loads 4 bytes from `src` as a u32 (native-endian). */ +static_inline u32 byte_load_4(const void *src) { + v32_uni uni; +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(&uni, src, 4); +#else + uni.v.c[0] = ((const char *)src)[0]; + uni.v.c[1] = ((const char *)src)[1]; + uni.v.c[2] = ((const char *)src)[2]; + uni.v.c[3] = ((const char *)src)[3]; +#endif + return uni.u; +} + + + +/*============================================================================== + * MARK: - Character Utils (Private) + * These lookup tables were generated by `misc/make_tables.c`. + *============================================================================*/ + +/* char_table1 */ +#define CHAR_TYPE_ASCII (1 << 0) /* Except: ["\], [0x00-0x1F, 0x80-0xFF] */ +#define CHAR_TYPE_ASCII_SQ (1 << 1) /* Except: ['\], [0x00-0x1F, 0x80-0xFF] */ +#define CHAR_TYPE_SPACE (1 << 2) /* Whitespace: [ \t\n\r] */ +#define CHAR_TYPE_SPACE_EXT (1 << 3) /* Whitespace: [ \t\n\r\v\f], JSON5 */ +#define CHAR_TYPE_NUM (1 << 4) /* Number: [.-+0-9] */ +#define CHAR_TYPE_COMMENT (1 << 5) /* Comment: [/] */ + +/* char_table2 */ +#define CHAR_TYPE_EOL (1 << 0) /* End of line: [\r\n] */ +#define CHAR_TYPE_EOL_EXT (1 << 1) /* End of line: [\r\n], JSON5 */ +#define CHAR_TYPE_ID_START (1 << 2) /* ID start: [_$A-Za-z\], U+0080+ */ +#define CHAR_TYPE_ID_NEXT (1 << 3) /* ID next: [_$A-Za-z0-9\], U+0080+ */ +#define CHAR_TYPE_ID_ASCII (1 << 4) /* ID next ASCII: [_$A-Za-z0-9] */ + +/* char_table3 */ +#define CHAR_TYPE_SIGN (1 << 0) /* [-+] */ +#define CHAR_TYPE_DIGIT (1 << 1) /* [0-9] */ +#define CHAR_TYPE_NONZERO (1 << 2) /* [1-9] */ +#define CHAR_TYPE_EXP (1 << 3) /* [eE] */ +#define CHAR_TYPE_DOT (1 << 4) /* [.] */ + +static const u8 char_table1[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0x0C, 0x08, 0x08, 0x0C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x03, 0x02, 0x03, 0x03, 0x03, 0x03, 0x01, + 0x03, 0x03, 0x03, 0x13, 0x03, 0x13, 0x13, 0x23, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x00, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +static const u8 char_table2[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x00, 0x0C, 0x00, 0x00, 0x1C, + 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C +}; + +static const u8 char_table3[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x00, + 0x02, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/** Match a whitespace: [ \t\n\r]. */ +static_inline bool char_is_space(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_SPACE); +} + +/** Match an extended whitespace: [ \t\n\r\\v\\f], JSON5 whitespace. */ +static_inline bool char_is_space_ext(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_SPACE_EXT); +} + +/** Match a JSON number: [.-+0-9]. */ +static_inline bool char_is_num(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_NUM); +} + +/** Match an ASCII character in string: ["\], [0x00-0x1F, 0x80-0xFF]. */ +static_inline bool char_is_ascii_skip(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_ASCII); +} + +/** Match an ASCII character single-quoted: ['\], [0x00-0x1F, 0x80-0xFF]. */ +static_inline bool char_is_ascii_skip_sq(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_ASCII_SQ); +} + +/** Match a trivia character: extended whitespace or comment. */ +static_inline bool char_is_trivia(u8 c) { + return !!(char_table1[c] & (CHAR_TYPE_SPACE_EXT | CHAR_TYPE_COMMENT)); +} + +/** Match a line end character: [\r\n]. */ +static_inline bool char_is_eol(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_EOL); +} + +/** Match an extended line end character: [\r\n], JSON5 line terminator. */ +static_inline bool char_is_eol_ext(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_EOL_EXT); +} + +/** Match an identifier name start: [_$A-Za-z\], U+0080+. */ +static_inline bool char_is_id_start(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_ID_START); +} + +/** Match an identifier name next: [_$A-Za-z0-9\], U+0080+. */ +static_inline bool char_is_id_next(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_ID_NEXT); +} + +/** Match an identifier name ASCII: [_$A-Za-z0-9]. */ +static_inline bool char_is_id_ascii(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_ID_ASCII); +} + +/** Match a sign: [+-] */ +static_inline bool char_is_sign(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_SIGN); +} + +/** Match a none-zero digit: [1-9] */ +static_inline bool char_is_nonzero(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_NONZERO); +} + +/** Match a digit: [0-9] */ +static_inline bool char_is_digit(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_DIGIT); +} + +/** Match an exponent sign: [eE]. */ +static_inline bool char_is_exp(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_EXP); +} + +/** Match a floating point indicator: [.eE]. */ +static_inline bool char_is_fp(u8 d) { + return !!(char_table3[d] & (CHAR_TYPE_DOT | CHAR_TYPE_EXP)); +} + +/** Match a digit or floating point indicator: [0-9.eE]. */ +static_inline bool char_is_digit_or_fp(u8 d) { + return !!(char_table3[d] & (CHAR_TYPE_DIGIT | CHAR_TYPE_DOT | + CHAR_TYPE_EXP)); +} + +/** Match a JSON container: `{` or `[`. */ +static_inline bool char_is_ctn(u8 c) { + return (c & 0xDF) == 0x5B; /* '[': 0x5B, '{': 0x7B */ +} + +/** Convert ASCII letter to lowercase; valid only for [A-Za-z]. */ +static_inline u8 char_to_lower(u8 c) { + return c | 0x20; +} + +/** Match UTF-8 byte order mask. */ +static_inline bool is_utf8_bom(const u8 *cur) { + return byte_load_3(cur) == byte_load_3("\xEF\xBB\xBF"); +} + +/** Match UTF-16 byte order mask. */ +static_inline bool is_utf16_bom(const u8 *cur) { + return byte_load_2(cur) == byte_load_2("\xFE\xFF") || + byte_load_2(cur) == byte_load_2("\xFF\xFE"); +} + +/** Match UTF-32 byte order mask, need length check to avoid zero padding. */ +static_inline bool is_utf32_bom(const u8 *cur) { + return byte_load_4(cur) == byte_load_4("\x00\x00\xFE\xFF") || + byte_load_4(cur) == byte_load_4("\xFF\xFE\x00\x00"); +} + +/** Get the extended line end length. Used with `char_is_eol_ext`. */ +static_inline usize ext_eol_len(const u8 *cur) { + if (cur[0] < 0x80) return 1; + if (cur[1] == 0x80 && (cur[2] == 0xA8 || cur[2] == 0xA9)) return 3; + return 0; +} + +/** Get the extended whitespace length. Used with `char_is_space_ext`. */ +static_inline usize ext_space_len(const u8 *cur) { + if (cur[0] < 0x80) { + return 1; + } else if (byte_load_2(cur) == byte_load_2("\xC2\xA0")) { + return 2; + } else if (byte_load_2(cur) == byte_load_2("\xE2\x80")) { + if (cur[2] >= 0x80 && cur[2] <= 0x8A) return 3; + if (cur[2] == 0xA8 || cur[2] == 0xA9 || cur[2] == 0xAF) return 3; + } else { + u32 uni = byte_load_3(cur); + if (uni == byte_load_3("\xE1\x9A\x80") || + uni == byte_load_3("\xE2\x81\x9F") || + uni == byte_load_3("\xE3\x80\x80") || + uni == byte_load_3("\xEF\xBB\xBF")) return 3; + } + return 0; +} + + + +/*============================================================================== + * MARK: - Hex Character Reader (Private) + * This function is used by JSON reader to read escaped characters. + *============================================================================*/ + +/** + This table is used to convert 4 hex character sequence to a number. + A valid hex character [0-9A-Fa-f] will mapped to it's raw number [0x00, 0x0F], + an invalid hex character will mapped to [0xF0]. + (generate with misc/make_tables.c) + */ +static const u8 hex_conv_table[256] = { + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0 +}; + +/** Load 4 hex characters to `u16`, return true on valid input. */ +static_inline bool hex_load_4(const u8 *src, u16 *dst) { + u16 c0 = hex_conv_table[src[0]]; + u16 c1 = hex_conv_table[src[1]]; + u16 c2 = hex_conv_table[src[2]]; + u16 c3 = hex_conv_table[src[3]]; + u16 t0 = (u16)((c0 << 8) | c2); + u16 t1 = (u16)((c1 << 8) | c3); + *dst = (u16)((t0 << 4) | t1); + return ((t0 | t1) & (u16)0xF0F0) == 0; +} + +/** Load 2 hex characters to `u8`, return true on valid input. */ +static_inline bool hex_load_2(const u8 *src, u8 *dst) { + u8 c0 = hex_conv_table[src[0]]; + u8 c1 = hex_conv_table[src[1]]; + *dst = (u8)((c0 << 4) | c1); + return ((c0 | c1) & 0xF0) == 0; +} + +/** Match a hexadecimal numeric character: [0-9a-fA-F]. */ +static_inline bool char_is_hex(u8 c) { + return hex_conv_table[c] != 0xF0; +} + + + +/*============================================================================== + * MARK: - UTF8 Validation (Private) + * Each Unicode code point is encoded using 1 to 4 bytes in UTF-8. + * Validation is performed using a 4-byte mask and pattern-based approach, + * which requires the input data to be padded with four zero bytes at the end. + *============================================================================*/ + +/* Macro for concatenating four u8 into a u32 and keeping the byte order. */ +#if YYJSON_ENDIAN == YYJSON_LITTLE_ENDIAN +# define utf8_seq_def(name, a, b, c, d) \ + static const u32 utf8_seq_##name = 0x##d##c##b##a##UL; +# define utf8_seq(name) utf8_seq_##name +#elif YYJSON_ENDIAN == YYJSON_BIG_ENDIAN +# define utf8_seq_def(name, a, b, c, d) \ + static const u32 utf8_seq_##name = 0x##a##b##c##d##UL; +# define utf8_seq(name) utf8_seq_##name +#else +# define utf8_seq_def(name, a, b, c, d) \ + static const v32_uni utf8_uni_##name = {{ 0x##a, 0x##b, 0x##c, 0x##d }}; +# define utf8_seq(name) utf8_uni_##name.u +#endif + +/* + 1-byte sequence (U+0000 to U+007F) + bit min [.......0] (U+0000) + bit max [.1111111] (U+007F) + bit mask [x.......] (80) + bit pattern [0.......] (00) + */ +utf8_seq_def(b1_mask, 80, 00, 00, 00) +utf8_seq_def(b1_patt, 00, 00, 00, 00) +#define is_utf8_seq1(uni) ( \ + ((uni & utf8_seq(b1_mask)) == utf8_seq(b1_patt)) ) + +/* + 2-byte sequence (U+0080 to U+07FF) + bit min [......10 ..000000] (U+0080) + bit max [...11111 ..111111] (U+07FF) + bit mask [xxx..... xx......] (E0 C0) + bit pattern [110..... 10......] (C0 80) + bit require [...xxxx. ........] (1E 00) + */ +utf8_seq_def(b2_mask, E0, C0, 00, 00) +utf8_seq_def(b2_patt, C0, 80, 00, 00) +utf8_seq_def(b2_requ, 1E, 00, 00, 00) +#define is_utf8_seq2(uni) ( \ + ((uni & utf8_seq(b2_mask)) == utf8_seq(b2_patt)) && \ + ((uni & utf8_seq(b2_requ))) ) + +/* + 3-byte sequence (U+0800 to U+FFFF) + bit min [........ ..100000 ..000000] (U+0800) + bit max [....1111 ..111111 ..111111] (U+FFFF) + bit mask [xxxx.... xx...... xx......] (F0 C0 C0) + bit pattern [1110.... 10...... 10......] (E0 80 80) + bit require [....xxxx ..x..... ........] (0F 20 00) + + 3-byte invalid sequence, reserved for surrogate halves (U+D800 to U+DFFF) + bit min [....1101 ..100000 ..000000] (U+D800) + bit max [....1101 ..111111 ..111111] (U+DFFF) + bit mask [....xxxx ..x..... ........] (0F 20 00) + bit pattern [....1101 ..1..... ........] (0D 20 00) + */ +utf8_seq_def(b3_mask, F0, C0, C0, 00) +utf8_seq_def(b3_patt, E0, 80, 80, 00) +utf8_seq_def(b3_requ, 0F, 20, 00, 00) +utf8_seq_def(b3_erro, 0D, 20, 00, 00) +#define is_utf8_seq3(uni) ( \ + ((uni & utf8_seq(b3_mask)) == utf8_seq(b3_patt)) && \ + ((tmp = (uni & utf8_seq(b3_requ)))) && \ + ((tmp != utf8_seq(b3_erro))) ) + +/* + 4-byte sequence (U+10000 to U+10FFFF) + bit min [........ ...10000 ..000000 ..000000] (U+10000) + bit max [.....100 ..001111 ..111111 ..111111] (U+10FFFF) + bit mask [xxxxx... xx...... xx...... xx......] (F8 C0 C0 C0) + bit pattern [11110... 10...... 10...... 10......] (F0 80 80 80) + bit require [.....xxx ..xx.... ........ ........] (07 30 00 00) + bit require 1 [.....x.. ........ ........ ........] (04 00 00 00) + bit require 2 [......xx ..xx.... ........ ........] (03 30 00 00) + */ +utf8_seq_def(b4_mask, F8, C0, C0, C0) +utf8_seq_def(b4_patt, F0, 80, 80, 80) +utf8_seq_def(b4_requ, 07, 30, 00, 00) +utf8_seq_def(b4_req1, 04, 00, 00, 00) +utf8_seq_def(b4_req2, 03, 30, 00, 00) +#define is_utf8_seq4(uni) ( \ + ((uni & utf8_seq(b4_mask)) == utf8_seq(b4_patt)) && \ + ((tmp = (uni & utf8_seq(b4_requ)))) && \ + ((tmp & utf8_seq(b4_req1)) == 0 || (tmp & utf8_seq(b4_req2)) == 0) ) + + + +/*============================================================================== + * MARK: - Power10 Lookup Table (Private) + * These data are used by the floating-point number reader and writer. + *============================================================================*/ + +#if !YYJSON_DISABLE_FAST_FP_CONV + +/** Maximum pow10 exponent that can be represented exactly as a float64. */ +#define F64_POW10_MAX_EXACT_EXP 22 + +/** Cached pow10 table. */ +static const f64 f64_pow10_table[F64_POW10_MAX_EXACT_EXP + 1] = { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, + 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 +}; + +/** Maximum pow10 exponent that can be represented exactly as a uint64. */ +#define U64_POW10_MAX_EXACT_EXP 19 + +/** Table: [ 10^0, ..., 10^19 ] (generate with misc/make_tables.c) */ +static const u64 u64_pow10_table[U64_POW10_MAX_EXACT_EXP + 1] = { + U64(0x00000000, 0x00000001), U64(0x00000000, 0x0000000A), + U64(0x00000000, 0x00000064), U64(0x00000000, 0x000003E8), + U64(0x00000000, 0x00002710), U64(0x00000000, 0x000186A0), + U64(0x00000000, 0x000F4240), U64(0x00000000, 0x00989680), + U64(0x00000000, 0x05F5E100), U64(0x00000000, 0x3B9ACA00), + U64(0x00000002, 0x540BE400), U64(0x00000017, 0x4876E800), + U64(0x000000E8, 0xD4A51000), U64(0x00000918, 0x4E72A000), + U64(0x00005AF3, 0x107A4000), U64(0x00038D7E, 0xA4C68000), + U64(0x002386F2, 0x6FC10000), U64(0x01634578, 0x5D8A0000), + U64(0x0DE0B6B3, 0xA7640000), U64(0x8AC72304, 0x89E80000) +}; + +/** Minimum decimal exponent in pow10_sig_table. */ +#define POW10_SIG_TABLE_MIN_EXP -343 + +/** Maximum decimal exponent in pow10_sig_table. */ +#define POW10_SIG_TABLE_MAX_EXP 324 + +/** Minimum exact decimal exponent in pow10_sig_table */ +#define POW10_SIG_TABLE_MIN_EXACT_EXP 0 + +/** Maximum exact decimal exponent in pow10_sig_table */ +#define POW10_SIG_TABLE_MAX_EXACT_EXP 55 + +/** Normalized significant 128 bits of pow10, no rounded up (size: 10.4KB). + This lookup table is used by both the double number reader and writer. + (generate with misc/make_tables.c) */ +static const u64 pow10_sig_table[] = { + U64(0xBF29DCAB, 0xA82FDEAE), U64(0x7432EE87, 0x3880FC33), /* ~= 10^-343 */ + U64(0xEEF453D6, 0x923BD65A), U64(0x113FAA29, 0x06A13B3F), /* ~= 10^-342 */ + U64(0x9558B466, 0x1B6565F8), U64(0x4AC7CA59, 0xA424C507), /* ~= 10^-341 */ + U64(0xBAAEE17F, 0xA23EBF76), U64(0x5D79BCF0, 0x0D2DF649), /* ~= 10^-340 */ + U64(0xE95A99DF, 0x8ACE6F53), U64(0xF4D82C2C, 0x107973DC), /* ~= 10^-339 */ + U64(0x91D8A02B, 0xB6C10594), U64(0x79071B9B, 0x8A4BE869), /* ~= 10^-338 */ + U64(0xB64EC836, 0xA47146F9), U64(0x9748E282, 0x6CDEE284), /* ~= 10^-337 */ + U64(0xE3E27A44, 0x4D8D98B7), U64(0xFD1B1B23, 0x08169B25), /* ~= 10^-336 */ + U64(0x8E6D8C6A, 0xB0787F72), U64(0xFE30F0F5, 0xE50E20F7), /* ~= 10^-335 */ + U64(0xB208EF85, 0x5C969F4F), U64(0xBDBD2D33, 0x5E51A935), /* ~= 10^-334 */ + U64(0xDE8B2B66, 0xB3BC4723), U64(0xAD2C7880, 0x35E61382), /* ~= 10^-333 */ + U64(0x8B16FB20, 0x3055AC76), U64(0x4C3BCB50, 0x21AFCC31), /* ~= 10^-332 */ + U64(0xADDCB9E8, 0x3C6B1793), U64(0xDF4ABE24, 0x2A1BBF3D), /* ~= 10^-331 */ + U64(0xD953E862, 0x4B85DD78), U64(0xD71D6DAD, 0x34A2AF0D), /* ~= 10^-330 */ + U64(0x87D4713D, 0x6F33AA6B), U64(0x8672648C, 0x40E5AD68), /* ~= 10^-329 */ + U64(0xA9C98D8C, 0xCB009506), U64(0x680EFDAF, 0x511F18C2), /* ~= 10^-328 */ + U64(0xD43BF0EF, 0xFDC0BA48), U64(0x0212BD1B, 0x2566DEF2), /* ~= 10^-327 */ + U64(0x84A57695, 0xFE98746D), U64(0x014BB630, 0xF7604B57), /* ~= 10^-326 */ + U64(0xA5CED43B, 0x7E3E9188), U64(0x419EA3BD, 0x35385E2D), /* ~= 10^-325 */ + U64(0xCF42894A, 0x5DCE35EA), U64(0x52064CAC, 0x828675B9), /* ~= 10^-324 */ + U64(0x818995CE, 0x7AA0E1B2), U64(0x7343EFEB, 0xD1940993), /* ~= 10^-323 */ + U64(0xA1EBFB42, 0x19491A1F), U64(0x1014EBE6, 0xC5F90BF8), /* ~= 10^-322 */ + U64(0xCA66FA12, 0x9F9B60A6), U64(0xD41A26E0, 0x77774EF6), /* ~= 10^-321 */ + U64(0xFD00B897, 0x478238D0), U64(0x8920B098, 0x955522B4), /* ~= 10^-320 */ + U64(0x9E20735E, 0x8CB16382), U64(0x55B46E5F, 0x5D5535B0), /* ~= 10^-319 */ + U64(0xC5A89036, 0x2FDDBC62), U64(0xEB2189F7, 0x34AA831D), /* ~= 10^-318 */ + U64(0xF712B443, 0xBBD52B7B), U64(0xA5E9EC75, 0x01D523E4), /* ~= 10^-317 */ + U64(0x9A6BB0AA, 0x55653B2D), U64(0x47B233C9, 0x2125366E), /* ~= 10^-316 */ + U64(0xC1069CD4, 0xEABE89F8), U64(0x999EC0BB, 0x696E840A), /* ~= 10^-315 */ + U64(0xF148440A, 0x256E2C76), U64(0xC00670EA, 0x43CA250D), /* ~= 10^-314 */ + U64(0x96CD2A86, 0x5764DBCA), U64(0x38040692, 0x6A5E5728), /* ~= 10^-313 */ + U64(0xBC807527, 0xED3E12BC), U64(0xC6050837, 0x04F5ECF2), /* ~= 10^-312 */ + U64(0xEBA09271, 0xE88D976B), U64(0xF7864A44, 0xC633682E), /* ~= 10^-311 */ + U64(0x93445B87, 0x31587EA3), U64(0x7AB3EE6A, 0xFBE0211D), /* ~= 10^-310 */ + U64(0xB8157268, 0xFDAE9E4C), U64(0x5960EA05, 0xBAD82964), /* ~= 10^-309 */ + U64(0xE61ACF03, 0x3D1A45DF), U64(0x6FB92487, 0x298E33BD), /* ~= 10^-308 */ + U64(0x8FD0C162, 0x06306BAB), U64(0xA5D3B6D4, 0x79F8E056), /* ~= 10^-307 */ + U64(0xB3C4F1BA, 0x87BC8696), U64(0x8F48A489, 0x9877186C), /* ~= 10^-306 */ + U64(0xE0B62E29, 0x29ABA83C), U64(0x331ACDAB, 0xFE94DE87), /* ~= 10^-305 */ + U64(0x8C71DCD9, 0xBA0B4925), U64(0x9FF0C08B, 0x7F1D0B14), /* ~= 10^-304 */ + U64(0xAF8E5410, 0x288E1B6F), U64(0x07ECF0AE, 0x5EE44DD9), /* ~= 10^-303 */ + U64(0xDB71E914, 0x32B1A24A), U64(0xC9E82CD9, 0xF69D6150), /* ~= 10^-302 */ + U64(0x892731AC, 0x9FAF056E), U64(0xBE311C08, 0x3A225CD2), /* ~= 10^-301 */ + U64(0xAB70FE17, 0xC79AC6CA), U64(0x6DBD630A, 0x48AAF406), /* ~= 10^-300 */ + U64(0xD64D3D9D, 0xB981787D), U64(0x092CBBCC, 0xDAD5B108), /* ~= 10^-299 */ + U64(0x85F04682, 0x93F0EB4E), U64(0x25BBF560, 0x08C58EA5), /* ~= 10^-298 */ + U64(0xA76C5823, 0x38ED2621), U64(0xAF2AF2B8, 0x0AF6F24E), /* ~= 10^-297 */ + U64(0xD1476E2C, 0x07286FAA), U64(0x1AF5AF66, 0x0DB4AEE1), /* ~= 10^-296 */ + U64(0x82CCA4DB, 0x847945CA), U64(0x50D98D9F, 0xC890ED4D), /* ~= 10^-295 */ + U64(0xA37FCE12, 0x6597973C), U64(0xE50FF107, 0xBAB528A0), /* ~= 10^-294 */ + U64(0xCC5FC196, 0xFEFD7D0C), U64(0x1E53ED49, 0xA96272C8), /* ~= 10^-293 */ + U64(0xFF77B1FC, 0xBEBCDC4F), U64(0x25E8E89C, 0x13BB0F7A), /* ~= 10^-292 */ + U64(0x9FAACF3D, 0xF73609B1), U64(0x77B19161, 0x8C54E9AC), /* ~= 10^-291 */ + U64(0xC795830D, 0x75038C1D), U64(0xD59DF5B9, 0xEF6A2417), /* ~= 10^-290 */ + U64(0xF97AE3D0, 0xD2446F25), U64(0x4B057328, 0x6B44AD1D), /* ~= 10^-289 */ + U64(0x9BECCE62, 0x836AC577), U64(0x4EE367F9, 0x430AEC32), /* ~= 10^-288 */ + U64(0xC2E801FB, 0x244576D5), U64(0x229C41F7, 0x93CDA73F), /* ~= 10^-287 */ + U64(0xF3A20279, 0xED56D48A), U64(0x6B435275, 0x78C1110F), /* ~= 10^-286 */ + U64(0x9845418C, 0x345644D6), U64(0x830A1389, 0x6B78AAA9), /* ~= 10^-285 */ + U64(0xBE5691EF, 0x416BD60C), U64(0x23CC986B, 0xC656D553), /* ~= 10^-284 */ + U64(0xEDEC366B, 0x11C6CB8F), U64(0x2CBFBE86, 0xB7EC8AA8), /* ~= 10^-283 */ + U64(0x94B3A202, 0xEB1C3F39), U64(0x7BF7D714, 0x32F3D6A9), /* ~= 10^-282 */ + U64(0xB9E08A83, 0xA5E34F07), U64(0xDAF5CCD9, 0x3FB0CC53), /* ~= 10^-281 */ + U64(0xE858AD24, 0x8F5C22C9), U64(0xD1B3400F, 0x8F9CFF68), /* ~= 10^-280 */ + U64(0x91376C36, 0xD99995BE), U64(0x23100809, 0xB9C21FA1), /* ~= 10^-279 */ + U64(0xB5854744, 0x8FFFFB2D), U64(0xABD40A0C, 0x2832A78A), /* ~= 10^-278 */ + U64(0xE2E69915, 0xB3FFF9F9), U64(0x16C90C8F, 0x323F516C), /* ~= 10^-277 */ + U64(0x8DD01FAD, 0x907FFC3B), U64(0xAE3DA7D9, 0x7F6792E3), /* ~= 10^-276 */ + U64(0xB1442798, 0xF49FFB4A), U64(0x99CD11CF, 0xDF41779C), /* ~= 10^-275 */ + U64(0xDD95317F, 0x31C7FA1D), U64(0x40405643, 0xD711D583), /* ~= 10^-274 */ + U64(0x8A7D3EEF, 0x7F1CFC52), U64(0x482835EA, 0x666B2572), /* ~= 10^-273 */ + U64(0xAD1C8EAB, 0x5EE43B66), U64(0xDA324365, 0x0005EECF), /* ~= 10^-272 */ + U64(0xD863B256, 0x369D4A40), U64(0x90BED43E, 0x40076A82), /* ~= 10^-271 */ + U64(0x873E4F75, 0xE2224E68), U64(0x5A7744A6, 0xE804A291), /* ~= 10^-270 */ + U64(0xA90DE353, 0x5AAAE202), U64(0x711515D0, 0xA205CB36), /* ~= 10^-269 */ + U64(0xD3515C28, 0x31559A83), U64(0x0D5A5B44, 0xCA873E03), /* ~= 10^-268 */ + U64(0x8412D999, 0x1ED58091), U64(0xE858790A, 0xFE9486C2), /* ~= 10^-267 */ + U64(0xA5178FFF, 0x668AE0B6), U64(0x626E974D, 0xBE39A872), /* ~= 10^-266 */ + U64(0xCE5D73FF, 0x402D98E3), U64(0xFB0A3D21, 0x2DC8128F), /* ~= 10^-265 */ + U64(0x80FA687F, 0x881C7F8E), U64(0x7CE66634, 0xBC9D0B99), /* ~= 10^-264 */ + U64(0xA139029F, 0x6A239F72), U64(0x1C1FFFC1, 0xEBC44E80), /* ~= 10^-263 */ + U64(0xC9874347, 0x44AC874E), U64(0xA327FFB2, 0x66B56220), /* ~= 10^-262 */ + U64(0xFBE91419, 0x15D7A922), U64(0x4BF1FF9F, 0x0062BAA8), /* ~= 10^-261 */ + U64(0x9D71AC8F, 0xADA6C9B5), U64(0x6F773FC3, 0x603DB4A9), /* ~= 10^-260 */ + U64(0xC4CE17B3, 0x99107C22), U64(0xCB550FB4, 0x384D21D3), /* ~= 10^-259 */ + U64(0xF6019DA0, 0x7F549B2B), U64(0x7E2A53A1, 0x46606A48), /* ~= 10^-258 */ + U64(0x99C10284, 0x4F94E0FB), U64(0x2EDA7444, 0xCBFC426D), /* ~= 10^-257 */ + U64(0xC0314325, 0x637A1939), U64(0xFA911155, 0xFEFB5308), /* ~= 10^-256 */ + U64(0xF03D93EE, 0xBC589F88), U64(0x793555AB, 0x7EBA27CA), /* ~= 10^-255 */ + U64(0x96267C75, 0x35B763B5), U64(0x4BC1558B, 0x2F3458DE), /* ~= 10^-254 */ + U64(0xBBB01B92, 0x83253CA2), U64(0x9EB1AAED, 0xFB016F16), /* ~= 10^-253 */ + U64(0xEA9C2277, 0x23EE8BCB), U64(0x465E15A9, 0x79C1CADC), /* ~= 10^-252 */ + U64(0x92A1958A, 0x7675175F), U64(0x0BFACD89, 0xEC191EC9), /* ~= 10^-251 */ + U64(0xB749FAED, 0x14125D36), U64(0xCEF980EC, 0x671F667B), /* ~= 10^-250 */ + U64(0xE51C79A8, 0x5916F484), U64(0x82B7E127, 0x80E7401A), /* ~= 10^-249 */ + U64(0x8F31CC09, 0x37AE58D2), U64(0xD1B2ECB8, 0xB0908810), /* ~= 10^-248 */ + U64(0xB2FE3F0B, 0x8599EF07), U64(0x861FA7E6, 0xDCB4AA15), /* ~= 10^-247 */ + U64(0xDFBDCECE, 0x67006AC9), U64(0x67A791E0, 0x93E1D49A), /* ~= 10^-246 */ + U64(0x8BD6A141, 0x006042BD), U64(0xE0C8BB2C, 0x5C6D24E0), /* ~= 10^-245 */ + U64(0xAECC4991, 0x4078536D), U64(0x58FAE9F7, 0x73886E18), /* ~= 10^-244 */ + U64(0xDA7F5BF5, 0x90966848), U64(0xAF39A475, 0x506A899E), /* ~= 10^-243 */ + U64(0x888F9979, 0x7A5E012D), U64(0x6D8406C9, 0x52429603), /* ~= 10^-242 */ + U64(0xAAB37FD7, 0xD8F58178), U64(0xC8E5087B, 0xA6D33B83), /* ~= 10^-241 */ + U64(0xD5605FCD, 0xCF32E1D6), U64(0xFB1E4A9A, 0x90880A64), /* ~= 10^-240 */ + U64(0x855C3BE0, 0xA17FCD26), U64(0x5CF2EEA0, 0x9A55067F), /* ~= 10^-239 */ + U64(0xA6B34AD8, 0xC9DFC06F), U64(0xF42FAA48, 0xC0EA481E), /* ~= 10^-238 */ + U64(0xD0601D8E, 0xFC57B08B), U64(0xF13B94DA, 0xF124DA26), /* ~= 10^-237 */ + U64(0x823C1279, 0x5DB6CE57), U64(0x76C53D08, 0xD6B70858), /* ~= 10^-236 */ + U64(0xA2CB1717, 0xB52481ED), U64(0x54768C4B, 0x0C64CA6E), /* ~= 10^-235 */ + U64(0xCB7DDCDD, 0xA26DA268), U64(0xA9942F5D, 0xCF7DFD09), /* ~= 10^-234 */ + U64(0xFE5D5415, 0x0B090B02), U64(0xD3F93B35, 0x435D7C4C), /* ~= 10^-233 */ + U64(0x9EFA548D, 0x26E5A6E1), U64(0xC47BC501, 0x4A1A6DAF), /* ~= 10^-232 */ + U64(0xC6B8E9B0, 0x709F109A), U64(0x359AB641, 0x9CA1091B), /* ~= 10^-231 */ + U64(0xF867241C, 0x8CC6D4C0), U64(0xC30163D2, 0x03C94B62), /* ~= 10^-230 */ + U64(0x9B407691, 0xD7FC44F8), U64(0x79E0DE63, 0x425DCF1D), /* ~= 10^-229 */ + U64(0xC2109436, 0x4DFB5636), U64(0x985915FC, 0x12F542E4), /* ~= 10^-228 */ + U64(0xF294B943, 0xE17A2BC4), U64(0x3E6F5B7B, 0x17B2939D), /* ~= 10^-227 */ + U64(0x979CF3CA, 0x6CEC5B5A), U64(0xA705992C, 0xEECF9C42), /* ~= 10^-226 */ + U64(0xBD8430BD, 0x08277231), U64(0x50C6FF78, 0x2A838353), /* ~= 10^-225 */ + U64(0xECE53CEC, 0x4A314EBD), U64(0xA4F8BF56, 0x35246428), /* ~= 10^-224 */ + U64(0x940F4613, 0xAE5ED136), U64(0x871B7795, 0xE136BE99), /* ~= 10^-223 */ + U64(0xB9131798, 0x99F68584), U64(0x28E2557B, 0x59846E3F), /* ~= 10^-222 */ + U64(0xE757DD7E, 0xC07426E5), U64(0x331AEADA, 0x2FE589CF), /* ~= 10^-221 */ + U64(0x9096EA6F, 0x3848984F), U64(0x3FF0D2C8, 0x5DEF7621), /* ~= 10^-220 */ + U64(0xB4BCA50B, 0x065ABE63), U64(0x0FED077A, 0x756B53A9), /* ~= 10^-219 */ + U64(0xE1EBCE4D, 0xC7F16DFB), U64(0xD3E84959, 0x12C62894), /* ~= 10^-218 */ + U64(0x8D3360F0, 0x9CF6E4BD), U64(0x64712DD7, 0xABBBD95C), /* ~= 10^-217 */ + U64(0xB080392C, 0xC4349DEC), U64(0xBD8D794D, 0x96AACFB3), /* ~= 10^-216 */ + U64(0xDCA04777, 0xF541C567), U64(0xECF0D7A0, 0xFC5583A0), /* ~= 10^-215 */ + U64(0x89E42CAA, 0xF9491B60), U64(0xF41686C4, 0x9DB57244), /* ~= 10^-214 */ + U64(0xAC5D37D5, 0xB79B6239), U64(0x311C2875, 0xC522CED5), /* ~= 10^-213 */ + U64(0xD77485CB, 0x25823AC7), U64(0x7D633293, 0x366B828B), /* ~= 10^-212 */ + U64(0x86A8D39E, 0xF77164BC), U64(0xAE5DFF9C, 0x02033197), /* ~= 10^-211 */ + U64(0xA8530886, 0xB54DBDEB), U64(0xD9F57F83, 0x0283FDFC), /* ~= 10^-210 */ + U64(0xD267CAA8, 0x62A12D66), U64(0xD072DF63, 0xC324FD7B), /* ~= 10^-209 */ + U64(0x8380DEA9, 0x3DA4BC60), U64(0x4247CB9E, 0x59F71E6D), /* ~= 10^-208 */ + U64(0xA4611653, 0x8D0DEB78), U64(0x52D9BE85, 0xF074E608), /* ~= 10^-207 */ + U64(0xCD795BE8, 0x70516656), U64(0x67902E27, 0x6C921F8B), /* ~= 10^-206 */ + U64(0x806BD971, 0x4632DFF6), U64(0x00BA1CD8, 0xA3DB53B6), /* ~= 10^-205 */ + U64(0xA086CFCD, 0x97BF97F3), U64(0x80E8A40E, 0xCCD228A4), /* ~= 10^-204 */ + U64(0xC8A883C0, 0xFDAF7DF0), U64(0x6122CD12, 0x8006B2CD), /* ~= 10^-203 */ + U64(0xFAD2A4B1, 0x3D1B5D6C), U64(0x796B8057, 0x20085F81), /* ~= 10^-202 */ + U64(0x9CC3A6EE, 0xC6311A63), U64(0xCBE33036, 0x74053BB0), /* ~= 10^-201 */ + U64(0xC3F490AA, 0x77BD60FC), U64(0xBEDBFC44, 0x11068A9C), /* ~= 10^-200 */ + U64(0xF4F1B4D5, 0x15ACB93B), U64(0xEE92FB55, 0x15482D44), /* ~= 10^-199 */ + U64(0x99171105, 0x2D8BF3C5), U64(0x751BDD15, 0x2D4D1C4A), /* ~= 10^-198 */ + U64(0xBF5CD546, 0x78EEF0B6), U64(0xD262D45A, 0x78A0635D), /* ~= 10^-197 */ + U64(0xEF340A98, 0x172AACE4), U64(0x86FB8971, 0x16C87C34), /* ~= 10^-196 */ + U64(0x9580869F, 0x0E7AAC0E), U64(0xD45D35E6, 0xAE3D4DA0), /* ~= 10^-195 */ + U64(0xBAE0A846, 0xD2195712), U64(0x89748360, 0x59CCA109), /* ~= 10^-194 */ + U64(0xE998D258, 0x869FACD7), U64(0x2BD1A438, 0x703FC94B), /* ~= 10^-193 */ + U64(0x91FF8377, 0x5423CC06), U64(0x7B6306A3, 0x4627DDCF), /* ~= 10^-192 */ + U64(0xB67F6455, 0x292CBF08), U64(0x1A3BC84C, 0x17B1D542), /* ~= 10^-191 */ + U64(0xE41F3D6A, 0x7377EECA), U64(0x20CABA5F, 0x1D9E4A93), /* ~= 10^-190 */ + U64(0x8E938662, 0x882AF53E), U64(0x547EB47B, 0x7282EE9C), /* ~= 10^-189 */ + U64(0xB23867FB, 0x2A35B28D), U64(0xE99E619A, 0x4F23AA43), /* ~= 10^-188 */ + U64(0xDEC681F9, 0xF4C31F31), U64(0x6405FA00, 0xE2EC94D4), /* ~= 10^-187 */ + U64(0x8B3C113C, 0x38F9F37E), U64(0xDE83BC40, 0x8DD3DD04), /* ~= 10^-186 */ + U64(0xAE0B158B, 0x4738705E), U64(0x9624AB50, 0xB148D445), /* ~= 10^-185 */ + U64(0xD98DDAEE, 0x19068C76), U64(0x3BADD624, 0xDD9B0957), /* ~= 10^-184 */ + U64(0x87F8A8D4, 0xCFA417C9), U64(0xE54CA5D7, 0x0A80E5D6), /* ~= 10^-183 */ + U64(0xA9F6D30A, 0x038D1DBC), U64(0x5E9FCF4C, 0xCD211F4C), /* ~= 10^-182 */ + U64(0xD47487CC, 0x8470652B), U64(0x7647C320, 0x0069671F), /* ~= 10^-181 */ + U64(0x84C8D4DF, 0xD2C63F3B), U64(0x29ECD9F4, 0x0041E073), /* ~= 10^-180 */ + U64(0xA5FB0A17, 0xC777CF09), U64(0xF4681071, 0x00525890), /* ~= 10^-179 */ + U64(0xCF79CC9D, 0xB955C2CC), U64(0x7182148D, 0x4066EEB4), /* ~= 10^-178 */ + U64(0x81AC1FE2, 0x93D599BF), U64(0xC6F14CD8, 0x48405530), /* ~= 10^-177 */ + U64(0xA21727DB, 0x38CB002F), U64(0xB8ADA00E, 0x5A506A7C), /* ~= 10^-176 */ + U64(0xCA9CF1D2, 0x06FDC03B), U64(0xA6D90811, 0xF0E4851C), /* ~= 10^-175 */ + U64(0xFD442E46, 0x88BD304A), U64(0x908F4A16, 0x6D1DA663), /* ~= 10^-174 */ + U64(0x9E4A9CEC, 0x15763E2E), U64(0x9A598E4E, 0x043287FE), /* ~= 10^-173 */ + U64(0xC5DD4427, 0x1AD3CDBA), U64(0x40EFF1E1, 0x853F29FD), /* ~= 10^-172 */ + U64(0xF7549530, 0xE188C128), U64(0xD12BEE59, 0xE68EF47C), /* ~= 10^-171 */ + U64(0x9A94DD3E, 0x8CF578B9), U64(0x82BB74F8, 0x301958CE), /* ~= 10^-170 */ + U64(0xC13A148E, 0x3032D6E7), U64(0xE36A5236, 0x3C1FAF01), /* ~= 10^-169 */ + U64(0xF18899B1, 0xBC3F8CA1), U64(0xDC44E6C3, 0xCB279AC1), /* ~= 10^-168 */ + U64(0x96F5600F, 0x15A7B7E5), U64(0x29AB103A, 0x5EF8C0B9), /* ~= 10^-167 */ + U64(0xBCB2B812, 0xDB11A5DE), U64(0x7415D448, 0xF6B6F0E7), /* ~= 10^-166 */ + U64(0xEBDF6617, 0x91D60F56), U64(0x111B495B, 0x3464AD21), /* ~= 10^-165 */ + U64(0x936B9FCE, 0xBB25C995), U64(0xCAB10DD9, 0x00BEEC34), /* ~= 10^-164 */ + U64(0xB84687C2, 0x69EF3BFB), U64(0x3D5D514F, 0x40EEA742), /* ~= 10^-163 */ + U64(0xE65829B3, 0x046B0AFA), U64(0x0CB4A5A3, 0x112A5112), /* ~= 10^-162 */ + U64(0x8FF71A0F, 0xE2C2E6DC), U64(0x47F0E785, 0xEABA72AB), /* ~= 10^-161 */ + U64(0xB3F4E093, 0xDB73A093), U64(0x59ED2167, 0x65690F56), /* ~= 10^-160 */ + U64(0xE0F218B8, 0xD25088B8), U64(0x306869C1, 0x3EC3532C), /* ~= 10^-159 */ + U64(0x8C974F73, 0x83725573), U64(0x1E414218, 0xC73A13FB), /* ~= 10^-158 */ + U64(0xAFBD2350, 0x644EEACF), U64(0xE5D1929E, 0xF90898FA), /* ~= 10^-157 */ + U64(0xDBAC6C24, 0x7D62A583), U64(0xDF45F746, 0xB74ABF39), /* ~= 10^-156 */ + U64(0x894BC396, 0xCE5DA772), U64(0x6B8BBA8C, 0x328EB783), /* ~= 10^-155 */ + U64(0xAB9EB47C, 0x81F5114F), U64(0x066EA92F, 0x3F326564), /* ~= 10^-154 */ + U64(0xD686619B, 0xA27255A2), U64(0xC80A537B, 0x0EFEFEBD), /* ~= 10^-153 */ + U64(0x8613FD01, 0x45877585), U64(0xBD06742C, 0xE95F5F36), /* ~= 10^-152 */ + U64(0xA798FC41, 0x96E952E7), U64(0x2C481138, 0x23B73704), /* ~= 10^-151 */ + U64(0xD17F3B51, 0xFCA3A7A0), U64(0xF75A1586, 0x2CA504C5), /* ~= 10^-150 */ + U64(0x82EF8513, 0x3DE648C4), U64(0x9A984D73, 0xDBE722FB), /* ~= 10^-149 */ + U64(0xA3AB6658, 0x0D5FDAF5), U64(0xC13E60D0, 0xD2E0EBBA), /* ~= 10^-148 */ + U64(0xCC963FEE, 0x10B7D1B3), U64(0x318DF905, 0x079926A8), /* ~= 10^-147 */ + U64(0xFFBBCFE9, 0x94E5C61F), U64(0xFDF17746, 0x497F7052), /* ~= 10^-146 */ + U64(0x9FD561F1, 0xFD0F9BD3), U64(0xFEB6EA8B, 0xEDEFA633), /* ~= 10^-145 */ + U64(0xC7CABA6E, 0x7C5382C8), U64(0xFE64A52E, 0xE96B8FC0), /* ~= 10^-144 */ + U64(0xF9BD690A, 0x1B68637B), U64(0x3DFDCE7A, 0xA3C673B0), /* ~= 10^-143 */ + U64(0x9C1661A6, 0x51213E2D), U64(0x06BEA10C, 0xA65C084E), /* ~= 10^-142 */ + U64(0xC31BFA0F, 0xE5698DB8), U64(0x486E494F, 0xCFF30A62), /* ~= 10^-141 */ + U64(0xF3E2F893, 0xDEC3F126), U64(0x5A89DBA3, 0xC3EFCCFA), /* ~= 10^-140 */ + U64(0x986DDB5C, 0x6B3A76B7), U64(0xF8962946, 0x5A75E01C), /* ~= 10^-139 */ + U64(0xBE895233, 0x86091465), U64(0xF6BBB397, 0xF1135823), /* ~= 10^-138 */ + U64(0xEE2BA6C0, 0x678B597F), U64(0x746AA07D, 0xED582E2C), /* ~= 10^-137 */ + U64(0x94DB4838, 0x40B717EF), U64(0xA8C2A44E, 0xB4571CDC), /* ~= 10^-136 */ + U64(0xBA121A46, 0x50E4DDEB), U64(0x92F34D62, 0x616CE413), /* ~= 10^-135 */ + U64(0xE896A0D7, 0xE51E1566), U64(0x77B020BA, 0xF9C81D17), /* ~= 10^-134 */ + U64(0x915E2486, 0xEF32CD60), U64(0x0ACE1474, 0xDC1D122E), /* ~= 10^-133 */ + U64(0xB5B5ADA8, 0xAAFF80B8), U64(0x0D819992, 0x132456BA), /* ~= 10^-132 */ + U64(0xE3231912, 0xD5BF60E6), U64(0x10E1FFF6, 0x97ED6C69), /* ~= 10^-131 */ + U64(0x8DF5EFAB, 0xC5979C8F), U64(0xCA8D3FFA, 0x1EF463C1), /* ~= 10^-130 */ + U64(0xB1736B96, 0xB6FD83B3), U64(0xBD308FF8, 0xA6B17CB2), /* ~= 10^-129 */ + U64(0xDDD0467C, 0x64BCE4A0), U64(0xAC7CB3F6, 0xD05DDBDE), /* ~= 10^-128 */ + U64(0x8AA22C0D, 0xBEF60EE4), U64(0x6BCDF07A, 0x423AA96B), /* ~= 10^-127 */ + U64(0xAD4AB711, 0x2EB3929D), U64(0x86C16C98, 0xD2C953C6), /* ~= 10^-126 */ + U64(0xD89D64D5, 0x7A607744), U64(0xE871C7BF, 0x077BA8B7), /* ~= 10^-125 */ + U64(0x87625F05, 0x6C7C4A8B), U64(0x11471CD7, 0x64AD4972), /* ~= 10^-124 */ + U64(0xA93AF6C6, 0xC79B5D2D), U64(0xD598E40D, 0x3DD89BCF), /* ~= 10^-123 */ + U64(0xD389B478, 0x79823479), U64(0x4AFF1D10, 0x8D4EC2C3), /* ~= 10^-122 */ + U64(0x843610CB, 0x4BF160CB), U64(0xCEDF722A, 0x585139BA), /* ~= 10^-121 */ + U64(0xA54394FE, 0x1EEDB8FE), U64(0xC2974EB4, 0xEE658828), /* ~= 10^-120 */ + U64(0xCE947A3D, 0xA6A9273E), U64(0x733D2262, 0x29FEEA32), /* ~= 10^-119 */ + U64(0x811CCC66, 0x8829B887), U64(0x0806357D, 0x5A3F525F), /* ~= 10^-118 */ + U64(0xA163FF80, 0x2A3426A8), U64(0xCA07C2DC, 0xB0CF26F7), /* ~= 10^-117 */ + U64(0xC9BCFF60, 0x34C13052), U64(0xFC89B393, 0xDD02F0B5), /* ~= 10^-116 */ + U64(0xFC2C3F38, 0x41F17C67), U64(0xBBAC2078, 0xD443ACE2), /* ~= 10^-115 */ + U64(0x9D9BA783, 0x2936EDC0), U64(0xD54B944B, 0x84AA4C0D), /* ~= 10^-114 */ + U64(0xC5029163, 0xF384A931), U64(0x0A9E795E, 0x65D4DF11), /* ~= 10^-113 */ + U64(0xF64335BC, 0xF065D37D), U64(0x4D4617B5, 0xFF4A16D5), /* ~= 10^-112 */ + U64(0x99EA0196, 0x163FA42E), U64(0x504BCED1, 0xBF8E4E45), /* ~= 10^-111 */ + U64(0xC06481FB, 0x9BCF8D39), U64(0xE45EC286, 0x2F71E1D6), /* ~= 10^-110 */ + U64(0xF07DA27A, 0x82C37088), U64(0x5D767327, 0xBB4E5A4C), /* ~= 10^-109 */ + U64(0x964E858C, 0x91BA2655), U64(0x3A6A07F8, 0xD510F86F), /* ~= 10^-108 */ + U64(0xBBE226EF, 0xB628AFEA), U64(0x890489F7, 0x0A55368B), /* ~= 10^-107 */ + U64(0xEADAB0AB, 0xA3B2DBE5), U64(0x2B45AC74, 0xCCEA842E), /* ~= 10^-106 */ + U64(0x92C8AE6B, 0x464FC96F), U64(0x3B0B8BC9, 0x0012929D), /* ~= 10^-105 */ + U64(0xB77ADA06, 0x17E3BBCB), U64(0x09CE6EBB, 0x40173744), /* ~= 10^-104 */ + U64(0xE5599087, 0x9DDCAABD), U64(0xCC420A6A, 0x101D0515), /* ~= 10^-103 */ + U64(0x8F57FA54, 0xC2A9EAB6), U64(0x9FA94682, 0x4A12232D), /* ~= 10^-102 */ + U64(0xB32DF8E9, 0xF3546564), U64(0x47939822, 0xDC96ABF9), /* ~= 10^-101 */ + U64(0xDFF97724, 0x70297EBD), U64(0x59787E2B, 0x93BC56F7), /* ~= 10^-100 */ + U64(0x8BFBEA76, 0xC619EF36), U64(0x57EB4EDB, 0x3C55B65A), /* ~= 10^-99 */ + U64(0xAEFAE514, 0x77A06B03), U64(0xEDE62292, 0x0B6B23F1), /* ~= 10^-98 */ + U64(0xDAB99E59, 0x958885C4), U64(0xE95FAB36, 0x8E45ECED), /* ~= 10^-97 */ + U64(0x88B402F7, 0xFD75539B), U64(0x11DBCB02, 0x18EBB414), /* ~= 10^-96 */ + U64(0xAAE103B5, 0xFCD2A881), U64(0xD652BDC2, 0x9F26A119), /* ~= 10^-95 */ + U64(0xD59944A3, 0x7C0752A2), U64(0x4BE76D33, 0x46F0495F), /* ~= 10^-94 */ + U64(0x857FCAE6, 0x2D8493A5), U64(0x6F70A440, 0x0C562DDB), /* ~= 10^-93 */ + U64(0xA6DFBD9F, 0xB8E5B88E), U64(0xCB4CCD50, 0x0F6BB952), /* ~= 10^-92 */ + U64(0xD097AD07, 0xA71F26B2), U64(0x7E2000A4, 0x1346A7A7), /* ~= 10^-91 */ + U64(0x825ECC24, 0xC873782F), U64(0x8ED40066, 0x8C0C28C8), /* ~= 10^-90 */ + U64(0xA2F67F2D, 0xFA90563B), U64(0x72890080, 0x2F0F32FA), /* ~= 10^-89 */ + U64(0xCBB41EF9, 0x79346BCA), U64(0x4F2B40A0, 0x3AD2FFB9), /* ~= 10^-88 */ + U64(0xFEA126B7, 0xD78186BC), U64(0xE2F610C8, 0x4987BFA8), /* ~= 10^-87 */ + U64(0x9F24B832, 0xE6B0F436), U64(0x0DD9CA7D, 0x2DF4D7C9), /* ~= 10^-86 */ + U64(0xC6EDE63F, 0xA05D3143), U64(0x91503D1C, 0x79720DBB), /* ~= 10^-85 */ + U64(0xF8A95FCF, 0x88747D94), U64(0x75A44C63, 0x97CE912A), /* ~= 10^-84 */ + U64(0x9B69DBE1, 0xB548CE7C), U64(0xC986AFBE, 0x3EE11ABA), /* ~= 10^-83 */ + U64(0xC24452DA, 0x229B021B), U64(0xFBE85BAD, 0xCE996168), /* ~= 10^-82 */ + U64(0xF2D56790, 0xAB41C2A2), U64(0xFAE27299, 0x423FB9C3), /* ~= 10^-81 */ + U64(0x97C560BA, 0x6B0919A5), U64(0xDCCD879F, 0xC967D41A), /* ~= 10^-80 */ + U64(0xBDB6B8E9, 0x05CB600F), U64(0x5400E987, 0xBBC1C920), /* ~= 10^-79 */ + U64(0xED246723, 0x473E3813), U64(0x290123E9, 0xAAB23B68), /* ~= 10^-78 */ + U64(0x9436C076, 0x0C86E30B), U64(0xF9A0B672, 0x0AAF6521), /* ~= 10^-77 */ + U64(0xB9447093, 0x8FA89BCE), U64(0xF808E40E, 0x8D5B3E69), /* ~= 10^-76 */ + U64(0xE7958CB8, 0x7392C2C2), U64(0xB60B1D12, 0x30B20E04), /* ~= 10^-75 */ + U64(0x90BD77F3, 0x483BB9B9), U64(0xB1C6F22B, 0x5E6F48C2), /* ~= 10^-74 */ + U64(0xB4ECD5F0, 0x1A4AA828), U64(0x1E38AEB6, 0x360B1AF3), /* ~= 10^-73 */ + U64(0xE2280B6C, 0x20DD5232), U64(0x25C6DA63, 0xC38DE1B0), /* ~= 10^-72 */ + U64(0x8D590723, 0x948A535F), U64(0x579C487E, 0x5A38AD0E), /* ~= 10^-71 */ + U64(0xB0AF48EC, 0x79ACE837), U64(0x2D835A9D, 0xF0C6D851), /* ~= 10^-70 */ + U64(0xDCDB1B27, 0x98182244), U64(0xF8E43145, 0x6CF88E65), /* ~= 10^-69 */ + U64(0x8A08F0F8, 0xBF0F156B), U64(0x1B8E9ECB, 0x641B58FF), /* ~= 10^-68 */ + U64(0xAC8B2D36, 0xEED2DAC5), U64(0xE272467E, 0x3D222F3F), /* ~= 10^-67 */ + U64(0xD7ADF884, 0xAA879177), U64(0x5B0ED81D, 0xCC6ABB0F), /* ~= 10^-66 */ + U64(0x86CCBB52, 0xEA94BAEA), U64(0x98E94712, 0x9FC2B4E9), /* ~= 10^-65 */ + U64(0xA87FEA27, 0xA539E9A5), U64(0x3F2398D7, 0x47B36224), /* ~= 10^-64 */ + U64(0xD29FE4B1, 0x8E88640E), U64(0x8EEC7F0D, 0x19A03AAD), /* ~= 10^-63 */ + U64(0x83A3EEEE, 0xF9153E89), U64(0x1953CF68, 0x300424AC), /* ~= 10^-62 */ + U64(0xA48CEAAA, 0xB75A8E2B), U64(0x5FA8C342, 0x3C052DD7), /* ~= 10^-61 */ + U64(0xCDB02555, 0x653131B6), U64(0x3792F412, 0xCB06794D), /* ~= 10^-60 */ + U64(0x808E1755, 0x5F3EBF11), U64(0xE2BBD88B, 0xBEE40BD0), /* ~= 10^-59 */ + U64(0xA0B19D2A, 0xB70E6ED6), U64(0x5B6ACEAE, 0xAE9D0EC4), /* ~= 10^-58 */ + U64(0xC8DE0475, 0x64D20A8B), U64(0xF245825A, 0x5A445275), /* ~= 10^-57 */ + U64(0xFB158592, 0xBE068D2E), U64(0xEED6E2F0, 0xF0D56712), /* ~= 10^-56 */ + U64(0x9CED737B, 0xB6C4183D), U64(0x55464DD6, 0x9685606B), /* ~= 10^-55 */ + U64(0xC428D05A, 0xA4751E4C), U64(0xAA97E14C, 0x3C26B886), /* ~= 10^-54 */ + U64(0xF5330471, 0x4D9265DF), U64(0xD53DD99F, 0x4B3066A8), /* ~= 10^-53 */ + U64(0x993FE2C6, 0xD07B7FAB), U64(0xE546A803, 0x8EFE4029), /* ~= 10^-52 */ + U64(0xBF8FDB78, 0x849A5F96), U64(0xDE985204, 0x72BDD033), /* ~= 10^-51 */ + U64(0xEF73D256, 0xA5C0F77C), U64(0x963E6685, 0x8F6D4440), /* ~= 10^-50 */ + U64(0x95A86376, 0x27989AAD), U64(0xDDE70013, 0x79A44AA8), /* ~= 10^-49 */ + U64(0xBB127C53, 0xB17EC159), U64(0x5560C018, 0x580D5D52), /* ~= 10^-48 */ + U64(0xE9D71B68, 0x9DDE71AF), U64(0xAAB8F01E, 0x6E10B4A6), /* ~= 10^-47 */ + U64(0x92267121, 0x62AB070D), U64(0xCAB39613, 0x04CA70E8), /* ~= 10^-46 */ + U64(0xB6B00D69, 0xBB55C8D1), U64(0x3D607B97, 0xC5FD0D22), /* ~= 10^-45 */ + U64(0xE45C10C4, 0x2A2B3B05), U64(0x8CB89A7D, 0xB77C506A), /* ~= 10^-44 */ + U64(0x8EB98A7A, 0x9A5B04E3), U64(0x77F3608E, 0x92ADB242), /* ~= 10^-43 */ + U64(0xB267ED19, 0x40F1C61C), U64(0x55F038B2, 0x37591ED3), /* ~= 10^-42 */ + U64(0xDF01E85F, 0x912E37A3), U64(0x6B6C46DE, 0xC52F6688), /* ~= 10^-41 */ + U64(0x8B61313B, 0xBABCE2C6), U64(0x2323AC4B, 0x3B3DA015), /* ~= 10^-40 */ + U64(0xAE397D8A, 0xA96C1B77), U64(0xABEC975E, 0x0A0D081A), /* ~= 10^-39 */ + U64(0xD9C7DCED, 0x53C72255), U64(0x96E7BD35, 0x8C904A21), /* ~= 10^-38 */ + U64(0x881CEA14, 0x545C7575), U64(0x7E50D641, 0x77DA2E54), /* ~= 10^-37 */ + U64(0xAA242499, 0x697392D2), U64(0xDDE50BD1, 0xD5D0B9E9), /* ~= 10^-36 */ + U64(0xD4AD2DBF, 0xC3D07787), U64(0x955E4EC6, 0x4B44E864), /* ~= 10^-35 */ + U64(0x84EC3C97, 0xDA624AB4), U64(0xBD5AF13B, 0xEF0B113E), /* ~= 10^-34 */ + U64(0xA6274BBD, 0xD0FADD61), U64(0xECB1AD8A, 0xEACDD58E), /* ~= 10^-33 */ + U64(0xCFB11EAD, 0x453994BA), U64(0x67DE18ED, 0xA5814AF2), /* ~= 10^-32 */ + U64(0x81CEB32C, 0x4B43FCF4), U64(0x80EACF94, 0x8770CED7), /* ~= 10^-31 */ + U64(0xA2425FF7, 0x5E14FC31), U64(0xA1258379, 0xA94D028D), /* ~= 10^-30 */ + U64(0xCAD2F7F5, 0x359A3B3E), U64(0x096EE458, 0x13A04330), /* ~= 10^-29 */ + U64(0xFD87B5F2, 0x8300CA0D), U64(0x8BCA9D6E, 0x188853FC), /* ~= 10^-28 */ + U64(0x9E74D1B7, 0x91E07E48), U64(0x775EA264, 0xCF55347D), /* ~= 10^-27 */ + U64(0xC6120625, 0x76589DDA), U64(0x95364AFE, 0x032A819D), /* ~= 10^-26 */ + U64(0xF79687AE, 0xD3EEC551), U64(0x3A83DDBD, 0x83F52204), /* ~= 10^-25 */ + U64(0x9ABE14CD, 0x44753B52), U64(0xC4926A96, 0x72793542), /* ~= 10^-24 */ + U64(0xC16D9A00, 0x95928A27), U64(0x75B7053C, 0x0F178293), /* ~= 10^-23 */ + U64(0xF1C90080, 0xBAF72CB1), U64(0x5324C68B, 0x12DD6338), /* ~= 10^-22 */ + U64(0x971DA050, 0x74DA7BEE), U64(0xD3F6FC16, 0xEBCA5E03), /* ~= 10^-21 */ + U64(0xBCE50864, 0x92111AEA), U64(0x88F4BB1C, 0xA6BCF584), /* ~= 10^-20 */ + U64(0xEC1E4A7D, 0xB69561A5), U64(0x2B31E9E3, 0xD06C32E5), /* ~= 10^-19 */ + U64(0x9392EE8E, 0x921D5D07), U64(0x3AFF322E, 0x62439FCF), /* ~= 10^-18 */ + U64(0xB877AA32, 0x36A4B449), U64(0x09BEFEB9, 0xFAD487C2), /* ~= 10^-17 */ + U64(0xE69594BE, 0xC44DE15B), U64(0x4C2EBE68, 0x7989A9B3), /* ~= 10^-16 */ + U64(0x901D7CF7, 0x3AB0ACD9), U64(0x0F9D3701, 0x4BF60A10), /* ~= 10^-15 */ + U64(0xB424DC35, 0x095CD80F), U64(0x538484C1, 0x9EF38C94), /* ~= 10^-14 */ + U64(0xE12E1342, 0x4BB40E13), U64(0x2865A5F2, 0x06B06FB9), /* ~= 10^-13 */ + U64(0x8CBCCC09, 0x6F5088CB), U64(0xF93F87B7, 0x442E45D3), /* ~= 10^-12 */ + U64(0xAFEBFF0B, 0xCB24AAFE), U64(0xF78F69A5, 0x1539D748), /* ~= 10^-11 */ + U64(0xDBE6FECE, 0xBDEDD5BE), U64(0xB573440E, 0x5A884D1B), /* ~= 10^-10 */ + U64(0x89705F41, 0x36B4A597), U64(0x31680A88, 0xF8953030), /* ~= 10^-9 */ + U64(0xABCC7711, 0x8461CEFC), U64(0xFDC20D2B, 0x36BA7C3D), /* ~= 10^-8 */ + U64(0xD6BF94D5, 0xE57A42BC), U64(0x3D329076, 0x04691B4C), /* ~= 10^-7 */ + U64(0x8637BD05, 0xAF6C69B5), U64(0xA63F9A49, 0xC2C1B10F), /* ~= 10^-6 */ + U64(0xA7C5AC47, 0x1B478423), U64(0x0FCF80DC, 0x33721D53), /* ~= 10^-5 */ + U64(0xD1B71758, 0xE219652B), U64(0xD3C36113, 0x404EA4A8), /* ~= 10^-4 */ + U64(0x83126E97, 0x8D4FDF3B), U64(0x645A1CAC, 0x083126E9), /* ~= 10^-3 */ + U64(0xA3D70A3D, 0x70A3D70A), U64(0x3D70A3D7, 0x0A3D70A3), /* ~= 10^-2 */ + U64(0xCCCCCCCC, 0xCCCCCCCC), U64(0xCCCCCCCC, 0xCCCCCCCC), /* ~= 10^-1 */ + U64(0x80000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^0 */ + U64(0xA0000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^1 */ + U64(0xC8000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^2 */ + U64(0xFA000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^3 */ + U64(0x9C400000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^4 */ + U64(0xC3500000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^5 */ + U64(0xF4240000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^6 */ + U64(0x98968000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^7 */ + U64(0xBEBC2000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^8 */ + U64(0xEE6B2800, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^9 */ + U64(0x9502F900, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^10 */ + U64(0xBA43B740, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^11 */ + U64(0xE8D4A510, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^12 */ + U64(0x9184E72A, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^13 */ + U64(0xB5E620F4, 0x80000000), U64(0x00000000, 0x00000000), /* == 10^14 */ + U64(0xE35FA931, 0xA0000000), U64(0x00000000, 0x00000000), /* == 10^15 */ + U64(0x8E1BC9BF, 0x04000000), U64(0x00000000, 0x00000000), /* == 10^16 */ + U64(0xB1A2BC2E, 0xC5000000), U64(0x00000000, 0x00000000), /* == 10^17 */ + U64(0xDE0B6B3A, 0x76400000), U64(0x00000000, 0x00000000), /* == 10^18 */ + U64(0x8AC72304, 0x89E80000), U64(0x00000000, 0x00000000), /* == 10^19 */ + U64(0xAD78EBC5, 0xAC620000), U64(0x00000000, 0x00000000), /* == 10^20 */ + U64(0xD8D726B7, 0x177A8000), U64(0x00000000, 0x00000000), /* == 10^21 */ + U64(0x87867832, 0x6EAC9000), U64(0x00000000, 0x00000000), /* == 10^22 */ + U64(0xA968163F, 0x0A57B400), U64(0x00000000, 0x00000000), /* == 10^23 */ + U64(0xD3C21BCE, 0xCCEDA100), U64(0x00000000, 0x00000000), /* == 10^24 */ + U64(0x84595161, 0x401484A0), U64(0x00000000, 0x00000000), /* == 10^25 */ + U64(0xA56FA5B9, 0x9019A5C8), U64(0x00000000, 0x00000000), /* == 10^26 */ + U64(0xCECB8F27, 0xF4200F3A), U64(0x00000000, 0x00000000), /* == 10^27 */ + U64(0x813F3978, 0xF8940984), U64(0x40000000, 0x00000000), /* == 10^28 */ + U64(0xA18F07D7, 0x36B90BE5), U64(0x50000000, 0x00000000), /* == 10^29 */ + U64(0xC9F2C9CD, 0x04674EDE), U64(0xA4000000, 0x00000000), /* == 10^30 */ + U64(0xFC6F7C40, 0x45812296), U64(0x4D000000, 0x00000000), /* == 10^31 */ + U64(0x9DC5ADA8, 0x2B70B59D), U64(0xF0200000, 0x00000000), /* == 10^32 */ + U64(0xC5371912, 0x364CE305), U64(0x6C280000, 0x00000000), /* == 10^33 */ + U64(0xF684DF56, 0xC3E01BC6), U64(0xC7320000, 0x00000000), /* == 10^34 */ + U64(0x9A130B96, 0x3A6C115C), U64(0x3C7F4000, 0x00000000), /* == 10^35 */ + U64(0xC097CE7B, 0xC90715B3), U64(0x4B9F1000, 0x00000000), /* == 10^36 */ + U64(0xF0BDC21A, 0xBB48DB20), U64(0x1E86D400, 0x00000000), /* == 10^37 */ + U64(0x96769950, 0xB50D88F4), U64(0x13144480, 0x00000000), /* == 10^38 */ + U64(0xBC143FA4, 0xE250EB31), U64(0x17D955A0, 0x00000000), /* == 10^39 */ + U64(0xEB194F8E, 0x1AE525FD), U64(0x5DCFAB08, 0x00000000), /* == 10^40 */ + U64(0x92EFD1B8, 0xD0CF37BE), U64(0x5AA1CAE5, 0x00000000), /* == 10^41 */ + U64(0xB7ABC627, 0x050305AD), U64(0xF14A3D9E, 0x40000000), /* == 10^42 */ + U64(0xE596B7B0, 0xC643C719), U64(0x6D9CCD05, 0xD0000000), /* == 10^43 */ + U64(0x8F7E32CE, 0x7BEA5C6F), U64(0xE4820023, 0xA2000000), /* == 10^44 */ + U64(0xB35DBF82, 0x1AE4F38B), U64(0xDDA2802C, 0x8A800000), /* == 10^45 */ + U64(0xE0352F62, 0xA19E306E), U64(0xD50B2037, 0xAD200000), /* == 10^46 */ + U64(0x8C213D9D, 0xA502DE45), U64(0x4526F422, 0xCC340000), /* == 10^47 */ + U64(0xAF298D05, 0x0E4395D6), U64(0x9670B12B, 0x7F410000), /* == 10^48 */ + U64(0xDAF3F046, 0x51D47B4C), U64(0x3C0CDD76, 0x5F114000), /* == 10^49 */ + U64(0x88D8762B, 0xF324CD0F), U64(0xA5880A69, 0xFB6AC800), /* == 10^50 */ + U64(0xAB0E93B6, 0xEFEE0053), U64(0x8EEA0D04, 0x7A457A00), /* == 10^51 */ + U64(0xD5D238A4, 0xABE98068), U64(0x72A49045, 0x98D6D880), /* == 10^52 */ + U64(0x85A36366, 0xEB71F041), U64(0x47A6DA2B, 0x7F864750), /* == 10^53 */ + U64(0xA70C3C40, 0xA64E6C51), U64(0x999090B6, 0x5F67D924), /* == 10^54 */ + U64(0xD0CF4B50, 0xCFE20765), U64(0xFFF4B4E3, 0xF741CF6D), /* == 10^55 */ + U64(0x82818F12, 0x81ED449F), U64(0xBFF8F10E, 0x7A8921A4), /* ~= 10^56 */ + U64(0xA321F2D7, 0x226895C7), U64(0xAFF72D52, 0x192B6A0D), /* ~= 10^57 */ + U64(0xCBEA6F8C, 0xEB02BB39), U64(0x9BF4F8A6, 0x9F764490), /* ~= 10^58 */ + U64(0xFEE50B70, 0x25C36A08), U64(0x02F236D0, 0x4753D5B4), /* ~= 10^59 */ + U64(0x9F4F2726, 0x179A2245), U64(0x01D76242, 0x2C946590), /* ~= 10^60 */ + U64(0xC722F0EF, 0x9D80AAD6), U64(0x424D3AD2, 0xB7B97EF5), /* ~= 10^61 */ + U64(0xF8EBAD2B, 0x84E0D58B), U64(0xD2E08987, 0x65A7DEB2), /* ~= 10^62 */ + U64(0x9B934C3B, 0x330C8577), U64(0x63CC55F4, 0x9F88EB2F), /* ~= 10^63 */ + U64(0xC2781F49, 0xFFCFA6D5), U64(0x3CBF6B71, 0xC76B25FB), /* ~= 10^64 */ + U64(0xF316271C, 0x7FC3908A), U64(0x8BEF464E, 0x3945EF7A), /* ~= 10^65 */ + U64(0x97EDD871, 0xCFDA3A56), U64(0x97758BF0, 0xE3CBB5AC), /* ~= 10^66 */ + U64(0xBDE94E8E, 0x43D0C8EC), U64(0x3D52EEED, 0x1CBEA317), /* ~= 10^67 */ + U64(0xED63A231, 0xD4C4FB27), U64(0x4CA7AAA8, 0x63EE4BDD), /* ~= 10^68 */ + U64(0x945E455F, 0x24FB1CF8), U64(0x8FE8CAA9, 0x3E74EF6A), /* ~= 10^69 */ + U64(0xB975D6B6, 0xEE39E436), U64(0xB3E2FD53, 0x8E122B44), /* ~= 10^70 */ + U64(0xE7D34C64, 0xA9C85D44), U64(0x60DBBCA8, 0x7196B616), /* ~= 10^71 */ + U64(0x90E40FBE, 0xEA1D3A4A), U64(0xBC8955E9, 0x46FE31CD), /* ~= 10^72 */ + U64(0xB51D13AE, 0xA4A488DD), U64(0x6BABAB63, 0x98BDBE41), /* ~= 10^73 */ + U64(0xE264589A, 0x4DCDAB14), U64(0xC696963C, 0x7EED2DD1), /* ~= 10^74 */ + U64(0x8D7EB760, 0x70A08AEC), U64(0xFC1E1DE5, 0xCF543CA2), /* ~= 10^75 */ + U64(0xB0DE6538, 0x8CC8ADA8), U64(0x3B25A55F, 0x43294BCB), /* ~= 10^76 */ + U64(0xDD15FE86, 0xAFFAD912), U64(0x49EF0EB7, 0x13F39EBE), /* ~= 10^77 */ + U64(0x8A2DBF14, 0x2DFCC7AB), U64(0x6E356932, 0x6C784337), /* ~= 10^78 */ + U64(0xACB92ED9, 0x397BF996), U64(0x49C2C37F, 0x07965404), /* ~= 10^79 */ + U64(0xD7E77A8F, 0x87DAF7FB), U64(0xDC33745E, 0xC97BE906), /* ~= 10^80 */ + U64(0x86F0AC99, 0xB4E8DAFD), U64(0x69A028BB, 0x3DED71A3), /* ~= 10^81 */ + U64(0xA8ACD7C0, 0x222311BC), U64(0xC40832EA, 0x0D68CE0C), /* ~= 10^82 */ + U64(0xD2D80DB0, 0x2AABD62B), U64(0xF50A3FA4, 0x90C30190), /* ~= 10^83 */ + U64(0x83C7088E, 0x1AAB65DB), U64(0x792667C6, 0xDA79E0FA), /* ~= 10^84 */ + U64(0xA4B8CAB1, 0xA1563F52), U64(0x577001B8, 0x91185938), /* ~= 10^85 */ + U64(0xCDE6FD5E, 0x09ABCF26), U64(0xED4C0226, 0xB55E6F86), /* ~= 10^86 */ + U64(0x80B05E5A, 0xC60B6178), U64(0x544F8158, 0x315B05B4), /* ~= 10^87 */ + U64(0xA0DC75F1, 0x778E39D6), U64(0x696361AE, 0x3DB1C721), /* ~= 10^88 */ + U64(0xC913936D, 0xD571C84C), U64(0x03BC3A19, 0xCD1E38E9), /* ~= 10^89 */ + U64(0xFB587849, 0x4ACE3A5F), U64(0x04AB48A0, 0x4065C723), /* ~= 10^90 */ + U64(0x9D174B2D, 0xCEC0E47B), U64(0x62EB0D64, 0x283F9C76), /* ~= 10^91 */ + U64(0xC45D1DF9, 0x42711D9A), U64(0x3BA5D0BD, 0x324F8394), /* ~= 10^92 */ + U64(0xF5746577, 0x930D6500), U64(0xCA8F44EC, 0x7EE36479), /* ~= 10^93 */ + U64(0x9968BF6A, 0xBBE85F20), U64(0x7E998B13, 0xCF4E1ECB), /* ~= 10^94 */ + U64(0xBFC2EF45, 0x6AE276E8), U64(0x9E3FEDD8, 0xC321A67E), /* ~= 10^95 */ + U64(0xEFB3AB16, 0xC59B14A2), U64(0xC5CFE94E, 0xF3EA101E), /* ~= 10^96 */ + U64(0x95D04AEE, 0x3B80ECE5), U64(0xBBA1F1D1, 0x58724A12), /* ~= 10^97 */ + U64(0xBB445DA9, 0xCA61281F), U64(0x2A8A6E45, 0xAE8EDC97), /* ~= 10^98 */ + U64(0xEA157514, 0x3CF97226), U64(0xF52D09D7, 0x1A3293BD), /* ~= 10^99 */ + U64(0x924D692C, 0xA61BE758), U64(0x593C2626, 0x705F9C56), /* ~= 10^100 */ + U64(0xB6E0C377, 0xCFA2E12E), U64(0x6F8B2FB0, 0x0C77836C), /* ~= 10^101 */ + U64(0xE498F455, 0xC38B997A), U64(0x0B6DFB9C, 0x0F956447), /* ~= 10^102 */ + U64(0x8EDF98B5, 0x9A373FEC), U64(0x4724BD41, 0x89BD5EAC), /* ~= 10^103 */ + U64(0xB2977EE3, 0x00C50FE7), U64(0x58EDEC91, 0xEC2CB657), /* ~= 10^104 */ + U64(0xDF3D5E9B, 0xC0F653E1), U64(0x2F2967B6, 0x6737E3ED), /* ~= 10^105 */ + U64(0x8B865B21, 0x5899F46C), U64(0xBD79E0D2, 0x0082EE74), /* ~= 10^106 */ + U64(0xAE67F1E9, 0xAEC07187), U64(0xECD85906, 0x80A3AA11), /* ~= 10^107 */ + U64(0xDA01EE64, 0x1A708DE9), U64(0xE80E6F48, 0x20CC9495), /* ~= 10^108 */ + U64(0x884134FE, 0x908658B2), U64(0x3109058D, 0x147FDCDD), /* ~= 10^109 */ + U64(0xAA51823E, 0x34A7EEDE), U64(0xBD4B46F0, 0x599FD415), /* ~= 10^110 */ + U64(0xD4E5E2CD, 0xC1D1EA96), U64(0x6C9E18AC, 0x7007C91A), /* ~= 10^111 */ + U64(0x850FADC0, 0x9923329E), U64(0x03E2CF6B, 0xC604DDB0), /* ~= 10^112 */ + U64(0xA6539930, 0xBF6BFF45), U64(0x84DB8346, 0xB786151C), /* ~= 10^113 */ + U64(0xCFE87F7C, 0xEF46FF16), U64(0xE6126418, 0x65679A63), /* ~= 10^114 */ + U64(0x81F14FAE, 0x158C5F6E), U64(0x4FCB7E8F, 0x3F60C07E), /* ~= 10^115 */ + U64(0xA26DA399, 0x9AEF7749), U64(0xE3BE5E33, 0x0F38F09D), /* ~= 10^116 */ + U64(0xCB090C80, 0x01AB551C), U64(0x5CADF5BF, 0xD3072CC5), /* ~= 10^117 */ + U64(0xFDCB4FA0, 0x02162A63), U64(0x73D9732F, 0xC7C8F7F6), /* ~= 10^118 */ + U64(0x9E9F11C4, 0x014DDA7E), U64(0x2867E7FD, 0xDCDD9AFA), /* ~= 10^119 */ + U64(0xC646D635, 0x01A1511D), U64(0xB281E1FD, 0x541501B8), /* ~= 10^120 */ + U64(0xF7D88BC2, 0x4209A565), U64(0x1F225A7C, 0xA91A4226), /* ~= 10^121 */ + U64(0x9AE75759, 0x6946075F), U64(0x3375788D, 0xE9B06958), /* ~= 10^122 */ + U64(0xC1A12D2F, 0xC3978937), U64(0x0052D6B1, 0x641C83AE), /* ~= 10^123 */ + U64(0xF209787B, 0xB47D6B84), U64(0xC0678C5D, 0xBD23A49A), /* ~= 10^124 */ + U64(0x9745EB4D, 0x50CE6332), U64(0xF840B7BA, 0x963646E0), /* ~= 10^125 */ + U64(0xBD176620, 0xA501FBFF), U64(0xB650E5A9, 0x3BC3D898), /* ~= 10^126 */ + U64(0xEC5D3FA8, 0xCE427AFF), U64(0xA3E51F13, 0x8AB4CEBE), /* ~= 10^127 */ + U64(0x93BA47C9, 0x80E98CDF), U64(0xC66F336C, 0x36B10137), /* ~= 10^128 */ + U64(0xB8A8D9BB, 0xE123F017), U64(0xB80B0047, 0x445D4184), /* ~= 10^129 */ + U64(0xE6D3102A, 0xD96CEC1D), U64(0xA60DC059, 0x157491E5), /* ~= 10^130 */ + U64(0x9043EA1A, 0xC7E41392), U64(0x87C89837, 0xAD68DB2F), /* ~= 10^131 */ + U64(0xB454E4A1, 0x79DD1877), U64(0x29BABE45, 0x98C311FB), /* ~= 10^132 */ + U64(0xE16A1DC9, 0xD8545E94), U64(0xF4296DD6, 0xFEF3D67A), /* ~= 10^133 */ + U64(0x8CE2529E, 0x2734BB1D), U64(0x1899E4A6, 0x5F58660C), /* ~= 10^134 */ + U64(0xB01AE745, 0xB101E9E4), U64(0x5EC05DCF, 0xF72E7F8F), /* ~= 10^135 */ + U64(0xDC21A117, 0x1D42645D), U64(0x76707543, 0xF4FA1F73), /* ~= 10^136 */ + U64(0x899504AE, 0x72497EBA), U64(0x6A06494A, 0x791C53A8), /* ~= 10^137 */ + U64(0xABFA45DA, 0x0EDBDE69), U64(0x0487DB9D, 0x17636892), /* ~= 10^138 */ + U64(0xD6F8D750, 0x9292D603), U64(0x45A9D284, 0x5D3C42B6), /* ~= 10^139 */ + U64(0x865B8692, 0x5B9BC5C2), U64(0x0B8A2392, 0xBA45A9B2), /* ~= 10^140 */ + U64(0xA7F26836, 0xF282B732), U64(0x8E6CAC77, 0x68D7141E), /* ~= 10^141 */ + U64(0xD1EF0244, 0xAF2364FF), U64(0x3207D795, 0x430CD926), /* ~= 10^142 */ + U64(0x8335616A, 0xED761F1F), U64(0x7F44E6BD, 0x49E807B8), /* ~= 10^143 */ + U64(0xA402B9C5, 0xA8D3A6E7), U64(0x5F16206C, 0x9C6209A6), /* ~= 10^144 */ + U64(0xCD036837, 0x130890A1), U64(0x36DBA887, 0xC37A8C0F), /* ~= 10^145 */ + U64(0x80222122, 0x6BE55A64), U64(0xC2494954, 0xDA2C9789), /* ~= 10^146 */ + U64(0xA02AA96B, 0x06DEB0FD), U64(0xF2DB9BAA, 0x10B7BD6C), /* ~= 10^147 */ + U64(0xC83553C5, 0xC8965D3D), U64(0x6F928294, 0x94E5ACC7), /* ~= 10^148 */ + U64(0xFA42A8B7, 0x3ABBF48C), U64(0xCB772339, 0xBA1F17F9), /* ~= 10^149 */ + U64(0x9C69A972, 0x84B578D7), U64(0xFF2A7604, 0x14536EFB), /* ~= 10^150 */ + U64(0xC38413CF, 0x25E2D70D), U64(0xFEF51385, 0x19684ABA), /* ~= 10^151 */ + U64(0xF46518C2, 0xEF5B8CD1), U64(0x7EB25866, 0x5FC25D69), /* ~= 10^152 */ + U64(0x98BF2F79, 0xD5993802), U64(0xEF2F773F, 0xFBD97A61), /* ~= 10^153 */ + U64(0xBEEEFB58, 0x4AFF8603), U64(0xAAFB550F, 0xFACFD8FA), /* ~= 10^154 */ + U64(0xEEAABA2E, 0x5DBF6784), U64(0x95BA2A53, 0xF983CF38), /* ~= 10^155 */ + U64(0x952AB45C, 0xFA97A0B2), U64(0xDD945A74, 0x7BF26183), /* ~= 10^156 */ + U64(0xBA756174, 0x393D88DF), U64(0x94F97111, 0x9AEEF9E4), /* ~= 10^157 */ + U64(0xE912B9D1, 0x478CEB17), U64(0x7A37CD56, 0x01AAB85D), /* ~= 10^158 */ + U64(0x91ABB422, 0xCCB812EE), U64(0xAC62E055, 0xC10AB33A), /* ~= 10^159 */ + U64(0xB616A12B, 0x7FE617AA), U64(0x577B986B, 0x314D6009), /* ~= 10^160 */ + U64(0xE39C4976, 0x5FDF9D94), U64(0xED5A7E85, 0xFDA0B80B), /* ~= 10^161 */ + U64(0x8E41ADE9, 0xFBEBC27D), U64(0x14588F13, 0xBE847307), /* ~= 10^162 */ + U64(0xB1D21964, 0x7AE6B31C), U64(0x596EB2D8, 0xAE258FC8), /* ~= 10^163 */ + U64(0xDE469FBD, 0x99A05FE3), U64(0x6FCA5F8E, 0xD9AEF3BB), /* ~= 10^164 */ + U64(0x8AEC23D6, 0x80043BEE), U64(0x25DE7BB9, 0x480D5854), /* ~= 10^165 */ + U64(0xADA72CCC, 0x20054AE9), U64(0xAF561AA7, 0x9A10AE6A), /* ~= 10^166 */ + U64(0xD910F7FF, 0x28069DA4), U64(0x1B2BA151, 0x8094DA04), /* ~= 10^167 */ + U64(0x87AA9AFF, 0x79042286), U64(0x90FB44D2, 0xF05D0842), /* ~= 10^168 */ + U64(0xA99541BF, 0x57452B28), U64(0x353A1607, 0xAC744A53), /* ~= 10^169 */ + U64(0xD3FA922F, 0x2D1675F2), U64(0x42889B89, 0x97915CE8), /* ~= 10^170 */ + U64(0x847C9B5D, 0x7C2E09B7), U64(0x69956135, 0xFEBADA11), /* ~= 10^171 */ + U64(0xA59BC234, 0xDB398C25), U64(0x43FAB983, 0x7E699095), /* ~= 10^172 */ + U64(0xCF02B2C2, 0x1207EF2E), U64(0x94F967E4, 0x5E03F4BB), /* ~= 10^173 */ + U64(0x8161AFB9, 0x4B44F57D), U64(0x1D1BE0EE, 0xBAC278F5), /* ~= 10^174 */ + U64(0xA1BA1BA7, 0x9E1632DC), U64(0x6462D92A, 0x69731732), /* ~= 10^175 */ + U64(0xCA28A291, 0x859BBF93), U64(0x7D7B8F75, 0x03CFDCFE), /* ~= 10^176 */ + U64(0xFCB2CB35, 0xE702AF78), U64(0x5CDA7352, 0x44C3D43E), /* ~= 10^177 */ + U64(0x9DEFBF01, 0xB061ADAB), U64(0x3A088813, 0x6AFA64A7), /* ~= 10^178 */ + U64(0xC56BAEC2, 0x1C7A1916), U64(0x088AAA18, 0x45B8FDD0), /* ~= 10^179 */ + U64(0xF6C69A72, 0xA3989F5B), U64(0x8AAD549E, 0x57273D45), /* ~= 10^180 */ + U64(0x9A3C2087, 0xA63F6399), U64(0x36AC54E2, 0xF678864B), /* ~= 10^181 */ + U64(0xC0CB28A9, 0x8FCF3C7F), U64(0x84576A1B, 0xB416A7DD), /* ~= 10^182 */ + U64(0xF0FDF2D3, 0xF3C30B9F), U64(0x656D44A2, 0xA11C51D5), /* ~= 10^183 */ + U64(0x969EB7C4, 0x7859E743), U64(0x9F644AE5, 0xA4B1B325), /* ~= 10^184 */ + U64(0xBC4665B5, 0x96706114), U64(0x873D5D9F, 0x0DDE1FEE), /* ~= 10^185 */ + U64(0xEB57FF22, 0xFC0C7959), U64(0xA90CB506, 0xD155A7EA), /* ~= 10^186 */ + U64(0x9316FF75, 0xDD87CBD8), U64(0x09A7F124, 0x42D588F2), /* ~= 10^187 */ + U64(0xB7DCBF53, 0x54E9BECE), U64(0x0C11ED6D, 0x538AEB2F), /* ~= 10^188 */ + U64(0xE5D3EF28, 0x2A242E81), U64(0x8F1668C8, 0xA86DA5FA), /* ~= 10^189 */ + U64(0x8FA47579, 0x1A569D10), U64(0xF96E017D, 0x694487BC), /* ~= 10^190 */ + U64(0xB38D92D7, 0x60EC4455), U64(0x37C981DC, 0xC395A9AC), /* ~= 10^191 */ + U64(0xE070F78D, 0x3927556A), U64(0x85BBE253, 0xF47B1417), /* ~= 10^192 */ + U64(0x8C469AB8, 0x43B89562), U64(0x93956D74, 0x78CCEC8E), /* ~= 10^193 */ + U64(0xAF584166, 0x54A6BABB), U64(0x387AC8D1, 0x970027B2), /* ~= 10^194 */ + U64(0xDB2E51BF, 0xE9D0696A), U64(0x06997B05, 0xFCC0319E), /* ~= 10^195 */ + U64(0x88FCF317, 0xF22241E2), U64(0x441FECE3, 0xBDF81F03), /* ~= 10^196 */ + U64(0xAB3C2FDD, 0xEEAAD25A), U64(0xD527E81C, 0xAD7626C3), /* ~= 10^197 */ + U64(0xD60B3BD5, 0x6A5586F1), U64(0x8A71E223, 0xD8D3B074), /* ~= 10^198 */ + U64(0x85C70565, 0x62757456), U64(0xF6872D56, 0x67844E49), /* ~= 10^199 */ + U64(0xA738C6BE, 0xBB12D16C), U64(0xB428F8AC, 0x016561DB), /* ~= 10^200 */ + U64(0xD106F86E, 0x69D785C7), U64(0xE13336D7, 0x01BEBA52), /* ~= 10^201 */ + U64(0x82A45B45, 0x0226B39C), U64(0xECC00246, 0x61173473), /* ~= 10^202 */ + U64(0xA34D7216, 0x42B06084), U64(0x27F002D7, 0xF95D0190), /* ~= 10^203 */ + U64(0xCC20CE9B, 0xD35C78A5), U64(0x31EC038D, 0xF7B441F4), /* ~= 10^204 */ + U64(0xFF290242, 0xC83396CE), U64(0x7E670471, 0x75A15271), /* ~= 10^205 */ + U64(0x9F79A169, 0xBD203E41), U64(0x0F0062C6, 0xE984D386), /* ~= 10^206 */ + U64(0xC75809C4, 0x2C684DD1), U64(0x52C07B78, 0xA3E60868), /* ~= 10^207 */ + U64(0xF92E0C35, 0x37826145), U64(0xA7709A56, 0xCCDF8A82), /* ~= 10^208 */ + U64(0x9BBCC7A1, 0x42B17CCB), U64(0x88A66076, 0x400BB691), /* ~= 10^209 */ + U64(0xC2ABF989, 0x935DDBFE), U64(0x6ACFF893, 0xD00EA435), /* ~= 10^210 */ + U64(0xF356F7EB, 0xF83552FE), U64(0x0583F6B8, 0xC4124D43), /* ~= 10^211 */ + U64(0x98165AF3, 0x7B2153DE), U64(0xC3727A33, 0x7A8B704A), /* ~= 10^212 */ + U64(0xBE1BF1B0, 0x59E9A8D6), U64(0x744F18C0, 0x592E4C5C), /* ~= 10^213 */ + U64(0xEDA2EE1C, 0x7064130C), U64(0x1162DEF0, 0x6F79DF73), /* ~= 10^214 */ + U64(0x9485D4D1, 0xC63E8BE7), U64(0x8ADDCB56, 0x45AC2BA8), /* ~= 10^215 */ + U64(0xB9A74A06, 0x37CE2EE1), U64(0x6D953E2B, 0xD7173692), /* ~= 10^216 */ + U64(0xE8111C87, 0xC5C1BA99), U64(0xC8FA8DB6, 0xCCDD0437), /* ~= 10^217 */ + U64(0x910AB1D4, 0xDB9914A0), U64(0x1D9C9892, 0x400A22A2), /* ~= 10^218 */ + U64(0xB54D5E4A, 0x127F59C8), U64(0x2503BEB6, 0xD00CAB4B), /* ~= 10^219 */ + U64(0xE2A0B5DC, 0x971F303A), U64(0x2E44AE64, 0x840FD61D), /* ~= 10^220 */ + U64(0x8DA471A9, 0xDE737E24), U64(0x5CEAECFE, 0xD289E5D2), /* ~= 10^221 */ + U64(0xB10D8E14, 0x56105DAD), U64(0x7425A83E, 0x872C5F47), /* ~= 10^222 */ + U64(0xDD50F199, 0x6B947518), U64(0xD12F124E, 0x28F77719), /* ~= 10^223 */ + U64(0x8A5296FF, 0xE33CC92F), U64(0x82BD6B70, 0xD99AAA6F), /* ~= 10^224 */ + U64(0xACE73CBF, 0xDC0BFB7B), U64(0x636CC64D, 0x1001550B), /* ~= 10^225 */ + U64(0xD8210BEF, 0xD30EFA5A), U64(0x3C47F7E0, 0x5401AA4E), /* ~= 10^226 */ + U64(0x8714A775, 0xE3E95C78), U64(0x65ACFAEC, 0x34810A71), /* ~= 10^227 */ + U64(0xA8D9D153, 0x5CE3B396), U64(0x7F1839A7, 0x41A14D0D), /* ~= 10^228 */ + U64(0xD31045A8, 0x341CA07C), U64(0x1EDE4811, 0x1209A050), /* ~= 10^229 */ + U64(0x83EA2B89, 0x2091E44D), U64(0x934AED0A, 0xAB460432), /* ~= 10^230 */ + U64(0xA4E4B66B, 0x68B65D60), U64(0xF81DA84D, 0x5617853F), /* ~= 10^231 */ + U64(0xCE1DE406, 0x42E3F4B9), U64(0x36251260, 0xAB9D668E), /* ~= 10^232 */ + U64(0x80D2AE83, 0xE9CE78F3), U64(0xC1D72B7C, 0x6B426019), /* ~= 10^233 */ + U64(0xA1075A24, 0xE4421730), U64(0xB24CF65B, 0x8612F81F), /* ~= 10^234 */ + U64(0xC94930AE, 0x1D529CFC), U64(0xDEE033F2, 0x6797B627), /* ~= 10^235 */ + U64(0xFB9B7CD9, 0xA4A7443C), U64(0x169840EF, 0x017DA3B1), /* ~= 10^236 */ + U64(0x9D412E08, 0x06E88AA5), U64(0x8E1F2895, 0x60EE864E), /* ~= 10^237 */ + U64(0xC491798A, 0x08A2AD4E), U64(0xF1A6F2BA, 0xB92A27E2), /* ~= 10^238 */ + U64(0xF5B5D7EC, 0x8ACB58A2), U64(0xAE10AF69, 0x6774B1DB), /* ~= 10^239 */ + U64(0x9991A6F3, 0xD6BF1765), U64(0xACCA6DA1, 0xE0A8EF29), /* ~= 10^240 */ + U64(0xBFF610B0, 0xCC6EDD3F), U64(0x17FD090A, 0x58D32AF3), /* ~= 10^241 */ + U64(0xEFF394DC, 0xFF8A948E), U64(0xDDFC4B4C, 0xEF07F5B0), /* ~= 10^242 */ + U64(0x95F83D0A, 0x1FB69CD9), U64(0x4ABDAF10, 0x1564F98E), /* ~= 10^243 */ + U64(0xBB764C4C, 0xA7A4440F), U64(0x9D6D1AD4, 0x1ABE37F1), /* ~= 10^244 */ + U64(0xEA53DF5F, 0xD18D5513), U64(0x84C86189, 0x216DC5ED), /* ~= 10^245 */ + U64(0x92746B9B, 0xE2F8552C), U64(0x32FD3CF5, 0xB4E49BB4), /* ~= 10^246 */ + U64(0xB7118682, 0xDBB66A77), U64(0x3FBC8C33, 0x221DC2A1), /* ~= 10^247 */ + U64(0xE4D5E823, 0x92A40515), U64(0x0FABAF3F, 0xEAA5334A), /* ~= 10^248 */ + U64(0x8F05B116, 0x3BA6832D), U64(0x29CB4D87, 0xF2A7400E), /* ~= 10^249 */ + U64(0xB2C71D5B, 0xCA9023F8), U64(0x743E20E9, 0xEF511012), /* ~= 10^250 */ + U64(0xDF78E4B2, 0xBD342CF6), U64(0x914DA924, 0x6B255416), /* ~= 10^251 */ + U64(0x8BAB8EEF, 0xB6409C1A), U64(0x1AD089B6, 0xC2F7548E), /* ~= 10^252 */ + U64(0xAE9672AB, 0xA3D0C320), U64(0xA184AC24, 0x73B529B1), /* ~= 10^253 */ + U64(0xDA3C0F56, 0x8CC4F3E8), U64(0xC9E5D72D, 0x90A2741E), /* ~= 10^254 */ + U64(0x88658996, 0x17FB1871), U64(0x7E2FA67C, 0x7A658892), /* ~= 10^255 */ + U64(0xAA7EEBFB, 0x9DF9DE8D), U64(0xDDBB901B, 0x98FEEAB7), /* ~= 10^256 */ + U64(0xD51EA6FA, 0x85785631), U64(0x552A7422, 0x7F3EA565), /* ~= 10^257 */ + U64(0x8533285C, 0x936B35DE), U64(0xD53A8895, 0x8F87275F), /* ~= 10^258 */ + U64(0xA67FF273, 0xB8460356), U64(0x8A892ABA, 0xF368F137), /* ~= 10^259 */ + U64(0xD01FEF10, 0xA657842C), U64(0x2D2B7569, 0xB0432D85), /* ~= 10^260 */ + U64(0x8213F56A, 0x67F6B29B), U64(0x9C3B2962, 0x0E29FC73), /* ~= 10^261 */ + U64(0xA298F2C5, 0x01F45F42), U64(0x8349F3BA, 0x91B47B8F), /* ~= 10^262 */ + U64(0xCB3F2F76, 0x42717713), U64(0x241C70A9, 0x36219A73), /* ~= 10^263 */ + U64(0xFE0EFB53, 0xD30DD4D7), U64(0xED238CD3, 0x83AA0110), /* ~= 10^264 */ + U64(0x9EC95D14, 0x63E8A506), U64(0xF4363804, 0x324A40AA), /* ~= 10^265 */ + U64(0xC67BB459, 0x7CE2CE48), U64(0xB143C605, 0x3EDCD0D5), /* ~= 10^266 */ + U64(0xF81AA16F, 0xDC1B81DA), U64(0xDD94B786, 0x8E94050A), /* ~= 10^267 */ + U64(0x9B10A4E5, 0xE9913128), U64(0xCA7CF2B4, 0x191C8326), /* ~= 10^268 */ + U64(0xC1D4CE1F, 0x63F57D72), U64(0xFD1C2F61, 0x1F63A3F0), /* ~= 10^269 */ + U64(0xF24A01A7, 0x3CF2DCCF), U64(0xBC633B39, 0x673C8CEC), /* ~= 10^270 */ + U64(0x976E4108, 0x8617CA01), U64(0xD5BE0503, 0xE085D813), /* ~= 10^271 */ + U64(0xBD49D14A, 0xA79DBC82), U64(0x4B2D8644, 0xD8A74E18), /* ~= 10^272 */ + U64(0xEC9C459D, 0x51852BA2), U64(0xDDF8E7D6, 0x0ED1219E), /* ~= 10^273 */ + U64(0x93E1AB82, 0x52F33B45), U64(0xCABB90E5, 0xC942B503), /* ~= 10^274 */ + U64(0xB8DA1662, 0xE7B00A17), U64(0x3D6A751F, 0x3B936243), /* ~= 10^275 */ + U64(0xE7109BFB, 0xA19C0C9D), U64(0x0CC51267, 0x0A783AD4), /* ~= 10^276 */ + U64(0x906A617D, 0x450187E2), U64(0x27FB2B80, 0x668B24C5), /* ~= 10^277 */ + U64(0xB484F9DC, 0x9641E9DA), U64(0xB1F9F660, 0x802DEDF6), /* ~= 10^278 */ + U64(0xE1A63853, 0xBBD26451), U64(0x5E7873F8, 0xA0396973), /* ~= 10^279 */ + U64(0x8D07E334, 0x55637EB2), U64(0xDB0B487B, 0x6423E1E8), /* ~= 10^280 */ + U64(0xB049DC01, 0x6ABC5E5F), U64(0x91CE1A9A, 0x3D2CDA62), /* ~= 10^281 */ + U64(0xDC5C5301, 0xC56B75F7), U64(0x7641A140, 0xCC7810FB), /* ~= 10^282 */ + U64(0x89B9B3E1, 0x1B6329BA), U64(0xA9E904C8, 0x7FCB0A9D), /* ~= 10^283 */ + U64(0xAC2820D9, 0x623BF429), U64(0x546345FA, 0x9FBDCD44), /* ~= 10^284 */ + U64(0xD732290F, 0xBACAF133), U64(0xA97C1779, 0x47AD4095), /* ~= 10^285 */ + U64(0x867F59A9, 0xD4BED6C0), U64(0x49ED8EAB, 0xCCCC485D), /* ~= 10^286 */ + U64(0xA81F3014, 0x49EE8C70), U64(0x5C68F256, 0xBFFF5A74), /* ~= 10^287 */ + U64(0xD226FC19, 0x5C6A2F8C), U64(0x73832EEC, 0x6FFF3111), /* ~= 10^288 */ + U64(0x83585D8F, 0xD9C25DB7), U64(0xC831FD53, 0xC5FF7EAB), /* ~= 10^289 */ + U64(0xA42E74F3, 0xD032F525), U64(0xBA3E7CA8, 0xB77F5E55), /* ~= 10^290 */ + U64(0xCD3A1230, 0xC43FB26F), U64(0x28CE1BD2, 0xE55F35EB), /* ~= 10^291 */ + U64(0x80444B5E, 0x7AA7CF85), U64(0x7980D163, 0xCF5B81B3), /* ~= 10^292 */ + U64(0xA0555E36, 0x1951C366), U64(0xD7E105BC, 0xC332621F), /* ~= 10^293 */ + U64(0xC86AB5C3, 0x9FA63440), U64(0x8DD9472B, 0xF3FEFAA7), /* ~= 10^294 */ + U64(0xFA856334, 0x878FC150), U64(0xB14F98F6, 0xF0FEB951), /* ~= 10^295 */ + U64(0x9C935E00, 0xD4B9D8D2), U64(0x6ED1BF9A, 0x569F33D3), /* ~= 10^296 */ + U64(0xC3B83581, 0x09E84F07), U64(0x0A862F80, 0xEC4700C8), /* ~= 10^297 */ + U64(0xF4A642E1, 0x4C6262C8), U64(0xCD27BB61, 0x2758C0FA), /* ~= 10^298 */ + U64(0x98E7E9CC, 0xCFBD7DBD), U64(0x8038D51C, 0xB897789C), /* ~= 10^299 */ + U64(0xBF21E440, 0x03ACDD2C), U64(0xE0470A63, 0xE6BD56C3), /* ~= 10^300 */ + U64(0xEEEA5D50, 0x04981478), U64(0x1858CCFC, 0xE06CAC74), /* ~= 10^301 */ + U64(0x95527A52, 0x02DF0CCB), U64(0x0F37801E, 0x0C43EBC8), /* ~= 10^302 */ + U64(0xBAA718E6, 0x8396CFFD), U64(0xD3056025, 0x8F54E6BA), /* ~= 10^303 */ + U64(0xE950DF20, 0x247C83FD), U64(0x47C6B82E, 0xF32A2069), /* ~= 10^304 */ + U64(0x91D28B74, 0x16CDD27E), U64(0x4CDC331D, 0x57FA5441), /* ~= 10^305 */ + U64(0xB6472E51, 0x1C81471D), U64(0xE0133FE4, 0xADF8E952), /* ~= 10^306 */ + U64(0xE3D8F9E5, 0x63A198E5), U64(0x58180FDD, 0xD97723A6), /* ~= 10^307 */ + U64(0x8E679C2F, 0x5E44FF8F), U64(0x570F09EA, 0xA7EA7648), /* ~= 10^308 */ + U64(0xB201833B, 0x35D63F73), U64(0x2CD2CC65, 0x51E513DA), /* ~= 10^309 */ + U64(0xDE81E40A, 0x034BCF4F), U64(0xF8077F7E, 0xA65E58D1), /* ~= 10^310 */ + U64(0x8B112E86, 0x420F6191), U64(0xFB04AFAF, 0x27FAF782), /* ~= 10^311 */ + U64(0xADD57A27, 0xD29339F6), U64(0x79C5DB9A, 0xF1F9B563), /* ~= 10^312 */ + U64(0xD94AD8B1, 0xC7380874), U64(0x18375281, 0xAE7822BC), /* ~= 10^313 */ + U64(0x87CEC76F, 0x1C830548), U64(0x8F229391, 0x0D0B15B5), /* ~= 10^314 */ + U64(0xA9C2794A, 0xE3A3C69A), U64(0xB2EB3875, 0x504DDB22), /* ~= 10^315 */ + U64(0xD433179D, 0x9C8CB841), U64(0x5FA60692, 0xA46151EB), /* ~= 10^316 */ + U64(0x849FEEC2, 0x81D7F328), U64(0xDBC7C41B, 0xA6BCD333), /* ~= 10^317 */ + U64(0xA5C7EA73, 0x224DEFF3), U64(0x12B9B522, 0x906C0800), /* ~= 10^318 */ + U64(0xCF39E50F, 0xEAE16BEF), U64(0xD768226B, 0x34870A00), /* ~= 10^319 */ + U64(0x81842F29, 0xF2CCE375), U64(0xE6A11583, 0x00D46640), /* ~= 10^320 */ + U64(0xA1E53AF4, 0x6F801C53), U64(0x60495AE3, 0xC1097FD0), /* ~= 10^321 */ + U64(0xCA5E89B1, 0x8B602368), U64(0x385BB19C, 0xB14BDFC4), /* ~= 10^322 */ + U64(0xFCF62C1D, 0xEE382C42), U64(0x46729E03, 0xDD9ED7B5), /* ~= 10^323 */ + U64(0x9E19DB92, 0xB4E31BA9), U64(0x6C07A2C2, 0x6A8346D1) /* ~= 10^324 */ +}; + +/** + Get the cached pow10 value from `pow10_sig_table`. + @param exp10 The exponent of pow(10, e). This value must in range + `POW10_SIG_TABLE_MIN_EXP` to `POW10_SIG_TABLE_MAX_EXP`. + @param hi The highest 64 bits of pow(10, e). + @param lo The lower 64 bits after `hi`. + */ +static_inline void pow10_table_get_sig(i32 exp10, u64 *hi, u64 *lo) { + i32 idx = exp10 - (POW10_SIG_TABLE_MIN_EXP); + *hi = pow10_sig_table[idx * 2]; + *lo = pow10_sig_table[idx * 2 + 1]; +} + +/** + Get the exponent (base 2) for highest 64 bits significand in `pow10_sig_table`. + */ +static_inline void pow10_table_get_exp(i32 exp10, i32 *exp2) { + /* e2 = floor(log2(pow(10, e))) - 64 + 1 */ + /* = floor(e * log2(10) - 63) */ + *exp2 = (exp10 * 217706 - 4128768) >> 16; +} + +#endif + + + +/*============================================================================== + * MARK: - Number and Bit Utils (Private) + *============================================================================*/ + +/** Convert bits to double. */ +static_inline f64 f64_from_bits(u64 u) { + f64 f; + memcpy(&f, &u, sizeof(u)); + return f; +} + +/** Convert double to bits. */ +static_inline u64 f64_to_bits(f64 f) { + u64 u; + memcpy(&u, &f, sizeof(u)); + return u; +} + +/** Convert double to bits. */ +static_inline u32 f32_to_bits(f32 f) { + u32 u; + memcpy(&u, &f, sizeof(u)); + return u; +} + +/** Get 'infinity' bits with sign. */ +static_inline u64 f64_bits_inf(bool sign) { +#if YYJSON_HAS_IEEE_754 + return F64_BITS_INF | ((u64)sign << 63); +#elif defined(INFINITY) + return f64_to_bits(sign ? -INFINITY : INFINITY); +#else + return f64_to_bits(sign ? -HUGE_VAL : HUGE_VAL); +#endif +} + +/** Get 'nan' bits with sign. */ +static_inline u64 f64_bits_nan(bool sign) { +#if YYJSON_HAS_IEEE_754 + return F64_BITS_NAN | ((u64)sign << 63); +#elif defined(NAN) + return f64_to_bits(sign ? (f64)-NAN : (f64)NAN); +#else + return f64_to_bits((sign ? -0.0 : 0.0) / 0.0); +#endif +} + +/** Casting double to float, allow overflow. */ +#if yyjson_has_attribute(no_sanitize) +__attribute__((no_sanitize("undefined"))) +#elif yyjson_gcc_available(4, 9, 0) +__attribute__((__no_sanitize_undefined__)) +#endif +static_inline f32 f64_to_f32(f64 val) { + return (f32)val; +} + +/** Returns the number of leading 0-bits in value (input should not be 0). */ +static_inline u32 u64_lz_bits(u64 v) { +#if GCC_HAS_CLZLL + return (u32)__builtin_clzll(v); +#elif MSC_HAS_BIT_SCAN_64 + unsigned long r; + _BitScanReverse64(&r, v); + return (u32)63 - (u32)r; +#elif MSC_HAS_BIT_SCAN + unsigned long hi, lo; + bool hi_set = _BitScanReverse(&hi, (u32)(v >> 32)) != 0; + _BitScanReverse(&lo, (u32)v); + hi |= 32; + return (u32)63 - (u32)(hi_set ? hi : lo); +#else + /* branchless, use De Bruijn sequence */ + /* see: https://www.chessprogramming.org/BitScan */ + const u8 table[64] = { + 63, 16, 62, 7, 15, 36, 61, 3, 6, 14, 22, 26, 35, 47, 60, 2, + 9, 5, 28, 11, 13, 21, 42, 19, 25, 31, 34, 40, 46, 52, 59, 1, + 17, 8, 37, 4, 23, 27, 48, 10, 29, 12, 43, 20, 32, 41, 53, 18, + 38, 24, 49, 30, 44, 33, 54, 39, 50, 45, 55, 51, 56, 57, 58, 0 + }; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v |= v >> 32; + return table[(v * U64(0x03F79D71, 0xB4CB0A89)) >> 58]; +#endif +} + +/** Returns the number of trailing 0-bits in value (input should not be 0). */ +static_inline u32 u64_tz_bits(u64 v) { +#if GCC_HAS_CTZLL + return (u32)__builtin_ctzll(v); +#elif MSC_HAS_BIT_SCAN_64 + unsigned long r; + _BitScanForward64(&r, v); + return (u32)r; +#elif MSC_HAS_BIT_SCAN + unsigned long lo, hi; + bool lo_set = _BitScanForward(&lo, (u32)(v)) != 0; + _BitScanForward(&hi, (u32)(v >> 32)); + hi += 32; + return lo_set ? lo : hi; +#else + /* branchless, use De Bruijn sequence */ + /* see: https://www.chessprogramming.org/BitScan */ + const u8 table[64] = { + 0, 1, 2, 53, 3, 7, 54, 27, 4, 38, 41, 8, 34, 55, 48, 28, + 62, 5, 39, 46, 44, 42, 22, 9, 24, 35, 59, 56, 49, 18, 29, 11, + 63, 52, 6, 26, 37, 40, 33, 47, 61, 45, 43, 21, 23, 58, 17, 10, + 51, 25, 36, 32, 60, 20, 57, 16, 50, 31, 19, 15, 30, 14, 13, 12 + }; + return table[((v & (~v + 1)) * U64(0x022FDD63, 0xCC95386D)) >> 58]; +#endif +} + +/** Multiplies two 64-bit unsigned integers (a * b), + returns the 128-bit result as 'hi' and 'lo'. */ +static_inline void u128_mul(u64 a, u64 b, u64 *hi, u64 *lo) { +#if YYJSON_HAS_INT128 + u128 m = (u128)a * b; + *hi = (u64)(m >> 64); + *lo = (u64)(m); +#elif MSC_HAS_UMUL128 + *lo = _umul128(a, b, hi); +#else + u32 a0 = (u32)(a), a1 = (u32)(a >> 32); + u32 b0 = (u32)(b), b1 = (u32)(b >> 32); + u64 p00 = (u64)a0 * b0, p01 = (u64)a0 * b1; + u64 p10 = (u64)a1 * b0, p11 = (u64)a1 * b1; + u64 m0 = p01 + (p00 >> 32); + u32 m00 = (u32)(m0), m01 = (u32)(m0 >> 32); + u64 m1 = p10 + m00; + u32 m10 = (u32)(m1), m11 = (u32)(m1 >> 32); + *hi = p11 + m01 + m11; + *lo = ((u64)m10 << 32) | (u32)p00; +#endif +} + +/** Multiplies two 64-bit unsigned integers and add a value (a * b + c), + returns the 128-bit result as 'hi' and 'lo'. */ +static_inline void u128_mul_add(u64 a, u64 b, u64 c, u64 *hi, u64 *lo) { +#if YYJSON_HAS_INT128 + u128 m = (u128)a * b + c; + *hi = (u64)(m >> 64); + *lo = (u64)(m); +#else + u64 h, l, t; + u128_mul(a, b, &h, &l); + t = l + c; + h += (u64)(((t < l) | (t < c))); + *hi = h; + *lo = t; +#endif +} + + + +/*============================================================================== + * MARK: - File Utils (Private) + * These functions are used to read and write JSON files. + *============================================================================*/ + +#define YYJSON_FOPEN_E +#if !defined(_MSC_VER) && defined(__GLIBC__) && defined(__GLIBC_PREREQ) +# if __GLIBC_PREREQ(2, 7) +# undef YYJSON_FOPEN_E +# define YYJSON_FOPEN_E "e" /* glibc extension to enable O_CLOEXEC */ +# endif +#endif + +static_inline FILE *fopen_safe(const char *path, const char *mode) { +#if YYJSON_MSC_VER >= 1400 + FILE *file = NULL; + if (fopen_s(&file, path, mode) != 0) return NULL; + return file; +#else + return fopen(path, mode); +#endif +} + +static_inline FILE *fopen_readonly(const char *path) { + return fopen_safe(path, "rb" YYJSON_FOPEN_E); +} + +static_inline FILE *fopen_writeonly(const char *path) { + return fopen_safe(path, "wb" YYJSON_FOPEN_E); +} + +static_inline usize fread_safe(void *buf, usize size, FILE *file) { +#if YYJSON_MSC_VER >= 1400 + return fread_s(buf, size, 1, size, file); +#else + return fread(buf, 1, size, file); +#endif +} + + + +/*============================================================================== + * MARK: - Size Utils (Private) + * These functions are used for memory allocation. + *============================================================================*/ + +/** Returns whether the size is overflow after increment. */ +static_inline bool size_add_is_overflow(usize size, usize add) { + return size > (size + add); +} + +/** Returns whether the size is power of 2 (size should not be 0). */ +static_inline bool size_is_pow2(usize size) { + return (size & (size - 1)) == 0; +} + +/** Align size upwards (may overflow). */ +static_inline usize size_align_up(usize size, usize align) { + if (size_is_pow2(align)) { + return (size + (align - 1)) & ~(align - 1); + } else { + return size + align - (size + align - 1) % align - 1; + } +} + +/** Align size downwards. */ +static_inline usize size_align_down(usize size, usize align) { + if (size_is_pow2(align)) { + return size & ~(align - 1); + } else { + return size - (size % align); + } +} + +/** Align address upwards (may overflow). */ +static_inline void *mem_align_up(void *mem, usize align) { + usize size; + memcpy(&size, &mem, sizeof(usize)); + size = size_align_up(size, align); + memcpy(&mem, &size, sizeof(usize)); + return mem; +} + + + +/*============================================================================== + * MARK: - Default Memory Allocator (Private) + * This is a simple libc memory allocator wrapper. + *============================================================================*/ + +static void *default_malloc(void *ctx, usize size) { + return malloc(size); +} + +static void *default_realloc(void *ctx, void *ptr, usize old_size, usize size) { + return realloc(ptr, size); +} + +static void default_free(void *ctx, void *ptr) { + free(ptr); +} + +static const yyjson_alc YYJSON_DEFAULT_ALC = { + default_malloc, default_realloc, default_free, NULL +}; + + + +/*============================================================================== + * MARK: - Null Memory Allocator (Private) + * This allocator is just a placeholder to ensure that the internal + * malloc/realloc/free function pointers are not null. + *============================================================================*/ + +static void *null_malloc(void *ctx, usize size) { + return NULL; +} + +static void *null_realloc(void *ctx, void *ptr, usize old_size, usize size) { + return NULL; +} + +static void null_free(void *ctx, void *ptr) { + return; +} + +static const yyjson_alc YYJSON_NULL_ALC = { + null_malloc, null_realloc, null_free, NULL +}; + + + +/*============================================================================== + * MARK: - Pool Memory Allocator (Public) + * This allocator is initialized with a fixed-size buffer. + * The buffer is split into multiple memory chunks for memory allocation. + *============================================================================*/ + +/** memory chunk header */ +typedef struct pool_chunk { + usize size; /* chunk memory size, include chunk header */ + struct pool_chunk *next; /* linked list, nullable */ + /* char mem[]; flexible array member */ +} pool_chunk; + +/** allocator ctx header */ +typedef struct pool_ctx { + usize size; /* total memory size, include ctx header */ + pool_chunk *free_list; /* linked list, nullable */ + /* pool_chunk chunks[]; flexible array member */ +} pool_ctx; + +/** align up the input size to chunk size */ +static_inline void pool_size_align(usize *size) { + *size = size_align_up(*size, sizeof(pool_chunk)) + sizeof(pool_chunk); +} + +static void *pool_malloc(void *ctx_ptr, usize size) { + /* assert(size != 0) */ + pool_ctx *ctx = (pool_ctx *)ctx_ptr; + pool_chunk *next, *prev = NULL, *cur = ctx->free_list; + + if (unlikely(size >= ctx->size)) return NULL; + pool_size_align(&size); + + while (cur) { + if (cur->size < size) { + /* not enough space, try next chunk */ + prev = cur; + cur = cur->next; + continue; + } + if (cur->size >= size + sizeof(pool_chunk) * 2) { + /* too much space, split this chunk */ + next = (pool_chunk *)(void *)((u8 *)cur + size); + next->size = cur->size - size; + next->next = cur->next; + cur->size = size; + } else { + /* just enough space, use whole chunk */ + next = cur->next; + } + if (prev) prev->next = next; + else ctx->free_list = next; + return (void *)(cur + 1); + } + return NULL; +} + +static void pool_free(void *ctx_ptr, void *ptr) { + /* assert(ptr != NULL) */ + pool_ctx *ctx = (pool_ctx *)ctx_ptr; + pool_chunk *cur = ((pool_chunk *)ptr) - 1; + pool_chunk *prev = NULL, *next = ctx->free_list; + + while (next && next < cur) { + prev = next; + next = next->next; + } + if (prev) prev->next = cur; + else ctx->free_list = cur; + cur->next = next; + + if (next && ((u8 *)cur + cur->size) == (u8 *)next) { + /* merge cur to higher chunk */ + cur->size += next->size; + cur->next = next->next; + } + if (prev && ((u8 *)prev + prev->size) == (u8 *)cur) { + /* merge cur to lower chunk */ + prev->size += cur->size; + prev->next = cur->next; + } +} + +static void *pool_realloc(void *ctx_ptr, void *ptr, + usize old_size, usize size) { + /* assert(ptr != NULL && size != 0 && old_size < size) */ + pool_ctx *ctx = (pool_ctx *)ctx_ptr; + pool_chunk *cur = ((pool_chunk *)ptr) - 1, *prev, *next, *tmp; + + /* check size */ + if (unlikely(size >= ctx->size)) return NULL; + pool_size_align(&old_size); + pool_size_align(&size); + if (unlikely(old_size == size)) return ptr; + + /* find next and prev chunk */ + prev = NULL; + next = ctx->free_list; + while (next && next < cur) { + prev = next; + next = next->next; + } + + if ((u8 *)cur + cur->size == (u8 *)next && cur->size + next->size >= size) { + /* merge to higher chunk if they are contiguous */ + usize free_size = cur->size + next->size - size; + if (free_size > sizeof(pool_chunk) * 2) { + tmp = (pool_chunk *)(void *)((u8 *)cur + size); + if (prev) prev->next = tmp; + else ctx->free_list = tmp; + tmp->next = next->next; + tmp->size = free_size; + cur->size = size; + } else { + if (prev) prev->next = next->next; + else ctx->free_list = next->next; + cur->size += next->size; + } + return ptr; + } else { + /* fallback to malloc and memcpy */ + void *new_ptr = pool_malloc(ctx_ptr, size - sizeof(pool_chunk)); + if (new_ptr) { + memcpy(new_ptr, ptr, cur->size - sizeof(pool_chunk)); + pool_free(ctx_ptr, ptr); + } + return new_ptr; + } +} + +bool yyjson_alc_pool_init(yyjson_alc *alc, void *buf, usize size) { + pool_chunk *chunk; + pool_ctx *ctx; + + if (unlikely(!alc)) return false; + *alc = YYJSON_NULL_ALC; + if (size < sizeof(pool_ctx) * 4) return false; + ctx = (pool_ctx *)mem_align_up(buf, sizeof(pool_ctx)); + if (unlikely(!ctx)) return false; + size -= (usize)((u8 *)ctx - (u8 *)buf); + size = size_align_down(size, sizeof(pool_ctx)); + + chunk = (pool_chunk *)(ctx + 1); + chunk->size = size - sizeof(pool_ctx); + chunk->next = NULL; + ctx->size = size; + ctx->free_list = chunk; + + alc->malloc = pool_malloc; + alc->realloc = pool_realloc; + alc->free = pool_free; + alc->ctx = (void *)ctx; + return true; +} + + + +/*============================================================================== + * MARK: - Dynamic Memory Allocator (Public) + * This allocator allocates memory on demand and does not immediately release + * unused memory. Instead, it places the unused memory into a freelist for + * potential reuse in the future. It is only when the entire allocator is + * destroyed that all previously allocated memory is released at once. + *============================================================================*/ + +/** memory chunk header */ +typedef struct dyn_chunk { + usize size; /* chunk size, include header */ + struct dyn_chunk *next; + /* char mem[]; flexible array member */ +} dyn_chunk; + +/** allocator ctx header */ +typedef struct { + dyn_chunk free_list; /* dummy header, sorted from small to large */ + dyn_chunk used_list; /* dummy header */ +} dyn_ctx; + +/** align up the input size to chunk size */ +static_inline bool dyn_size_align(usize *size) { + usize alc_size = *size + sizeof(dyn_chunk); + alc_size = size_align_up(alc_size, YYJSON_ALC_DYN_MIN_SIZE); + if (unlikely(alc_size < *size)) return false; /* overflow */ + *size = alc_size; + return true; +} + +/** remove a chunk from list (the chunk must already be in the list) */ +static_inline void dyn_chunk_list_remove(dyn_chunk *list, dyn_chunk *chunk) { + dyn_chunk *prev = list, *cur; + for (cur = prev->next; cur; cur = cur->next) { + if (cur == chunk) { + prev->next = cur->next; + cur->next = NULL; + return; + } + prev = cur; + } +} + +/** add a chunk to list header (the chunk must not be in the list) */ +static_inline void dyn_chunk_list_add(dyn_chunk *list, dyn_chunk *chunk) { + chunk->next = list->next; + list->next = chunk; +} + +static void *dyn_malloc(void *ctx_ptr, usize size) { + /* assert(size != 0) */ + const yyjson_alc def = YYJSON_DEFAULT_ALC; + dyn_ctx *ctx = (dyn_ctx *)ctx_ptr; + dyn_chunk *chunk, *prev; + if (unlikely(!dyn_size_align(&size))) return NULL; + + /* freelist is empty, create new chunk */ + if (!ctx->free_list.next) { + chunk = (dyn_chunk *)def.malloc(def.ctx, size); + if (unlikely(!chunk)) return NULL; + chunk->size = size; + chunk->next = NULL; + dyn_chunk_list_add(&ctx->used_list, chunk); + return (void *)(chunk + 1); + } + + /* find a large enough chunk, or resize the largest chunk */ + prev = &ctx->free_list; + while (true) { + chunk = prev->next; + if (chunk->size >= size) { /* enough size, reuse this chunk */ + prev->next = chunk->next; + dyn_chunk_list_add(&ctx->used_list, chunk); + return (void *)(chunk + 1); + } + if (!chunk->next) { /* resize the largest chunk */ + chunk = (dyn_chunk *)def.realloc(def.ctx, chunk, chunk->size, size); + if (unlikely(!chunk)) return NULL; + prev->next = NULL; + chunk->size = size; + dyn_chunk_list_add(&ctx->used_list, chunk); + return (void *)(chunk + 1); + } + prev = chunk; + } +} + +static void *dyn_realloc(void *ctx_ptr, void *ptr, + usize old_size, usize size) { + /* assert(ptr != NULL && size != 0 && old_size < size) */ + const yyjson_alc def = YYJSON_DEFAULT_ALC; + dyn_ctx *ctx = (dyn_ctx *)ctx_ptr; + dyn_chunk *new_chunk, *chunk = (dyn_chunk *)ptr - 1; + if (unlikely(!dyn_size_align(&size))) return NULL; + if (chunk->size >= size) return ptr; + + dyn_chunk_list_remove(&ctx->used_list, chunk); + new_chunk = (dyn_chunk *)def.realloc(def.ctx, chunk, chunk->size, size); + if (likely(new_chunk)) { + new_chunk->size = size; + chunk = new_chunk; + } + dyn_chunk_list_add(&ctx->used_list, chunk); + return new_chunk ? (void *)(new_chunk + 1) : NULL; +} + +static void dyn_free(void *ctx_ptr, void *ptr) { + /* assert(ptr != NULL) */ + dyn_ctx *ctx = (dyn_ctx *)ctx_ptr; + dyn_chunk *chunk = (dyn_chunk *)ptr - 1, *prev; + + dyn_chunk_list_remove(&ctx->used_list, chunk); + for (prev = &ctx->free_list; prev; prev = prev->next) { + if (!prev->next || prev->next->size >= chunk->size) { + chunk->next = prev->next; + prev->next = chunk; + break; + } + } +} + +yyjson_alc *yyjson_alc_dyn_new(void) { + const yyjson_alc def = YYJSON_DEFAULT_ALC; + usize hdr_len = sizeof(yyjson_alc) + sizeof(dyn_ctx); + yyjson_alc *alc = (yyjson_alc *)def.malloc(def.ctx, hdr_len); + dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1); + if (unlikely(!alc)) return NULL; + alc->malloc = dyn_malloc; + alc->realloc = dyn_realloc; + alc->free = dyn_free; + alc->ctx = alc + 1; + memset(ctx, 0, sizeof(*ctx)); + return alc; +} + +void yyjson_alc_dyn_free(yyjson_alc *alc) { + const yyjson_alc def = YYJSON_DEFAULT_ALC; + dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1); + dyn_chunk *chunk, *next; + if (unlikely(!alc)) return; + for (chunk = ctx->free_list.next; chunk; chunk = next) { + next = chunk->next; + def.free(def.ctx, chunk); + } + for (chunk = ctx->used_list.next; chunk; chunk = next) { + next = chunk->next; + def.free(def.ctx, chunk); + } + def.free(def.ctx, alc); +} + + + +/*============================================================================== + * MARK: - JSON Struct Utils (Public) + * These functions are used for creating, copying, releasing, and comparing + * JSON documents and values. They are widely used throughout this library. + *============================================================================*/ + +static_inline void unsafe_yyjson_str_pool_release(yyjson_str_pool *pool, + yyjson_alc *alc) { + yyjson_str_chunk *chunk = pool->chunks, *next; + while (chunk) { + next = chunk->next; + alc->free(alc->ctx, chunk); + chunk = next; + } +} + +static_inline void unsafe_yyjson_val_pool_release(yyjson_val_pool *pool, + yyjson_alc *alc) { + yyjson_val_chunk *chunk = pool->chunks, *next; + while (chunk) { + next = chunk->next; + alc->free(alc->ctx, chunk); + chunk = next; + } +} + +bool unsafe_yyjson_str_pool_grow(yyjson_str_pool *pool, + const yyjson_alc *alc, usize len) { + yyjson_str_chunk *chunk; + usize size, max_len; + + /* create a new chunk */ + max_len = USIZE_MAX - sizeof(yyjson_str_chunk); + if (unlikely(len > max_len)) return false; + size = len + sizeof(yyjson_str_chunk); + size = yyjson_max(pool->chunk_size, size); + chunk = (yyjson_str_chunk *)alc->malloc(alc->ctx, size); + if (unlikely(!chunk)) return false; + + /* insert the new chunk as the head of the linked list */ + chunk->next = pool->chunks; + chunk->chunk_size = size; + pool->chunks = chunk; + pool->cur = (char *)chunk + sizeof(yyjson_str_chunk); + pool->end = (char *)chunk + size; + + /* the next chunk is twice the size of the current one */ + size = yyjson_min(pool->chunk_size * 2, pool->chunk_size_max); + if (size < pool->chunk_size) size = pool->chunk_size_max; /* overflow */ + pool->chunk_size = size; + return true; +} + +bool unsafe_yyjson_val_pool_grow(yyjson_val_pool *pool, + const yyjson_alc *alc, usize count) { + yyjson_val_chunk *chunk; + usize size, max_count; + + /* create a new chunk */ + max_count = USIZE_MAX / sizeof(yyjson_mut_val) - 1; + if (unlikely(count > max_count)) return false; + size = (count + 1) * sizeof(yyjson_mut_val); + size = yyjson_max(pool->chunk_size, size); + chunk = (yyjson_val_chunk *)alc->malloc(alc->ctx, size); + if (unlikely(!chunk)) return false; + + /* insert the new chunk as the head of the linked list */ + chunk->next = pool->chunks; + chunk->chunk_size = size; + pool->chunks = chunk; + pool->cur = (yyjson_mut_val *)(void *)((u8 *)chunk) + 1; + pool->end = (yyjson_mut_val *)(void *)((u8 *)chunk + size); + + /* the next chunk is twice the size of the current one */ + size = yyjson_min(pool->chunk_size * 2, pool->chunk_size_max); + if (size < pool->chunk_size) size = pool->chunk_size_max; /* overflow */ + pool->chunk_size = size; + return true; +} + +bool yyjson_mut_doc_set_str_pool_size(yyjson_mut_doc *doc, size_t len) { + usize max_size = USIZE_MAX - sizeof(yyjson_str_chunk); + if (!doc || !len || len > max_size) return false; + doc->str_pool.chunk_size = len + sizeof(yyjson_str_chunk); + return true; +} + +bool yyjson_mut_doc_set_val_pool_size(yyjson_mut_doc *doc, size_t count) { + usize max_count = USIZE_MAX / sizeof(yyjson_mut_val) - 1; + if (!doc || !count || count > max_count) return false; + doc->val_pool.chunk_size = (count + 1) * sizeof(yyjson_mut_val); + return true; +} + +void yyjson_mut_doc_free(yyjson_mut_doc *doc) { + if (doc) { + yyjson_alc alc = doc->alc; + memset(&doc->alc, 0, sizeof(alc)); + unsafe_yyjson_str_pool_release(&doc->str_pool, &alc); + unsafe_yyjson_val_pool_release(&doc->val_pool, &alc); + alc.free(alc.ctx, doc); + } +} + +yyjson_mut_doc *yyjson_mut_doc_new(const yyjson_alc *alc) { + yyjson_mut_doc *doc; + if (!alc) alc = &YYJSON_DEFAULT_ALC; + doc = (yyjson_mut_doc *)alc->malloc(alc->ctx, sizeof(yyjson_mut_doc)); + if (!doc) return NULL; + memset(doc, 0, sizeof(yyjson_mut_doc)); + + doc->alc = *alc; + doc->str_pool.chunk_size = YYJSON_MUT_DOC_STR_POOL_INIT_SIZE; + doc->str_pool.chunk_size_max = YYJSON_MUT_DOC_STR_POOL_MAX_SIZE; + doc->val_pool.chunk_size = YYJSON_MUT_DOC_VAL_POOL_INIT_SIZE; + doc->val_pool.chunk_size_max = YYJSON_MUT_DOC_VAL_POOL_MAX_SIZE; + return doc; +} + +yyjson_mut_doc *yyjson_doc_mut_copy(yyjson_doc *doc, const yyjson_alc *alc) { + yyjson_mut_doc *m_doc; + yyjson_mut_val *m_val; + + if (!doc || !doc->root) return NULL; + m_doc = yyjson_mut_doc_new(alc); + if (!m_doc) return NULL; + m_val = yyjson_val_mut_copy(m_doc, doc->root); + if (!m_val) { + yyjson_mut_doc_free(m_doc); + return NULL; + } + yyjson_mut_doc_set_root(m_doc, m_val); + return m_doc; +} + +yyjson_mut_doc *yyjson_mut_doc_mut_copy(yyjson_mut_doc *doc, + const yyjson_alc *alc) { + yyjson_mut_doc *m_doc; + yyjson_mut_val *m_val; + + if (!doc) return NULL; + if (!doc->root) return yyjson_mut_doc_new(alc); + + m_doc = yyjson_mut_doc_new(alc); + if (!m_doc) return NULL; + m_val = yyjson_mut_val_mut_copy(m_doc, doc->root); + if (!m_val) { + yyjson_mut_doc_free(m_doc); + return NULL; + } + yyjson_mut_doc_set_root(m_doc, m_val); + return m_doc; +} + +yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *m_doc, + yyjson_val *i_vals) { + /* + The immutable object or array stores all sub-values in a contiguous memory, + We copy them to another contiguous memory as mutable values, + then reconnect the mutable values with the original relationship. + */ + usize i_vals_len; + yyjson_mut_val *m_vals, *m_val; + yyjson_val *i_val, *i_end; + + if (!m_doc || !i_vals) return NULL; + i_end = unsafe_yyjson_get_next(i_vals); + i_vals_len = (usize)(unsafe_yyjson_get_next(i_vals) - i_vals); + m_vals = unsafe_yyjson_mut_val(m_doc, i_vals_len); + if (!m_vals) return NULL; + i_val = i_vals; + m_val = m_vals; + + for (; i_val < i_end; i_val++, m_val++) { + yyjson_type type = unsafe_yyjson_get_type(i_val); + m_val->tag = i_val->tag; + m_val->uni.u64 = i_val->uni.u64; + if (type == YYJSON_TYPE_STR || type == YYJSON_TYPE_RAW) { + const char *str = i_val->uni.str; + usize str_len = unsafe_yyjson_get_len(i_val); + m_val->uni.str = unsafe_yyjson_mut_strncpy(m_doc, str, str_len); + if (!m_val->uni.str) return NULL; + } else if (type == YYJSON_TYPE_ARR) { + usize len = unsafe_yyjson_get_len(i_val); + if (len > 0) { + yyjson_val *ii_val = i_val + 1, *ii_next; + yyjson_mut_val *mm_val = m_val + 1, *mm_ctn = m_val, *mm_next; + while (len-- > 1) { + ii_next = unsafe_yyjson_get_next(ii_val); + mm_next = mm_val + (ii_next - ii_val); + mm_val->next = mm_next; + ii_val = ii_next; + mm_val = mm_next; + } + mm_val->next = mm_ctn + 1; + mm_ctn->uni.ptr = mm_val; + } + } else if (type == YYJSON_TYPE_OBJ) { + usize len = unsafe_yyjson_get_len(i_val); + if (len > 0) { + yyjson_val *ii_key = i_val + 1, *ii_nextkey; + yyjson_mut_val *mm_key = m_val + 1, *mm_ctn = m_val; + yyjson_mut_val *mm_nextkey; + while (len-- > 1) { + ii_nextkey = unsafe_yyjson_get_next(ii_key + 1); + mm_nextkey = mm_key + (ii_nextkey - ii_key); + mm_key->next = mm_key + 1; + mm_key->next->next = mm_nextkey; + ii_key = ii_nextkey; + mm_key = mm_nextkey; + } + mm_key->next = mm_key + 1; + mm_key->next->next = mm_ctn + 1; + mm_ctn->uni.ptr = mm_key; + } + } + } + return m_vals; +} + +static yyjson_mut_val *unsafe_yyjson_mut_val_mut_copy(yyjson_mut_doc *m_doc, + yyjson_mut_val *m_vals) { + /* + The mutable object or array stores all sub-values in a circular linked + list, so we can traverse them in the same loop. The traversal starts from + the last item, continues with the first item in a list, and ends with the + second to last item, which needs to be linked to the last item to close the + circle. + */ + yyjson_mut_val *m_val = unsafe_yyjson_mut_val(m_doc, 1); + if (unlikely(!m_val)) return NULL; + m_val->tag = m_vals->tag; + + switch (unsafe_yyjson_get_type(m_vals)) { + case YYJSON_TYPE_OBJ: + case YYJSON_TYPE_ARR: + if (unsafe_yyjson_get_len(m_vals) > 0) { + yyjson_mut_val *last = (yyjson_mut_val *)m_vals->uni.ptr; + yyjson_mut_val *next = last->next, *prev; + prev = unsafe_yyjson_mut_val_mut_copy(m_doc, last); + if (!prev) return NULL; + m_val->uni.ptr = (void *)prev; + while (next != last) { + prev->next = unsafe_yyjson_mut_val_mut_copy(m_doc, next); + if (!prev->next) return NULL; + prev = prev->next; + next = next->next; + } + prev->next = (yyjson_mut_val *)m_val->uni.ptr; + } + break; + case YYJSON_TYPE_RAW: + case YYJSON_TYPE_STR: { + const char *str = m_vals->uni.str; + usize str_len = unsafe_yyjson_get_len(m_vals); + m_val->uni.str = unsafe_yyjson_mut_strncpy(m_doc, str, str_len); + if (!m_val->uni.str) return NULL; + break; + } + default: + m_val->uni = m_vals->uni; + break; + } + return m_val; +} + +yyjson_mut_val *yyjson_mut_val_mut_copy(yyjson_mut_doc *doc, + yyjson_mut_val *val) { + if (doc && val) return unsafe_yyjson_mut_val_mut_copy(doc, val); + return NULL; +} + +/* Count the number of values and the total length of the strings. */ +static void yyjson_mut_stat(yyjson_mut_val *val, + usize *val_sum, usize *str_sum) { + yyjson_type type = unsafe_yyjson_get_type(val); + *val_sum += 1; + if (type == YYJSON_TYPE_ARR || type == YYJSON_TYPE_OBJ) { + yyjson_mut_val *child = (yyjson_mut_val *)val->uni.ptr; + usize len = unsafe_yyjson_get_len(val), i; + len <<= (u8)(type == YYJSON_TYPE_OBJ); + *val_sum += len; + for (i = 0; i < len; i++) { + yyjson_type stype = unsafe_yyjson_get_type(child); + if (stype == YYJSON_TYPE_STR || stype == YYJSON_TYPE_RAW) { + *str_sum += unsafe_yyjson_get_len(child) + 1; + } else if (stype == YYJSON_TYPE_ARR || stype == YYJSON_TYPE_OBJ) { + yyjson_mut_stat(child, val_sum, str_sum); + *val_sum -= 1; + } + child = child->next; + } + } else if (type == YYJSON_TYPE_STR || type == YYJSON_TYPE_RAW) { + *str_sum += unsafe_yyjson_get_len(val) + 1; + } +} + +/* Copy mutable values to immutable value pool. */ +static usize yyjson_imut_copy(yyjson_val **val_ptr, char **buf_ptr, + yyjson_mut_val *mval) { + yyjson_val *val = *val_ptr; + yyjson_type type = unsafe_yyjson_get_type(mval); + if (type == YYJSON_TYPE_ARR || type == YYJSON_TYPE_OBJ) { + yyjson_mut_val *child = (yyjson_mut_val *)mval->uni.ptr; + usize len = unsafe_yyjson_get_len(mval), i; + usize val_sum = 1; + if (type == YYJSON_TYPE_OBJ) { + if (len) child = child->next->next; + len <<= 1; + } else { + if (len) child = child->next; + } + *val_ptr = val + 1; + for (i = 0; i < len; i++) { + val_sum += yyjson_imut_copy(val_ptr, buf_ptr, child); + child = child->next; + } + val->tag = mval->tag; + val->uni.ofs = val_sum * sizeof(yyjson_val); + return val_sum; + } else if (type == YYJSON_TYPE_STR || type == YYJSON_TYPE_RAW) { + char *buf = *buf_ptr; + usize len = unsafe_yyjson_get_len(mval); + memcpy((void *)buf, (const void *)mval->uni.str, len); + buf[len] = '\0'; + val->tag = mval->tag; + val->uni.str = buf; + *val_ptr = val + 1; + *buf_ptr = buf + len + 1; + return 1; + } else { + val->tag = mval->tag; + val->uni = mval->uni; + *val_ptr = val + 1; + return 1; + } +} + +yyjson_doc *yyjson_mut_doc_imut_copy(yyjson_mut_doc *mdoc, + const yyjson_alc *alc) { + if (!mdoc) return NULL; + return yyjson_mut_val_imut_copy(mdoc->root, alc); +} + +yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *mval, + const yyjson_alc *alc) { + usize val_num = 0, str_sum = 0, hdr_size, buf_size; + yyjson_doc *doc = NULL; + yyjson_val *val_hdr = NULL; + + /* This value should be NULL here. Setting a non-null value suppresses + warning from the clang analyzer. */ + char *str_hdr = (char *)(void *)&str_sum; + if (!mval) return NULL; + if (!alc) alc = &YYJSON_DEFAULT_ALC; + + /* traverse the input value to get pool size */ + yyjson_mut_stat(mval, &val_num, &str_sum); + + /* create doc and val pool */ + hdr_size = size_align_up(sizeof(yyjson_doc), sizeof(yyjson_val)); + buf_size = hdr_size + val_num * sizeof(yyjson_val); + doc = (yyjson_doc *)alc->malloc(alc->ctx, buf_size); + if (!doc) return NULL; + memset(doc, 0, sizeof(yyjson_doc)); + val_hdr = (yyjson_val *)(void *)((char *)(void *)doc + hdr_size); + doc->root = val_hdr; + doc->alc = *alc; + + /* create str pool */ + if (str_sum > 0) { + str_hdr = (char *)alc->malloc(alc->ctx, str_sum); + doc->str_pool = str_hdr; + if (!str_hdr) { + alc->free(alc->ctx, (void *)doc); + return NULL; + } + } + + /* copy vals and strs */ + doc->val_read = yyjson_imut_copy(&val_hdr, &str_hdr, mval); + doc->dat_read = str_sum + 1; + return doc; +} + +static_inline bool unsafe_yyjson_num_equals(void *lhs, void *rhs) { + yyjson_val_uni *luni = &((yyjson_val *)lhs)->uni; + yyjson_val_uni *runi = &((yyjson_val *)rhs)->uni; + yyjson_subtype lt = unsafe_yyjson_get_subtype(lhs); + yyjson_subtype rt = unsafe_yyjson_get_subtype(rhs); + if (lt == rt) return luni->u64 == runi->u64; + if (lt == YYJSON_SUBTYPE_SINT && rt == YYJSON_SUBTYPE_UINT) { + return luni->i64 >= 0 && luni->u64 == runi->u64; + } + if (lt == YYJSON_SUBTYPE_UINT && rt == YYJSON_SUBTYPE_SINT) { + return runi->i64 >= 0 && luni->u64 == runi->u64; + } + return false; +} + +static_inline bool unsafe_yyjson_str_equals(void *lhs, void *rhs) { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + return !memcmp(unsafe_yyjson_get_str(lhs), + unsafe_yyjson_get_str(rhs), len); +} + +bool unsafe_yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { + yyjson_type type = unsafe_yyjson_get_type(lhs); + if (type != unsafe_yyjson_get_type(rhs)) return false; + + switch (type) { + case YYJSON_TYPE_OBJ: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + yyjson_obj_iter iter; + yyjson_obj_iter_init(rhs, &iter); + lhs = unsafe_yyjson_get_first(lhs); + while (len-- > 0) { + rhs = yyjson_obj_iter_getn(&iter, lhs->uni.str, + unsafe_yyjson_get_len(lhs)); + if (!rhs) return false; + if (!unsafe_yyjson_equals(lhs + 1, rhs)) return false; + lhs = unsafe_yyjson_get_next(lhs + 1); + } + } + /* yyjson allows duplicate keys, so the check may be inaccurate */ + return true; + } + + case YYJSON_TYPE_ARR: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + lhs = unsafe_yyjson_get_first(lhs); + rhs = unsafe_yyjson_get_first(rhs); + while (len-- > 0) { + if (!unsafe_yyjson_equals(lhs, rhs)) return false; + lhs = unsafe_yyjson_get_next(lhs); + rhs = unsafe_yyjson_get_next(rhs); + } + } + return true; + } + + case YYJSON_TYPE_NUM: + return unsafe_yyjson_num_equals(lhs, rhs); + + case YYJSON_TYPE_RAW: + case YYJSON_TYPE_STR: + return unsafe_yyjson_str_equals(lhs, rhs); + + case YYJSON_TYPE_NULL: + case YYJSON_TYPE_BOOL: + return lhs->tag == rhs->tag; + + default: + return false; + } +} + +bool unsafe_yyjson_mut_equals(yyjson_mut_val *lhs, yyjson_mut_val *rhs) { + yyjson_type type = unsafe_yyjson_get_type(lhs); + if (type != unsafe_yyjson_get_type(rhs)) return false; + + switch (type) { + case YYJSON_TYPE_OBJ: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + yyjson_mut_obj_iter iter; + yyjson_mut_obj_iter_init(rhs, &iter); + lhs = (yyjson_mut_val *)lhs->uni.ptr; + while (len-- > 0) { + rhs = yyjson_mut_obj_iter_getn(&iter, lhs->uni.str, + unsafe_yyjson_get_len(lhs)); + if (!rhs) return false; + if (!unsafe_yyjson_mut_equals(lhs->next, rhs)) return false; + lhs = lhs->next->next; + } + } + /* yyjson allows duplicate keys, so the check may be inaccurate */ + return true; + } + + case YYJSON_TYPE_ARR: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + lhs = (yyjson_mut_val *)lhs->uni.ptr; + rhs = (yyjson_mut_val *)rhs->uni.ptr; + while (len-- > 0) { + if (!unsafe_yyjson_mut_equals(lhs, rhs)) return false; + lhs = lhs->next; + rhs = rhs->next; + } + } + return true; + } + + case YYJSON_TYPE_NUM: + return unsafe_yyjson_num_equals(lhs, rhs); + + case YYJSON_TYPE_RAW: + case YYJSON_TYPE_STR: + return unsafe_yyjson_str_equals(lhs, rhs); + + case YYJSON_TYPE_NULL: + case YYJSON_TYPE_BOOL: + return lhs->tag == rhs->tag; + + default: + return false; + } +} + +bool yyjson_locate_pos(const char *str, size_t len, size_t pos, + size_t *line, size_t *col, size_t *chr) { + usize line_sum = 0, line_pos = 0, chr_sum = 0; + const u8 *cur = (const u8 *)str; + const u8 *end = cur + pos; + + if (!str || pos > len) { + if (line) *line = 0; + if (col) *col = 0; + if (chr) *chr = 0; + return false; + } + + if (pos >= 3 && is_utf8_bom(cur)) cur += 3; /* don't count BOM */ + while (cur < end) { + u8 c = *cur; + chr_sum += 1; + if (likely(c < 0x80)) { /* 0xxxxxxx (0x00-0x7F) ASCII */ + if (c == '\n') { + line_sum += 1; + line_pos = chr_sum; + } + cur += 1; + } + else if (c < 0xC0) cur += 1; /* 10xxxxxx (0x80-0xBF) Invalid */ + else if (c < 0xE0) cur += 2; /* 110xxxxx (0xC0-0xDF) 2-byte UTF-8 */ + else if (c < 0xF0) cur += 3; /* 1110xxxx (0xE0-0xEF) 3-byte UTF-8 */ + else if (c < 0xF8) cur += 4; /* 11110xxx (0xF0-0xF7) 4-byte UTF-8 */ + else cur += 1; /* 11111xxx (0xF8-0xFF) Invalid */ + } + if (line) *line = line_sum + 1; + if (col) *col = chr_sum - line_pos + 1; + if (chr) *chr = chr_sum; + return true; +} + + + +#if !YYJSON_DISABLE_READER /* reader begin */ + +/* Check read flag, avoids `always false` warning when disabled. */ +#define has_flg(_flg) unlikely(has_rflag(flg, YYJSON_READ_##_flg, 0)) +#define has_allow(_flg) unlikely(has_rflag(flg, YYJSON_READ_ALLOW_##_flg, 1)) +#define YYJSON_READ_ALLOW_TRIVIA (YYJSON_READ_ALLOW_COMMENTS | \ + YYJSON_READ_ALLOW_EXT_WHITESPACE) +static_inline bool has_rflag(yyjson_read_flag flg, yyjson_read_flag chk, + bool non_standard) { +#if YYJSON_DISABLE_NON_STANDARD + if (non_standard) return false; +#endif + return (flg & chk) != 0; +} + + + +/*============================================================================== + * MARK: - JSON Reader Utils (Private) + * These functions are used by JSON reader to read literals and comments. + *============================================================================*/ + +/** Read `true` literal, `*ptr[0]` should be `t`. */ +static_inline bool read_true(u8 **ptr, yyjson_val *val) { + u8 *cur = *ptr; + if (likely(byte_match_4(cur, "true"))) { + val->tag = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_TRUE; + *ptr = cur + 4; + return true; + } + return false; +} + +/** Read `false` literal, `*ptr[0]` should be `f`. */ +static_inline bool read_false(u8 **ptr, yyjson_val *val) { + u8 *cur = *ptr; + if (likely(byte_match_4(cur + 1, "alse"))) { + val->tag = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_FALSE; + *ptr = cur + 5; + return true; + } + return false; +} + +/** Read `null` literal, `*ptr[0]` should be `n`. */ +static_inline bool read_null(u8 **ptr, yyjson_val *val) { + u8 *cur = *ptr; + if (likely(byte_match_4(cur, "null"))) { + val->tag = YYJSON_TYPE_NULL; + *ptr = cur + 4; + return true; + } + return false; +} + +/** Read `Inf` or `Infinity` literal (ignoring case). */ +static_inline bool read_inf(u8 **ptr, u8 **pre, + yyjson_read_flag flg, yyjson_val *val) { + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + bool sign = (*cur == '-'); + if (*cur == '+' && !has_allow(EXT_NUMBER)) return false; + cur += char_is_sign(*cur); + if (char_to_lower(cur[0]) == 'i' && + char_to_lower(cur[1]) == 'n' && + char_to_lower(cur[2]) == 'f') { + if (char_to_lower(cur[3]) == 'i') { + if (char_to_lower(cur[4]) == 'n' && + char_to_lower(cur[5]) == 'i' && + char_to_lower(cur[6]) == 't' && + char_to_lower(cur[7]) == 'y') { + cur += 8; + } else { + return false; + } + } else { + cur += 3; + } + *end = cur; + if (has_flg(NUMBER_AS_RAW)) { + **pre = '\0'; /* add null-terminator for previous raw string */ + *pre = cur; /* save end position for current raw string */ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; + val->uni.str = (const char *)hdr; + } else { + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + val->uni.u64 = f64_bits_inf(sign); + } + return true; + } + return false; +} + +/** Read `NaN` literal (ignoring case). */ +static_inline bool read_nan(u8 **ptr, u8 **pre, + yyjson_read_flag flg, yyjson_val *val) { + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + bool sign = (*cur == '-'); + if (*cur == '+' && !has_allow(EXT_NUMBER)) return false; + cur += char_is_sign(*cur); + if (char_to_lower(cur[0]) == 'n' && + char_to_lower(cur[1]) == 'a' && + char_to_lower(cur[2]) == 'n') { + cur += 3; + *end = cur; + if (has_flg(NUMBER_AS_RAW)) { + **pre = '\0'; /* add null-terminator for previous raw string */ + *pre = cur; /* save end position for current raw string */ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; + val->uni.str = (const char *)hdr; + } else { + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + val->uni.u64 = f64_bits_nan(sign); + } + return true; + } + return false; +} + +/** Read `Inf`, `Infinity` or `NaN` literal (ignoring case). */ +static_inline bool read_inf_or_nan(u8 **ptr, u8 **pre, + yyjson_read_flag flg, yyjson_val *val) { + if (read_inf(ptr, pre, flg, val)) return true; + if (read_nan(ptr, pre, flg, val)) return true; + return false; +} + +/** Read a JSON number as raw string. */ +static_noinline bool read_num_raw(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { +#define return_err(_pos, _msg) do { \ + *msg = _msg; *end = _pos; return false; \ +} while (false) + +#define return_raw() do { \ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; \ + val->uni.str = (const char *)hdr; \ + **pre = '\0'; *pre = cur; *end = cur; return true; \ +} while (false) + + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + + /* skip sign */ + cur += (*cur == '-'); + + /* read first digit, check leading zero */ + while (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + if (*cur == '+' && cur == hdr) { /* leading `+` sign */ + cur++; + continue; + } + if (*cur == '.' && char_is_digit(cur[1])) { /* e.g. '.123' */ + goto read_double; + } + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(ptr, pre, flg, val)) return true; + } + return_err(cur, "no digit after sign"); + } + + /* read integral part */ + if (*cur == '0') { + cur++; + if (unlikely(char_is_digit(*cur))) { + return_err(cur - 1, "number with leading zero is not allowed"); + } + if (!char_is_fp(*cur)) { + if (has_allow(EXT_NUMBER) && char_to_lower(*cur) == 'x') { /* hex */ + if (!char_is_hex(*++cur)) return_err(cur, "invalid hex number"); + while(char_is_hex(*cur)) cur++; + } + return_raw(); + } + } else { + while (char_is_digit(*cur)) cur++; + if (!char_is_fp(*cur)) return_raw(); + } + +read_double: + /* read fraction part */ + if (*cur == '.') { + cur++; + if (!char_is_digit(*cur)) { + if (has_allow(EXT_NUMBER)) { + if (!char_is_exp(*cur)) return_raw(); + } else { + return_err(cur, "no digit after decimal point"); + } + } + while (char_is_digit(*cur)) cur++; + } + + /* read exponent part */ + if (char_is_exp(*cur)) { + cur += 1 + char_is_sign(cur[1]); + if (!char_is_digit(*cur++)) { + return_err(cur, "no digit after exponent sign"); + } + while (char_is_digit(*cur)) cur++; + } + + return_raw(); + +#undef return_err +#undef return_raw +} + +/** Read a hex number. */ +static_noinline bool read_num_hex(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + u64 sig = 0, i = 0; + bool sign; + + /* skip sign and '0x' */ + sign = (*cur == '-'); + cur += (*cur == '-' || *cur == '+') + 2; + + /* read hex */ + for(; i < 16; i++) { + u8 c = hex_conv_table[cur[i]]; + if (c == 0xF0) break; + sig <<= 4; + sig |= c; + } + + /* check error */ + if (unlikely(i == 0)) { + *msg = "invalid hex number"; + return false; + } + + /* check overflow */ + if (unlikely(i == 16)) { + if (char_is_hex(cur[16]) || (sign && sig > ((u64)1 << 63))) { + if (!has_flg(BIGNUM_AS_RAW)) { + *msg = "hex number overflow"; + return false; + } + cur += 16; + while (char_is_hex(*cur)) cur++; + **pre = '\0'; + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; + val->uni.str = (const char *)hdr; + *pre = cur; *end = cur; + return true; + } + } + + val->tag = YYJSON_TYPE_NUM | (u64)((u8)sign << 3); + val->uni.u64 = (u64)(sign ? (u64)(~(sig) + 1) : (u64)(sig)); + *end = cur + i; + return true; +} + +/** + Skip trivia (whitespace and comments). + This function should be used only when `char_is_trivia()` returns true. + @param ptr (inout) Input current position, output end position. + @param eof JSON end position. + @param flg JSON read flags. + @return true if at least one character was skipped. + false if no characters were skipped, + or if a multi-line comment is unterminated; + in the latter case, `ptr` will be set to `eof`. + */ +static_noinline bool skip_trivia(u8 **ptr, u8 *eof, yyjson_read_flag flg) { + u8 *hdr = *ptr, *cur = *ptr; + usize len; + + while (cur < eof) { + u8 *loop_begin = cur; + + /* skip standard whitespace */ + while(char_is_space(*cur)) cur++; + + /* skip extended whitespace */ + if (has_allow(EXT_WHITESPACE)) { + while (char_is_space_ext(*cur)) { + cur += (len = ext_space_len(cur)); + if (!len) break; + } + } + + /* skip comment, do not validate encoding */ + if (has_allow(COMMENTS) && cur[0] == '/') { + if (cur[1] == '/') { /* single-line comment */ + cur += 2; + if (has_allow(EXT_WHITESPACE)) { + while (cur < eof) { + if (char_is_eol_ext(*cur)) { + cur += (len = ext_eol_len(cur)); + if (len) break; + } + cur++; + } + } else { + while (cur < eof && !char_is_eol(*cur)) cur++; + } + } else if (cur[1] == '*') { /* multi-line comment */ + cur += 2; + while (!byte_match_2(cur, "*/") && cur < eof) cur++; + if (cur == eof) { + *ptr = eof; + return false; /* unclosed comment */ + } + cur += 2; + } + } + if (cur == loop_begin) break; + } + *ptr = cur; + return cur > hdr; +} + +/** + Check truncated UTF-8 character. + Return true if `cur` starts a valid UTF-8 sequence that is truncated. + */ +static bool is_truncated_utf8(u8 *cur, u8 *eof) { + u8 c0, c1, c2; + usize len = (usize)(eof - cur); + if (cur >= eof || len >= 4) return false; + c0 = cur[0]; c1 = cur[1]; c2 = cur[2]; + /* 1-byte UTF-8, not truncated */ + if (c0 < 0x80) return false; + if (len == 1) { + /* 2-byte UTF-8, truncated */ + if ((c0 & 0xE0) == 0xC0 && (c0 & 0x1E) != 0x00) return true; + /* 3-byte UTF-8, truncated */ + if ((c0 & 0xF0) == 0xE0) return true; + /* 4-byte UTF-8, truncated */ + if ((c0 & 0xF8) == 0xF0 && (c0 & 0x07) <= 0x04) return true; + } else if (len == 2) { + /* 3-byte UTF-8, truncated */ + if ((c0 & 0xF0) == 0xE0 && (c1 & 0xC0) == 0x80) { + u8 t = (u8)(((c0 & 0x0F) << 1) | ((c1 & 0x20) >> 5)); + return 0x01 <= t && t != 0x1B; + } + /* 4-byte UTF-8, truncated */ + if ((c0 & 0xF8) == 0xF0 && (c1 & 0xC0) == 0x80) { + u8 t = (u8)(((c0 & 0x07) << 2) | ((c1 & 0x30) >> 4)); + return 0x01 <= t && t <= 0x10; + } + } else if (len == 3) { + /* 4 bytes UTF-8, truncated */ + if ((c0 & 0xF8) == 0xF0 && (c1 & 0xC0) == 0x80 && (c2 & 0xC0) == 0x80) { + u8 t = (u8)(((c0 & 0x07) << 2) | ((c1 & 0x30) >> 4)); + return 0x01 <= t && t <= 0x10; + } + } + return false; +} + +/** + Check truncated string. + Returns true if `cur` match `str` but is truncated. + The `str` should be lowercase ASCII letters. + */ +static bool is_truncated_str(u8 *cur, u8 *eof, const char *str, + bool case_sensitive) { + usize len = strlen(str); + if (cur + len <= eof || eof <= cur) return false; + if (case_sensitive) { + return memcmp(cur, str, (usize)(eof - cur)) == 0; + } + for (; cur < eof; cur++, str++) { + if (char_to_lower(*cur) != *(const u8 *)str) return false; + } + return true; +} + +/** + Check truncated JSON on parsing errors. + Returns true if the input is valid but truncated. + */ +static_noinline bool is_truncated_end(u8 *hdr, u8 *cur, u8 *eof, + yyjson_read_code code, + yyjson_read_flag flg) { + if (cur >= eof) return true; + if (code == YYJSON_READ_ERROR_LITERAL) { + if (is_truncated_str(cur, eof, "true", true) || + is_truncated_str(cur, eof, "false", true) || + is_truncated_str(cur, eof, "null", true)) { + return true; + } + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CHARACTER || + code == YYJSON_READ_ERROR_INVALID_NUMBER || + code == YYJSON_READ_ERROR_LITERAL) { + if (has_allow(INF_AND_NAN)) { + if (*cur == '-') cur++; + if (is_truncated_str(cur, eof, "infinity", false) || + is_truncated_str(cur, eof, "nan", false)) { + return true; + } + } + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CONTENT) { + if (has_allow(INF_AND_NAN)) { + if (hdr + 3 <= cur && + is_truncated_str(cur - 3, eof, "infinity", false)) { + return true; /* e.g. infin would be read as inf + in */ + } + } + } + if (code == YYJSON_READ_ERROR_INVALID_STRING) { + usize len = (usize)(eof - cur); + + /* unicode escape sequence */ + if (*cur == '\\') { + if (len == 1) return true; + if (len <= 5) { + if (*++cur != 'u') return false; + for (++cur; cur < eof; cur++) { + if (!char_is_hex(*cur)) return false; + } + return true; + } else if (len <= 11) { + /* incomplete surrogate pair? */ + u16 hi; + if (*++cur != 'u') return false; + if (!hex_load_4(++cur, &hi)) return false; + if ((hi & 0xF800) != 0xD800) return false; + cur += 4; + if (cur >= eof) return true; + /* valid low surrogate is DC00...DFFF */ + if (*cur != '\\') return false; + if (++cur >= eof) return true; + if (*cur != 'u') return false; + if (++cur >= eof) return true; + if (*cur != 'd' && *cur != 'D') return false; + if (++cur >= eof) return true; + if ((*cur < 'c' || *cur > 'f') && (*cur < 'C' || *cur > 'F')) + return false; + if (++cur >= eof) return true; + if (!char_is_hex(*cur)) return false; + return true; + } + return false; + } + + /* 2 to 4 bytes UTF-8 */ + if (is_truncated_utf8(cur, eof)) { + return true; + } + } + if (has_allow(COMMENTS)) { + if (code == YYJSON_READ_ERROR_INVALID_COMMENT) { + /* unclosed multiline comment */ + return true; + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CHARACTER && + *cur == '/' && cur + 1 == eof) { + /* truncated beginning of comment */ + return true; + } + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CHARACTER && + has_allow(BOM)) { + /* truncated UTF-8 BOM */ + usize len = (usize)(eof - cur); + if (cur == hdr && len < 3 && !memcmp(hdr, "\xEF\xBB\xBF", len)) { + return true; + } + } + return false; +} + + + +#if !YYJSON_DISABLE_FAST_FP_CONV /* FP_READER */ + +/*============================================================================== + * MARK: - BigInt For Floating Point Number Reader (Private) + * + * The bigint algorithm is used by floating-point number reader to get correctly + * rounded result for numbers with lots of digits. This part of code is rarely + * used for common numbers. + *============================================================================*/ + +/** Unsigned arbitrarily large integer */ +typedef struct bigint { + u32 used; /* used chunks count, should not be 0 */ + u64 bits[64]; /* chunks (58 is enough here) */ +} bigint; + +/** + Evaluate 'big += val'. + @param big A big number (can be 0). + @param val An unsigned integer (can be 0). + */ +static_inline void bigint_add_u64(bigint *big, u64 val) { + u32 idx, max; + u64 num = big->bits[0]; + u64 add = num + val; + big->bits[0] = add; + if (likely((add >= num) || (add >= val))) return; + for ((void)(idx = 1), max = big->used; idx < max; idx++) { + if (likely(big->bits[idx] != U64_MAX)) { + big->bits[idx] += 1; + return; + } + big->bits[idx] = 0; + } + big->bits[big->used++] = 1; +} + +/** + Evaluate 'big *= val'. + @param big A big number (can be 0). + @param val An unsigned integer (cannot be 0). + */ +static_inline void bigint_mul_u64(bigint *big, u64 val) { + u32 idx = 0, max = big->used; + u64 hi, lo, carry = 0; + for (; idx < max; idx++) { + if (big->bits[idx]) break; + } + for (; idx < max; idx++) { + u128_mul_add(big->bits[idx], val, carry, &hi, &lo); + big->bits[idx] = lo; + carry = hi; + } + if (carry) big->bits[big->used++] = carry; +} + +/** + Evaluate 'big *= 2^exp'. + @param big A big number (can be 0). + @param exp An exponent integer (can be 0). + */ +static_inline void bigint_mul_pow2(bigint *big, u32 exp) { + u32 shft = exp % 64; + u32 move = exp / 64; + u32 idx = big->used; + if (unlikely(shft == 0)) { + for (; idx > 0; idx--) { + big->bits[idx + move - 1] = big->bits[idx - 1]; + } + big->used += move; + while (move) big->bits[--move] = 0; + } else { + big->bits[idx] = 0; + for (; idx > 0; idx--) { + u64 num = big->bits[idx] << shft; + num |= big->bits[idx - 1] >> (64 - shft); + big->bits[idx + move] = num; + } + big->bits[move] = big->bits[0] << shft; + big->used += move + (big->bits[big->used + move] > 0); + while (move) big->bits[--move] = 0; + } +} + +/** + Evaluate 'big *= 10^exp'. + @param big A big number (can be 0). + @param exp An exponent integer (cannot be 0). + */ +static_inline void bigint_mul_pow10(bigint *big, i32 exp) { + for (; exp >= U64_POW10_MAX_EXACT_EXP; exp -= U64_POW10_MAX_EXACT_EXP) { + bigint_mul_u64(big, u64_pow10_table[U64_POW10_MAX_EXACT_EXP]); + } + if (exp) { + bigint_mul_u64(big, u64_pow10_table[exp]); + } +} + +/** + Compare two bigint. + @return -1 if 'a < b', +1 if 'a > b', 0 if 'a == b'. + */ +static_inline i32 bigint_cmp(bigint *a, bigint *b) { + u32 idx = a->used; + if (a->used < b->used) return -1; + if (a->used > b->used) return +1; + while (idx-- > 0) { + u64 av = a->bits[idx]; + u64 bv = b->bits[idx]; + if (av < bv) return -1; + if (av > bv) return +1; + } + return 0; +} + +/** + Evaluate 'big = val'. + @param big A big number (can be 0). + @param val An unsigned integer (can be 0). + */ +static_inline void bigint_set_u64(bigint *big, u64 val) { + big->used = 1; + big->bits[0] = val; +} + +/** Set a bigint with floating point number string. */ +static_noinline void bigint_set_buf(bigint *big, u64 sig, i32 *exp, + u8 *sig_cut, u8 *sig_end, u8 *dot_pos) { + + if (unlikely(!sig_cut)) { + /* no digit cut, set significant part only */ + bigint_set_u64(big, sig); + return; + + } else { + /* some digits were cut, read them from 'sig_cut' to 'sig_end' */ + u8 *hdr = sig_cut; + u8 *cur = hdr; + u32 len = 0; + u64 val = 0; + bool dig_big_cut = false; + bool has_dot = (hdr < dot_pos) & (dot_pos < sig_end); + u32 dig_len_total = U64_SAFE_DIG + (u32)(sig_end - hdr) - has_dot; + + sig -= (*sig_cut >= '5'); /* sig was rounded before */ + if (dig_len_total > F64_MAX_DEC_DIG) { + dig_big_cut = true; + sig_end -= dig_len_total - (F64_MAX_DEC_DIG + 1); + sig_end -= (dot_pos + 1 == sig_end); + dig_len_total = (F64_MAX_DEC_DIG + 1); + } + *exp -= (i32)dig_len_total - U64_SAFE_DIG; + + big->used = 1; + big->bits[0] = sig; + while (cur < sig_end) { + if (likely(cur != dot_pos)) { + val = val * 10 + (u8)(*cur++ - '0'); + len++; + if (unlikely(cur == sig_end && dig_big_cut)) { + /* The last digit must be non-zero, */ + /* set it to '1' for correct rounding. */ + val = val - (val % 10) + 1; + } + if (len == U64_SAFE_DIG || cur == sig_end) { + bigint_mul_pow10(big, (i32)len); + bigint_add_u64(big, val); + val = 0; + len = 0; + } + } else { + cur++; + } + } + } +} + + + +/*============================================================================== + * MARK: - Diy Floating Point (Private) + *============================================================================*/ + +/** "Do It Yourself Floating Point" struct. */ +typedef struct diy_fp { + u64 sig; /* significand */ + i32 exp; /* exponent, base 2 */ + i32 pad; /* padding, useless */ +} diy_fp; + +/** Get cached rounded diy_fp with pow(10, e) The input value must in range + [POW10_SIG_TABLE_MIN_EXP, POW10_SIG_TABLE_MAX_EXP]. */ +static_inline diy_fp diy_fp_get_cached_pow10(i32 exp10) { + diy_fp fp; + u64 sig_ext; + pow10_table_get_sig(exp10, &fp.sig, &sig_ext); + pow10_table_get_exp(exp10, &fp.exp); + fp.sig += (sig_ext >> 63); + return fp; +} + +/** Returns fp * fp2. */ +static_inline diy_fp diy_fp_mul(diy_fp fp, diy_fp fp2) { + u64 hi, lo; + u128_mul(fp.sig, fp2.sig, &hi, &lo); + fp.sig = hi + (lo >> 63); + fp.exp += fp2.exp + 64; + return fp; +} + +/** Convert diy_fp to IEEE-754 raw value. */ +static_inline u64 diy_fp_to_ieee_raw(diy_fp fp) { + u64 sig = fp.sig; + i32 exp = fp.exp; + u32 lz_bits; + if (unlikely(fp.sig == 0)) return 0; + + lz_bits = u64_lz_bits(sig); + sig <<= lz_bits; + sig >>= F64_BITS - F64_SIG_FULL_BITS; + exp -= (i32)lz_bits; + exp += F64_BITS - F64_SIG_FULL_BITS; + exp += F64_SIG_BITS; + + if (unlikely(exp >= F64_MAX_BIN_EXP)) { + /* overflow */ + return F64_BITS_INF; + } else if (likely(exp >= F64_MIN_BIN_EXP - 1)) { + /* normal */ + exp += F64_EXP_BIAS; + return ((u64)exp << F64_SIG_BITS) | (sig & F64_SIG_MASK); + } else if (likely(exp >= F64_MIN_BIN_EXP - F64_SIG_FULL_BITS)) { + /* subnormal */ + return sig >> (F64_MIN_BIN_EXP - exp - 1); + } else { + /* underflow */ + return 0; + } +} + + + +/*============================================================================== + * MARK: - Number Reader (Private) + *============================================================================*/ + +/** + Read a JSON number. + + 1. This function assume that the floating-point number is in IEEE-754 format. + 2. This function support uint64/int64/double number. If an integer number + cannot fit in uint64/int64, it will returns as a double number. If a double + number is infinite, the return value is based on flag. + 3. This function (with inline attribute) may generate a lot of instructions. + */ +static_inline bool read_num(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { +#define return_err(_pos, _msg) do { \ + *msg = _msg; \ + *end = _pos; \ + return false; \ +} while (false) + +#define return_0() do { \ + val->tag = YYJSON_TYPE_NUM | (u8)((u8)sign << 3); \ + val->uni.u64 = 0; \ + *end = cur; return true; \ +} while (false) + +#define return_i64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | (u8)((u8)sign << 3); \ + val->uni.u64 = (u64)(sign ? (u64)(~(_v) + 1) : (u64)(_v)); \ + *end = cur; return true; \ +} while (false) + +#define return_f64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.f64 = sign ? -(f64)(_v) : (f64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_f64_bin(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.u64 = ((u64)sign << 63) | (u64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_inf() do { \ + if (has_flg(BIGNUM_AS_RAW)) return_raw(); \ + if (has_allow(INF_AND_NAN)) return_f64_bin(F64_BITS_INF); \ + else return_err(hdr, "number is infinity when parsed as double"); \ +} while (false) + +#define return_raw() do { \ + **pre = '\0'; /* add null-terminator for previous raw string */ \ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; \ + val->uni.str = (const char *)hdr; \ + *pre = cur; *end = cur; return true; \ +} while (false) + + u8 *sig_cut = NULL; /* significant part cutting position for long number */ + u8 *sig_end = NULL; /* significant part ending position */ + u8 *dot_pos = NULL; /* decimal point position */ + + u64 sig = 0; /* significant part of the number */ + i32 exp = 0; /* exponent part of the number */ + + bool exp_sign; /* temporary exponent sign from literal part */ + i64 exp_sig = 0; /* temporary exponent number from significant part */ + i64 exp_lit = 0; /* temporary exponent number from exponent literal part */ + u64 num; /* temporary number for reading */ + u8 *tmp; /* temporary cursor for reading */ + + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + bool sign; + + /* read number as raw string if has `YYJSON_READ_NUMBER_AS_RAW` flag */ + if (has_flg(NUMBER_AS_RAW)) { + return read_num_raw(ptr, pre, flg, val, msg); + } + + sign = (*hdr == '-'); + cur += sign; + + /* begin with a leading zero or non-digit */ + while (unlikely(!char_is_nonzero(*cur))) { /* 0 or non-digit char */ + if (unlikely(*cur != '0')) { /* non-digit char */ + if (has_allow(EXT_NUMBER)) { + if (*cur == '+' && cur == hdr) { /* leading `+` sign */ + cur++; + continue; + } + if (*cur == '.' && char_is_digit(cur[1])) { /* e.g. '.123' */ + goto leading_dot; + } + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(ptr, pre, flg, val)) return true; + } + return_err(cur, "no digit after sign"); + } + /* begin with 0 */ + if (likely(!char_is_digit_or_fp(*++cur))) { + if (has_allow(EXT_NUMBER) && char_to_lower(*cur) == 'x') { /* hex */ + return read_num_hex(ptr, pre, flg, val, msg); + } + return_0(); + } + if (likely(*cur == '.')) { +leading_dot: + dot_pos = cur++; + if (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + if (char_is_exp(*cur)) { + goto digi_exp_more; + } else { + return_f64_bin(0); + } + } + return_err(cur, "no digit after decimal point"); + } + while (unlikely(*cur == '0')) cur++; + if (likely(char_is_digit(*cur))) { + /* first non-zero digit after decimal point */ + sig = (u64)(*cur - '0'); /* read first digit */ + cur--; + goto digi_frac_1; /* continue read fraction part */ + } + } + if (unlikely(char_is_digit(*cur))) { + return_err(cur - 1, "number with leading zero is not allowed"); + } + if (unlikely(char_is_exp(*cur))) { /* 0 with any exponent is still 0 */ + cur += (usize)1 + char_is_sign(cur[1]); + if (unlikely(!char_is_digit(*cur))) { + return_err(cur, "no digit after exponent sign"); + } + while (char_is_digit(*++cur)); + } + return_f64_bin(0); + } + + /* begin with non-zero digit */ + sig = (u64)(*cur - '0'); + + /* + Read integral part, same as the following code. + + for (int i = 1; i <= 18; i++) { + num = cur[i] - '0'; + if (num <= 9) sig = num + sig * 10; + else goto digi_sepr_i; + } + */ +#define expr_intg(i) \ + if (likely((num = (u64)(cur[i] - (u8)'0')) <= 9)) sig = num + sig * 10; \ + else { goto digi_sepr_##i; } + repeat_in_1_18(expr_intg) +#undef expr_intg + + + cur += 19; /* skip continuous 19 digits */ + if (!char_is_digit_or_fp(*cur)) { + /* this number is an integer consisting of 19 digits */ + if (sign && (sig > ((u64)1 << 63))) { /* overflow */ + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + goto digi_intg_more; /* read more digits in integral part */ + + + /* process first non-digit character */ +#define expr_sepr(i) \ + digi_sepr_##i: \ + if (likely(!char_is_fp(cur[i]))) { cur += i; return_i64(sig); } \ + dot_pos = cur + i; \ + if (likely(cur[i] == '.')) goto digi_frac_##i; \ + cur += i; sig_end = cur; goto digi_exp_more; + repeat_in_1_18(expr_sepr) +#undef expr_sepr + + + /* read fraction part */ +#define expr_frac(i) \ + digi_frac_##i: \ + if (likely((num = (u64)(cur[i + 1] - (u8)'0')) <= 9)) \ + sig = num + sig * 10; \ + else { goto digi_stop_##i; } + repeat_in_1_18(expr_frac) +#undef expr_frac + + cur += 20; /* skip 19 digits and 1 decimal point */ + if (!char_is_digit(*cur)) goto digi_frac_end; /* fraction part end */ + goto digi_frac_more; /* read more digits in fraction part */ + + + /* significant part end */ +#define expr_stop(i) \ + digi_stop_##i: \ + cur += i + 1; \ + goto digi_frac_end; + repeat_in_1_18(expr_stop) +#undef expr_stop + + + /* read more digits in integral part */ +digi_intg_more: + if (char_is_digit(*cur)) { + if (!char_is_digit_or_fp(cur[1])) { + /* this number is an integer consisting of 20 digits */ + num = (u64)(*cur - '0'); + if ((sig < (U64_MAX / 10)) || + (sig == (U64_MAX / 10) && num <= (U64_MAX % 10))) { + sig = num + sig * 10; + cur++; + /* convert to double if overflow */ + if (sign) { + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + } + } + + if (char_is_exp(*cur)) { + dot_pos = cur; + goto digi_exp_more; + } + + if (*cur == '.') { + dot_pos = cur++; + if (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + goto digi_frac_end; + } + return_err(cur, "no digit after decimal point"); + } + } + + + /* read more digits in fraction part */ +digi_frac_more: + sig_cut = cur; /* too large to fit in u64, excess digits need to be cut */ + sig += (*cur >= '5'); /* round */ + while (char_is_digit(*++cur)); + if (!dot_pos) { + if (!char_is_fp(*cur) && has_flg(BIGNUM_AS_RAW)) { + return_raw(); /* it's a large integer */ + } + dot_pos = cur; + if (*cur == '.') { + if (unlikely(!char_is_digit(*++cur))) { + if (!has_allow(EXT_NUMBER)) { + return_err(cur, "no digit after decimal point"); + } + } + while (char_is_digit(*cur)) cur++; + } + } + exp_sig = (i64)(dot_pos - sig_cut); + exp_sig += (dot_pos < sig_cut); + + /* ignore trailing zeros */ + tmp = cur - 1; + while ((*tmp == '0' || *tmp == '.') && tmp > hdr) tmp--; + if (tmp < sig_cut) { + sig_cut = NULL; + } else { + sig_end = cur; + } + + if (char_is_exp(*cur)) goto digi_exp_more; + goto digi_exp_finish; + + + /* fraction part end */ +digi_frac_end: + if (unlikely(dot_pos + 1 == cur)) { + if (!has_allow(EXT_NUMBER)) { + return_err(cur, "no digit after decimal point"); + } + } + sig_end = cur; + exp_sig = -(i64)((u64)(cur - dot_pos) - 1); + if (likely(!char_is_exp(*cur))) { + if (unlikely(exp_sig < F64_MIN_DEC_EXP - 19)) { + return_f64_bin(0); /* underflow */ + } + exp = (i32)exp_sig; + goto digi_finish; + } else { + goto digi_exp_more; + } + + + /* read exponent part */ +digi_exp_more: + exp_sign = (*++cur == '-'); + cur += char_is_sign(*cur); + if (unlikely(!char_is_digit(*cur))) { + return_err(cur, "no digit after exponent sign"); + } + while (*cur == '0') cur++; + + /* read exponent literal */ + tmp = cur; + while (char_is_digit(*cur)) { + exp_lit = (i64)((u8)(*cur++ - '0') + (u64)exp_lit * 10); + } + if (unlikely(cur - tmp >= U64_SAFE_DIG)) { + if (exp_sign) { + return_f64_bin(0); /* underflow */ + } else { + return_inf(); /* overflow */ + } + } + exp_sig += exp_sign ? -exp_lit : exp_lit; + + + /* validate exponent value */ +digi_exp_finish: + if (unlikely(exp_sig < F64_MIN_DEC_EXP - 19)) { + return_f64_bin(0); /* underflow */ + } + if (unlikely(exp_sig > F64_MAX_DEC_EXP)) { + return_inf(); /* overflow */ + } + exp = (i32)exp_sig; + + + /* all digit read finished */ +digi_finish: + + /* + Fast path 1: + + 1. The floating-point number calculation should be accurate, see the + comments of macro `YYJSON_DOUBLE_MATH_CORRECT`. + 2. Correct rounding should be performed (fegetround() == FE_TONEAREST). + 3. The input of floating point number calculation does not lose precision, + which means: 64 - leading_zero(input) - trailing_zero(input) < 53. + + We don't check all available inputs here, because that would make the code + more complicated, and not friendly to branch predictor. + */ +#if YYJSON_DOUBLE_MATH_CORRECT + if (sig < ((u64)1 << 53) && + exp >= -F64_POW10_MAX_EXACT_EXP && + exp <= +F64_POW10_MAX_EXACT_EXP) { + f64 dbl = (f64)sig; + if (exp < 0) { + dbl /= f64_pow10_table[-exp]; + } else { + dbl *= f64_pow10_table[+exp]; + } + return_f64(dbl); + } +#endif + + /* + Fast path 2: + + To keep it simple, we only accept normal number here, + let the slow path to handle subnormal and infinity number. + */ + if (likely(!sig_cut && + exp > -F64_MAX_DEC_EXP + 1 && + exp < +F64_MAX_DEC_EXP - 20)) { + /* + The result value is exactly equal to (sig * 10^exp), + the exponent part (10^exp) can be converted to (sig2 * 2^exp2). + + The sig2 can be an infinite length number, only the highest 128 bits + is cached in the pow10_sig_table. + + Now we have these bits: + sig1 (normalized 64bit) : aaaaaaaa + sig2 (higher 64bit) : bbbbbbbb + sig2_ext (lower 64bit) : cccccccc + sig2_cut (extra unknown bits) : dddddddddddd.... + + And the calculation process is: + ---------------------------------------- + aaaaaaaa * + bbbbbbbbccccccccdddddddddddd.... + ---------------------------------------- + abababababababab + + acacacacacacacac + + adadadadadadadadadad.... + ---------------------------------------- + [hi____][lo____] + + [hi2___][lo2___] + + [unknown___________....] + ---------------------------------------- + + The addition with carry may affect higher bits, but if there is a 0 + in higher bits, the bits higher than 0 will not be affected. + + `lo2` + `unknown` may get a carry bit and may affect `hi2`, the max + value of `hi2` is 0xFFFFFFFFFFFFFFFE, so `hi2` will not overflow. + + `lo` + `hi2` may also get a carry bit and may affect `hi`, but only + the highest significant 53 bits of `hi` is needed. If there is a 0 + in the lower bits of `hi`, then all the following bits can be dropped. + + To convert the result to IEEE-754 double number, we need to perform + correct rounding: + 1. if bit 54 is 0, round down, + 2. if bit 54 is 1 and any bit beyond bit 54 is 1, round up, + 3. if bit 54 is 1 and all bits beyond bit 54 are 0, round to even, + as the extra bits is unknown, this case will not be handled here. + */ + + u64 raw; + u64 sig1, sig2, sig2_ext, hi, lo, hi2, lo2, add, bits; + i32 exp2; + u32 lz; + bool exact = false, carry, round_up; + + /* convert (10^exp) to (sig2 * 2^exp2) */ + pow10_table_get_sig(exp, &sig2, &sig2_ext); + pow10_table_get_exp(exp, &exp2); + + /* normalize and multiply */ + lz = u64_lz_bits(sig); + sig1 = sig << lz; + exp2 -= (i32)lz; + u128_mul(sig1, sig2, &hi, &lo); + + /* + The `hi` is in range [0x4000000000000000, 0xFFFFFFFFFFFFFFFE], + To get normalized value, `hi` should be shifted to the left by 0 or 1. + + The highest significant 53 bits is used by IEEE-754 double number, + and the bit 54 is used to detect rounding direction. + + The lowest (64 - 54 - 1) bits is used to check whether it contains 0. + */ + bits = hi & (((u64)1 << (64 - 54 - 1)) - 1); + if (bits - 1 < (((u64)1 << (64 - 54 - 1)) - 2)) { + /* + (bits != 0 && bits != 0x1FF) => (bits - 1 < 0x1FF - 1) + The `bits` is not zero, so we don't need to check `round to even` + case. The `bits` contains bit `0`, so we can drop the extra bits + after `0`. + */ + exact = true; + + } else { + /* + (bits == 0 || bits == 0x1FF) + The `bits` is filled with all `0` or all `1`, so we need to check + lower bits with another 64-bit multiplication. + */ + u128_mul(sig1, sig2_ext, &hi2, &lo2); + + add = lo + hi2; + if (add + 1 > (u64)1) { + /* + (add != 0 && add != U64_MAX) => (add + 1 > 1) + The `add` is not zero, so we don't need to check `round to + even` case. The `add` contains bit `0`, so we can drop the + extra bits after `0`. The `hi` cannot be U64_MAX, so it will + not overflow. + */ + carry = add < lo || add < hi2; + hi += carry; + exact = true; + } + } + + if (exact) { + /* normalize */ + lz = hi < ((u64)1 << 63); + hi <<= lz; + exp2 -= (i32)lz; + exp2 += 64; + + /* test the bit 54 and get rounding direction */ + round_up = (hi & ((u64)1 << (64 - 54))) > (u64)0; + hi += (round_up ? ((u64)1 << (64 - 54)) : (u64)0); + + /* test overflow */ + if (hi < ((u64)1 << (64 - 54))) { + hi = ((u64)1 << 63); + exp2 += 1; + } + + /* This is a normal number, convert it to IEEE-754 format. */ + hi >>= F64_BITS - F64_SIG_FULL_BITS; + exp2 += F64_BITS - F64_SIG_FULL_BITS + F64_SIG_BITS; + exp2 += F64_EXP_BIAS; + raw = ((u64)exp2 << F64_SIG_BITS) | (hi & F64_SIG_MASK); + return_f64_bin(raw); + } + } + + /* + Slow path: read double number exactly with diyfp. + 1. Use cached diyfp to get an approximation value. + 2. Use bigcomp to check the approximation value if needed. + + This algorithm refers to google's double-conversion project: + https://github.com/google/double-conversion + */ + { + const i32 ERR_ULP_LOG = 3; + const i32 ERR_ULP = 1 << ERR_ULP_LOG; + const i32 ERR_CACHED_POW = ERR_ULP / 2; + const i32 ERR_MUL_FIXED = ERR_ULP / 2; + const i32 DIY_SIG_BITS = 64; + const i32 EXP_BIAS = F64_EXP_BIAS + F64_SIG_BITS; + const i32 EXP_SUBNORMAL = -EXP_BIAS + 1; + + u64 fp_err; + u32 bits; + i32 order_of_magnitude; + i32 effective_significand_size; + i32 precision_digits_count; + u64 precision_bits; + u64 half_way; + + u64 raw; + diy_fp fp, fp_upper; + bigint big_full, big_comp; + i32 cmp; + + fp.sig = sig; + fp.exp = 0; + fp_err = sig_cut ? (u64)(ERR_ULP / 2) : (u64)0; + + /* normalize */ + bits = u64_lz_bits(fp.sig); + fp.sig <<= bits; + fp.exp -= (i32)bits; + fp_err <<= bits; + + /* multiply and add error */ + fp = diy_fp_mul(fp, diy_fp_get_cached_pow10(exp)); + fp_err += (u64)ERR_CACHED_POW + (fp_err != 0) + (u64)ERR_MUL_FIXED; + + /* normalize */ + bits = u64_lz_bits(fp.sig); + fp.sig <<= bits; + fp.exp -= (i32)bits; + fp_err <<= bits; + + /* effective significand */ + order_of_magnitude = DIY_SIG_BITS + fp.exp; + if (likely(order_of_magnitude >= EXP_SUBNORMAL + F64_SIG_FULL_BITS)) { + effective_significand_size = F64_SIG_FULL_BITS; + } else if (order_of_magnitude <= EXP_SUBNORMAL) { + effective_significand_size = 0; + } else { + effective_significand_size = order_of_magnitude - EXP_SUBNORMAL; + } + + /* precision digits count */ + precision_digits_count = DIY_SIG_BITS - effective_significand_size; + if (unlikely(precision_digits_count + ERR_ULP_LOG >= DIY_SIG_BITS)) { + i32 shr = (precision_digits_count + ERR_ULP_LOG) - DIY_SIG_BITS + 1; + fp.sig >>= shr; + fp.exp += shr; + fp_err = (fp_err >> shr) + 1 + (u32)ERR_ULP; + precision_digits_count -= shr; + } + + /* half way */ + precision_bits = fp.sig & (((u64)1 << precision_digits_count) - 1); + precision_bits *= (u32)ERR_ULP; + half_way = (u64)1 << (precision_digits_count - 1); + half_way *= (u32)ERR_ULP; + + /* rounding */ + fp.sig >>= precision_digits_count; + fp.sig += (precision_bits >= half_way + fp_err); + fp.exp += precision_digits_count; + + /* get IEEE double raw value */ + raw = diy_fp_to_ieee_raw(fp); + if (unlikely(raw == F64_BITS_INF)) return_inf(); + if (likely(precision_bits <= half_way - fp_err || + precision_bits >= half_way + fp_err)) { + return_f64_bin(raw); /* number is accurate */ + } + /* now the number is the correct value, or the next lower value */ + + /* upper boundary */ + if (raw & F64_EXP_MASK) { + fp_upper.sig = (raw & F64_SIG_MASK) + ((u64)1 << F64_SIG_BITS); + fp_upper.exp = (i32)((raw & F64_EXP_MASK) >> F64_SIG_BITS); + } else { + fp_upper.sig = (raw & F64_SIG_MASK); + fp_upper.exp = 1; + } + fp_upper.exp -= F64_EXP_BIAS + F64_SIG_BITS; + fp_upper.sig <<= 1; + fp_upper.exp -= 1; + fp_upper.sig += 1; /* add half ulp */ + + /* compare with bigint */ + bigint_set_buf(&big_full, sig, &exp, sig_cut, sig_end, dot_pos); + bigint_set_u64(&big_comp, fp_upper.sig); + if (exp >= 0) { + bigint_mul_pow10(&big_full, +exp); + } else { + bigint_mul_pow10(&big_comp, -exp); + } + if (fp_upper.exp > 0) { + bigint_mul_pow2(&big_comp, (u32)+fp_upper.exp); + } else { + bigint_mul_pow2(&big_full, (u32)-fp_upper.exp); + } + cmp = bigint_cmp(&big_full, &big_comp); + if (likely(cmp != 0)) { + /* round down or round up */ + raw += (cmp > 0); + } else { + /* falls midway, round to even */ + raw += (raw & 1); + } + + if (unlikely(raw == F64_BITS_INF)) return_inf(); + return_f64_bin(raw); + } + +#undef return_err +#undef return_inf +#undef return_0 +#undef return_i64 +#undef return_f64 +#undef return_f64_bin +#undef return_raw +} + + + +#else /* FP_READER */ + +/** + Read a JSON number. + This is a fallback function if the custom number reader is disabled. + This function use libc's strtod() to read floating-point number. + */ +static_inline bool read_num(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { +#define return_err(_pos, _msg) do { \ + *msg = _msg; \ + *end = _pos; \ + return false; \ +} while (false) + +#define return_0() do { \ + val->tag = YYJSON_TYPE_NUM | (u64)((u8)sign << 3); \ + val->uni.u64 = 0; \ + *end = cur; return true; \ +} while (false) + +#define return_i64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | (u64)((u8)sign << 3); \ + val->uni.u64 = (u64)(sign ? (u64)(~(_v) + 1) : (u64)(_v)); \ + *end = cur; return true; \ +} while (false) + +#define return_f64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.f64 = sign ? -(f64)(_v) : (f64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_f64_bin(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.u64 = ((u64)sign << 63) | (u64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_inf() do { \ + if (has_flg(BIGNUM_AS_RAW)) return_raw(); \ + if (has_allow(INF_AND_NAN)) return_f64_bin(F64_BITS_INF); \ + else return_err(hdr, "number is infinity when parsed as double"); \ +} while (false) + +#define return_raw() do { \ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; \ + val->uni.str = (const char *)hdr; \ + **pre = '\0'; *pre = cur; *end = cur; return true; \ +} while (false) + + u64 sig, num; + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + u8 *dot = NULL; + u8 *f64_end = NULL; + bool sign; + + /* read number as raw string if has `YYJSON_READ_NUMBER_AS_RAW` flag */ + if (has_flg(NUMBER_AS_RAW)) { + return read_num_raw(ptr, pre, flg, val, msg); + } + + sign = (*hdr == '-'); + cur += sign; + sig = (u8)(*cur - '0'); + + /* read first digit, check leading zero */ + while (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + if (*cur == '+' && cur == hdr) { /* leading `+` sign */ + cur++; + sig = (u8)(*cur - '0'); + continue; + } + if (*cur == '.' && char_is_num(cur[1])) { /* no integer part */ + goto read_double; /* e.g. '.123' */ + } + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(ptr, pre, flg, val)) return true; + } + return_err(cur, "no digit after sign"); + } + if (*cur == '0') { + cur++; + if (unlikely(char_is_digit(*cur))) { + return_err(cur - 1, "number with leading zero is not allowed"); + } + if (!char_is_fp(*cur)) { + if (has_allow(EXT_NUMBER) && + (*cur == 'x' || *cur == 'X')) { /* hex integer */ + return read_num_hex(ptr, pre, flg, val, msg); + } + return_0(); + } + goto read_double; + } + + /* read continuous digits, up to 19 characters */ +#define expr_intg(i) \ + if (likely((num = (u64)(cur[i] - (u8)'0')) <= 9)) sig = num + sig * 10; \ + else { cur += i; goto intg_end; } + repeat_in_1_18(expr_intg) +#undef expr_intg + + /* here are 19 continuous digits, skip them */ + cur += 19; + if (char_is_digit(cur[0]) && !char_is_digit_or_fp(cur[1])) { + /* this number is an integer consisting of 20 digits */ + num = (u8)(*cur - '0'); + if ((sig < (U64_MAX / 10)) || + (sig == (U64_MAX / 10) && num <= (U64_MAX % 10))) { + sig = num + sig * 10; + cur++; + if (sign) { + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + } + +intg_end: + /* continuous digits ended */ + if (!char_is_digit_or_fp(*cur)) { + /* this number is an integer consisting of 1 to 19 digits */ + if (sign && (sig > ((u64)1 << 63))) { + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + +read_double: + /* this number should be read as double */ + while (char_is_digit(*cur)) cur++; + if (!char_is_fp(*cur) && has_flg(BIGNUM_AS_RAW)) { + return_raw(); /* it's a large integer */ + } + while (*cur == '.') { + /* skip fraction part */ + dot = cur; + cur++; + if (!char_is_digit(*cur)) { + if (has_allow(EXT_NUMBER)) { + break; + } else { + return_err(cur, "no digit after decimal point"); + } + } + cur++; + while (char_is_digit(*cur)) cur++; + break; + } + if (char_is_exp(*cur)) { + /* skip exponent part */ + cur += 1 + char_is_sign(cur[1]); + if (!char_is_digit(*cur)) { + return_err(cur, "no digit after exponent sign"); + } + cur++; + while (char_is_digit(*cur)) cur++; + } + + /* + libc's strtod() is used to parse the floating-point number. + + Note that the decimal point character used by strtod() is locale-dependent, + and the rounding direction may affected by fesetround(). + + For currently known locales, (en, zh, ja, ko, am, he, hi) use '.' as the + decimal point, while other locales use ',' as the decimal point. + + Here strtod() is called twice for different locales, but if another thread + happens calls setlocale() between two strtod(), parsing may still fail. + */ + val->uni.f64 = strtod((const char *)hdr, (char **)&f64_end); + if (unlikely(f64_end != cur)) { + /* replace '.' with ',' for locale */ + bool cut = (*cur == ','); + if (cut) *cur = ' '; + if (dot) *dot = ','; + val->uni.f64 = strtod((const char *)hdr, (char **)&f64_end); + /* restore ',' to '.' */ + if (cut) *cur = ','; + if (dot) *dot = '.'; + if (unlikely(f64_end != cur)) { + return_err(hdr, "strtod() failed to parse the number"); + } + } + if (unlikely(val->uni.f64 >= HUGE_VAL || val->uni.f64 <= -HUGE_VAL)) { + return_inf(); + } + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + *end = cur; + return true; + +#undef return_err +#undef return_0 +#undef return_i64 +#undef return_f64 +#undef return_f64_bin +#undef return_inf +#undef return_raw +} + +#endif /* FP_READER */ + + + +/*============================================================================== + * MARK: - String Reader (Private) + *============================================================================*/ + +/** Read unicode escape sequence. */ +static_inline bool read_uni_esc(u8 **src_ptr, u8 **dst_ptr, const char **msg) { +#define return_err(_end, _msg) *msg = _msg; *src_ptr = _end; return false + + u8 *src = *src_ptr; + u8 *dst = *dst_ptr; + u16 hi, lo; + u32 uni; + + src += 2; /* skip `\u` */ + if (unlikely(!hex_load_4(src, &hi))) { + return_err(src - 2, "invalid escaped sequence in string"); + } + src += 4; /* skip hex */ + if (likely((hi & 0xF800) != 0xD800)) { + /* a BMP character */ + if (hi >= 0x800) { + *dst++ = (u8)(0xE0 | (hi >> 12)); + *dst++ = (u8)(0x80 | ((hi >> 6) & 0x3F)); + *dst++ = (u8)(0x80 | (hi & 0x3F)); + } else if (hi >= 0x80) { + *dst++ = (u8)(0xC0 | (hi >> 6)); + *dst++ = (u8)(0x80 | (hi & 0x3F)); + } else { + *dst++ = (u8)hi; + } + } else { + /* a non-BMP character, represented as a surrogate pair */ + if (unlikely((hi & 0xFC00) != 0xD800)) { + return_err(src - 6, "invalid high surrogate in string"); + } + if (unlikely(!byte_match_2(src, "\\u"))) { + return_err(src - 6, "no low surrogate in string"); + } + if (unlikely(!hex_load_4(src + 2, &lo))) { + return_err(src - 6, "invalid escape in string"); + } + if (unlikely((lo & 0xFC00) != 0xDC00)) { + return_err(src - 6, "invalid low surrogate in string"); + } + uni = ((((u32)hi - 0xD800) << 10) | + ((u32)lo - 0xDC00)) + 0x10000; + *dst++ = (u8)(0xF0 | (uni >> 18)); + *dst++ = (u8)(0x80 | ((uni >> 12) & 0x3F)); + *dst++ = (u8)(0x80 | ((uni >> 6) & 0x3F)); + *dst++ = (u8)(0x80 | (uni & 0x3F)); + src += 6; + } + *src_ptr = src; + *dst_ptr = dst; + return true; +#undef return_err +} + +/** + Read a JSON string. + @param quo The quote character (single quote or double quote). + @param ptr The head pointer of string before quote (inout). + @param eof JSON end position. + @param flg JSON read flag. + @param val The string value to be written. + @param msg The error message pointer. + @param con Continuation for incremental parsing. + @return Whether success. + */ +static_inline bool read_str_opt(u8 quo, u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg, u8 *con[2]) { + /* + GCC may sometimes load variables into registers too early, causing + unnecessary instructions and performance degradation. This inline assembly + serves as a hint to GCC: 'This variable will be modified, so avoid loading + it too early.' Other compilers like MSVC, Clang, and ICC can generate the + expected instructions without needing this hint. + + Check out this example: https://godbolt.org/z/YG6a5W5Ec + */ +#define return_err(_end, _msg) do { \ + *msg = _msg; \ + *end = _end; \ + if (con) { con[0] = _end; con[1] = dst; } \ + return false; \ +} while (false) + + u8 *hdr = *ptr + 1; + u8 **end = ptr; + u8 *src = hdr, *dst = NULL, *pos; + u16 hi, lo; + u32 uni, tmp; + + /* Resume incremental parsing. */ + if (con && unlikely(con[0])) { + src = con[0]; + dst = con[1]; + if (dst) goto copy_ascii; + } + +skip_ascii: + /* + Most strings have no escaped characters, so we can jump them quickly. + + We want to make loop unrolling, as shown in the following code. Some + compiler may not generate instructions as expected, so we rewrite it with + explicit goto statements. We hope the compiler can generate instructions + like this: https://godbolt.org/z/8vjsYq + + while (true) repeat16({ + if (likely((char_is_ascii_skip(*src)))) src++; + else break; + }) + */ + if (quo == '"') { +#define expr_jump(i) \ + if (likely(char_is_ascii_skip(src[i]))) {} \ + else goto skip_ascii_stop##i; + +#define expr_stop(i) \ + skip_ascii_stop##i: \ + src += i; \ + goto skip_ascii_end; + + repeat16_incr(expr_jump) + src += 16; + goto skip_ascii; + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + } else { +#define expr_jump(i) \ + if (likely(char_is_ascii_skip_sq(src[i]))) {} \ + else goto skip_ascii_stop_sq##i; + +#define expr_stop(i) \ + skip_ascii_stop_sq##i: \ + src += i; \ + goto skip_ascii_end; + + repeat16_incr(expr_jump) + src += 16; + goto skip_ascii; + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + } + +skip_ascii_end: + gcc_store_barrier(*src); + if (likely(*src == quo)) { + val->tag = ((u64)(src - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_STR | + (quo == '"' ? YYJSON_SUBTYPE_NOESC : 0); + val->uni.str = (const char *)hdr; + *src = '\0'; + *end = src + 1; + if (con) con[0] = con[1] = NULL; + return true; + } + +skip_utf8: + if (*src & 0x80) { /* non-ASCII character */ + /* + Non-ASCII character appears here, which means that the text is likely + to be written in non-English or emoticons. According to some common + data set statistics, byte sequences of the same length may appear + consecutively. We process the byte sequences of the same length in each + loop, which is more friendly to branch prediction. + */ + pos = src; +#if YYJSON_DISABLE_UTF8_VALIDATION + while (true) repeat8({ + if (likely((*src & 0xF0) == 0xE0)) src += 3; + else break; + }) + if (*src < 0x80) goto skip_ascii; + while (true) repeat8({ + if (likely((*src & 0xE0) == 0xC0)) src += 2; + else break; + }) + while (true) repeat8({ + if (likely((*src & 0xF8) == 0xF0)) src += 4; + else break; + }) +#else + uni = byte_load_4(src); + while (is_utf8_seq3(uni)) { + src += 3; + uni = byte_load_4(src); + } + if (is_utf8_seq1(uni)) goto skip_ascii; + while (is_utf8_seq2(uni)) { + src += 2; + uni = byte_load_4(src); + } + while (is_utf8_seq4(uni)) { + src += 4; + uni = byte_load_4(src); + } +#endif + if (unlikely(pos == src)) { + if (has_allow(INVALID_UNICODE)) ++src; + else return_err(src, "invalid UTF-8 encoding in string"); + } + goto skip_ascii; + } + + /* The escape character appears, we need to copy it. */ + dst = src; +copy_escape: + if (likely(*src == '\\')) { + switch (*++src) { + case '"': *dst++ = '"'; src++; break; + case '\\': *dst++ = '\\'; src++; break; + case '/': *dst++ = '/'; src++; break; + case 'b': *dst++ = '\b'; src++; break; + case 'f': *dst++ = '\f'; src++; break; + case 'n': *dst++ = '\n'; src++; break; + case 'r': *dst++ = '\r'; src++; break; + case 't': *dst++ = '\t'; src++; break; + case 'u': + src--; + if (!read_uni_esc(&src, &dst, msg)) return_err(src, *msg); + break; + default: { + if (has_allow(EXT_ESCAPE)) { + /* read extended escape (non-standard) */ + switch (*src) { + case '\'': *dst++ = '\''; src++; break; + case 'a': *dst++ = '\a'; src++; break; + case 'v': *dst++ = '\v'; src++; break; + case '?': *dst++ = '\?'; src++; break; + case 'e': *dst++ = 0x1B; src++; break; + case '0': + if (!char_is_digit(src[1])) { + *dst++ = '\0'; src++; break; + } + return_err(src - 1, "octal escape is not allowed"); + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return_err(src - 1, "invalid number escape"); + case 'x': { + u8 c; + if (hex_load_2(src + 1, &c)) { + src += 3; + if (c <= 0x7F) { /* 1-byte ASCII */ + *dst++ = c; + } else { /* 2-byte UTF-8 */ + *dst++ = (u8)(0xC0 | (c >> 6)); + *dst++ = (u8)(0x80 | (c & 0x3F)); + } + break; + } + return_err(src - 1, "invalid hex escape"); + } + case '\n': src++; break; + case '\r': src++; src += (*src == '\n'); break; + case 0xE2: /* Line terminator: U+2028, U+2029 */ + if ((src[1] == 0x80 && src[2] == 0xA8) || + (src[1] == 0x80 && src[2] == 0xA9)) { + src += 3; + } + break; + default: + break; /* skip */ + } + } else if (quo == '\'' && *src == '\'') { + *dst++ = '\''; src++; break; + } else { + return_err(src - 1, "invalid escaped sequence in string"); + } + } + } + } else if (likely(*src == quo)) { + val->tag = ((u64)(dst - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->uni.str = (const char *)hdr; + *dst = '\0'; + *end = src + 1; + if (con) con[0] = con[1] = NULL; + return true; + } else { + if (!has_allow(INVALID_UNICODE)) { + return_err(src, "unexpected control character in string"); + } + if (src >= eof) return_err(src, "unclosed string"); + *dst++ = *src++; + } + +copy_ascii: + /* + Copy continuous ASCII, loop unrolling, same as the following code: + + while (true) repeat16({ + if (char_is_ascii_skip(*src)) *dst++ = *src++; + else break; + }) + */ + if (quo == '"') { +#define expr_jump(i) \ + if (likely((char_is_ascii_skip(src[i])))) {} \ + else { gcc_store_barrier(src[i]); goto copy_ascii_stop_##i; } + repeat16_incr(expr_jump) +#undef expr_jump + } else { +#define expr_jump(i) \ + if (likely((char_is_ascii_skip_sq(src[i])))) {} \ + else { gcc_store_barrier(src[i]); goto copy_ascii_stop_##i; } + repeat16_incr(expr_jump) +#undef expr_jump + } + + byte_move_16(dst, src); + dst += 16; src += 16; + goto copy_ascii; + + /* + The memory is copied forward since `dst < src`. + So it's safe to move one extra byte to reduce instruction count. + */ +#define expr_jump(i) \ + copy_ascii_stop_##i: \ + byte_move_forward(dst, src, i); \ + dst += i; src += i; \ + goto copy_utf8; + repeat16_incr(expr_jump) +#undef expr_jump + +copy_utf8: + if (*src & 0x80) { /* non-ASCII character */ + pos = src; + uni = byte_load_4(src); +#if YYJSON_DISABLE_UTF8_VALIDATION + while (true) repeat4({ + if ((uni & utf8_seq(b3_mask)) == utf8_seq(b3_patt)) { + byte_copy_4(dst, &uni); + dst += 3; src += 3; + uni = byte_load_4(src); + } else break; + }) + if ((uni & utf8_seq(b1_mask)) == utf8_seq(b1_patt)) goto copy_ascii; + while (true) repeat4({ + if ((uni & utf8_seq(b2_mask)) == utf8_seq(b2_patt)) { + byte_copy_2(dst, &uni); + dst += 2; src += 2; + uni = byte_load_4(src); + } else break; + }) + while (true) repeat4({ + if ((uni & utf8_seq(b4_mask)) == utf8_seq(b4_patt)) { + byte_copy_4(dst, &uni); + dst += 4; src += 4; + uni = byte_load_4(src); + } else break; + }) +#else + while (is_utf8_seq3(uni)) { + byte_copy_4(dst, &uni); + dst += 3; src += 3; + uni = byte_load_4(src); + } + if (is_utf8_seq1(uni)) goto copy_ascii; + while (is_utf8_seq2(uni)) { + byte_copy_2(dst, &uni); + dst += 2; src += 2; + uni = byte_load_4(src); + } + while (is_utf8_seq4(uni)) { + byte_copy_4(dst, &uni); + dst += 4; src += 4; + uni = byte_load_4(src); + } +#endif + if (unlikely(pos == src)) { + if (!has_allow(INVALID_UNICODE)) { + return_err(src, MSG_ERR_UTF8); + } + goto copy_ascii_stop_1; + } + goto copy_ascii; + } + goto copy_escape; + +#undef return_err +} + +static_inline bool read_str(u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { + return read_str_opt('\"', ptr, eof, flg, val, msg, NULL); +} + +static_inline bool read_str_con(u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg, u8 **con) { + return read_str_opt('\"', ptr, eof, flg, val, msg, con); +} + +static_noinline bool read_str_sq(u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { + return read_str_opt('\'', ptr, eof, flg, val, msg, NULL); +} + +/** Read unquoted key (identifier name). */ +static_noinline bool read_str_id(u8 **ptr, u8 *eof, yyjson_read_flag flg, + u8 **pre, yyjson_val *val, const char **msg) { +#define return_err(_end, _msg) do { \ + *msg = _msg; \ + *end = _end; \ + return false; \ +} while (false) + +#define return_suc(_str_end, _cur_end) do { \ + val->tag = ((u64)(_str_end - hdr) << YYJSON_TAG_BIT) | \ + (u64)(YYJSON_TYPE_STR); \ + val->uni.str = (const char *)hdr; \ + *pre = _str_end; *end = _cur_end; \ + return true; \ +} while (false) + + u8 *hdr = *ptr; + u8 **end = ptr; + u8 *src = hdr, *dst = NULL; + u16 hi, lo; + u32 uni, tmp; + + /* add null-terminator for previous raw string */ + **pre = '\0'; + +skip_ascii: +#define expr_jump(i) \ + if (likely(char_is_id_ascii(src[i]))) {} \ + else goto skip_ascii_stop##i; + +#define expr_stop(i) \ + skip_ascii_stop##i: \ + src += i; \ + goto skip_ascii_end; + + repeat16_incr(expr_jump) + src += 16; + goto skip_ascii; + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + +skip_ascii_end: + gcc_store_barrier(*src); + if (likely(!char_is_id_next(*src))) { + return_suc(src, src); + } + +skip_utf8: + while (*src >= 0x80) { + if (has_allow(EXT_WHITESPACE)) { + if (char_is_space_ext(*src) && ext_space_len(src)) { + return_suc(src, src); + } + } + uni = byte_load_4(src); + if (is_utf8_seq2(uni)) { + src += 2; + } else if (is_utf8_seq3(uni)) { + src += 3; + } else if (is_utf8_seq4(uni)) { + src += 4; + } else { +#if !YYJSON_DISABLE_UTF8_VALIDATION + if (!has_allow(INVALID_UNICODE)) return_err(src, MSG_ERR_UTF8); +#endif + src += 1; + } + } + if (char_is_id_ascii(*src)) goto skip_ascii; + + /* The escape character appears, we need to copy it. */ + dst = src; +copy_escape: + if (byte_match_2(src, "\\u")) { + if (!read_uni_esc(&src, &dst, msg)) return_err(src, *msg); + } else { + if (!char_is_id_next(*src)) return_suc(dst, src); + return_err(src, "unexpected character in key"); + } + +copy_ascii: + /* + Copy continuous ASCII, loop unrolling, same as the following code: + + while (true) repeat16({ + if (char_is_ascii_skip(*src)) *dst++ = *src++; + else break; + }) + */ +#define expr_jump(i) \ + if (likely((char_is_id_ascii(src[i])))) {} \ + else { gcc_store_barrier(src[i]); goto copy_ascii_stop_##i; } + repeat16_incr(expr_jump) +#undef expr_jump + + byte_move_16(dst, src); + dst += 16; src += 16; + goto copy_ascii; + +#define expr_jump(i) \ + copy_ascii_stop_##i: \ + byte_move_forward(dst, src, i); \ + dst += i; src += i; \ + goto copy_utf8; + repeat16_incr(expr_jump) +#undef expr_jump + +copy_utf8: + while (*src >= 0x80) { /* non-ASCII character */ + if (has_allow(EXT_WHITESPACE)) { + if (char_is_space_ext(*src) && ext_space_len(src)) { + return_suc(dst, src); + } + } + uni = byte_load_4(src); + if (is_utf8_seq2(uni)) { + byte_copy_2(dst, &uni); + dst += 2; src += 2; + } else if (is_utf8_seq3(uni)) { + byte_copy_4(dst, &uni); + dst += 3; src += 3; + } else if (is_utf8_seq4(uni)) { + byte_copy_4(dst, &uni); + dst += 4; src += 4; + } else { +#if !YYJSON_DISABLE_UTF8_VALIDATION + if (!has_allow(INVALID_UNICODE)) return_err(src, MSG_ERR_UTF8); +#endif + *dst = *src; + dst += 1; src += 1; + } + } + if (char_is_id_ascii(*src)) goto copy_ascii; + goto copy_escape; + +#undef return_err +#undef return_suc +} + + + +/*============================================================================== + * MARK: - JSON Reader Implementation (Private) + * + * We use goto statements to build the finite state machine (FSM). + * The FSM's state was held by program counter (PC) and the 'goto' make the + * state transitions. + *============================================================================*/ + +/** Read single value JSON document. */ +static_noinline yyjson_doc *read_root_single(u8 *hdr, u8 *cur, u8 *eof, + yyjson_alc alc, + yyjson_read_flag flg, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, eof, YYJSON_READ_ERROR_##_code, flg)) { \ + err->pos = (usize)(eof - hdr); \ + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; \ + err->msg = MSG_NOT_END; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + if (val_hdr) alc.free(alc.ctx, val_hdr); \ + return NULL; \ +} while (false) + + usize hdr_len; /* value count used by doc */ + usize alc_num; /* value count capacity */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val; /* current value */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + alc_num = hdr_len + 1; /* single value */ + + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, alc_num * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val = val_hdr + hdr_len; + + if (char_is_num(*cur)) { + if (likely(read_num(&cur, pre, flg, val, &msg))) goto doc_end; + goto fail_number; + } + if (*cur == '"') { + if (likely(read_str(&cur, eof, flg, val, &msg))) goto doc_end; + goto fail_string; + } + if (*cur == 't') { + if (likely(read_true(&cur, val))) goto doc_end; + goto fail_literal_true; + } + if (*cur == 'f') { + if (likely(read_false(&cur, val))) goto doc_end; + goto fail_literal_false; + } + if (*cur == 'n') { + if (likely(read_null(&cur, val))) goto doc_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto doc_end; + } + goto fail_literal_null; + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(&cur, pre, flg, val)) goto doc_end; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto doc_end; + goto fail_string; + } + goto fail_character; + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < eof) && !has_flg(STOP_WHEN_DONE)) { + while (char_is_space(*cur)) cur++; + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + goto fail_comment; + } + } + if (unlikely(cur < eof)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = 1; + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + return doc; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef return_err +} + +/** Read JSON document (accept all style, but optimized for minify). */ +static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, + yyjson_alc alc, + yyjson_read_flag flg, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, eof, YYJSON_READ_ERROR_##_code, flg)) { \ + err->pos = (usize)(eof - hdr); \ + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; \ + err->msg = MSG_NOT_END; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + if (val_hdr) alc.free(alc.ctx, val_hdr); \ + return NULL; \ +} while (false) + +#define val_incr() do { \ + val++; \ + if (unlikely(val >= val_end)) { \ + usize alc_old = alc_len; \ + usize val_ofs = (usize)(val - val_hdr); \ + usize ctn_ofs = (usize)(ctn - val_hdr); \ + alc_len += alc_len / 2; \ + if ((sizeof(usize) < 8) && (alc_len >= alc_max)) goto fail_alloc; \ + val_tmp = (yyjson_val *)alc.realloc(alc.ctx, (void *)val_hdr, \ + alc_old * sizeof(yyjson_val), \ + alc_len * sizeof(yyjson_val)); \ + if ((!val_tmp)) goto fail_alloc; \ + val = val_tmp + val_ofs; \ + ctn = val_tmp + ctn_ofs; \ + val_hdr = val_tmp; \ + val_end = val_tmp + (alc_len - 2); \ + } \ +} while (false) + + usize dat_len; /* data length in bytes, hint for allocator */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize alc_max; /* maximum value count for allocator */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val_tmp; /* temporary pointer for realloc */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + yyjson_val *ctn_parent; /* parent of current container */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + + dat_len = has_flg(STOP_WHEN_DONE) ? 256 : (usize)(eof - cur); + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + alc_max = USIZE_MAX / sizeof(yyjson_val); + alc_len = hdr_len + (dat_len / YYJSON_READER_ESTIMATED_MINIFY_RATIO) + 4; + alc_len = yyjson_min(alc_len, alc_max); + + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, alc_len * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val_end = val_hdr + (alc_len - 2); /* padding for key-value pair reading */ + val = val_hdr + hdr_len; + ctn = val; + ctn_len = 0; + + if (*cur++ == '{') { + ctn->tag = YYJSON_TYPE_OBJ; + ctn->uni.ofs = 0; + goto obj_key_begin; + } else { + ctn->tag = YYJSON_TYPE_ARR; + ctn->uni.ofs = 0; + goto arr_val_begin; + } + +arr_begin: + /* save current container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + + /* create a new array value, save parent container offset */ + val_incr(); + val->tag = YYJSON_TYPE_ARR; + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + + /* push the new array value as current container */ + ctn = val; + ctn_len = 0; + +arr_val_begin: + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (char_is_num(*cur)) { + val_incr(); + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto arr_val_end; + goto fail_number; + } + if (*cur == '"') { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (*cur == 't') { + val_incr(); + ctn_len++; + if (likely(read_true(&cur, val))) goto arr_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val_incr(); + ctn_len++; + if (likely(read_false(&cur, val))) goto arr_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val_incr(); + ctn_len++; + if (likely(read_null(&cur, val))) goto arr_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto arr_val_end; + } + goto fail_literal_null; + } + if (*cur == ']') { + cur++; + if (likely(ctn_len == 0)) goto arr_end; + if (has_allow(TRAILING_COMMAS)) goto arr_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val_incr(); + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto arr_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +arr_val_end: + if (*cur == ',') { + cur++; + goto arr_val_begin; + } + if (*cur == ']') { + cur++; + goto arr_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_arr_end; + +arr_end: + /* get parent container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + + /* save the next sibling value offset */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = ((ctn_len) << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; + if (unlikely(ctn == ctn_parent)) goto doc_end; + + /* pop parent as current container */ + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +obj_begin: + /* push container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + val_incr(); + val->tag = YYJSON_TYPE_OBJ; + /* offset to the parent */ + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + ctn = val; + ctn_len = 0; + +obj_key_begin: + if (likely(*cur == '"')) { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (likely(*cur == '}')) { + cur++; + if (likely(ctn_len == 0)) goto obj_end; + if (has_allow(TRAILING_COMMAS)) goto obj_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_begin; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (has_allow(UNQUOTED_KEY) && char_is_id_start(*cur)) { + val_incr(); + ctn_len++; + if (read_str_id(&cur, eof, flg, pre, val, &msg)) goto obj_key_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_key; + +obj_key_end: + if (*cur == ':') { + cur++; + goto obj_val_begin; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_sep; + +obj_val_begin: + if (*cur == '"') { + val++; + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (char_is_num(*cur)) { + val++; + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto obj_val_end; + goto fail_number; + } + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (*cur == 't') { + val++; + ctn_len++; + if (likely(read_true(&cur, val))) goto obj_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val++; + ctn_len++; + if (likely(read_false(&cur, val))) goto obj_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val++; + ctn_len++; + if (likely(read_null(&cur, val))) goto obj_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto obj_val_end; + } + goto fail_literal_null; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val++; + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto obj_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val++; + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +obj_val_end: + if (likely(*cur == ',')) { + cur++; + goto obj_key_begin; + } + if (likely(*cur == '}')) { + cur++; + goto obj_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_end; + +obj_end: + /* pop container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + /* point to the next value */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = (ctn_len << (YYJSON_TAG_BIT - 1)) | YYJSON_TYPE_OBJ; + if (unlikely(ctn == ctn_parent)) goto doc_end; + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < eof) && !has_flg(STOP_WHEN_DONE)) { + while (char_is_space(*cur)) cur++; + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + goto fail_comment; + } + } + if (unlikely(cur < eof)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = (usize)((val - doc->root) + 1); + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + return doc; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_trailing_comma: return_err(cur, JSON_STRUCTURE, MSG_COMMA); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character_val: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_character_arr_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_ARR_END); +fail_character_obj_key: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_KEY); +fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); +fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); +fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef val_incr +#undef return_err +} + +/** Read JSON document (accept all style, but optimized for pretty). */ +static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, + yyjson_alc alc, + yyjson_read_flag flg, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, eof, YYJSON_READ_ERROR_##_code, flg)) { \ + err->pos = (usize)(eof - hdr); \ + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; \ + err->msg = MSG_NOT_END; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + if (val_hdr) alc.free(alc.ctx, val_hdr); \ + return NULL; \ +} while (false) + +#define val_incr() do { \ + val++; \ + if (unlikely(val >= val_end)) { \ + usize alc_old = alc_len; \ + usize val_ofs = (usize)(val - val_hdr); \ + usize ctn_ofs = (usize)(ctn - val_hdr); \ + alc_len += alc_len / 2; \ + if ((sizeof(usize) < 8) && (alc_len >= alc_max)) goto fail_alloc; \ + val_tmp = (yyjson_val *)alc.realloc(alc.ctx, (void *)val_hdr, \ + alc_old * sizeof(yyjson_val), \ + alc_len * sizeof(yyjson_val)); \ + if ((!val_tmp)) goto fail_alloc; \ + val = val_tmp + val_ofs; \ + ctn = val_tmp + ctn_ofs; \ + val_hdr = val_tmp; \ + val_end = val_tmp + (alc_len - 2); \ + } \ +} while (false) + + usize dat_len; /* data length in bytes, hint for allocator */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize alc_max; /* maximum value count for allocator */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val_tmp; /* temporary pointer for realloc */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + yyjson_val *ctn_parent; /* parent of current container */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + + dat_len = has_flg(STOP_WHEN_DONE) ? 256 : (usize)(eof - cur); + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + alc_max = USIZE_MAX / sizeof(yyjson_val); + alc_len = hdr_len + (dat_len / YYJSON_READER_ESTIMATED_PRETTY_RATIO) + 4; + alc_len = yyjson_min(alc_len, alc_max); + + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, alc_len * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val_end = val_hdr + (alc_len - 2); /* padding for key-value pair reading */ + val = val_hdr + hdr_len; + ctn = val; + ctn_len = 0; + + if (*cur++ == '{') { + ctn->tag = YYJSON_TYPE_OBJ; + ctn->uni.ofs = 0; + if (*cur == '\n') cur++; + goto obj_key_begin; + } else { + ctn->tag = YYJSON_TYPE_ARR; + ctn->uni.ofs = 0; + if (*cur == '\n') cur++; + goto arr_val_begin; + } + +arr_begin: + /* save current container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + + /* create a new array value, save parent container offset */ + val_incr(); + val->tag = YYJSON_TYPE_ARR; + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + + /* push the new array value as current container */ + ctn = val; + ctn_len = 0; + if (*cur == '\n') cur++; + +arr_val_begin: +#if YYJSON_IS_REAL_GCC + while (true) repeat16({ + if (byte_match_2(cur, " ")) cur += 2; + else break; + }) +#else + while (true) repeat16({ + if (likely(byte_match_2(cur, " "))) cur += 2; + else break; + }) +#endif + + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (char_is_num(*cur)) { + val_incr(); + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto arr_val_end; + goto fail_number; + } + if (*cur == '"') { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (*cur == 't') { + val_incr(); + ctn_len++; + if (likely(read_true(&cur, val))) goto arr_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val_incr(); + ctn_len++; + if (likely(read_false(&cur, val))) goto arr_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val_incr(); + ctn_len++; + if (likely(read_null(&cur, val))) goto arr_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto arr_val_end; + } + goto fail_literal_null; + } + if (*cur == ']') { + cur++; + if (likely(ctn_len == 0)) goto arr_end; + if (has_allow(TRAILING_COMMAS)) goto arr_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val_incr(); + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto arr_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +arr_val_end: + if (byte_match_2(cur, ",\n")) { + cur += 2; + goto arr_val_begin; + } + if (*cur == ',') { + cur++; + goto arr_val_begin; + } + if (*cur == ']') { + cur++; + goto arr_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_arr_end; + +arr_end: + /* get parent container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + + /* save the next sibling value offset */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = ((ctn_len) << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; + if (unlikely(ctn == ctn_parent)) goto doc_end; + + /* pop parent as current container */ + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if (*cur == '\n') cur++; + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +obj_begin: + /* push container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + val_incr(); + val->tag = YYJSON_TYPE_OBJ; + /* offset to the parent */ + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + ctn = val; + ctn_len = 0; + if (*cur == '\n') cur++; + +obj_key_begin: +#if YYJSON_IS_REAL_GCC + while (true) repeat16({ + if (byte_match_2(cur, " ")) cur += 2; + else break; + }) +#else + while (true) repeat16({ + if (likely(byte_match_2(cur, " "))) cur += 2; + else break; + }) +#endif + if (likely(*cur == '"')) { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (likely(*cur == '}')) { + cur++; + if (likely(ctn_len == 0)) goto obj_end; + if (has_allow(TRAILING_COMMAS)) goto obj_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_begin; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (has_allow(UNQUOTED_KEY) && char_is_id_start(*cur)) { + val_incr(); + ctn_len++; + if (read_str_id(&cur, eof, flg, pre, val, &msg)) goto obj_key_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_key; + +obj_key_end: + if (byte_match_2(cur, ": ")) { + cur += 2; + goto obj_val_begin; + } + if (*cur == ':') { + cur++; + goto obj_val_begin; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_sep; + +obj_val_begin: + if (*cur == '"') { + val++; + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (char_is_num(*cur)) { + val++; + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto obj_val_end; + goto fail_number; + } + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (*cur == 't') { + val++; + ctn_len++; + if (likely(read_true(&cur, val))) goto obj_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val++; + ctn_len++; + if (likely(read_false(&cur, val))) goto obj_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val++; + ctn_len++; + if (likely(read_null(&cur, val))) goto obj_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto obj_val_end; + } + goto fail_literal_null; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val++; + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto obj_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val++; + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +obj_val_end: + if (byte_match_2(cur, ",\n")) { + cur += 2; + goto obj_key_begin; + } + if (likely(*cur == ',')) { + cur++; + goto obj_key_begin; + } + if (likely(*cur == '}')) { + cur++; + goto obj_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_end; + +obj_end: + /* pop container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + /* point to the next value */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = (ctn_len << (YYJSON_TAG_BIT - 1)) | YYJSON_TYPE_OBJ; + if (unlikely(ctn == ctn_parent)) goto doc_end; + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if (*cur == '\n') cur++; + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < eof) && !has_flg(STOP_WHEN_DONE)) { + while (char_is_space(*cur)) cur++; + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + goto fail_comment; + } + } + if (unlikely(cur < eof)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = (usize)((val - doc->root) + 1); + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + return doc; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_trailing_comma: return_err(cur, JSON_STRUCTURE, MSG_COMMA); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character_val: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_character_arr_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_ARR_END); +fail_character_obj_key: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_KEY); +fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); +fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); +fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef val_incr +#undef return_err +} + + + +/*============================================================================== + * MARK: - JSON Reader (Public) + *============================================================================*/ + +yyjson_doc *yyjson_read_opts(char *dat, usize len, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + err->pos = (usize)(_pos); \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + if (!has_flg(INSITU) && hdr) alc.free(alc.ctx, (void *)hdr); \ + return NULL; \ +} while (false) + + yyjson_read_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_doc *doc; + u8 *hdr = NULL, *eof, *cur; + + /* validate input parameters */ + if (!err) err = &tmp_err; + if (unlikely(!dat)) return_err(0, INVALID_PARAMETER, "input data is NULL"); + if (unlikely(!len)) return_err(0, INVALID_PARAMETER, "input length is 0"); + + /* add 4-byte zero padding for input data if necessary */ + if (has_flg(INSITU)) { + hdr = (u8 *)dat; + eof = (u8 *)dat + len; + cur = (u8 *)dat; + } else { + if (unlikely(len >= USIZE_MAX - YYJSON_PADDING_SIZE)) { + return_err(0, MEMORY_ALLOCATION, MSG_MALLOC); + } + hdr = (u8 *)alc.malloc(alc.ctx, len + YYJSON_PADDING_SIZE); + if (unlikely(!hdr)) { + return_err(0, MEMORY_ALLOCATION, MSG_MALLOC); + } + eof = hdr + len; + cur = hdr; + memcpy(hdr, dat, len); + } + memset(eof, 0, YYJSON_PADDING_SIZE); + + if (has_allow(BOM)) { + if (len >= 3 && is_utf8_bom(cur)) cur += 3; + } + + /* skip empty contents before json document */ + if (unlikely(!char_is_ctn(*cur))) { + while (char_is_space(*cur)) cur++; + if (unlikely(!char_is_ctn(*cur))) { + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + return_err(cur - hdr, INVALID_COMMENT, MSG_COMMENT); + } + } + } + if (unlikely(cur >= eof)) { + return_err(0, EMPTY_CONTENT, "input data is empty"); + } + } + + /* read json document */ + if (likely(char_is_ctn(*cur))) { + if (char_is_space(cur[1]) && char_is_space(cur[2])) { + doc = read_root_pretty(hdr, cur, eof, alc, flg, err); + } else { + doc = read_root_minify(hdr, cur, eof, alc, flg, err); + } + } else { + doc = read_root_single(hdr, cur, eof, alc, flg, err); + } + + /* check result */ + if (likely(doc)) { + memset(err, 0, sizeof(yyjson_read_err)); + } else { + /* RFC 8259: JSON text MUST be encoded using UTF-8 */ + if (err->pos == 0 && err->code != YYJSON_READ_ERROR_MEMORY_ALLOCATION) { + if (is_utf8_bom(hdr)) err->msg = MSG_ERR_BOM; + else if (len >= 4 && is_utf32_bom(hdr)) err->msg = MSG_ERR_UTF32; + else if (len >= 2 && is_utf16_bom(hdr)) err->msg = MSG_ERR_UTF16; + } + if (!has_flg(INSITU)) alc.free(alc.ctx, hdr); + } + return doc; + +#undef return_err +} + +yyjson_doc *yyjson_read_file(const char *path, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr, + yyjson_read_err *err) { +#define return_err(_code, _msg) do { \ + err->pos = 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + return NULL; \ +} while (false) + + yyjson_read_err tmp_err; + yyjson_doc *doc; + FILE *file; + + if (!err) err = &tmp_err; + if (unlikely(!path)) return_err(INVALID_PARAMETER, "input path is NULL"); + + file = fopen_readonly(path); + if (unlikely(!file)) return_err(FILE_OPEN, MSG_FREAD); + + doc = yyjson_read_fp(file, flg, alc_ptr, err); + fclose(file); + return doc; + +#undef return_err +} + +yyjson_doc *yyjson_read_fp(FILE *file, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr, + yyjson_read_err *err) { +#define return_err(_code, _msg) do { \ + err->pos = 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + if (buf) alc.free(alc.ctx, buf); \ + return NULL; \ +} while (false) + + yyjson_read_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_doc *doc; + + long file_size = 0, file_pos; + void *buf = NULL; + usize buf_size = 0; + + /* validate input parameters */ + if (!err) err = &tmp_err; + if (unlikely(!file)) return_err(INVALID_PARAMETER, "input file is NULL"); + + /* get current position */ + file_pos = ftell(file); + if (file_pos != -1) { + /* get total file size, may fail */ + if (fseek(file, 0, SEEK_END) == 0) file_size = ftell(file); + /* reset to original position, may fail */ + if (fseek(file, file_pos, SEEK_SET) != 0) file_size = 0; + /* get file size from current postion to end */ + if (file_size > 0) file_size -= file_pos; + } + + /* read file */ + if (file_size > 0) { + /* read the entire file in one call */ + buf_size = (usize)file_size + YYJSON_PADDING_SIZE; + buf = alc.malloc(alc.ctx, buf_size); + if (buf == NULL) { + return_err(MEMORY_ALLOCATION, MSG_MALLOC); + } + if (fread_safe(buf, (usize)file_size, file) != (usize)file_size) { + return_err(FILE_READ, MSG_FREAD); + } + } else { + /* failed to get file size, read it as a stream */ + usize chunk_min = (usize)64; + usize chunk_max = (usize)512 * 1024 * 1024; + usize chunk_now = chunk_min; + usize read_size; + void *tmp; + + buf_size = YYJSON_PADDING_SIZE; + while (true) { + if (buf_size + chunk_now < buf_size) { /* overflow */ + return_err(MEMORY_ALLOCATION, MSG_MALLOC); + } + buf_size += chunk_now; + if (!buf) { + buf = alc.malloc(alc.ctx, buf_size); + if (!buf) return_err(MEMORY_ALLOCATION, MSG_MALLOC); + } else { + tmp = alc.realloc(alc.ctx, buf, buf_size - chunk_now, buf_size); + if (!tmp) return_err(MEMORY_ALLOCATION, MSG_MALLOC); + buf = tmp; + } + tmp = ((u8 *)buf) + buf_size - YYJSON_PADDING_SIZE - chunk_now; + read_size = fread_safe(tmp, chunk_now, file); + file_size += (long)read_size; + if (read_size != chunk_now) break; + + chunk_now *= 2; + if (chunk_now > chunk_max) chunk_now = chunk_max; + } + } + + /* read JSON */ + memset((u8 *)buf + file_size, 0, YYJSON_PADDING_SIZE); + flg |= YYJSON_READ_INSITU; + doc = yyjson_read_opts((char *)buf, (usize)file_size, flg, &alc, err); + if (doc) { + doc->str_pool = (char *)buf; + return doc; + } else { + alc.free(alc.ctx, buf); + return NULL; + } + +#undef return_err +} + +const char *yyjson_read_number(const char *dat, + yyjson_val *val, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + err->pos = _pos > hdr ? (usize)(_pos - hdr) : 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + return NULL; \ +} while (false) + + u8 *hdr = constcast(u8 *)dat, *cur = hdr; + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + const char *msg; + yyjson_read_err tmp_err; + +#if YYJSON_DISABLE_FAST_FP_CONV + u8 buf[128]; + usize dat_len; +#endif + + if (!err) err = &tmp_err; + if (unlikely(!dat)) { + return_err(cur, INVALID_PARAMETER, "input data is NULL"); + } + if (unlikely(!val)) { + return_err(cur, INVALID_PARAMETER, "output value is NULL"); + } + +#if YYJSON_DISABLE_FAST_FP_CONV + if (!alc) alc = &YYJSON_DEFAULT_ALC; + dat_len = strlen(dat); + if (dat_len < sizeof(buf)) { + memcpy(buf, dat, dat_len + 1); + hdr = buf; + cur = hdr; + } else { + hdr = (u8 *)alc->malloc(alc->ctx, dat_len + 1); + cur = hdr; + if (unlikely(!hdr)) { + return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); + } + memcpy(hdr, dat, dat_len + 1); + } + hdr[dat_len] = 0; +#endif + +#if YYJSON_DISABLE_FAST_FP_CONV + if (!read_num(&cur, pre, flg, val, &msg)) { + if (dat_len >= sizeof(buf)) alc->free(alc->ctx, hdr); + return_err(cur, INVALID_NUMBER, msg); + } + if (dat_len >= sizeof(buf)) alc->free(alc->ctx, hdr); + if (yyjson_is_raw(val)) val->uni.str = dat; + return dat + (cur - hdr); +#else + if (!read_num(&cur, pre, flg, val, &msg)) { + return_err(cur, INVALID_NUMBER, msg); + } + return (const char *)cur; +#endif + +#undef return_err +} + + + +/*============================================================================== + * MARK: - Incremental JSON Reader (Public) + *============================================================================*/ + +#if !YYJSON_DISABLE_INCR_READER + +/* labels within yyjson_incr_read() to resume incremental parsing */ +#define LABEL_doc_begin 0 +#define LABEL_arr_val_begin 1 +#define LABEL_arr_val_end 2 +#define LABEL_obj_key_begin 3 +#define LABEL_obj_key_end 4 +#define LABEL_obj_val_begin 5 +#define LABEL_obj_val_end 6 +#define LABEL_doc_end 7 + +/** State for incremental JSON reader, opaque in the API. */ +struct yyjson_incr_state { + u32 label; /* current parser goto label */ + yyjson_alc alc; /* allocator */ + yyjson_read_flag flg; /* read flags */ + u8 *hdr; /* JSON data header */ + u8 *cur; /* current position in JSON data */ + usize buf_len; /* total buffer length (without padding) */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + u8 *str_con[2]; /* string parser incremental state */ +}; + +yyjson_incr_state *yyjson_incr_new(char *buf, size_t buf_len, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr) { + yyjson_incr_state *state = NULL; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + + /* remove non-standard flags */ + flg &= ~YYJSON_READ_JSON5; + flg &= ~YYJSON_READ_ALLOW_BOM; + flg &= ~YYJSON_READ_ALLOW_INVALID_UNICODE; + + if (unlikely(!buf)) return NULL; + if (unlikely(buf_len >= USIZE_MAX - YYJSON_PADDING_SIZE)) return NULL; + state = (yyjson_incr_state *)alc.malloc(alc.ctx, sizeof(*state)); + if (!state) return NULL; + memset(state, 0, sizeof(yyjson_incr_state)); + state->alc = alc; + state->flg = flg; + state->buf_len = buf_len; + + /* add 4-byte zero padding for input data if necessary */ + if (has_flg(INSITU)) { + state->hdr = (u8 *)buf; + } else { + state->hdr = (u8 *)alc.malloc(alc.ctx, buf_len + YYJSON_PADDING_SIZE); + if (unlikely(!state->hdr)) { + alc.free(alc.ctx, state); + return NULL; + } + memcpy(state->hdr, buf, buf_len); + } + memset(state->hdr + buf_len, 0, YYJSON_PADDING_SIZE); + state->cur = state->hdr; + state->label = LABEL_doc_begin; + return state; +} + +void yyjson_incr_free(yyjson_incr_state *state) { + if (state) { + yyjson_alc alc = state->alc; + memset(&state->alc, 0, sizeof(alc)); + if (state->val_hdr) { + alc.free(alc.ctx, (void *)state->val_hdr); + } + if (state->hdr && !(state->flg & YYJSON_READ_INSITU)) { + alc.free(alc.ctx, state->hdr); + } + alc.free(alc.ctx, state); + } +} + +yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, + yyjson_read_err *err) { +#define return_err_inv_param(_msg) do { \ + err->pos = 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; \ + return NULL; \ +} while (false) + +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, end, YYJSON_READ_ERROR_##_code, flg)) { \ + goto unexpected_end; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + return NULL; \ +} while (false) + +#define val_incr() do { \ + val++; \ + if (unlikely(val >= val_end)) { \ + usize alc_old = alc_len; \ + alc_len += alc_len / 2; \ + if ((sizeof(usize) < 8) && (alc_len >= alc_max)) goto fail_alloc; \ + val_tmp = (yyjson_val *)alc.realloc(alc.ctx, (void *)val_hdr, \ + alc_old * sizeof(yyjson_val), \ + alc_len * sizeof(yyjson_val)); \ + if ((!val_tmp)) goto fail_alloc; \ + val = val_tmp + (usize)(val - val_hdr); \ + ctn = val_tmp + (usize)(ctn - val_hdr); \ + state->val = val_tmp + (usize)(state->val - val_hdr); \ + state->val_hdr = val_hdr = val_tmp; \ + val_end = val_tmp + (alc_len - 2); \ + state->val_end = val_end; \ + } \ +} while (false) + + /* save position where it's possible to resume incremental parsing */ +#define save_incr_state(_label) do { \ + state->label = LABEL_##_label; \ + state->cur = cur; \ + state->val = val; \ + state->ctn_len = ctn_len; \ + state->hdr_len = hdr_len; \ + if (unlikely(cur >= end)) goto unexpected_end; \ +} while (false) + +#define check_maybe_truncated_number() do { \ + if (unlikely(cur >= end)) { \ + if (unlikely(cur > state->cur + INCR_NUM_MAX_LEN)) { \ + msg = "number too long"; \ + goto fail_number; \ + } \ + goto unexpected_end; \ + } \ +} while (false) + + u8 *hdr = NULL, *end = NULL, *cur = NULL; + yyjson_read_flag flg; + yyjson_alc alc; + usize dat_len; /* data length in bytes, hint for allocator */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize alc_max; /* maximum value count for allocator */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val_tmp; /* temporary pointer for realloc */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + yyjson_val *ctn_parent; /* parent of current container */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + yyjson_read_err tmp_err; + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + u8 **con = NULL; /* for incremental string parsing */ + u8 saved_end = '\0'; /* saved end char */ + + /* validate input parameters */ + if (!err) err = &tmp_err; + if (unlikely(!state)) { + return_err_inv_param("input state is NULL"); + } + if (unlikely(!len)) { + return_err_inv_param("input length is 0"); + } + if (unlikely(len > state->buf_len)) { + return_err_inv_param("length is greater than total input length"); + } + + /* restore state saved from the previous call */ + hdr = state->hdr; + end = state->hdr + len; + cur = state->cur; + flg = state->flg; + alc = state->alc; + ctn_len = state->ctn_len; + hdr_len = state->hdr_len; + alc_len = state->alc_len; + val = state->val; + val_hdr = state->val_hdr; + val_end = state->val_end; + ctn = state->ctn; + con = state->str_con; + alc_max = USIZE_MAX / sizeof(yyjson_val); + + /* insert null terminator to make us stop at the specified end, even if + the data contains more valid JSON */ + saved_end = *end; + *end = '\0'; + + /* resume parsing from the last save point */ + switch (state->label) { + case LABEL_doc_begin: goto doc_begin; + case LABEL_arr_val_begin: goto arr_val_begin; + case LABEL_arr_val_end: goto arr_val_end; + case LABEL_obj_key_begin: goto obj_key_begin; + case LABEL_obj_key_end: goto obj_key_end; + case LABEL_obj_val_begin: goto obj_val_begin; + case LABEL_obj_val_end: goto obj_val_end; + case LABEL_doc_end: goto doc_end; + default: return_err_inv_param("invalid incremental state"); + } + +doc_begin: + /* skip empty contents before json document */ + if (unlikely(!char_is_ctn(*cur))) { + while (char_is_space(*cur)) cur++; + if (unlikely(cur >= end)) goto unexpected_end; /* input data is empty */ + } + + /* allocate memory for document */ + if (!val_hdr) { + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + if (likely(char_is_ctn(*cur))) { + dat_len = has_flg(STOP_WHEN_DONE) ? 256 : state->buf_len; + alc_len = hdr_len + + (dat_len / YYJSON_READER_ESTIMATED_MINIFY_RATIO) + 4; + alc_len = yyjson_min(alc_len, alc_max); + } else { + alc_len = hdr_len + 1; /* single value */ + } + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, + alc_len * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val_end = val_hdr + (alc_len - 2); /* padding for kv pair reading */ + val = val_hdr + hdr_len; + ctn = val; + ctn_len = 0; + state->val_hdr = val_hdr; + state->val_end = val_end; + save_incr_state(doc_begin); + } + + /* read json document */ + if (*cur == '{') { + cur++; + ctn->tag = YYJSON_TYPE_OBJ; + ctn->uni.ofs = 0; + goto obj_key_begin; + } + if (*cur == '[') { + cur++; + ctn->tag = YYJSON_TYPE_ARR; + ctn->uni.ofs = 0; + goto arr_val_begin; + } + if (char_is_num(*cur)) { + if (likely(read_num(&cur, pre, flg, val, &msg))) goto doc_end; + goto fail_number; + } + if (*cur == '"') { + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) goto doc_end; + goto fail_string; + } + if (*cur == 't') { + if (likely(read_true(&cur, val))) goto doc_end; + goto fail_literal_true; + } + if (*cur == 'f') { + if (likely(read_false(&cur, val))) goto doc_end; + goto fail_literal_false; + } + if (*cur == 'n') { + if (likely(read_null(&cur, val))) goto doc_end; + goto fail_literal_null; + } + + msg = "unexpected character, expected a valid root value"; + if (cur == hdr) { + /* RFC 8259: JSON text MUST be encoded using UTF-8 */ + if (is_utf8_bom(hdr)) msg = MSG_ERR_BOM; + else if (len >= 4 && is_utf32_bom(hdr)) msg = MSG_ERR_UTF32; + else if (len >= 2 && is_utf16_bom(hdr)) msg = MSG_ERR_UTF16; + } + return_err(cur, UNEXPECTED_CHARACTER, msg); + +arr_begin: + /* save current container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + + /* create a new array value, save parent container offset */ + val_incr(); + val->tag = YYJSON_TYPE_ARR; + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + + /* push the new array value as current container */ + ctn = val; + ctn_len = 0; + +arr_val_begin: + save_incr_state(arr_val_begin); +arr_val_continue: + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (char_is_num(*cur)) { + val_incr(); + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto arr_val_maybe_end; + goto fail_number; + } + if (*cur == '"') { + val_incr(); + ctn_len++; + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) + goto arr_val_end; + goto fail_string; + } + if (*cur == 't') { + val_incr(); + ctn_len++; + if (likely(read_true(&cur, val))) goto arr_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val_incr(); + ctn_len++; + if (likely(read_false(&cur, val))) goto arr_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val_incr(); + ctn_len++; + if (likely(read_null(&cur, val))) goto arr_val_end; + goto fail_literal_null; + } + if (*cur == ']') { + cur++; + if (likely(ctn_len == 0)) goto arr_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_continue; + } + goto fail_character_val; + +arr_val_maybe_end: + /* if incremental parsing stops in the middle of a number, it may continue + with more digits, so arr val maybe didn't end yet */ + check_maybe_truncated_number(); + +arr_val_end: + save_incr_state(arr_val_end); + if (*cur == ',') { + cur++; + goto arr_val_begin; + } + if (*cur == ']') { + cur++; + goto arr_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_end; + } + goto fail_character_arr_end; + +arr_end: + /* get parent container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + + /* save the next sibling value offset */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = ((ctn_len) << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; + if (unlikely(ctn == ctn_parent)) goto doc_end; + + /* pop parent as current container */ + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +obj_begin: + /* push container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + val_incr(); + val->tag = YYJSON_TYPE_OBJ; + /* offset to the parent */ + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + ctn = val; + ctn_len = 0; + +obj_key_begin: + save_incr_state(obj_key_begin); +obj_key_continue: + if (likely(*cur == '"')) { + val_incr(); + ctn_len++; + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) + goto obj_key_end; + goto fail_string; + } + if (likely(*cur == '}')) { + cur++; + if (likely(ctn_len == 0)) goto obj_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_continue; + } + goto fail_character_obj_key; + +obj_key_end: + save_incr_state(obj_key_end); + if (*cur == ':') { + cur++; + goto obj_val_begin; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_end; + } + goto fail_character_obj_sep; + +obj_val_begin: + save_incr_state(obj_val_begin); +obj_val_continue: + if (*cur == '"') { + val++; + ctn_len++; + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) + goto obj_val_end; + goto fail_string; + } + if (char_is_num(*cur)) { + val++; + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto obj_val_maybe_end; + goto fail_number; + } + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (*cur == 't') { + val++; + ctn_len++; + if (likely(read_true(&cur, val))) goto obj_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val++; + ctn_len++; + if (likely(read_false(&cur, val))) goto obj_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val++; + ctn_len++; + if (likely(read_null(&cur, val))) goto obj_val_end; + goto fail_literal_null; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_continue; + } + goto fail_character_val; + +obj_val_maybe_end: + /* if incremental parsing stops in the middle of a number, it may continue + with more digits, so obj val maybe didn't end yet */ + check_maybe_truncated_number(); + +obj_val_end: + save_incr_state(obj_val_end); + if (likely(*cur == ',')) { + cur++; + goto obj_key_begin; + } + if (likely(*cur == '}')) { + cur++; + goto obj_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_end; + } + goto fail_character_obj_end; + +obj_end: + /* pop container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + /* point to the next value */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = (ctn_len << (YYJSON_TAG_BIT - 1)) | YYJSON_TYPE_OBJ; + if (unlikely(ctn == ctn_parent)) goto doc_end; + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < end) && !has_flg(STOP_WHEN_DONE)) { + save_incr_state(doc_end); + while (char_is_space(*cur)) cur++; + if (unlikely(cur < end)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = (usize)((val - doc->root) + 1); + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + state->hdr = NULL; + state->val_hdr = NULL; + memset(err, 0, sizeof(yyjson_read_err)); + return doc; + +unexpected_end: + err->pos = len; + /* if no nore data, stop the incr read */ + if (unlikely(len >= state->buf_len)) { + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; + err->msg = MSG_NOT_END; + return NULL; + } + /* save parser state in extended error struct, in addition to what was + * stored in the last save_incr_state */ + err->code = YYJSON_READ_ERROR_MORE; + err->msg = "need more data"; + state->val_end = val_end; + state->ctn = ctn; + state->alc_len = alc_len; + /* restore the end where we've inserted a null terminator */ + *end = saved_end; + return NULL; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_trailing_comma: return_err(cur, JSON_STRUCTURE, MSG_COMMA); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character_val: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_character_arr_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_ARR_END); +fail_character_obj_key: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_KEY); +fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); +fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef val_incr +#undef return_err +#undef return_err_inv_param +#undef save_incr_state +#undef check_maybe_truncated_number +} + +#endif /* YYJSON_DISABLE_INCR_READER */ + +#undef has_flg +#undef has_allow +#endif /* YYJSON_DISABLE_READER */ + + + +#if !YYJSON_DISABLE_WRITER /* writer begin */ + +/* Check write flag, avoids `always false` warning when disabled. */ +#define has_flg(_flg) unlikely(has_wflag(flg, YYJSON_WRITE_##_flg, 0)) +#define has_allow(_flg) unlikely(has_wflag(flg, YYJSON_WRITE_ALLOW_##_flg, 1)) +static_inline bool has_wflag(yyjson_write_flag flg, yyjson_write_flag chk, + bool non_standard) { +#if YYJSON_DISABLE_NON_STANDARD + if (non_standard) return false; +#endif + return (flg & chk) != 0; +} + +/*============================================================================== + * MARK: - Integer Writer (Private) + * + * The maximum value of uint32_t is 4294967295 (10 digits), + * these digits are named as 'aabbccddee' here. + * + * Although most compilers may convert the "division by constant value" into + * "multiply and shift", manual conversion can still help some compilers + * generate fewer and better instructions. + * + * Reference: + * Division by Invariant Integers using Multiplication, 1994. + * https://gmplib.org/~tege/divcnst-pldi94.pdf + * Improved division by invariant integers, 2011. + * https://gmplib.org/~tege/division-paper.pdf + *============================================================================*/ + +/** Digit table from 00 to 99. */ +yyjson_align(2) +static const char digit_table[200] = { + '0', '0', '0', '1', '0', '2', '0', '3', '0', '4', + '0', '5', '0', '6', '0', '7', '0', '8', '0', '9', + '1', '0', '1', '1', '1', '2', '1', '3', '1', '4', + '1', '5', '1', '6', '1', '7', '1', '8', '1', '9', + '2', '0', '2', '1', '2', '2', '2', '3', '2', '4', + '2', '5', '2', '6', '2', '7', '2', '8', '2', '9', + '3', '0', '3', '1', '3', '2', '3', '3', '3', '4', + '3', '5', '3', '6', '3', '7', '3', '8', '3', '9', + '4', '0', '4', '1', '4', '2', '4', '3', '4', '4', + '4', '5', '4', '6', '4', '7', '4', '8', '4', '9', + '5', '0', '5', '1', '5', '2', '5', '3', '5', '4', + '5', '5', '5', '6', '5', '7', '5', '8', '5', '9', + '6', '0', '6', '1', '6', '2', '6', '3', '6', '4', + '6', '5', '6', '6', '6', '7', '6', '8', '6', '9', + '7', '0', '7', '1', '7', '2', '7', '3', '7', '4', + '7', '5', '7', '6', '7', '7', '7', '8', '7', '9', + '8', '0', '8', '1', '8', '2', '8', '3', '8', '4', + '8', '5', '8', '6', '8', '7', '8', '8', '8', '9', + '9', '0', '9', '1', '9', '2', '9', '3', '9', '4', + '9', '5', '9', '6', '9', '7', '9', '8', '9', '9' +}; + +static_inline u8 *write_u32_len_8(u32 val, u8 *buf) { + u32 aa, bb, cc, dd, aabb, ccdd; /* 8 digits: aabbccdd */ + aabb = (u32)(((u64)val * 109951163) >> 40); /* (val / 10000) */ + ccdd = val - aabb * 10000; /* (val % 10000) */ + aa = (aabb * 5243) >> 19; /* (aabb / 100) */ + cc = (ccdd * 5243) >> 19; /* (ccdd / 100) */ + bb = aabb - aa * 100; /* (aabb % 100) */ + dd = ccdd - cc * 100; /* (ccdd % 100) */ + byte_copy_2(buf + 0, digit_table + aa * 2); + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + byte_copy_2(buf + 6, digit_table + dd * 2); + return buf + 8; +} + +static_inline u8 *write_u32_len_4(u32 val, u8 *buf) { + u32 aa, bb; /* 4 digits: aabb */ + aa = (val * 5243) >> 19; /* (val / 100) */ + bb = val - aa * 100; /* (val % 100) */ + byte_copy_2(buf + 0, digit_table + aa * 2); + byte_copy_2(buf + 2, digit_table + bb * 2); + return buf + 4; +} + +static_inline u8 *write_u32_len_1_to_8(u32 val, u8 *buf) { + u32 aa, bb, cc, dd, aabb, bbcc, ccdd, lz; + + if (val < 100) { /* 1-2 digits: aa */ + lz = val < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + val * 2 + lz); + buf -= lz; + return buf + 2; + + } else if (val < 10000) { /* 3-4 digits: aabb */ + aa = (val * 5243) >> 19; /* (val / 100) */ + bb = val - aa * 100; /* (val % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + return buf + 4; + + } else if (val < 1000000) { /* 5-6 digits: aabbcc */ + aa = (u32)(((u64)val * 429497) >> 32); /* (val / 10000) */ + bbcc = val - aa * 10000; /* (val % 10000) */ + bb = (bbcc * 5243) >> 19; /* (bbcc / 100) */ + cc = bbcc - bb * 100; /* (bbcc % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + return buf + 6; + + } else { /* 7-8 digits: aabbccdd */ + aabb = (u32)(((u64)val * 109951163) >> 40); /* (val / 10000) */ + ccdd = val - aabb * 10000; /* (val % 10000) */ + aa = (aabb * 5243) >> 19; /* (aabb / 100) */ + cc = (ccdd * 5243) >> 19; /* (ccdd / 100) */ + bb = aabb - aa * 100; /* (aabb % 100) */ + dd = ccdd - cc * 100; /* (ccdd % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + byte_copy_2(buf + 6, digit_table + dd * 2); + return buf + 8; + } +} + +static_inline u8 *write_u32_len_5_to_8(u32 val, u8 *buf) { + u32 aa, bb, cc, dd, aabb, bbcc, ccdd, lz; + + if (val < 1000000) { /* 5-6 digits: aabbcc */ + aa = (u32)(((u64)val * 429497) >> 32); /* (val / 10000) */ + bbcc = val - aa * 10000; /* (val % 10000) */ + bb = (bbcc * 5243) >> 19; /* (bbcc / 100) */ + cc = bbcc - bb * 100; /* (bbcc % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + return buf + 6; + + } else { /* 7-8 digits: aabbccdd */ + aabb = (u32)(((u64)val * 109951163) >> 40); /* (val / 10000) */ + ccdd = val - aabb * 10000; /* (val % 10000) */ + aa = (aabb * 5243) >> 19; /* (aabb / 100) */ + cc = (ccdd * 5243) >> 19; /* (ccdd / 100) */ + bb = aabb - aa * 100; /* (aabb % 100) */ + dd = ccdd - cc * 100; /* (ccdd % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + byte_copy_2(buf + 6, digit_table + dd * 2); + return buf + 8; + } +} + +static_inline u8 *write_u64(u64 val, u8 *buf) { + u64 tmp, hgh; + u32 mid, low; + + if (val < 100000000) { /* 1-8 digits */ + buf = write_u32_len_1_to_8((u32)val, buf); + return buf; + + } else if (val < (u64)100000000 * 100000000) { /* 9-16 digits */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + buf = write_u32_len_1_to_8((u32)hgh, buf); + buf = write_u32_len_8(low, buf); + return buf; + + } else { /* 17-20 digits */ + tmp = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - tmp * 100000000); /* (val % 100000000) */ + hgh = (u32)(tmp / 10000); /* (tmp / 10000) */ + mid = (u32)(tmp - hgh * 10000); /* (tmp % 10000) */ + buf = write_u32_len_5_to_8((u32)hgh, buf); + buf = write_u32_len_4(mid, buf); + buf = write_u32_len_8(low, buf); + return buf; + } +} + + + +/*============================================================================== + * MARK: - Number Writer (Private) + *============================================================================*/ + +#if !YYJSON_DISABLE_FAST_FP_CONV /* FP_WRITER */ + +/** Trailing zero count table for number 0 to 99. + (generate with misc/make_tables.c) */ +static const u8 dec_trailing_zero_table[] = { + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static_inline u8 *write_u64_len_1_to_16(u64 val, u8 *buf) { + u64 hgh; + u32 low; + if (val < 100000000) { /* 1-8 digits */ + buf = write_u32_len_1_to_8((u32)val, buf); + return buf; + } else { /* 9-16 digits */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + buf = write_u32_len_1_to_8((u32)hgh, buf); + buf = write_u32_len_8(low, buf); + return buf; + } +} + +static_inline u8 *write_u64_len_1_to_17(u64 val, u8 *buf) { + u64 hgh; + u32 mid, low, one; + if (val >= (u64)100000000 * 10000000) { /* len: 16 to 17 */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + one = (u32)(hgh / 100000000); /* (hgh / 100000000) */ + mid = (u32)(hgh - (u64)one * 100000000); /* (hgh % 100000000) */ + *buf = (u8)((u8)one + (u8)'0'); + buf += one > 0; + buf = write_u32_len_8(mid, buf); + buf = write_u32_len_8(low, buf); + return buf; + } else if (val >= (u64)100000000){ /* len: 9 to 15 */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + buf = write_u32_len_1_to_8((u32)hgh, buf); + buf = write_u32_len_8(low, buf); + return buf; + } else { /* len: 1 to 8 */ + buf = write_u32_len_1_to_8((u32)val, buf); + return buf; + } +} + +/** + Write an unsigned integer with a length of 7 to 9 with trailing zero trimmed. + These digits are named as "abbccddee" here. + For example, input 123456000, output "123456". + */ +static_inline u8 *write_u32_len_7_to_9_trim(u32 val, u8 *buf) { + bool lz; + u32 tz, tz1, tz2; + + u32 abbcc = val / 10000; /* (abbccddee / 10000) */ + u32 ddee = val - abbcc * 10000; /* (abbccddee % 10000) */ + u32 abb = (u32)(((u64)abbcc * 167773) >> 24); /* (abbcc / 100) */ + u32 a = (abb * 41) >> 12; /* (abb / 100) */ + u32 bb = abb - a * 100; /* (abb % 100) */ + u32 cc = abbcc - abb * 100; /* (abbcc % 100) */ + + /* write abbcc */ + buf[0] = (u8)(a + '0'); + buf += a > 0; + lz = bb < 10 && a == 0; + byte_copy_2(buf + 0, digit_table + bb * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + cc * 2); + + if (ddee) { + u32 dd = (ddee * 5243) >> 19; /* (ddee / 100) */ + u32 ee = ddee - dd * 100; /* (ddee % 100) */ + byte_copy_2(buf + 4, digit_table + dd * 2); + byte_copy_2(buf + 6, digit_table + ee * 2); + tz1 = dec_trailing_zero_table[dd]; + tz2 = dec_trailing_zero_table[ee]; + tz = ee ? tz2 : (tz1 + 2); + buf += 8 - tz; + return buf; + } else { + tz1 = dec_trailing_zero_table[bb]; + tz2 = dec_trailing_zero_table[cc]; + tz = cc ? tz2 : (tz1 + tz2); + buf += 4 - tz; + return buf; + } +} + +/** + Write an unsigned integer with a length of 16 or 17 with trailing zero trimmed. + These digits are named as "abbccddeeffgghhii" here. + For example, input 1234567890123000, output "1234567890123". + */ +static_inline u8 *write_u64_len_16_to_17_trim(u64 val, u8 *buf) { + u32 tz, tz1, tz2; + + u32 abbccddee = (u32)(val / 100000000); + u32 ffgghhii = (u32)(val - (u64)abbccddee * 100000000); + u32 abbcc = abbccddee / 10000; + u32 ddee = abbccddee - abbcc * 10000; + u32 abb = (u32)(((u64)abbcc * 167773) >> 24); /* (abbcc / 100) */ + u32 a = (abb * 41) >> 12; /* (abb / 100) */ + u32 bb = abb - a * 100; /* (abb % 100) */ + u32 cc = abbcc - abb * 100; /* (abbcc % 100) */ + buf[0] = (u8)(a + '0'); + buf += a > 0; + byte_copy_2(buf + 0, digit_table + bb * 2); + byte_copy_2(buf + 2, digit_table + cc * 2); + + if (ffgghhii) { + u32 dd = (ddee * 5243) >> 19; /* (ddee / 100) */ + u32 ee = ddee - dd * 100; /* (ddee % 100) */ + u32 ffgg = (u32)(((u64)ffgghhii * 109951163) >> 40); /* (val / 10000) */ + u32 hhii = ffgghhii - ffgg * 10000; /* (val % 10000) */ + u32 ff = (ffgg * 5243) >> 19; /* (aabb / 100) */ + u32 gg = ffgg - ff * 100; /* (aabb % 100) */ + byte_copy_2(buf + 4, digit_table + dd * 2); + byte_copy_2(buf + 6, digit_table + ee * 2); + byte_copy_2(buf + 8, digit_table + ff * 2); + byte_copy_2(buf + 10, digit_table + gg * 2); + if (hhii) { + u32 hh = (hhii * 5243) >> 19; /* (ccdd / 100) */ + u32 ii = hhii - hh * 100; /* (ccdd % 100) */ + byte_copy_2(buf + 12, digit_table + hh * 2); + byte_copy_2(buf + 14, digit_table + ii * 2); + tz1 = dec_trailing_zero_table[hh]; + tz2 = dec_trailing_zero_table[ii]; + tz = ii ? tz2 : (tz1 + 2); + return buf + 16 - tz; + } else { + tz1 = dec_trailing_zero_table[ff]; + tz2 = dec_trailing_zero_table[gg]; + tz = gg ? tz2 : (tz1 + 2); + return buf + 12 - tz; + } + } else { + if (ddee) { + u32 dd = (ddee * 5243) >> 19; /* (ddee / 100) */ + u32 ee = ddee - dd * 100; /* (ddee % 100) */ + byte_copy_2(buf + 4, digit_table + dd * 2); + byte_copy_2(buf + 6, digit_table + ee * 2); + tz1 = dec_trailing_zero_table[dd]; + tz2 = dec_trailing_zero_table[ee]; + tz = ee ? tz2 : (tz1 + 2); + return buf + 8 - tz; + } else { + tz1 = dec_trailing_zero_table[bb]; + tz2 = dec_trailing_zero_table[cc]; + tz = cc ? tz2 : (tz1 + tz2); + return buf + 4 - tz; + } + } +} + +/** Write exponent part in range `e-45` to `e38`. */ +static_inline u8 *write_f32_exp(i32 exp, u8 *buf) { + bool lz; + byte_copy_2(buf, "e-"); + buf += 2 - (exp >= 0); + exp = exp < 0 ? -exp : exp; + lz = exp < 10; + byte_copy_2(buf + 0, digit_table + (u32)exp * 2 + lz); + return buf + 2 - lz; +} + +/** Write exponent part in range `e-324` to `e308`. */ +static_inline u8 *write_f64_exp(i32 exp, u8 *buf) { + byte_copy_2(buf, "e-"); + buf += 2 - (exp >= 0); + exp = exp < 0 ? -exp : exp; + if (exp < 100) { + bool lz = exp < 10; + byte_copy_2(buf + 0, digit_table + (u32)exp * 2 + lz); + return buf + 2 - lz; + } else { + u32 hi = ((u32)exp * 656) >> 16; /* exp / 100 */ + u32 lo = (u32)exp - hi * 100; /* exp % 100 */ + buf[0] = (u8)((u8)hi + (u8)'0'); + byte_copy_2(buf + 1, digit_table + lo * 2); + return buf + 3; + } +} + +/** Magic number for fast `divide by power of 10`. */ +typedef struct { + u64 p10, mul; + u32 shr1, shr2; +} div_pow10_magic; + +/** Generated with llvm, see https://github.com/llvm/llvm-project/ + blob/main/llvm/lib/Support/DivisionByConstantInfo.cpp */ +static const div_pow10_magic div_pow10_table[] = { + { U64(0x00000000, 0x00000001), U64(0x00000000, 0x00000000), 0, 0 }, + { U64(0x00000000, 0x0000000A), U64(0xCCCCCCCC, 0xCCCCCCCD), 0, 3 }, + { U64(0x00000000, 0x00000064), U64(0x28F5C28F, 0x5C28F5C3), 2, 2 }, + { U64(0x00000000, 0x000003E8), U64(0x20C49BA5, 0xE353F7CF), 3, 4 }, + { U64(0x00000000, 0x00002710), U64(0x346DC5D6, 0x3886594B), 0, 11 }, + { U64(0x00000000, 0x000186A0), U64(0x0A7C5AC4, 0x71B47843), 5, 7 }, + { U64(0x00000000, 0x000F4240), U64(0x431BDE82, 0xD7B634DB), 0, 18 }, + { U64(0x00000000, 0x00989680), U64(0xD6BF94D5, 0xE57A42BD), 0, 23 }, + { U64(0x00000000, 0x05F5E100), U64(0xABCC7711, 0x8461CEFD), 0, 26 }, + { U64(0x00000000, 0x3B9ACA00), U64(0x0044B82F, 0xA09B5A53), 9, 11 }, + { U64(0x00000002, 0x540BE400), U64(0xDBE6FECE, 0xBDEDD5BF), 0, 33 }, + { U64(0x00000017, 0x4876E800), U64(0xAFEBFF0B, 0xCB24AAFF), 0, 36 }, + { U64(0x000000E8, 0xD4A51000), U64(0x232F3302, 0x5BD42233), 0, 37 }, + { U64(0x00000918, 0x4E72A000), U64(0x384B84D0, 0x92ED0385), 0, 41 }, + { U64(0x00005AF3, 0x107A4000), U64(0x0B424DC3, 0x5095CD81), 0, 42 }, + { U64(0x00038D7E, 0xA4C68000), U64(0x00024075, 0xF3DCEAC3), 15, 20 }, + { U64(0x002386F2, 0x6FC10000), U64(0x39A5652F, 0xB1137857), 0, 51 }, + { U64(0x01634578, 0x5D8A0000), U64(0x00005C3B, 0xD5191B53), 17, 22 }, + { U64(0x0DE0B6B3, 0xA7640000), U64(0x000049C9, 0x7747490F), 18, 24 }, + { U64(0x8AC72304, 0x89E80000), U64(0x760F253E, 0xDB4AB0d3), 0, 62 }, +}; + +/** Divide a number by power of 10. */ +static_inline void div_pow10(u64 num, u32 exp, u64 *div, u64 *mod, u64 *p10) { + u64 hi, lo; + div_pow10_magic m = div_pow10_table[exp]; + u128_mul(num >> m.shr1, m.mul, &hi, &lo); + *div = hi >> m.shr2; + *mod = num - (*div * m.p10); + *p10 = m.p10; +} + +/** Multiplies 64-bit integer and returns highest 64-bit rounded value. */ +static_inline u32 u64_round_to_odd(u64 u, u32 cp) { + u64 hi, lo; + u32 y_hi, y_lo; + u128_mul(cp, u, &hi, &lo); + y_hi = (u32)hi; + y_lo = (u32)(lo >> 32); + return y_hi | (y_lo > 1); +} + +/** Multiplies 128-bit integer and returns highest 64-bit rounded value. */ +static_inline u64 u128_round_to_odd(u64 hi, u64 lo, u64 cp) { + u64 x_hi, x_lo, y_hi, y_lo; + u128_mul(cp, lo, &x_hi, &x_lo); + u128_mul_add(cp, hi, x_hi, &y_hi, &y_lo); + return y_hi | (y_lo > 1); +} + +/** Convert f32 from binary to decimal (shortest but may have trailing zeros). + The input should not be 0, inf or nan. */ +static_inline void f32_bin_to_dec(u32 sig_raw, u32 exp_raw, + u32 sig_bin, i32 exp_bin, + u32 *sig_dec, i32 *exp_dec) { + + bool is_even, irregular, round_up, trim; + bool u0_inside, u1_inside, w0_inside, w1_inside; + u64 p10_hi, p10_lo, hi, lo; + u32 s, sp, cb, cbl, cbr, vb, vbl, vbr, upper, lower, mid; + i32 k, h; + + /* Fast path, see f64_bin_to_dec(). */ + while (likely(sig_raw)) { + u32 mod, dec, add_1, add_10, s_hi, s_lo; + u32 c, half_ulp, t0, t1; + + /* k = floor(exp_bin * log10(2)); */ + /* h = exp_bin + floor(log2(10) * -k); (h = 0/1/2/3) */ + k = (i32)(exp_bin * 315653) >> 20; + h = exp_bin + ((-k * 217707) >> 16); + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + + /* sig_bin << (1/2/3/4) */ + cb = sig_bin << (h + 1); + u128_mul(cb, p10_hi, &hi, &lo); + s_hi = (u32)(hi); + s_lo = (u32)(lo >> 32); + mod = s_hi % 10; + dec = s_hi - mod; + + /* right shift 4 to fit in u32 */ + c = (mod << (32 - 4)) | (s_lo >> 4); + half_ulp = (u32)(p10_hi >> (32 + 4 - h)); + + /* check w1, u0, w0 range */ + w1_inside = (s_lo >= ((u32)1 << 31)); + if (unlikely(s_lo == ((u32)1 << 31))) break; + u0_inside = (half_ulp >= c); + if (unlikely(half_ulp == c)) break; + t0 = (u32)10 << (32 - 4); + t1 = c + half_ulp; + w0_inside = (t1 >= t0); + if (unlikely(t0 - t1 <= (u32)1)) break; + + trim = (u0_inside | w0_inside); + add_10 = (w0_inside ? 10 : 0); + add_1 = mod + w1_inside; + *sig_dec = dec + (trim ? add_10 : add_1); + *exp_dec = k; + return; + } + + /* Schubfach algorithm, see f64_bin_to_dec(). */ + irregular = (sig_raw == 0 && exp_raw > 1); + is_even = !(sig_bin & 1); + cbl = 4 * sig_bin - 2 + irregular; + cb = 4 * sig_bin; + cbr = 4 * sig_bin + 2; + + /* k = floor(exp_bin * log10(2) + (irregular ? log10(3.0 / 4.0) : 0)); */ + /* h = exp_bin + floor(log2(10) * -k) + 1; (h = 1/2/3/4) */ + k = (i32)(exp_bin * 315653 - (irregular ? 131237 : 0)) >> 20; + h = exp_bin + ((-k * 217707) >> 16) + 1; + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + p10_hi += 1; + + vbl = u64_round_to_odd(p10_hi, cbl << h); + vb = u64_round_to_odd(p10_hi, cb << h); + vbr = u64_round_to_odd(p10_hi, cbr << h); + lower = vbl + !is_even; + upper = vbr - !is_even; + + s = vb / 4; + if (s >= 10) { + sp = s / 10; + u0_inside = (lower <= 40 * sp); + w0_inside = (upper >= 40 * sp + 40); + if (u0_inside != w0_inside) { + *sig_dec = sp * 10 + (w0_inside ? 10 : 0); + *exp_dec = k; + return; + } + } + u1_inside = (lower <= 4 * s); + w1_inside = (upper >= 4 * s + 4); + mid = 4 * s + 2; + round_up = (vb > mid) || (vb == mid && (s & 1) != 0); + *sig_dec = s + ((u1_inside != w1_inside) ? w1_inside : round_up); + *exp_dec = k; +} + +/** Convert f64 from binary to decimal (shortest but may have trailing zeros). + The input should not be 0, inf or nan. */ +static_inline void f64_bin_to_dec(u64 sig_raw, u32 exp_raw, + u64 sig_bin, i32 exp_bin, + u64 *sig_dec, i32 *exp_dec) { + + bool is_even, irregular, round_up, trim; + bool u0_inside, u1_inside, w0_inside, w1_inside; + u64 s, sp, cb, cbl, cbr, vb, vbl, vbr, p10_hi, p10_lo, upper, lower, mid; + i32 k, h; + + /* + Fast path: + For regular spacing significand 'c', there are 4 candidates: + + u0 u1 c w1 w0 + ----|----|----|----|----|-*--|----|----|----|----|----|----|----|---- + 9 0 1 2 3 4 5 6 7 8 9 0 1 + |___________________|___________________| + 1ulp + + The `1ulp` is in the range [1.0, 10.0). + If (c - 0.5ulp < u0), trim the last digit and round down. + If (c + 0.5ulp > w0), trim the last digit and round up. + If (c - 0.5ulp < u1), round down. + If (c + 0.5ulp > w1), round up. + */ + while (likely(sig_raw)) { + u64 mod, dec, add_1, add_10, s_hi, s_lo; + u64 c, half_ulp, t0, t1; + + /* k = floor(exp_bin * log10(2)); */ + /* h = exp_bin + floor(log2(10) * -k); (h = 0/1/2/3) */ + k = (i32)(exp_bin * 315653) >> 20; + h = exp_bin + ((-k * 217707) >> 16); + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + + /* sig_bin << (1/2/3/4) */ + cb = sig_bin << (h + 1); + u128_mul(cb, p10_lo, &s_hi, &s_lo); + u128_mul_add(cb, p10_hi, s_hi, &s_hi, &s_lo); + mod = s_hi % 10; + dec = s_hi - mod; + + /* right shift 4 to fit in u64 */ + c = (mod << (64 - 4)) | (s_lo >> 4); + half_ulp = p10_hi >> (4 - h); + + /* check w1, u0, w0 range */ + w1_inside = (s_lo >= ((u64)1 << 63)); + if (unlikely(s_lo == ((u64)1 << 63))) break; + u0_inside = (half_ulp >= c); + if (unlikely(half_ulp == c)) break; + t0 = ((u64)10 << (64 - 4)); + t1 = c + half_ulp; + w0_inside = (t1 >= t0); + if (unlikely(t0 - t1 <= (u64)1)) break; + + trim = (u0_inside | w0_inside); + add_10 = (w0_inside ? 10 : 0); + add_1 = mod + w1_inside; + *sig_dec = dec + (trim ? add_10 : add_1); + *exp_dec = k; + return; + } + + /* + Schubfach algorithm: + Raffaello Giulietti, The Schubfach way to render doubles, 2022. + https://drive.google.com/file/d/1gp5xv4CAa78SVgCeWfGqqI4FfYYYuNFb (Paper) + https://github.com/openjdk/jdk/pull/3402 (Java implementation) + https://github.com/abolz/Drachennest (C++ implementation) + */ + irregular = (sig_raw == 0 && exp_raw > 1); + is_even = !(sig_bin & 1); + cbl = 4 * sig_bin - 2 + irregular; + cb = 4 * sig_bin; + cbr = 4 * sig_bin + 2; + + /* k = floor(exp_bin * log10(2) + (irregular ? log10(3.0 / 4.0) : 0)); */ + /* h = exp_bin + floor(log2(10) * -k) + 1; (h = 1/2/3/4) */ + k = (i32)(exp_bin * 315653 - (irregular ? 131237 : 0)) >> 20; + h = exp_bin + ((-k * 217707) >> 16) + 1; + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + p10_lo += 1; + + vbl = u128_round_to_odd(p10_hi, p10_lo, cbl << h); + vb = u128_round_to_odd(p10_hi, p10_lo, cb << h); + vbr = u128_round_to_odd(p10_hi, p10_lo, cbr << h); + lower = vbl + !is_even; + upper = vbr - !is_even; + + s = vb / 4; + if (s >= 10) { + sp = s / 10; + u0_inside = (lower <= 40 * sp); + w0_inside = (upper >= 40 * sp + 40); + if (u0_inside != w0_inside) { + *sig_dec = sp * 10 + (w0_inside ? 10 : 0); + *exp_dec = k; + return; + } + } + u1_inside = (lower <= 4 * s); + w1_inside = (upper >= 4 * s + 4); + mid = 4 * s + 2; + round_up = (vb > mid) || (vb == mid && (s & 1) != 0); + *sig_dec = s + ((u1_inside != w1_inside) ? w1_inside : round_up); + *exp_dec = k; +} + +/** Convert f64 from binary to decimal (fast but not the shortest). + The input should not be 0, inf, nan. */ +static_inline void f64_bin_to_dec_fast(u64 sig_raw, u32 exp_raw, + u64 sig_bin, i32 exp_bin, + u64 *sig_dec, i32 *exp_dec, + bool *round_up) { + u64 cb, p10_hi, p10_lo, s_hi, s_lo; + i32 k, h; + bool irregular, u; + + irregular = (sig_raw == 0 && exp_raw > 1); + + /* k = floor(exp_bin * log10(2) + (irregular ? log10(3.0 / 4.0) : 0)); */ + /* h = exp_bin + floor(log2(10) * -k) + 1; (h = 1/2/3/4) */ + k = (i32)(exp_bin * 315653 - (irregular ? 131237 : 0)) >> 20; + h = exp_bin + ((-k * 217707) >> 16); + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + + /* sig_bin << (1/2/3/4) */ + cb = sig_bin << (h + 1); + u128_mul(cb, p10_lo, &s_hi, &s_lo); + u128_mul_add(cb, p10_hi, s_hi, &s_hi, &s_lo); + + /* round up */ + u = s_lo >= (irregular ? U64(0x55555555, 0x55555555) : ((u64)1 << 63)); + + *sig_dec = s_hi + u; + *exp_dec = k; + *round_up = u; + return; +} + +/** Write inf/nan if allowed. */ +static_inline u8 *write_inf_or_nan(u8 *buf, yyjson_write_flag flg, + u64 sig_raw, bool sign) { + if (has_flg(INF_AND_NAN_AS_NULL)) { + byte_copy_4(buf, "null"); + return buf + 4; + } + if (has_allow(INF_AND_NAN)) { + if (sig_raw == 0) { + buf[0] = '-'; + buf += sign; + byte_copy_8(buf, "Infinity"); + return buf + 8; + } else { + byte_copy_4(buf, "NaN"); + return buf + 3; + } + } + return NULL; +} + +/** + Write a float number (requires 40 bytes buffer). + We follow the ECMAScript specification for printing floating-point numbers, + similar to `Number.prototype.toString()`, but with the following changes: + 1. Keep the negative sign of `-0.0` to preserve input information. + 2. Keep decimal point to indicate the number is floating point. + 3. Remove positive sign in the exponent part. + */ +static_noinline u8 *write_f32_raw(u8 *buf, u64 raw_f64, + yyjson_write_flag flg) { + u32 sig_bin, sig_dec, sig_raw; + i32 exp_bin, exp_dec, sig_len, dot_ofs; + u32 exp_raw, raw; + u8 *end; + bool sign; + + /* cast double to float */ + raw = f32_to_bits(f64_to_f32(f64_from_bits(raw_f64))); + + /* decode raw bytes from IEEE-754 double format. */ + sign = (bool)(raw >> (F32_BITS - 1)); + sig_raw = raw & F32_SIG_MASK; + exp_raw = (raw & F32_EXP_MASK) >> F32_SIG_BITS; + + /* return inf or nan */ + if (unlikely(exp_raw == ((u32)1 << F32_EXP_BITS) - 1)) { + return write_inf_or_nan(buf, flg, sig_raw, sign); + } + + /* add sign for all finite number */ + buf[0] = '-'; + buf += sign; + + /* return zero */ + if ((raw << 1) == 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + + if (likely(exp_raw != 0)) { + /* normal number */ + sig_bin = sig_raw | ((u32)1 << F32_SIG_BITS); + exp_bin = (i32)exp_raw - F32_EXP_BIAS - F32_SIG_BITS; + + /* fast path for small integer number without fraction */ + if ((-F32_SIG_BITS <= exp_bin && exp_bin <= 0) && + (u64_tz_bits(sig_bin) >= (u32)-exp_bin)) { + sig_dec = sig_bin >> -exp_bin; /* range: [1, 0xFFFFFF] */ + buf = write_u32_len_1_to_8(sig_dec, buf); + byte_copy_2(buf, ".0"); + return buf + 2; + } + + /* binary to decimal */ + f32_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* the sig length is 7 or 9 */ + sig_len = 7 + (sig_dec >= (u32)10000000) + (sig_dec >= (u32)100000000); + + /* the decimal point offset relative to the first digit */ + dot_ofs = sig_len + exp_dec; + + if (-6 < dot_ofs && dot_ofs <= 21) { + i32 num_sep_pos, dot_set_pos, pre_ofs; + u8 *num_hdr, *num_end, *num_sep, *dot_end; + bool no_pre_zero; + + /* fill zeros */ + memset(buf, '0', 32); + + /* not prefixed with zero, e.g. 1.234, 1234.0 */ + no_pre_zero = (dot_ofs > 0); + + /* write the number as digits */ + pre_ofs = no_pre_zero ? 0 : (2 - dot_ofs); + num_hdr = buf + pre_ofs; + num_end = write_u32_len_7_to_9_trim(sig_dec, num_hdr); + + /* seperate these digits to leave a space for dot */ + num_sep_pos = no_pre_zero ? dot_ofs : 0; + num_sep = num_hdr + num_sep_pos; + byte_move_8(num_sep + no_pre_zero, num_sep); + num_end += no_pre_zero; + + /* write the dot */ + dot_set_pos = yyjson_max(dot_ofs, 1); + buf[dot_set_pos] = '.'; + + /* return the ending */ + dot_end = buf + dot_ofs + 2; + return yyjson_max(dot_end, num_end); + + } else { + /* write with scientific notation, e.g. 1.234e56 */ + end = write_u32_len_7_to_9_trim(sig_dec, buf + 1); + end -= (end == buf + 2); /* remove '.0', e.g. 2.0e34 -> 2e34 */ + exp_dec += sig_len - 1; + buf[0] = buf[1]; + buf[1] = '.'; + return write_f32_exp(exp_dec, end); + } + + } else { + /* subnormal number */ + sig_bin = sig_raw; + exp_bin = 1 - F32_EXP_BIAS - F32_SIG_BITS; + + /* binary to decimal */ + f32_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* write significand part */ + end = write_u32_len_1_to_8(sig_dec, buf + 1); + buf[0] = buf[1]; + buf[1] = '.'; + exp_dec += (i32)(end - buf) - 2; + + /* trim trailing zeros */ + end -= *(end - 1) == '0'; /* branchless for last zero */ + end -= *(end - 1) == '0'; /* branchless for second last zero */ + while (*(end - 1) == '0') end--; /* for unlikely more zeros */ + end -= *(end - 1) == '.'; /* remove dot, e.g. 2.e-321 -> 2e-321 */ + + /* write exponent part */ + return write_f32_exp(exp_dec, end); + } +} + +/** + Write a double number (requires 40 bytes buffer). + We follow the ECMAScript specification for printing floating-point numbers, + similar to `Number.prototype.toString()`, but with the following changes: + 1. Keep the negative sign of `-0.0` to preserve input information. + 2. Keep decimal point to indicate the number is floating point. + 3. Remove positive sign in the exponent part. + */ +static_noinline u8 *write_f64_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { + u64 sig_bin, sig_dec, sig_raw; + i32 exp_bin, exp_dec, sig_len, dot_ofs; + u32 exp_raw; + u8 *end; + bool sign; + + /* decode raw bytes from IEEE-754 double format. */ + sign = (bool)(raw >> (F64_BITS - 1)); + sig_raw = raw & F64_SIG_MASK; + exp_raw = (u32)((raw & F64_EXP_MASK) >> F64_SIG_BITS); + + /* return inf or nan */ + if (unlikely(exp_raw == ((u32)1 << F64_EXP_BITS) - 1)) { + return write_inf_or_nan(buf, flg, sig_raw, sign); + } + + /* add sign for all finite number */ + buf[0] = '-'; + buf += sign; + + /* return zero */ + if ((raw << 1) == 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + + if (likely(exp_raw != 0)) { + /* normal number */ + sig_bin = sig_raw | ((u64)1 << F64_SIG_BITS); + exp_bin = (i32)exp_raw - F64_EXP_BIAS - F64_SIG_BITS; + + /* fast path for small integer number without fraction */ + if ((-F64_SIG_BITS <= exp_bin && exp_bin <= 0) && + (u64_tz_bits(sig_bin) >= (u32)-exp_bin)) { + sig_dec = sig_bin >> -exp_bin; /* range: [1, 0x1FFFFFFFFFFFFF] */ + buf = write_u64_len_1_to_16(sig_dec, buf); + byte_copy_2(buf, ".0"); + return buf + 2; + } + + /* binary to decimal */ + f64_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* the sig length is 16 or 17 */ + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* the decimal point offset relative to the first digit */ + dot_ofs = sig_len + exp_dec; + + if (-6 < dot_ofs && dot_ofs <= 21) { + i32 num_sep_pos, dot_set_pos, pre_ofs; + u8 *num_hdr, *num_end, *num_sep, *dot_end; + bool no_pre_zero; + + /* fill zeros */ + memset(buf, '0', 32); + + /* not prefixed with zero, e.g. 1.234, 1234.0 */ + no_pre_zero = (dot_ofs > 0); + + /* write the number as digits */ + pre_ofs = no_pre_zero ? 0 : (2 - dot_ofs); + num_hdr = buf + pre_ofs; + num_end = write_u64_len_16_to_17_trim(sig_dec, num_hdr); + + /* seperate these digits to leave a space for dot */ + num_sep_pos = no_pre_zero ? dot_ofs : 0; + num_sep = num_hdr + num_sep_pos; + byte_move_16(num_sep + no_pre_zero, num_sep); + num_end += no_pre_zero; + + /* write the dot */ + dot_set_pos = yyjson_max(dot_ofs, 1); + buf[dot_set_pos] = '.'; + + /* return the ending */ + dot_end = buf + dot_ofs + 2; + return yyjson_max(dot_end, num_end); + + } else { + /* write with scientific notation, e.g. 1.234e56 */ + end = write_u64_len_16_to_17_trim(sig_dec, buf + 1); + end -= (end == buf + 2); /* remove '.0', e.g. 2.0e34 -> 2e34 */ + exp_dec += sig_len - 1; + buf[0] = buf[1]; + buf[1] = '.'; + return write_f64_exp(exp_dec, end); + } + + } else { + /* subnormal number */ + sig_bin = sig_raw; + exp_bin = 1 - F64_EXP_BIAS - F64_SIG_BITS; + + /* binary to decimal */ + f64_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* write significand part */ + end = write_u64_len_1_to_17(sig_dec, buf + 1); + buf[0] = buf[1]; + buf[1] = '.'; + exp_dec += (i32)(end - buf) - 2; + + /* trim trailing zeros */ + end -= *(end - 1) == '0'; /* branchless for last zero */ + end -= *(end - 1) == '0'; /* branchless for second last zero */ + while (*(end - 1) == '0') end--; /* for unlikely more zeros */ + end -= *(end - 1) == '.'; /* remove dot, e.g. 2.e-321 -> 2e-321 */ + + /* write exponent part */ + return write_f64_exp(exp_dec, end); + } +} + +/** + Write a double number using fixed-point notation (requires 40 bytes buffer). + + We follow the ECMAScript specification for printing floating-point numbers, + similar to `Number.prototype.toFixed(prec)`, but with the following changes: + 1. Keep the negative sign of `-0.0` to preserve input information. + 2. Keep decimal point to indicate the number is floating point. + 3. Remove positive sign in the exponent part. + 4. Remove trailing zeros and reduce unnecessary precision. + */ +static_noinline u8 *write_f64_raw_fixed(u8 *buf, u64 raw, yyjson_write_flag flg, + u32 prec) { + u64 sig_bin, sig_dec, sig_raw; + i32 exp_bin, exp_dec, sig_len, dot_ofs; + u32 exp_raw; + u8 *end; + bool sign; + + /* decode raw bytes from IEEE-754 double format. */ + sign = (bool)(raw >> (F64_BITS - 1)); + sig_raw = raw & F64_SIG_MASK; + exp_raw = (u32)((raw & F64_EXP_MASK) >> F64_SIG_BITS); + + /* return inf or nan */ + if (unlikely(exp_raw == ((u32)1 << F64_EXP_BITS) - 1)) { + return write_inf_or_nan(buf, flg, sig_raw, sign); + } + + /* add sign for all finite number */ + buf[0] = '-'; + buf += sign; + + /* return zero */ + if ((raw << 1) == 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + + if (likely(exp_raw != 0)) { + /* normal number */ + sig_bin = sig_raw | ((u64)1 << F64_SIG_BITS); + exp_bin = (i32)exp_raw - F64_EXP_BIAS - F64_SIG_BITS; + + /* fast path for small integer number without fraction */ + if ((-F64_SIG_BITS <= exp_bin && exp_bin <= 0) && + (u64_tz_bits(sig_bin) >= (u32)-exp_bin)) { + sig_dec = sig_bin >> -exp_bin; /* range: [1, 0x1FFFFFFFFFFFFF] */ + buf = write_u64_len_1_to_16(sig_dec, buf); + byte_copy_2(buf, ".0"); + return buf + 2; + } + + /* only `fabs(num) < 1e21` are processed here. */ + if ((raw << 1) < (U64(0x444B1AE4, 0xD6E2EF50) << 1)) { + i32 num_sep_pos, dot_set_pos, pre_ofs; + u8 *num_hdr, *num_end, *num_sep; + bool round_up, no_pre_zero; + + /* binary to decimal */ + f64_bin_to_dec_fast(sig_raw, exp_raw, sig_bin, exp_bin, + &sig_dec, &exp_dec, &round_up); + + /* the sig length is 16 or 17 */ + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* limit the length of digits after the decimal point */ + if (exp_dec < -1) { + i32 sig_len_cut = -exp_dec - (i32)prec; + if (sig_len_cut > sig_len) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + if (sig_len_cut > 0) { + u64 div, mod, p10; + + /* remove round up */ + sig_dec -= round_up; + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* cut off some digits */ + div_pow10(sig_dec, (u32)sig_len_cut, &div, &mod, &p10); + + /* add round up */ + sig_dec = div + (mod >= p10 / 2); + + /* update exp and sig length */ + exp_dec += sig_len_cut; + sig_len -= sig_len_cut; + sig_len += (sig_len >= 0) && + (sig_dec >= div_pow10_table[sig_len].p10); + } + if (sig_len <= 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + } + + /* fill zeros */ + memset(buf, '0', 32); + + /* the decimal point offset relative to the first digit */ + dot_ofs = sig_len + exp_dec; + + /* not prefixed with zero, e.g. 1.234, 1234.0 */ + no_pre_zero = (dot_ofs > 0); + + /* write the number as digits */ + pre_ofs = no_pre_zero ? 0 : (1 - dot_ofs); + num_hdr = buf + pre_ofs; + num_end = write_u64_len_1_to_17(sig_dec, num_hdr); + + /* seperate these digits to leave a space for dot */ + num_sep_pos = no_pre_zero ? dot_ofs : -dot_ofs; + num_sep = buf + num_sep_pos; + byte_move_16(num_sep + 1, num_sep); + num_end += (exp_dec < 0); + + /* write the dot */ + dot_set_pos = yyjson_max(dot_ofs, 1); + buf[dot_set_pos] = '.'; + + /* remove trailing zeros */ + buf += dot_set_pos + 2; + buf = yyjson_max(buf, num_end); + buf -= *(buf - 1) == '0'; /* branchless for last zero */ + buf -= *(buf - 1) == '0'; /* branchless for second last zero */ + while (*(buf - 1) == '0') buf--; /* for unlikely more zeros */ + buf += *(buf - 1) == '.'; /* keep a zero after dot */ + return buf; + + } else { + /* binary to decimal */ + f64_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, + &sig_dec, &exp_dec); + + /* the sig length is 16 or 17 */ + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* write with scientific notation, e.g. 1.234e56 */ + end = write_u64_len_16_to_17_trim(sig_dec, buf + 1); + end -= (end == buf + 2); /* remove '.0', e.g. 2.0e34 -> 2e34 */ + exp_dec += sig_len - 1; + buf[0] = buf[1]; + buf[1] = '.'; + return write_f64_exp(exp_dec, end); + } + } else { + /* subnormal number */ + byte_copy_4(buf, "0.0"); + return buf + 3; + } +} + +#else /* FP_WRITER */ + +#if YYJSON_MSC_VER >= 1400 +#define snprintf_num(buf, len, fmt, dig, val) \ + sprintf_s((char *)buf, len, fmt, dig, val) +#elif defined(snprintf) || (YYJSON_STDC_VER >= 199901L) +#define snprintf_num(buf, len, fmt, dig, val) \ + snprintf((char *)buf, len, fmt, dig, val) +#else +#define snprintf_num(buf, len, fmt, dig, val) \ + sprintf((char *)buf, fmt, dig, val) +#endif + +static_noinline u8 *write_fp_reformat(u8 *buf, int len, + yyjson_write_flag flg, bool fixed) { + u8 *cur = buf; + if (unlikely(len < 1)) return NULL; + cur += (*cur == '-'); + if (unlikely(!char_is_digit(*cur))) { + /* nan, inf, or bad output */ + if (has_flg(INF_AND_NAN_AS_NULL)) { + byte_copy_4(buf, "null"); + return buf + 4; + } else if (has_allow(INF_AND_NAN)) { + if (*cur == 'i') { + byte_copy_8(cur, "Infinity"); + return cur + 8; + } else if (*cur == 'n') { + byte_copy_4(buf, "NaN"); + return buf + 3; + } + } + return NULL; + } else { + /* finite number */ + u8 *end = buf + len, *dot = NULL, *exp = NULL; + + /* + The snprintf() function is locale-dependent. For currently known + locales, (en, zh, ja, ko, am, he, hi) use '.' as the decimal point, + while other locales use ',' as the decimal point. we need to replace + ',' with '.' to avoid the locale setting. + */ + for (; cur < end; cur++) { + switch (*cur) { + case ',': *cur = '.'; /* fallthrough */ + case '.': dot = cur; break; + case 'e': exp = cur; break; + default: break; + } + } + if (fixed) { + /* remove trailing zeros */ + while (*(end - 1) == '0') end--; + end += *(end - 1) == '.'; + } else { + if (!dot && !exp) { + /* add decimal point, e.g. 123 -> 123.0 */ + byte_copy_2(end, ".0"); + end += 2; + } else if (exp) { + cur = exp + 1; + /* remove positive sign in the exponent part */ + if (*cur == '+') { + memmove(cur, cur + 1, (usize)(end - cur - 1)); + end--; + } + cur += (*cur == '-'); + /* remove leading zeros in the exponent part */ + if (*cur == '0') { + u8 *hdr = cur++; + while (*cur == '0') cur++; + memmove(hdr, cur, (usize)(end - cur)); + end -= (usize)(cur - hdr); + } + } + } + return end; + } +} + +/** Write a double number (requires 40 bytes buffer). */ +static_noinline u8 *write_f64_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { +#if defined(DBL_DECIMAL_DIG) && DBL_DECIMAL_DIG < F64_DEC_DIG + int dig = DBL_DECIMAL_DIG; +#else + int dig = F64_DEC_DIG; +#endif + f64 val = f64_from_bits(raw); + int len = snprintf_num(buf, FP_BUF_LEN, "%.*g", dig, val); + return write_fp_reformat(buf, len, flg, false); +} + +/** Write a double number (requires 40 bytes buffer). */ +static_noinline u8 *write_f32_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { +#if defined(FLT_DECIMAL_DIG) && FLT_DECIMAL_DIG < F32_DEC_DIG + int dig = FLT_DECIMAL_DIG; +#else + int dig = F32_DEC_DIG; +#endif + f64 val = (f64)f64_to_f32(f64_from_bits(raw)); + int len = snprintf_num(buf, FP_BUF_LEN, "%.*g", dig, val); + return write_fp_reformat(buf, len, flg, false); +} + +/** Write a double number (requires 40 bytes buffer). */ +static_noinline u8 *write_f64_raw_fixed(u8 *buf, u64 raw, + yyjson_write_flag flg, u32 prec) { + f64 val = (f64)f64_from_bits(raw); + if (-1e21 < val && val < 1e21) { + int len = snprintf_num(buf, FP_BUF_LEN, "%.*f", (int)prec, val); + return write_fp_reformat(buf, len, flg, true); + } else { + return write_f64_raw(buf, raw, flg); + } +} + +#endif /* FP_WRITER */ + +/** Write a JSON number (requires 40 bytes buffer). */ +static_inline u8 *write_num(u8 *cur, yyjson_val *val, yyjson_write_flag flg) { + if (!(val->tag & YYJSON_SUBTYPE_REAL)) { + u64 pos = val->uni.u64; + u64 neg = ~pos + 1; + usize sign = ((val->tag & YYJSON_SUBTYPE_SINT) > 0) & ((i64)pos < 0); + *cur = '-'; + return write_u64(sign ? neg : pos, cur + sign); + } else { + u64 raw = val->uni.u64; + u32 val_fmt = (u32)(val->tag >> 32); + u32 all_fmt = flg; + u32 fmt = val_fmt | all_fmt; + if (likely(!(fmt >> (32 - YYJSON_WRITE_FP_FLAG_BITS)))) { + /* double to shortest */ + return write_f64_raw(cur, raw, flg); + } else if (fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS)) { + /* double to fixed */ + u32 val_prec = val_fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS); + u32 all_prec = all_fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS); + u32 prec = val_prec ? val_prec : all_prec; + return write_f64_raw_fixed(cur, raw, flg, prec); + } else { + if (fmt & YYJSON_WRITE_FP_TO_FLOAT) { + /* float to shortest */ + return write_f32_raw(cur, raw, flg); + } else { + /* double to shortest */ + return write_f64_raw(cur, raw, flg); + } + } + } +} + +char *yyjson_write_number(const yyjson_val *val, char *buf) { + if (unlikely(!val || !buf)) return NULL; + switch (val->tag & YYJSON_TAG_MASK) { + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT: { + buf = (char *)write_u64(val->uni.u64, (u8 *)buf); + *buf = '\0'; + return buf; + } + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT: { + u64 pos = val->uni.u64; + u64 neg = ~pos + 1; + usize sign = ((i64)pos < 0); + *buf = '-'; + buf = (char *)write_u64(sign ? neg : pos, (u8 *)buf + sign); + *buf = '\0'; + return buf; + } + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL: { + u64 raw = val->uni.u64; + u32 fmt = (u32)(val->tag >> 32); + u32 flg = YYJSON_WRITE_ALLOW_INF_AND_NAN; + if (likely(!(fmt >> (32 - YYJSON_WRITE_FP_FLAG_BITS)))) { + buf = (char *)write_f64_raw((u8 *)buf, raw, flg); + } else if (fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS)) { + u32 prec = fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS); + buf = (char *)write_f64_raw_fixed((u8 *)buf, raw, flg, prec); + } else { + if (fmt & YYJSON_WRITE_FP_TO_FLOAT) { + buf = (char *)write_f32_raw((u8 *)buf, raw, flg); + } else { + buf = (char *)write_f64_raw((u8 *)buf, raw, flg); + } + } + if (buf) *buf = '\0'; + return buf; + } + default: return NULL; + } +} + + + +/*============================================================================== + * MARK: - String Writer (Private) + *============================================================================*/ + +/** Character encode type, if (type > CHAR_ENC_ERR_1) bytes = type / 2; */ +typedef u8 char_enc_type; +#define CHAR_ENC_CPY_1 0 /* 1-byte UTF-8, copy. */ +#define CHAR_ENC_ERR_1 1 /* 1-byte UTF-8, error. */ +#define CHAR_ENC_ESC_A 2 /* 1-byte ASCII, escaped as '\x'. */ +#define CHAR_ENC_ESC_1 3 /* 1-byte UTF-8, escaped as '\uXXXX'. */ +#define CHAR_ENC_CPY_2 4 /* 2-byte UTF-8, copy. */ +#define CHAR_ENC_ESC_2 5 /* 2-byte UTF-8, escaped as '\uXXXX'. */ +#define CHAR_ENC_CPY_3 6 /* 3-byte UTF-8, copy. */ +#define CHAR_ENC_ESC_3 7 /* 3-byte UTF-8, escaped as '\uXXXX'. */ +#define CHAR_ENC_CPY_4 8 /* 4-byte UTF-8, copy. */ +#define CHAR_ENC_ESC_4 9 /* 4-byte UTF-8, escaped as '\uXXXX\uXXXX'. */ + +/** Character encode type table: don't escape unicode, don't escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_cpy[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Character encode type table: don't escape unicode, escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_cpy_slash[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Character encode type table: escape unicode, don't escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_esc[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 9, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Character encode type table: escape unicode, escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_esc_slash[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 9, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Escaped hex character table: ["00" "01" "02" ... "FD" "FE" "FF"]. + (generate with misc/make_tables.c) */ +yyjson_align(2) +static const u8 esc_hex_char_table[512] = { + '0', '0', '0', '1', '0', '2', '0', '3', + '0', '4', '0', '5', '0', '6', '0', '7', + '0', '8', '0', '9', '0', 'A', '0', 'B', + '0', 'C', '0', 'D', '0', 'E', '0', 'F', + '1', '0', '1', '1', '1', '2', '1', '3', + '1', '4', '1', '5', '1', '6', '1', '7', + '1', '8', '1', '9', '1', 'A', '1', 'B', + '1', 'C', '1', 'D', '1', 'E', '1', 'F', + '2', '0', '2', '1', '2', '2', '2', '3', + '2', '4', '2', '5', '2', '6', '2', '7', + '2', '8', '2', '9', '2', 'A', '2', 'B', + '2', 'C', '2', 'D', '2', 'E', '2', 'F', + '3', '0', '3', '1', '3', '2', '3', '3', + '3', '4', '3', '5', '3', '6', '3', '7', + '3', '8', '3', '9', '3', 'A', '3', 'B', + '3', 'C', '3', 'D', '3', 'E', '3', 'F', + '4', '0', '4', '1', '4', '2', '4', '3', + '4', '4', '4', '5', '4', '6', '4', '7', + '4', '8', '4', '9', '4', 'A', '4', 'B', + '4', 'C', '4', 'D', '4', 'E', '4', 'F', + '5', '0', '5', '1', '5', '2', '5', '3', + '5', '4', '5', '5', '5', '6', '5', '7', + '5', '8', '5', '9', '5', 'A', '5', 'B', + '5', 'C', '5', 'D', '5', 'E', '5', 'F', + '6', '0', '6', '1', '6', '2', '6', '3', + '6', '4', '6', '5', '6', '6', '6', '7', + '6', '8', '6', '9', '6', 'A', '6', 'B', + '6', 'C', '6', 'D', '6', 'E', '6', 'F', + '7', '0', '7', '1', '7', '2', '7', '3', + '7', '4', '7', '5', '7', '6', '7', '7', + '7', '8', '7', '9', '7', 'A', '7', 'B', + '7', 'C', '7', 'D', '7', 'E', '7', 'F', + '8', '0', '8', '1', '8', '2', '8', '3', + '8', '4', '8', '5', '8', '6', '8', '7', + '8', '8', '8', '9', '8', 'A', '8', 'B', + '8', 'C', '8', 'D', '8', 'E', '8', 'F', + '9', '0', '9', '1', '9', '2', '9', '3', + '9', '4', '9', '5', '9', '6', '9', '7', + '9', '8', '9', '9', '9', 'A', '9', 'B', + '9', 'C', '9', 'D', '9', 'E', '9', 'F', + 'A', '0', 'A', '1', 'A', '2', 'A', '3', + 'A', '4', 'A', '5', 'A', '6', 'A', '7', + 'A', '8', 'A', '9', 'A', 'A', 'A', 'B', + 'A', 'C', 'A', 'D', 'A', 'E', 'A', 'F', + 'B', '0', 'B', '1', 'B', '2', 'B', '3', + 'B', '4', 'B', '5', 'B', '6', 'B', '7', + 'B', '8', 'B', '9', 'B', 'A', 'B', 'B', + 'B', 'C', 'B', 'D', 'B', 'E', 'B', 'F', + 'C', '0', 'C', '1', 'C', '2', 'C', '3', + 'C', '4', 'C', '5', 'C', '6', 'C', '7', + 'C', '8', 'C', '9', 'C', 'A', 'C', 'B', + 'C', 'C', 'C', 'D', 'C', 'E', 'C', 'F', + 'D', '0', 'D', '1', 'D', '2', 'D', '3', + 'D', '4', 'D', '5', 'D', '6', 'D', '7', + 'D', '8', 'D', '9', 'D', 'A', 'D', 'B', + 'D', 'C', 'D', 'D', 'D', 'E', 'D', 'F', + 'E', '0', 'E', '1', 'E', '2', 'E', '3', + 'E', '4', 'E', '5', 'E', '6', 'E', '7', + 'E', '8', 'E', '9', 'E', 'A', 'E', 'B', + 'E', 'C', 'E', 'D', 'E', 'E', 'E', 'F', + 'F', '0', 'F', '1', 'F', '2', 'F', '3', + 'F', '4', 'F', '5', 'F', '6', 'F', '7', + 'F', '8', 'F', '9', 'F', 'A', 'F', 'B', + 'F', 'C', 'F', 'D', 'F', 'E', 'F', 'F' +}; + +/** Escaped single character table. (generate with misc/make_tables.c) */ +yyjson_align(2) +static const u8 esc_single_char_table[512] = { + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + '\\', 'b', '\\', 't', '\\', 'n', ' ', ' ', + '\\', 'f', '\\', 'r', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', '\\', '"', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', '\\', '/', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + '\\', '\\', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' +}; + +/** Returns the encode table with options. */ +static_inline const char_enc_type *get_enc_table_with_flag( + yyjson_write_flag flg) { + if (has_flg(ESCAPE_UNICODE)) { + if (has_flg(ESCAPE_SLASHES)) { + return enc_table_esc_slash; + } else { + return enc_table_esc; + } + } else { + if (has_flg(ESCAPE_SLASHES)) { + return enc_table_cpy_slash; + } else { + return enc_table_cpy; + } + } +} + +/** Write raw string. */ +static_inline u8 *write_raw(u8 *cur, const u8 *raw, usize raw_len) { + memcpy(cur, raw, raw_len); + return cur + raw_len; +} + +/** + Write string no-escape. + @param cur Buffer cursor. + @param str A UTF-8 string, null-terminator is not required. + @param str_len Length of string in bytes. + @return The buffer cursor after string. + */ +static_inline u8 *write_str_noesc(u8 *cur, const u8 *str, usize str_len) { + *cur++ = '"'; + while (str_len >= 16) { + byte_copy_16(cur, str); + cur += 16; + str += 16; + str_len -= 16; + } + while (str_len >= 4) { + byte_copy_4(cur, str); + cur += 4; + str += 4; + str_len -= 4; + } + while (str_len) { + *cur++ = *str++; + str_len -= 1; + } + *cur++ = '"'; + return cur; +} + +/** + Write UTF-8 string (requires len * 6 + 2 bytes buffer). + @param cur Buffer cursor. + @param esc Escape unicode. + @param inv Allow invalid unicode. + @param str A UTF-8 string, null-terminator is not required. + @param str_len Length of string in bytes. + @param enc_table Encode type table for character. + @return The buffer cursor after string, or NULL on invalid unicode. + */ +static_inline u8 *write_str(u8 *cur, bool esc, bool inv, + const u8 *str, usize str_len, + const char_enc_type *enc_table) { + /* The replacement character U+FFFD, used to indicate invalid character. */ + const v32 rep = {{ 'F', 'F', 'F', 'D' }}; + const v32 pre = {{ '\\', 'u', '0', '0' }}; + + const u8 *src = str; + const u8 *end = str + str_len; + *cur++ = '"'; + +copy_ascii: + /* + Copy continuous ASCII, loop unrolling, same as the following code: + + while (end > src) ( + if (unlikely(enc_table[*src])) break; + *cur++ = *src++; + ); + */ +#define expr_jump(i) \ + if (unlikely(enc_table[src[i]])) goto stop_char_##i; + +#define expr_stop(i) \ + stop_char_##i: \ + memcpy(cur, src, i); \ + cur += i; src += i; goto copy_utf8; + + while (end - src >= 16) { + repeat16_incr(expr_jump) + byte_copy_16(cur, src); + cur += 16; src += 16; + } + + while (end - src >= 4) { + repeat4_incr(expr_jump) + byte_copy_4(cur, src); + cur += 4; src += 4; + } + + while (end > src) { + expr_jump(0) + *cur++ = *src++; + } + + *cur++ = '"'; + return cur; + + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + +copy_utf8: + if (unlikely(src + 4 > end)) { + if (end == src) goto copy_end; + if (end - src < enc_table[*src] / 2) goto err_one; + } + switch (enc_table[*src]) { + case CHAR_ENC_CPY_1: { + *cur++ = *src++; + goto copy_ascii; + } + case CHAR_ENC_CPY_2: { +#if YYJSON_DISABLE_UTF8_VALIDATION + byte_copy_2(cur, src); +#else + u32 uni = 0; + byte_copy_2(&uni, src); + if (unlikely(!is_utf8_seq2(uni))) goto err_cpy; + byte_copy_2(cur, &uni); +#endif + cur += 2; + src += 2; + goto copy_utf8; + } + case CHAR_ENC_CPY_3: { +#if YYJSON_DISABLE_UTF8_VALIDATION + if (likely(src + 4 <= end)) { + byte_copy_4(cur, src); + } else { + byte_copy_2(cur, src); + cur[2] = src[2]; + } +#else + u32 uni, tmp; + if (likely(src + 4 <= end)) { + uni = byte_load_4(src); + if (unlikely(!is_utf8_seq3(uni))) goto err_cpy; + byte_copy_4(cur, src); + } else { + uni = byte_load_3(src); + if (unlikely(!is_utf8_seq3(uni))) goto err_cpy; + byte_copy_4(cur, &uni); + } +#endif + cur += 3; + src += 3; + goto copy_utf8; + } + case CHAR_ENC_CPY_4: { +#if YYJSON_DISABLE_UTF8_VALIDATION + byte_copy_4(cur, src); +#else + u32 uni, tmp; + uni = byte_load_4(src); + if (unlikely(!is_utf8_seq4(uni))) goto err_cpy; + byte_copy_4(cur, src); +#endif + cur += 4; + src += 4; + goto copy_utf8; + } + case CHAR_ENC_ESC_A: { + byte_copy_2(cur, &esc_single_char_table[*src * 2]); + cur += 2; + src += 1; + goto copy_utf8; + } + case CHAR_ENC_ESC_1: { + byte_copy_4(cur + 0, &pre); + byte_copy_2(cur + 4, &esc_hex_char_table[*src * 2]); + cur += 6; + src += 1; + goto copy_utf8; + } + case CHAR_ENC_ESC_2: { + u16 u; +#if !YYJSON_DISABLE_UTF8_VALIDATION + u32 v4 = 0; + u16 v2 = byte_load_2(src); + byte_copy_2(&v4, &v2); + if (unlikely(!is_utf8_seq2(v4))) goto err_esc; +#endif + u = (u16)(((u16)(src[0] & 0x1F) << 6) | + ((u16)(src[1] & 0x3F) << 0)); + byte_copy_2(cur + 0, &pre); + byte_copy_2(cur + 2, &esc_hex_char_table[(u >> 8) * 2]); + byte_copy_2(cur + 4, &esc_hex_char_table[(u & 0xFF) * 2]); + cur += 6; + src += 2; + goto copy_utf8; + } + case CHAR_ENC_ESC_3: { + u16 u; + u32 v, tmp; +#if !YYJSON_DISABLE_UTF8_VALIDATION + v = byte_load_3(src); + if (unlikely(!is_utf8_seq3(v))) goto err_esc; +#endif + u = (u16)(((u16)(src[0] & 0x0F) << 12) | + ((u16)(src[1] & 0x3F) << 6) | + ((u16)(src[2] & 0x3F) << 0)); + byte_copy_2(cur + 0, &pre); + byte_copy_2(cur + 2, &esc_hex_char_table[(u >> 8) * 2]); + byte_copy_2(cur + 4, &esc_hex_char_table[(u & 0xFF) * 2]); + cur += 6; + src += 3; + goto copy_utf8; + } + case CHAR_ENC_ESC_4: { + u32 hi, lo, u, v, tmp; +#if !YYJSON_DISABLE_UTF8_VALIDATION + v = byte_load_4(src); + if (unlikely(!is_utf8_seq4(v))) goto err_esc; +#endif + u = ((u32)(src[0] & 0x07) << 18) | + ((u32)(src[1] & 0x3F) << 12) | + ((u32)(src[2] & 0x3F) << 6) | + ((u32)(src[3] & 0x3F) << 0); + u -= 0x10000; + hi = (u >> 10) + 0xD800; + lo = (u & 0x3FF) + 0xDC00; + byte_copy_2(cur + 0, &pre); + byte_copy_2(cur + 2, &esc_hex_char_table[(hi >> 8) * 2]); + byte_copy_2(cur + 4, &esc_hex_char_table[(hi & 0xFF) * 2]); + byte_copy_2(cur + 6, &pre); + byte_copy_2(cur + 8, &esc_hex_char_table[(lo >> 8) * 2]); + byte_copy_2(cur + 10, &esc_hex_char_table[(lo & 0xFF) * 2]); + cur += 12; + src += 4; + goto copy_utf8; + } + case CHAR_ENC_ERR_1: { + goto err_one; + } + default: break; /* unreachable */ + } + +copy_end: + *cur++ = '"'; + return cur; + +err_one: + if (esc) goto err_esc; + else goto err_cpy; + +err_cpy: + if (!inv) return NULL; + *cur++ = *src++; + goto copy_utf8; + +err_esc: + if (!inv) return NULL; + byte_copy_2(cur + 0, &pre); + byte_copy_4(cur + 2, &rep); + cur += 6; + src += 1; + goto copy_utf8; +} + + + +/*============================================================================== + * MARK: - JSON Writer Utilities (Private) + *============================================================================*/ + +/** Write null (requires 8 bytes buffer). */ +static_inline u8 *write_null(u8 *cur) { + v64 v = {{ 'n', 'u', 'l', 'l', ',', '\n', 0, 0 }}; + byte_copy_8(cur, &v); + return cur + 4; +} + +/** Write bool (requires 8 bytes buffer). */ +static_inline u8 *write_bool(u8 *cur, bool val) { + v64 v0 = {{ 'f', 'a', 'l', 's', 'e', ',', '\n', 0 }}; + v64 v1 = {{ 't', 'r', 'u', 'e', ',', '\n', 0, 0 }}; + if (val) { + byte_copy_8(cur, &v1); + } else { + byte_copy_8(cur, &v0); + } + return cur + 5 - val; +} + +/** Write indent (requires level x 4 bytes buffer). + Param spaces should not larger than 4. */ +static_inline u8 *write_indent(u8 *cur, usize level, usize spaces) { + while (level-- > 0) { + byte_copy_4(cur, " "); + cur += spaces; + } + return cur; +} + +/** Write data to file pointer. */ +static bool write_dat_to_fp(FILE *fp, u8 *dat, usize len, + yyjson_write_err *err) { + if (fwrite(dat, len, 1, fp) != 1) { + err->msg = "file writing failed"; + err->code = YYJSON_WRITE_ERROR_FILE_WRITE; + return false; + } + return true; +} + +/** Write data to file. */ +static bool write_dat_to_file(const char *path, u8 *dat, usize len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + err->msg = _msg; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + if (file) fclose(file); \ + return false; \ +} while (false) + + FILE *file = fopen_writeonly(path); + if (file == NULL) { + return_err(FILE_OPEN, MSG_FOPEN); + } + if (fwrite(dat, len, 1, file) != 1) { + return_err(FILE_WRITE, MSG_FWRITE); + } + if (fclose(file) != 0) { + file = NULL; + return_err(FILE_WRITE, MSG_FCLOSE); + } + return true; + +#undef return_err +} + + + +/*============================================================================== + * MARK: - JSON Writer Implementation (Private) + *============================================================================*/ + +typedef struct yyjson_write_ctx { + usize tag; +} yyjson_write_ctx; + +static_inline void yyjson_write_ctx_set(yyjson_write_ctx *ctx, + usize size, bool is_obj) { + ctx->tag = (size << 1) | (usize)is_obj; +} + +static_inline void yyjson_write_ctx_get(yyjson_write_ctx *ctx, + usize *size, bool *is_obj) { + usize tag = ctx->tag; + *size = tag >> 1; + *is_obj = (bool)(tag & 1); +} + +/** Write single JSON value. */ +static_inline u8 *yyjson_write_single(yyjson_val *val, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + if (hdr) alc.free(alc.ctx, (void *)hdr); \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + hdr = (u8 *)alc.malloc(alc.ctx, _len); \ + if (!hdr) goto fail_alloc; \ + cur = hdr; \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + u8 *hdr = NULL, *cur; + usize str_len; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + bool newline = has_flg(NEWLINE_AT_END) != 0; + const usize end_len = 2; /* '\n' and '\0' */ + + switch (unsafe_yyjson_get_type(val)) { + case YYJSON_TYPE_RAW: + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + end_len); + cur = write_raw(cur, str_ptr, str_len); + break; + + case YYJSON_TYPE_STR: + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 2 + end_len); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + break; + + case YYJSON_TYPE_NUM: + incr_len(FP_BUF_LEN + end_len); + cur = write_num(cur, val, flg); + if (unlikely(!cur)) goto fail_num; + break; + + case YYJSON_TYPE_BOOL: + incr_len(8); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + break; + + case YYJSON_TYPE_NULL: + incr_len(8); + cur = write_null(cur); + break; + + case YYJSON_TYPE_ARR: + incr_len(2 + end_len); + byte_copy_2(cur, "[]"); + cur += 2; + break; + + case YYJSON_TYPE_OBJ: + incr_len(2 + end_len); + byte_copy_2(cur, "{}"); + cur += 2; + break; + + default: + goto fail_type; + } + + if (newline) *cur++ = '\n'; + *cur = '\0'; + *dat_len = (usize)(cur - hdr); + memset(err, 0, sizeof(yyjson_write_err)); + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef check_str_len +#undef incr_len +} + +/** Write JSON document minify. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_write_minify(const yyjson_val *root, + const yyjson_write_flag flg, + const yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_val *val; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key; + u8 *hdr, *cur, *end, *tmp; + yyjson_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = root->uni.ofs / sizeof(yyjson_val); + alc_len = alc_len * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + val++; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = ((u8)ctn_obj & (u8)~ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + incr_len(FP_BUF_LEN); + cur = write_num(cur, val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + incr_len(16); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + goto val_end; + } else { + /* push context, setup new container */ + yyjson_write_ctx_set(--ctx, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + val++; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + incr_len(16); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + incr_len(16); + cur = write_null(cur); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 2); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + goto val_end; + } + goto fail_type; + +val_end: + val++; + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + goto val_begin; + +ctn_end: + cur--; + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + *cur++ = ','; + if (unlikely((u8 *)ctx >= end)) goto doc_end; + yyjson_write_ctx_get(ctx++, &ctn_len, &ctn_obj); + ctn_len--; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *(cur - 1) = '\n'; + cur++; + } + *--cur = '\0'; + *dat_len = (usize)(cur - hdr); + memset(err, 0, sizeof(yyjson_write_err)); + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + +/** Write JSON document pretty. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_write_pretty(const yyjson_val *root, + const yyjson_write_flag flg, + const yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_val *val; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key, no_indent; + u8 *hdr, *cur, *end, *tmp; + yyjson_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len, level; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + usize spaces = has_flg(PRETTY_TWO_SPACES) ? 2 : 4; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = root->uni.ofs / sizeof(yyjson_val); + alc_len = alc_len * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + val++; + level = 1; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = (bool)((u8)ctn_obj & (u8)~ctn_len); + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + *cur++ = is_key ? ' ' : '\n'; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(FP_BUF_LEN + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_num(cur, val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } else { + /* push context, setup new container */ + incr_len(32 + (no_indent ? 0 : level * 4)); + yyjson_write_ctx_set(--ctx, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + cur = write_indent(cur, no_indent ? 0 : level, spaces); + level++; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + val++; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_null(cur); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 3 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + goto fail_type; + +val_end: + val++; + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + goto val_begin; + +ctn_end: + cur -= 2; + *cur++ = '\n'; + incr_len(level * 4); + cur = write_indent(cur, --level, spaces); + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + if (unlikely((u8 *)ctx >= end)) goto doc_end; + yyjson_write_ctx_get(ctx++, &ctn_len, &ctn_obj); + ctn_len--; + *cur++ = ','; + *cur++ = '\n'; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *cur++ = '\n'; + } + *cur = '\0'; + *dat_len = (usize)(cur - hdr); + memset(err, 0, sizeof(yyjson_write_err)); + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + + + +/*============================================================================== + * MARK: - JSON Writer (Public) + *============================================================================*/ + +char *yyjson_val_write_opts(const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + usize tmp_dat_len; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_val *root = constcast(yyjson_val *)val; + + if (!err) err = &tmp_err; + if (!dat_len) dat_len = &tmp_dat_len; + + if (unlikely(!root)) { + *dat_len = 0; + err->msg = "input JSON is NULL"; + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; + return NULL; + } + + if (!unsafe_yyjson_is_ctn(root) || unsafe_yyjson_get_len(root) == 0) { + return (char *)yyjson_write_single(root, flg, alc, dat_len, err); + } else if (flg & (YYJSON_WRITE_PRETTY | YYJSON_WRITE_PRETTY_TWO_SPACES)) { + return (char *)yyjson_write_pretty(root, flg, alc, dat_len, err); + } else { + return (char *)yyjson_write_minify(root, flg, alc, dat_len, err); + } +} + +char *yyjson_write_opts(const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_val *root = doc ? doc->root : NULL; + return yyjson_val_write_opts(root, flg, alc_ptr, dat_len, err); +} + +bool yyjson_val_write_file(const char *path, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_val *root = constcast(yyjson_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!path || !*path)) { + err->msg = "input path is invalid"; + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_file(path, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_val_write_fp(FILE *fp, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_val *root = constcast(yyjson_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!fp)) { + err->msg = "input fp is invalid"; + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_fp(fp, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_write_file(const char *path, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_val *root = doc ? doc->root : NULL; + return yyjson_val_write_file(path, root, flg, alc_ptr, err); +} + +bool yyjson_write_fp(FILE *fp, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_val *root = doc ? doc->root : NULL; + return yyjson_val_write_fp(fp, root, flg, alc_ptr, err); +} + + + +/*============================================================================== + * MARK: - Mutable JSON Writer Implementation (Private) + *============================================================================*/ + +typedef struct yyjson_mut_write_ctx { + usize tag; + yyjson_mut_val *ctn; +} yyjson_mut_write_ctx; + +static_inline void yyjson_mut_write_ctx_set(yyjson_mut_write_ctx *ctx, + yyjson_mut_val *ctn, + usize size, bool is_obj) { + ctx->tag = (size << 1) | (usize)is_obj; + ctx->ctn = ctn; +} + +static_inline void yyjson_mut_write_ctx_get(yyjson_mut_write_ctx *ctx, + yyjson_mut_val **ctn, + usize *size, bool *is_obj) { + usize tag = ctx->tag; + *size = tag >> 1; + *is_obj = (bool)(tag & 1); + *ctn = ctx->ctn; +} + +/** Get the estimated number of values for the mutable JSON document. */ +static_inline usize yyjson_mut_doc_estimated_val_num( + const yyjson_mut_doc *doc) { + usize sum = 0; + yyjson_val_chunk *chunk = doc->val_pool.chunks; + while (chunk) { + sum += chunk->chunk_size / sizeof(yyjson_mut_val) - 1; + if (chunk == doc->val_pool.chunks) { + sum -= (usize)(doc->val_pool.end - doc->val_pool.cur); + } + chunk = chunk->next; + } + return sum; +} + +/** Write single JSON value. */ +static_inline u8 *yyjson_mut_write_single(yyjson_mut_val *val, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { + return yyjson_write_single((yyjson_val *)val, flg, alc, dat_len, err); +} + +/** Write JSON document minify. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, + usize estimated_val_num, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_mut_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_mut_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_mut_val *val, *ctn; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key; + u8 *hdr, *cur, *end, *tmp; + yyjson_mut_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_mut_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_mut_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + ctn = val; + val = (yyjson_mut_val *)val->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = ((u8)ctn_obj & (u8)~ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + incr_len(FP_BUF_LEN); + cur = write_num(cur, (yyjson_val *)val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + incr_len(16); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + goto val_end; + } else { + /* push context, setup new container */ + yyjson_mut_write_ctx_set(--ctx, ctn, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + ctn = val; + val = (yyjson_mut_val *)ctn->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + incr_len(16); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + incr_len(16); + cur = write_null(cur); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 2); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + goto val_end; + } + goto fail_type; + +val_end: + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + val = val->next; + goto val_begin; + +ctn_end: + cur--; + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + *cur++ = ','; + if (unlikely((u8 *)ctx >= end)) goto doc_end; + val = ctn->next; + yyjson_mut_write_ctx_get(ctx++, &ctn, &ctn_len, &ctn_obj); + ctn_len--; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *(cur - 1) = '\n'; + cur++; + } + *--cur = '\0'; + *dat_len = (usize)(cur - hdr); + err->code = YYJSON_WRITE_SUCCESS; + err->msg = NULL; + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + +/** Write JSON document pretty. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, + usize estimated_val_num, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_mut_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_mut_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_mut_val *val, *ctn; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key, no_indent; + u8 *hdr, *cur, *end, *tmp; + yyjson_mut_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len, level; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + usize spaces = has_flg(PRETTY_TWO_SPACES) ? 2 : 4; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_mut_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_mut_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + ctn = val; + val = (yyjson_mut_val *)val->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + level = 1; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = (bool)((u8)ctn_obj & (u8)~ctn_len); + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + *cur++ = is_key ? ' ' : '\n'; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(FP_BUF_LEN + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_num(cur, (yyjson_val *)val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } else { + /* push context, setup new container */ + incr_len(32 + (no_indent ? 0 : level * 4)); + yyjson_mut_write_ctx_set(--ctx, ctn, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + cur = write_indent(cur, no_indent ? 0 : level, spaces); + level++; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + ctn = val; + val = (yyjson_mut_val *)ctn->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_null(cur); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 3 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + goto fail_type; + +val_end: + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + val = val->next; + goto val_begin; + +ctn_end: + cur -= 2; + *cur++ = '\n'; + incr_len(level * 4); + cur = write_indent(cur, --level, spaces); + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + if (unlikely((u8 *)ctx >= end)) goto doc_end; + val = ctn->next; + yyjson_mut_write_ctx_get(ctx++, &ctn, &ctn_len, &ctn_obj); + ctn_len--; + *cur++ = ','; + *cur++ = '\n'; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *cur++ = '\n'; + } + *cur = '\0'; + *dat_len = (usize)(cur - hdr); + err->code = YYJSON_WRITE_SUCCESS; + err->msg = NULL; + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + +static char *yyjson_mut_write_opts_impl(const yyjson_mut_val *val, + usize estimated_val_num, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + usize tmp_dat_len; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_mut_val *root = constcast(yyjson_mut_val *)val; + + if (!err) err = &tmp_err; + if (!dat_len) dat_len = &tmp_dat_len; + + if (unlikely(!root)) { + *dat_len = 0; + err->msg = "input JSON is NULL"; + err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + return NULL; + } + + if (!unsafe_yyjson_is_ctn(root) || unsafe_yyjson_get_len(root) == 0) { + return (char *)yyjson_mut_write_single(root, flg, alc, dat_len, err); + } else if (flg & (YYJSON_WRITE_PRETTY | YYJSON_WRITE_PRETTY_TWO_SPACES)) { + return (char *)yyjson_mut_write_pretty(root, estimated_val_num, + flg, alc, dat_len, err); + } else { + return (char *)yyjson_mut_write_minify(root, estimated_val_num, + flg, alc, dat_len, err); + } +} + + + +/*============================================================================== + * MARK: - Mutable JSON Writer (Public) + *============================================================================*/ + +char *yyjson_mut_val_write_opts(const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + return yyjson_mut_write_opts_impl(val, 0, flg, alc_ptr, dat_len, err); +} + +char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_mut_val *root; + usize estimated_val_num; + if (likely(doc)) { + root = doc->root; + estimated_val_num = yyjson_mut_doc_estimated_val_num(doc); + } else { + root = NULL; + estimated_val_num = 0; + } + return yyjson_mut_write_opts_impl(root, estimated_val_num, + flg, alc_ptr, dat_len, err); +} + +bool yyjson_mut_val_write_file(const char *path, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_mut_val *root = constcast(yyjson_mut_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!path || !*path)) { + err->msg = "input path is invalid"; + err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_mut_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_file(path, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_mut_val_write_fp(FILE *fp, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_mut_val *root = constcast(yyjson_mut_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!fp)) { + err->msg = "input fp is invalid"; + err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_mut_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_fp(fp, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_mut_write_file(const char *path, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_mut_val *root = doc ? doc->root : NULL; + return yyjson_mut_val_write_file(path, root, flg, alc_ptr, err); +} + +bool yyjson_mut_write_fp(FILE *fp, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_mut_val *root = doc ? doc->root : NULL; + return yyjson_mut_val_write_fp(fp, root, flg, alc_ptr, err); +} + +#undef has_flg +#undef has_allow +#endif /* YYJSON_DISABLE_WRITER */ + + + +#if !YYJSON_DISABLE_UTILS + +/*============================================================================== + * MARK: - JSON Pointer API (RFC 6901) (Public) + *============================================================================*/ + +/** + Get a token from JSON pointer string. + @param ptr [in] string that points to current token prefix `/` + [out] string that points to next token prefix `/`, or string end + @param end [in] end of the entire JSON Pointer string + @param len [out] unescaped token length + @param esc [out] number of escaped characters in this token + @return head of the token, or NULL if syntax error + */ +static_inline const char *ptr_next_token(const char **ptr, const char *end, + usize *len, usize *esc) { + const char *hdr = *ptr + 1; + const char *cur = hdr; + /* skip unescaped characters */ + while (cur < end && *cur != '/' && *cur != '~') cur++; + if (likely(cur == end || *cur != '~')) { + /* no escaped characters, return */ + *ptr = cur; + *len = (usize)(cur - hdr); + *esc = 0; + return hdr; + } else { + /* handle escaped characters */ + usize esc_num = 0; + while (cur < end && *cur != '/') { + if (*cur++ == '~') { + if (cur == end || (*cur != '0' && *cur != '1')) { + *ptr = cur - 1; + return NULL; + } + esc_num++; + } + } + *ptr = cur; + *len = (usize)(cur - hdr) - esc_num; + *esc = esc_num; + return hdr; + } +} + +/** + Convert token string to index. + @param cur [in] token head + @param len [in] token length + @param idx [out] the index number, or USIZE_MAX if token is '-' + @return true if token is a valid array index + */ +static_inline bool ptr_token_to_idx(const char *cur, usize len, usize *idx) { + const char *end = cur + len; + usize num = 0, add; + if (unlikely(len == 0 || len > USIZE_SAFE_DIG)) return false; + if (*cur == '0') { + if (unlikely(len > 1)) return false; + *idx = 0; + return true; + } + if (*cur == '-') { + if (unlikely(len > 1)) return false; + *idx = USIZE_MAX; + return true; + } + for (; cur < end && (add = (usize)((u8)*cur - (u8)'0')) <= 9; cur++) { + num = num * 10 + add; + } + if (unlikely(num == 0 || cur < end)) return false; + *idx = num; + return true; +} + +/** + Compare JSON key with token. + @param key a string key (yyjson_val or yyjson_mut_val) + @param token a JSON pointer token + @param len unescaped token length + @param esc number of escaped characters in this token + @return true if `str` is equals to `token` + */ +static_inline bool ptr_token_eq(void *key, + const char *token, usize len, usize esc) { + yyjson_val *val = (yyjson_val *)key; + if (unsafe_yyjson_get_len(val) != len) return false; + if (likely(!esc)) { + return memcmp(val->uni.str, token, len) == 0; + } else { + const char *str = val->uni.str; + for (; len-- > 0; token++, str++) { + if (*token == '~') { + if (*str != (*++token == '0' ? '~' : '/')) return false; + } else { + if (*str != *token) return false; + } + } + return true; + } +} + +/** + Get a value from array by token. + @param arr an array, should not be NULL or non-array type + @param token a JSON pointer token + @param len unescaped token length + @param esc number of escaped characters in this token + @return value at index, or NULL if token is not index or index is out of range + */ +static_inline yyjson_val *ptr_arr_get(yyjson_val *arr, const char *token, + usize len, usize esc) { + yyjson_val *val = unsafe_yyjson_get_first(arr); + usize num = unsafe_yyjson_get_len(arr), idx = 0; + if (unlikely(num == 0)) return NULL; + if (unlikely(!ptr_token_to_idx(token, len, &idx))) return NULL; + if (unlikely(idx >= num)) return NULL; + if (unsafe_yyjson_arr_is_flat(arr)) { + return val + idx; + } else { + while (idx-- > 0) val = unsafe_yyjson_get_next(val); + return val; + } +} + +/** + Get a value from object by token. + @param obj [in] an object, should not be NULL or non-object type + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @return value associated with the token, or NULL if no value + */ +static_inline yyjson_val *ptr_obj_get(yyjson_val *obj, const char *token, + usize len, usize esc) { + yyjson_val *key = unsafe_yyjson_get_first(obj); + usize num = unsafe_yyjson_get_len(obj); + if (unlikely(num == 0)) return NULL; + for (; num > 0; num--, key = unsafe_yyjson_get_next(key + 1)) { + if (ptr_token_eq(key, token, len, esc)) return key + 1; + } + return NULL; +} + +/** + Get a value from array by token. + @param arr [in] an array, should not be NULL or non-array type + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @param pre [out] previous (sibling) value of the returned value + @param last [out] whether index is last + @return value at index, or NULL if token is not index or index is out of range + */ +static_inline yyjson_mut_val *ptr_mut_arr_get(yyjson_mut_val *arr, + const char *token, + usize len, usize esc, + yyjson_mut_val **pre, + bool *last) { + yyjson_mut_val *val = (yyjson_mut_val *)arr->uni.ptr; /* last (tail) */ + usize num = unsafe_yyjson_get_len(arr), idx; + if (last) *last = false; + if (pre) *pre = NULL; + if (unlikely(num == 0)) { + if (last && len == 1 && (*token == '0' || *token == '-')) *last = true; + return NULL; + } + if (unlikely(!ptr_token_to_idx(token, len, &idx))) return NULL; + if (last) *last = (idx == num || idx == USIZE_MAX); + if (unlikely(idx >= num)) return NULL; + while (idx-- > 0) val = val->next; + if (pre) *pre = val; + return val->next; +} + +/** + Get a value from object by token. + @param obj [in] an object, should not be NULL or non-object type + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @param pre [out] previous (sibling) key of the returned value's key + @return value associated with the token, or NULL if no value + */ +static_inline yyjson_mut_val *ptr_mut_obj_get(yyjson_mut_val *obj, + const char *token, + usize len, usize esc, + yyjson_mut_val **pre) { + yyjson_mut_val *pre_key = (yyjson_mut_val *)obj->uni.ptr, *key; + usize num = unsafe_yyjson_get_len(obj); + if (pre) *pre = NULL; + if (unlikely(num == 0)) return NULL; + for (; num > 0; num--, pre_key = key) { + key = pre_key->next->next; + if (ptr_token_eq(key, token, len, esc)) { + if (pre) *pre = pre_key; + return key->next; + } + } + return NULL; +} + +/** + Create a string value with JSON pointer token. + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @param doc [in] used for memory allocation when creating value + @return new string value, or NULL if memory allocation failed + */ +static_inline yyjson_mut_val *ptr_new_key(const char *token, + usize len, usize esc, + yyjson_mut_doc *doc) { + const char *src = token; + if (likely(!esc)) { + return yyjson_mut_strncpy(doc, src, len); + } else { + const char *end = src + len + esc; + char *dst = unsafe_yyjson_mut_str_alc(doc, len + esc); + char *str = dst; + if (unlikely(!dst)) return NULL; + for (; src < end; src++, dst++) { + if (*src != '~') *dst = *src; + else *dst = (*++src == '0' ? '~' : '/'); + } + *dst = '\0'; + return yyjson_mut_strn(doc, str, len); + } +} + +/* macros for yyjson_ptr */ +#define return_err(_ret, _code, _pos, _msg) do { \ + if (err) { \ + err->code = YYJSON_PTR_ERR_##_code; \ + err->msg = _msg; \ + err->pos = (usize)(_pos); \ + } \ + return _ret; \ +} while (false) + +#define return_err_resolve(_ret, _pos) \ + return_err(_ret, RESOLVE, _pos, "JSON pointer cannot be resolved") +#define return_err_syntax(_ret, _pos) \ + return_err(_ret, SYNTAX, _pos, "invalid escaped character") +#define return_err_alloc(_ret) \ + return_err(_ret, MEMORY_ALLOCATION, 0, "failed to create value") + +yyjson_val *unsafe_yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t ptr_len, + yyjson_ptr_err *err) { + + const char *hdr = ptr, *end = ptr + ptr_len, *token; + usize len, esc; + yyjson_type type; + + while (true) { + token = ptr_next_token(&ptr, end, &len, &esc); + if (unlikely(!token)) return_err_syntax(NULL, ptr - hdr); + type = unsafe_yyjson_get_type(val); + if (type == YYJSON_TYPE_OBJ) { + val = ptr_obj_get(val, token, len, esc); + } else if (type == YYJSON_TYPE_ARR) { + val = ptr_arr_get(val, token, len, esc); + } else { + val = NULL; + } + if (!val) return_err_resolve(NULL, token - hdr); + if (ptr == end) return val; + } +} + +yyjson_mut_val *unsafe_yyjson_mut_ptr_getx( + yyjson_mut_val *val, const char *ptr, size_t ptr_len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + const char *hdr = ptr, *end = ptr + ptr_len, *token; + usize len, esc; + yyjson_mut_val *ctn, *pre = NULL; + yyjson_type type; + bool idx_is_last = false; + + while (true) { + token = ptr_next_token(&ptr, end, &len, &esc); + if (unlikely(!token)) return_err_syntax(NULL, ptr - hdr); + ctn = val; + type = unsafe_yyjson_get_type(val); + if (type == YYJSON_TYPE_OBJ) { + val = ptr_mut_obj_get(val, token, len, esc, &pre); + } else if (type == YYJSON_TYPE_ARR) { + val = ptr_mut_arr_get(val, token, len, esc, &pre, &idx_is_last); + } else { + val = NULL; + } + if (ctx && (ptr == end)) { + if (type == YYJSON_TYPE_OBJ || + (type == YYJSON_TYPE_ARR && (val || idx_is_last))) { + ctx->ctn = ctn; + ctx->pre = pre; + } + } + if (!val) return_err_resolve(NULL, token - hdr); + if (ptr == end) return val; + } +} + +bool unsafe_yyjson_mut_ptr_putx( + yyjson_mut_val *val, const char *ptr, size_t ptr_len, + yyjson_mut_val *new_val, yyjson_mut_doc *doc, bool create_parent, + bool insert_new, yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + const char *hdr = ptr, *end = ptr + ptr_len, *token; + usize token_len, esc, ctn_len; + yyjson_mut_val *ctn, *key, *pre = NULL; + yyjson_mut_val *sep_ctn = NULL, *sep_key = NULL, *sep_val = NULL; + yyjson_type ctn_type; + bool idx_is_last = false; + + /* skip exist parent nodes */ + while (true) { + token = ptr_next_token(&ptr, end, &token_len, &esc); + if (unlikely(!token)) return_err_syntax(false, ptr - hdr); + ctn = val; + ctn_type = unsafe_yyjson_get_type(ctn); + if (ctn_type == YYJSON_TYPE_OBJ) { + val = ptr_mut_obj_get(ctn, token, token_len, esc, &pre); + } else if (ctn_type == YYJSON_TYPE_ARR) { + val = ptr_mut_arr_get(ctn, token, token_len, esc, &pre, + &idx_is_last); + } else return_err_resolve(false, token - hdr); + if (!val) break; + if (ptr == end) break; /* is last token */ + } + + /* create parent nodes if not exist */ + if (unlikely(ptr != end)) { /* not last token */ + if (!create_parent) return_err_resolve(false, token - hdr); + + /* add value at last index if container is array */ + if (ctn_type == YYJSON_TYPE_ARR) { + if (!idx_is_last || !insert_new) { + return_err_resolve(false, token - hdr); + } + val = yyjson_mut_obj(doc); + if (!val) return_err_alloc(false); + + /* delay attaching until all operations are completed */ + sep_ctn = ctn; + sep_key = NULL; + sep_val = val; + + /* move to next token */ + ctn = val; + val = NULL; + ctn_type = YYJSON_TYPE_OBJ; + token = ptr_next_token(&ptr, end, &token_len, &esc); + if (unlikely(!token)) return_err_resolve(false, token - hdr); + } + + /* container is object, create parent nodes */ + while (ptr != end) { /* not last token */ + key = ptr_new_key(token, token_len, esc, doc); + if (!key) return_err_alloc(false); + val = yyjson_mut_obj(doc); + if (!val) return_err_alloc(false); + + /* delay attaching until all operations are completed */ + if (!sep_ctn) { + sep_ctn = ctn; + sep_key = key; + sep_val = val; + } else { + yyjson_mut_obj_add(ctn, key, val); + } + + /* move to next token */ + ctn = val; + val = NULL; + token = ptr_next_token(&ptr, end, &token_len, &esc); + if (unlikely(!token)) return_err_syntax(false, ptr - hdr); + } + } + + /* JSON pointer is resolved, insert or replace target value */ + ctn_len = unsafe_yyjson_get_len(ctn); + if (ctn_type == YYJSON_TYPE_OBJ) { + if (ctx) ctx->ctn = ctn; + if (!val || insert_new) { + /* insert new key-value pair */ + key = ptr_new_key(token, token_len, esc, doc); + if (unlikely(!key)) return_err_alloc(false); + if (ctx) ctx->pre = ctn_len ? (yyjson_mut_val *)ctn->uni.ptr : key; + unsafe_yyjson_mut_obj_add(ctn, key, new_val, ctn_len); + } else { + /* replace exist value */ + key = pre->next->next; + if (ctx) ctx->pre = pre; + if (ctx) ctx->old = val; + yyjson_mut_obj_put(ctn, key, new_val); + } + } else { + /* array */ + if (ctx && (val || idx_is_last)) ctx->ctn = ctn; + if (insert_new) { + /* append new value */ + if (val) { + pre->next = new_val; + new_val->next = val; + if (ctx) ctx->pre = pre; + unsafe_yyjson_set_len(ctn, ctn_len + 1); + } else if (idx_is_last) { + if (ctx) ctx->pre = ctn_len ? + (yyjson_mut_val *)ctn->uni.ptr : new_val; + yyjson_mut_arr_append(ctn, new_val); + } else { + return_err_resolve(false, token - hdr); + } + } else { + /* replace exist value */ + if (!val) return_err_resolve(false, token - hdr); + if (ctn_len > 1) { + new_val->next = val->next; + pre->next = new_val; + if (ctn->uni.ptr == val) ctn->uni.ptr = new_val; + } else { + new_val->next = new_val; + ctn->uni.ptr = new_val; + pre = new_val; + } + if (ctx) ctx->pre = pre; + if (ctx) ctx->old = val; + } + } + + /* all operations are completed, attach the new components to the target */ + if (unlikely(sep_ctn)) { + if (sep_key) yyjson_mut_obj_add(sep_ctn, sep_key, sep_val); + else yyjson_mut_arr_append(sep_ctn, sep_val); + } + return true; +} + +yyjson_mut_val *unsafe_yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_mut_val *cur_val; + yyjson_ptr_ctx cur_ctx; + memset(&cur_ctx, 0, sizeof(cur_ctx)); + if (!ctx) ctx = &cur_ctx; + cur_val = unsafe_yyjson_mut_ptr_getx(val, ptr, len, ctx, err); + if (!cur_val) return NULL; + + if (yyjson_mut_is_obj(ctx->ctn)) { + yyjson_mut_val *key = ctx->pre->next->next; + yyjson_mut_obj_put(ctx->ctn, key, new_val); + } else { + yyjson_ptr_ctx_replace(ctx, new_val); + } + ctx->old = cur_val; + return cur_val; +} + +yyjson_mut_val *unsafe_yyjson_mut_ptr_removex( + yyjson_mut_val *val, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_mut_val *cur_val; + yyjson_ptr_ctx cur_ctx; + memset(&cur_ctx, 0, sizeof(cur_ctx)); + if (!ctx) ctx = &cur_ctx; + cur_val = unsafe_yyjson_mut_ptr_getx(val, ptr, len, ctx, err); + if (cur_val) { + if (yyjson_mut_is_obj(ctx->ctn)) { + yyjson_mut_val *key = ctx->pre->next->next; + yyjson_mut_obj_put(ctx->ctn, key, NULL); + } else { + yyjson_ptr_ctx_remove(ctx); + } + ctx->pre = NULL; + ctx->old = cur_val; + } + return cur_val; +} + +/* macros for yyjson_ptr */ +#undef return_err +#undef return_err_resolve +#undef return_err_syntax +#undef return_err_alloc + + + +/*============================================================================== + * MARK: - JSON Patch API (RFC 6902) (Public) + *============================================================================*/ + +/* JSON Patch operation */ +typedef enum patch_op { + PATCH_OP_ADD, /* path, value */ + PATCH_OP_REMOVE, /* path */ + PATCH_OP_REPLACE, /* path, value */ + PATCH_OP_MOVE, /* from, path */ + PATCH_OP_COPY, /* from, path */ + PATCH_OP_TEST, /* path, value */ + PATCH_OP_NONE /* invalid */ +} patch_op; + +static patch_op patch_op_get(yyjson_val *op) { + const char *str = op->uni.str; + switch (unsafe_yyjson_get_len(op)) { + case 3: + if (!memcmp(str, "add", 3)) return PATCH_OP_ADD; + return PATCH_OP_NONE; + case 4: + if (!memcmp(str, "move", 4)) return PATCH_OP_MOVE; + if (!memcmp(str, "copy", 4)) return PATCH_OP_COPY; + if (!memcmp(str, "test", 4)) return PATCH_OP_TEST; + return PATCH_OP_NONE; + case 6: + if (!memcmp(str, "remove", 6)) return PATCH_OP_REMOVE; + return PATCH_OP_NONE; + case 7: + if (!memcmp(str, "replace", 7)) return PATCH_OP_REPLACE; + return PATCH_OP_NONE; + default: + return PATCH_OP_NONE; + } +} + +/* macros for yyjson_patch */ +#define return_err(_code, _msg) do { \ + if (err->ptr.code == YYJSON_PTR_ERR_MEMORY_ALLOCATION) { \ + err->code = YYJSON_PATCH_ERROR_MEMORY_ALLOCATION; \ + err->msg = _msg; \ + memset(&err->ptr, 0, sizeof(yyjson_ptr_err)); \ + } else { \ + err->code = YYJSON_PATCH_ERROR_##_code; \ + err->msg = _msg; \ + err->idx = iter.idx ? iter.idx - 1 : 0; \ + } \ + return NULL; \ +} while (false) + +#define return_err_copy() \ + return_err(MEMORY_ALLOCATION, "failed to copy value") +#define return_err_key(_key) \ + return_err(MISSING_KEY, "missing key " _key) +#define return_err_val(_key) \ + return_err(INVALID_MEMBER, "invalid member " _key) + +#define ptr_get(_ptr) yyjson_mut_ptr_getx( \ + root, _ptr->uni.str, _ptr##_len, NULL, &err->ptr) +#define ptr_add(_ptr, _val) yyjson_mut_ptr_addx( \ + root, _ptr->uni.str, _ptr##_len, _val, doc, false, NULL, &err->ptr) +#define ptr_remove(_ptr) yyjson_mut_ptr_removex( \ + root, _ptr->uni.str, _ptr##_len, NULL, &err->ptr) +#define ptr_replace(_ptr, _val)yyjson_mut_ptr_replacex( \ + root, _ptr->uni.str, _ptr##_len, _val, NULL, &err->ptr) + +yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch, + yyjson_patch_err *err) { + + yyjson_mut_val *root; + yyjson_val *obj; + yyjson_arr_iter iter; + yyjson_patch_err err_tmp; + if (!err) err = &err_tmp; + memset(err, 0, sizeof(*err)); + memset(&iter, 0, sizeof(iter)); + + if (unlikely(!doc || !orig || !patch)) { + return_err(INVALID_PARAMETER, "input parameter is NULL"); + } + if (unlikely(!yyjson_is_arr(patch))) { + return_err(INVALID_PARAMETER, "input patch is not array"); + } + root = yyjson_val_mut_copy(doc, orig); + if (unlikely(!root)) return_err_copy(); + + /* iterate through the patch array */ + yyjson_arr_iter_init(patch, &iter); + while ((obj = yyjson_arr_iter_next(&iter))) { + patch_op op_enum; + yyjson_val *op, *path, *from = NULL, *value; + yyjson_mut_val *val = NULL, *test; + usize path_len, from_len = 0; + if (unlikely(!unsafe_yyjson_is_obj(obj))) { + return_err(INVALID_OPERATION, "JSON patch operation is not object"); + } + + /* get required member: op */ + op = yyjson_obj_get(obj, "op"); + if (unlikely(!op)) return_err_key("`op`"); + if (unlikely(!yyjson_is_str(op))) return_err_val("`op`"); + op_enum = patch_op_get(op); + + /* get required member: path */ + path = yyjson_obj_get(obj, "path"); + if (unlikely(!path)) return_err_key("`path`"); + if (unlikely(!yyjson_is_str(path))) return_err_val("`path`"); + path_len = unsafe_yyjson_get_len(path); + + /* get required member: value, from */ + switch ((int)op_enum) { + case PATCH_OP_ADD: case PATCH_OP_REPLACE: case PATCH_OP_TEST: + value = yyjson_obj_get(obj, "value"); + if (unlikely(!value)) return_err_key("`value`"); + val = yyjson_val_mut_copy(doc, value); + if (unlikely(!val)) return_err_copy(); + break; + case PATCH_OP_MOVE: case PATCH_OP_COPY: + from = yyjson_obj_get(obj, "from"); + if (unlikely(!from)) return_err_key("`from`"); + if (unlikely(!yyjson_is_str(from))) return_err_val("`from`"); + from_len = unsafe_yyjson_get_len(from); + break; + default: + break; + } + + /* perform an operation */ + switch ((int)op_enum) { + case PATCH_OP_ADD: /* add(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_REMOVE: /* remove(path) */ + if (unlikely(!ptr_remove(path))) { + return_err(POINTER, "failed to remove `path`"); + } + break; + case PATCH_OP_REPLACE: /* replace(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_replace(path, val))) { + return_err(POINTER, "failed to replace `path`"); + } + break; + case PATCH_OP_MOVE: /* val = remove(from), add(path, val) */ + if (unlikely(from_len == 0 && path_len == 0)) break; + val = ptr_remove(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to remove `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_COPY: /* val = get(from).copy, add(path, val) */ + val = ptr_get(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to get `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + val = yyjson_mut_val_mut_copy(doc, val); + if (unlikely(!val)) return_err_copy(); + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_TEST: /* test = get(path), test.eq(val) */ + test = ptr_get(path); + if (unlikely(!test)) { + return_err(POINTER, "failed to get `path`"); + } + if (unlikely(!yyjson_mut_equals(val, test))) { + return_err(EQUAL, "failed to test equal"); + } + break; + default: + return_err(INVALID_MEMBER, "unsupported `op`"); + } + } + return root; +} + +yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch, + yyjson_patch_err *err) { + yyjson_mut_val *root, *obj; + yyjson_mut_arr_iter iter; + yyjson_patch_err err_tmp; + if (!err) err = &err_tmp; + memset(err, 0, sizeof(*err)); + memset(&iter, 0, sizeof(iter)); + + if (unlikely(!doc || !orig || !patch)) { + return_err(INVALID_PARAMETER, "input parameter is NULL"); + } + if (unlikely(!yyjson_mut_is_arr(patch))) { + return_err(INVALID_PARAMETER, "input patch is not array"); + } + root = yyjson_mut_val_mut_copy(doc, orig); + if (unlikely(!root)) return_err_copy(); + + /* iterate through the patch array */ + yyjson_mut_arr_iter_init(patch, &iter); + while ((obj = yyjson_mut_arr_iter_next(&iter))) { + patch_op op_enum; + yyjson_mut_val *op, *path, *from = NULL, *value; + yyjson_mut_val *val = NULL, *test; + usize path_len, from_len = 0; + if (!unsafe_yyjson_is_obj(obj)) { + return_err(INVALID_OPERATION, "JSON patch operation is not object"); + } + + /* get required member: op */ + op = yyjson_mut_obj_get(obj, "op"); + if (unlikely(!op)) return_err_key("`op`"); + if (unlikely(!yyjson_mut_is_str(op))) return_err_val("`op`"); + op_enum = patch_op_get((yyjson_val *)(void *)op); + + /* get required member: path */ + path = yyjson_mut_obj_get(obj, "path"); + if (unlikely(!path)) return_err_key("`path`"); + if (unlikely(!yyjson_mut_is_str(path))) return_err_val("`path`"); + path_len = unsafe_yyjson_get_len(path); + + /* get required member: value, from */ + switch ((int)op_enum) { + case PATCH_OP_ADD: case PATCH_OP_REPLACE: case PATCH_OP_TEST: + value = yyjson_mut_obj_get(obj, "value"); + if (unlikely(!value)) return_err_key("`value`"); + val = yyjson_mut_val_mut_copy(doc, value); + if (unlikely(!val)) return_err_copy(); + break; + case PATCH_OP_MOVE: case PATCH_OP_COPY: + from = yyjson_mut_obj_get(obj, "from"); + if (unlikely(!from)) return_err_key("`from`"); + if (unlikely(!yyjson_mut_is_str(from))) { + return_err_val("`from`"); + } + from_len = unsafe_yyjson_get_len(from); + break; + default: + break; + } + + /* perform an operation */ + switch ((int)op_enum) { + case PATCH_OP_ADD: /* add(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_REMOVE: /* remove(path) */ + if (unlikely(!ptr_remove(path))) { + return_err(POINTER, "failed to remove `path`"); + } + break; + case PATCH_OP_REPLACE: /* replace(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_replace(path, val))) { + return_err(POINTER, "failed to replace `path`"); + } + break; + case PATCH_OP_MOVE: /* val = remove(from), add(path, val) */ + if (unlikely(from_len == 0 && path_len == 0)) break; + val = ptr_remove(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to remove `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_COPY: /* val = get(from).copy, add(path, val) */ + val = ptr_get(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to get `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + val = yyjson_mut_val_mut_copy(doc, val); + if (unlikely(!val)) return_err_copy(); + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_TEST: /* test = get(path), test.eq(val) */ + test = ptr_get(path); + if (unlikely(!test)) { + return_err(POINTER, "failed to get `path`"); + } + if (unlikely(!yyjson_mut_equals(val, test))) { + return_err(EQUAL, "failed to test equal"); + } + break; + default: + return_err(INVALID_MEMBER, "unsupported `op`"); + } + } + return root; +} + +/* macros for yyjson_patch */ +#undef return_err +#undef return_err_copy +#undef return_err_key +#undef return_err_val +#undef ptr_get +#undef ptr_add +#undef ptr_remove +#undef ptr_replace + + + +/*============================================================================== + * MARK: - JSON Merge-Patch API (RFC 7386) (Public) + *============================================================================*/ + +yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch) { + usize idx, max; + yyjson_val *key, *orig_val, *patch_val, local_orig; + yyjson_mut_val *builder, *mut_key, *mut_val, *merged_val; + + if (unlikely(!yyjson_is_obj(patch))) { + return yyjson_val_mut_copy(doc, patch); + } + + builder = yyjson_mut_obj(doc); + if (unlikely(!builder)) return NULL; + + memset(&local_orig, 0, sizeof(local_orig)); + if (!yyjson_is_obj(orig)) { + orig = &local_orig; + orig->tag = builder->tag; + orig->uni = builder->uni; + } + + /* If orig is contributing, copy any items not modified by the patch */ + if (orig != &local_orig) { + yyjson_obj_foreach(orig, idx, max, key, orig_val) { + patch_val = yyjson_obj_getn(patch, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + if (!patch_val) { + mut_key = yyjson_val_mut_copy(doc, key); + mut_val = yyjson_val_mut_copy(doc, orig_val); + if (!yyjson_mut_obj_add(builder, mut_key, mut_val)) return NULL; + } + } + } + + /* Merge items modified by the patch. */ + yyjson_obj_foreach(patch, idx, max, key, patch_val) { + /* null indicates the field is removed. */ + if (unsafe_yyjson_is_null(patch_val)) { + continue; + } + mut_key = yyjson_val_mut_copy(doc, key); + orig_val = yyjson_obj_getn(orig, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + merged_val = yyjson_merge_patch(doc, orig_val, patch_val); + if (!yyjson_mut_obj_add(builder, mut_key, merged_val)) return NULL; + } + + return builder; +} + +yyjson_mut_val *yyjson_mut_merge_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch) { + usize idx, max; + yyjson_mut_val *key, *orig_val, *patch_val, local_orig; + yyjson_mut_val *builder, *mut_key, *mut_val, *merged_val; + + if (unlikely(!yyjson_mut_is_obj(patch))) { + return yyjson_mut_val_mut_copy(doc, patch); + } + + builder = yyjson_mut_obj(doc); + if (unlikely(!builder)) return NULL; + + memset(&local_orig, 0, sizeof(local_orig)); + if (!yyjson_mut_is_obj(orig)) { + orig = &local_orig; + orig->tag = builder->tag; + orig->uni = builder->uni; + } + + /* If orig is contributing, copy any items not modified by the patch */ + if (orig != &local_orig) { + yyjson_mut_obj_foreach(orig, idx, max, key, orig_val) { + patch_val = yyjson_mut_obj_getn(patch, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + if (!patch_val) { + mut_key = yyjson_mut_val_mut_copy(doc, key); + mut_val = yyjson_mut_val_mut_copy(doc, orig_val); + if (!yyjson_mut_obj_add(builder, mut_key, mut_val)) return NULL; + } + } + } + + /* Merge items modified by the patch. */ + yyjson_mut_obj_foreach(patch, idx, max, key, patch_val) { + /* null indicates the field is removed. */ + if (unsafe_yyjson_is_null(patch_val)) { + continue; + } + mut_key = yyjson_mut_val_mut_copy(doc, key); + orig_val = yyjson_mut_obj_getn(orig, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + merged_val = yyjson_mut_merge_patch(doc, orig_val, patch_val); + if (!yyjson_mut_obj_add(builder, mut_key, merged_val)) return NULL; + } + + return builder; +} + +#endif /* YYJSON_DISABLE_UTILS */ diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.h b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.h new file mode 100644 index 0000000..5eb6d46 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/sources/vendor/yyjson/yyjson.h @@ -0,0 +1,8230 @@ +/*============================================================================== + Copyright (c) 2020 YaoYuan + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + *============================================================================*/ + +/** + @file yyjson.h + @date 2019-03-09 + @author YaoYuan + */ + +#ifndef YYJSON_H +#define YYJSON_H + + + +/*============================================================================== + * MARK: - Header Files + *============================================================================*/ + +#include +#include +#include +#include +#include +#include + + + +/*============================================================================== + * MARK: - Compile-time Options + *============================================================================*/ + +/* + Define as 1 to disable JSON reader at compile-time. + This disables functions with "read" in their name. + Reduces binary size by about 60%. + */ +#ifndef YYJSON_DISABLE_READER +#endif + +/* + Define as 1 to disable JSON writer at compile-time. + This disables functions with "write" in their name. + Reduces binary size by about 30%. + */ +#ifndef YYJSON_DISABLE_WRITER +#endif + +/* + Define as 1 to disable JSON incremental reader at compile-time. + This disables functions with "incr" in their name. + */ +#ifndef YYJSON_DISABLE_INCR_READER +#endif + +/* + Define as 1 to disable JSON Pointer, JSON Patch and JSON Merge Patch supports. + This disables functions with "ptr" or "patch" in their name. + */ +#ifndef YYJSON_DISABLE_UTILS +#endif + +/* + Define as 1 to disable the fast floating-point number conversion in yyjson. + Libc's `strtod/snprintf` will be used instead. + + This reduces binary size by about 30%, but significantly slows down the + floating-point read/write speed. + */ +#ifndef YYJSON_DISABLE_FAST_FP_CONV +#endif + +/* + Define as 1 to disable non-standard JSON features support at compile-time, + such as YYJSON_READ_ALLOW_XXX and YYJSON_WRITE_ALLOW_XXX. + + This reduces binary size by about 10%, and slightly improves performance. + */ +#ifndef YYJSON_DISABLE_NON_STANDARD +#endif + +/* + Define as 1 to disable UTF-8 validation at compile-time. + + Use this if all input strings are guaranteed to be valid UTF-8 + (e.g. language-level String types are already validated). + + Disabling UTF-8 validation improves performance for non-ASCII strings by about + 3% to 7%. + + Note: If this flag is enabled while passing illegal UTF-8 strings, + the following errors may occur: + - Escaped characters may be ignored when parsing JSON strings. + - Ending quotes may be ignored when parsing JSON strings, causing the + string to merge with the next value. + - When serializing with `yyjson_mut_val`, the string's end may be accessed + out of bounds, potentially causing a segmentation fault. + */ +#ifndef YYJSON_DISABLE_UTF8_VALIDATION +#endif + +/* + Define as 1 to improve performance on architectures that do not support + unaligned memory access. + + Normally, this does not need to be set manually. See the C file for details. + */ +#ifndef YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS +#endif + +/* Define as 1 to export symbols when building this library as a Windows DLL. */ +#ifndef YYJSON_EXPORTS +#endif + +/* Define as 1 to import symbols when using this library as a Windows DLL. */ +#ifndef YYJSON_IMPORTS +#endif + +/* Define as 1 to include for compilers without C99 support. */ +#ifndef YYJSON_HAS_STDINT_H +#endif + +/* Define as 1 to include for compilers without C99 support. */ +#ifndef YYJSON_HAS_STDBOOL_H +#endif + + + +/*============================================================================== + * MARK: - Compiler Macros + *============================================================================*/ + +/** compiler version (MSVC) */ +#ifdef _MSC_VER +# define YYJSON_MSC_VER _MSC_VER +#else +# define YYJSON_MSC_VER 0 +#endif + +/** compiler version (GCC) */ +#ifdef __GNUC__ +# define YYJSON_GCC_VER __GNUC__ +# if defined(__GNUC_PATCHLEVEL__) +# define yyjson_gcc_available(major, minor, patch) \ + ((__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) \ + >= (major * 10000 + minor * 100 + patch)) +# else +# define yyjson_gcc_available(major, minor, patch) \ + ((__GNUC__ * 10000 + __GNUC_MINOR__ * 100) \ + >= (major * 10000 + minor * 100 + patch)) +# endif +#else +# define YYJSON_GCC_VER 0 +# define yyjson_gcc_available(major, minor, patch) 0 +#endif + +/** real gcc check */ +#if defined(__GNUC__) && defined(__GNUC_MINOR__) && \ + !defined(__clang__) && !defined(__llvm__) && \ + !defined(__INTEL_COMPILER) && !defined(__ICC) && \ + !defined(__NVCC__) && !defined(__PGI) && !defined(__TINYC__) +# define YYJSON_IS_REAL_GCC 1 +#else +# define YYJSON_IS_REAL_GCC 0 +#endif + +/** C version (STDC) */ +#if defined(__STDC__) && (__STDC__ >= 1) && defined(__STDC_VERSION__) +# define YYJSON_STDC_VER __STDC_VERSION__ +#else +# define YYJSON_STDC_VER 0 +#endif + +/** C++ version */ +#if defined(__cplusplus) +# define YYJSON_CPP_VER __cplusplus +#else +# define YYJSON_CPP_VER 0 +#endif + +/** compiler builtin check (since gcc 10.0, clang 2.6, icc 2021) */ +#ifndef yyjson_has_builtin +# ifdef __has_builtin +# define yyjson_has_builtin(x) __has_builtin(x) +# else +# define yyjson_has_builtin(x) 0 +# endif +#endif + +/** compiler attribute check (since gcc 5.0, clang 2.9, icc 17) */ +#ifndef yyjson_has_attribute +# ifdef __has_attribute +# define yyjson_has_attribute(x) __has_attribute(x) +# else +# define yyjson_has_attribute(x) 0 +# endif +#endif + +/** compiler feature check (since clang 2.6, icc 17) */ +#ifndef yyjson_has_feature +# ifdef __has_feature +# define yyjson_has_feature(x) __has_feature(x) +# else +# define yyjson_has_feature(x) 0 +# endif +#endif + +/** include check (since gcc 5.0, clang 2.7, icc 16, msvc 2017 15.3) */ +#ifndef yyjson_has_include +# ifdef __has_include +# define yyjson_has_include(x) __has_include(x) +# else +# define yyjson_has_include(x) 0 +# endif +#endif + +/** inline for compiler */ +#ifndef yyjson_inline +# if YYJSON_MSC_VER >= 1200 +# define yyjson_inline __forceinline +# elif defined(_MSC_VER) +# define yyjson_inline __inline +# elif yyjson_has_attribute(always_inline) || YYJSON_GCC_VER >= 4 +# define yyjson_inline __inline__ __attribute__((always_inline)) +# elif defined(__clang__) || defined(__GNUC__) +# define yyjson_inline __inline__ +# elif defined(__cplusplus) || YYJSON_STDC_VER >= 199901L +# define yyjson_inline inline +# else +# define yyjson_inline +# endif +#endif + +/** noinline for compiler */ +#ifndef yyjson_noinline +# if YYJSON_MSC_VER >= 1400 +# define yyjson_noinline __declspec(noinline) +# elif yyjson_has_attribute(noinline) || YYJSON_GCC_VER >= 4 +# define yyjson_noinline __attribute__((noinline)) +# else +# define yyjson_noinline +# endif +#endif + +/** align for compiler */ +#ifndef yyjson_align +# if YYJSON_MSC_VER >= 1300 +# define yyjson_align(x) __declspec(align(x)) +# elif yyjson_has_attribute(aligned) || defined(__GNUC__) +# define yyjson_align(x) __attribute__((aligned(x))) +# elif YYJSON_CPP_VER >= 201103L +# define yyjson_align(x) alignas(x) +# else +# define yyjson_align(x) +# endif +#endif + +/** likely for compiler */ +#ifndef yyjson_likely +# if yyjson_has_builtin(__builtin_expect) || \ + (YYJSON_GCC_VER >= 4 && YYJSON_GCC_VER != 5) +# define yyjson_likely(expr) __builtin_expect(!!(expr), 1) +# else +# define yyjson_likely(expr) (expr) +# endif +#endif + +/** unlikely for compiler */ +#ifndef yyjson_unlikely +# if yyjson_has_builtin(__builtin_expect) || \ + (YYJSON_GCC_VER >= 4 && YYJSON_GCC_VER != 5) +# define yyjson_unlikely(expr) __builtin_expect(!!(expr), 0) +# else +# define yyjson_unlikely(expr) (expr) +# endif +#endif + +/** compile-time constant check for compiler */ +#ifndef yyjson_constant_p +# if yyjson_has_builtin(__builtin_constant_p) || (YYJSON_GCC_VER >= 3) +# define YYJSON_HAS_CONSTANT_P 1 +# define yyjson_constant_p(value) __builtin_constant_p(value) +# else +# define YYJSON_HAS_CONSTANT_P 0 +# define yyjson_constant_p(value) 0 +# endif +#endif + +/** deprecate warning */ +#ifndef yyjson_deprecated +# if YYJSON_MSC_VER >= 1400 +# define yyjson_deprecated(msg) __declspec(deprecated(msg)) +# elif yyjson_has_feature(attribute_deprecated_with_message) || \ + (YYJSON_GCC_VER > 4 || (YYJSON_GCC_VER == 4 && __GNUC_MINOR__ >= 5)) +# define yyjson_deprecated(msg) __attribute__((deprecated(msg))) +# elif YYJSON_GCC_VER >= 3 +# define yyjson_deprecated(msg) __attribute__((deprecated)) +# else +# define yyjson_deprecated(msg) +# endif +#endif + +/** function export */ +#ifndef yyjson_api +# if defined(_WIN32) +# if defined(YYJSON_EXPORTS) && YYJSON_EXPORTS +# define yyjson_api __declspec(dllexport) +# elif defined(YYJSON_IMPORTS) && YYJSON_IMPORTS +# define yyjson_api __declspec(dllimport) +# else +# define yyjson_api +# endif +# elif yyjson_has_attribute(visibility) || YYJSON_GCC_VER >= 4 +# define yyjson_api __attribute__((visibility("default"))) +# else +# define yyjson_api +# endif +#endif + +/** inline function export */ +#ifndef yyjson_api_inline +# define yyjson_api_inline static yyjson_inline +#endif + +/** stdint (C89 compatible) */ +#if (defined(YYJSON_HAS_STDINT_H) && YYJSON_HAS_STDINT_H) || \ + YYJSON_MSC_VER >= 1600 || YYJSON_STDC_VER >= 199901L || \ + defined(_STDINT_H) || defined(_STDINT_H_) || \ + defined(__CLANG_STDINT_H) || defined(_STDINT_H_INCLUDED) || \ + yyjson_has_include() +# include +#elif defined(_MSC_VER) +# if _MSC_VER < 1300 + typedef signed char int8_t; + typedef signed short int16_t; + typedef signed int int32_t; + typedef unsigned char uint8_t; + typedef unsigned short uint16_t; + typedef unsigned int uint32_t; + typedef signed __int64 int64_t; + typedef unsigned __int64 uint64_t; +# else + typedef signed __int8 int8_t; + typedef signed __int16 int16_t; + typedef signed __int32 int32_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef signed __int64 int64_t; + typedef unsigned __int64 uint64_t; +# endif +#else +# if UCHAR_MAX == 0xFFU + typedef signed char int8_t; + typedef unsigned char uint8_t; +# else +# error cannot find 8-bit integer type +# endif +# if USHRT_MAX == 0xFFFFU + typedef unsigned short uint16_t; + typedef signed short int16_t; +# elif UINT_MAX == 0xFFFFU + typedef unsigned int uint16_t; + typedef signed int int16_t; +# else +# error cannot find 16-bit integer type +# endif +# if UINT_MAX == 0xFFFFFFFFUL + typedef unsigned int uint32_t; + typedef signed int int32_t; +# elif ULONG_MAX == 0xFFFFFFFFUL + typedef unsigned long uint32_t; + typedef signed long int32_t; +# elif USHRT_MAX == 0xFFFFFFFFUL + typedef unsigned short uint32_t; + typedef signed short int32_t; +# else +# error cannot find 32-bit integer type +# endif +# if defined(__INT64_TYPE__) && defined(__UINT64_TYPE__) + typedef __INT64_TYPE__ int64_t; + typedef __UINT64_TYPE__ uint64_t; +# elif defined(__GNUC__) || defined(__clang__) +# if !defined(_SYS_TYPES_H) && !defined(__int8_t_defined) + __extension__ typedef long long int64_t; +# endif + __extension__ typedef unsigned long long uint64_t; +# elif defined(_LONG_LONG) || defined(__MWERKS__) || defined(_CRAYC) || \ + defined(__SUNPRO_C) || defined(__SUNPRO_CC) + typedef long long int64_t; + typedef unsigned long long uint64_t; +# elif (defined(__BORLANDC__) && __BORLANDC__ > 0x460) || \ + defined(__WATCOM_INT64__) || defined (__alpha) || defined (__DECC) + typedef __int64 int64_t; + typedef unsigned __int64 uint64_t; +# else +# error cannot find 64-bit integer type +# endif +#endif + +/** stdbool (C89 compatible) */ +#if (defined(YYJSON_HAS_STDBOOL_H) && YYJSON_HAS_STDBOOL_H) || \ + (yyjson_has_include() && !defined(__STRICT_ANSI__)) || \ + YYJSON_MSC_VER >= 1800 || YYJSON_STDC_VER >= 199901L +# include +#elif !defined(__bool_true_false_are_defined) +# define __bool_true_false_are_defined 1 +# if defined(__cplusplus) +# if defined(__GNUC__) && !defined(__STRICT_ANSI__) +# define _Bool bool +# if __cplusplus < 201103L +# define bool bool +# define false false +# define true true +# endif +# endif +# else +# define bool unsigned char +# define true 1 +# define false 0 +# endif +#endif + +/** char bit check */ +#if defined(CHAR_BIT) +# if CHAR_BIT != 8 +# error non 8-bit char is not supported +# endif +#endif + +/** + Microsoft Visual C++ 6.0 doesn't support converting number from u64 to f64: + error C2520: conversion from unsigned __int64 to double not implemented. + */ +#ifndef YYJSON_U64_TO_F64_NO_IMPL +# if (0 < YYJSON_MSC_VER) && (YYJSON_MSC_VER <= 1200) +# define YYJSON_U64_TO_F64_NO_IMPL 1 +# else +# define YYJSON_U64_TO_F64_NO_IMPL 0 +# endif +#endif + + + +/*============================================================================== + * MARK: - Compile Hint Begin + *============================================================================*/ + +/* extern "C" begin */ +#ifdef __cplusplus +extern "C" { +#endif + +/* warning suppress begin */ +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wunused-parameter" +#elif defined(__GNUC__) +# if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +# pragma GCC diagnostic push +# endif +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wunused-parameter" +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable:4800) /* 'int': forcing value to 'true' or 'false' */ +#endif + + + +/*============================================================================== + * MARK: - Version + *============================================================================*/ + +/** The major version of yyjson. */ +#define YYJSON_VERSION_MAJOR 0 + +/** The minor version of yyjson. */ +#define YYJSON_VERSION_MINOR 12 + +/** The patch version of yyjson. */ +#define YYJSON_VERSION_PATCH 0 + +/** The version of yyjson in hex: `(major << 16) | (minor << 8) | (patch)`. */ +#define YYJSON_VERSION_HEX 0x000C00 + +/** The version string of yyjson. */ +#define YYJSON_VERSION_STRING "0.12.0" + +/** The version of yyjson in hex, same as `YYJSON_VERSION_HEX`. */ +yyjson_api uint32_t yyjson_version(void); + + + +/*============================================================================== + * MARK: - JSON Types + *============================================================================*/ + +/** Type of a JSON value (3 bit). */ +typedef uint8_t yyjson_type; +/** No type, invalid. */ +#define YYJSON_TYPE_NONE ((uint8_t)0) /* _____000 */ +/** Raw string type, no subtype. */ +#define YYJSON_TYPE_RAW ((uint8_t)1) /* _____001 */ +/** Null type: `null` literal, no subtype. */ +#define YYJSON_TYPE_NULL ((uint8_t)2) /* _____010 */ +/** Boolean type, subtype: TRUE, FALSE. */ +#define YYJSON_TYPE_BOOL ((uint8_t)3) /* _____011 */ +/** Number type, subtype: UINT, SINT, REAL. */ +#define YYJSON_TYPE_NUM ((uint8_t)4) /* _____100 */ +/** String type, subtype: NONE, NOESC. */ +#define YYJSON_TYPE_STR ((uint8_t)5) /* _____101 */ +/** Array type, no subtype. */ +#define YYJSON_TYPE_ARR ((uint8_t)6) /* _____110 */ +/** Object type, no subtype. */ +#define YYJSON_TYPE_OBJ ((uint8_t)7) /* _____111 */ + +/** Subtype of a JSON value (2 bit). */ +typedef uint8_t yyjson_subtype; +/** No subtype. */ +#define YYJSON_SUBTYPE_NONE ((uint8_t)(0 << 3)) /* ___00___ */ +/** False subtype: `false` literal. */ +#define YYJSON_SUBTYPE_FALSE ((uint8_t)(0 << 3)) /* ___00___ */ +/** True subtype: `true` literal. */ +#define YYJSON_SUBTYPE_TRUE ((uint8_t)(1 << 3)) /* ___01___ */ +/** Unsigned integer subtype: `uint64_t`. */ +#define YYJSON_SUBTYPE_UINT ((uint8_t)(0 << 3)) /* ___00___ */ +/** Signed integer subtype: `int64_t`. */ +#define YYJSON_SUBTYPE_SINT ((uint8_t)(1 << 3)) /* ___01___ */ +/** Real number subtype: `double`. */ +#define YYJSON_SUBTYPE_REAL ((uint8_t)(2 << 3)) /* ___10___ */ +/** String that do not need to be escaped for writing (internal use). */ +#define YYJSON_SUBTYPE_NOESC ((uint8_t)(1 << 3)) /* ___01___ */ + +/** The mask used to extract the type of a JSON value. */ +#define YYJSON_TYPE_MASK ((uint8_t)0x07) /* _____111 */ +/** The number of bits used by the type. */ +#define YYJSON_TYPE_BIT ((uint8_t)3) +/** The mask used to extract the subtype of a JSON value. */ +#define YYJSON_SUBTYPE_MASK ((uint8_t)0x18) /* ___11___ */ +/** The number of bits used by the subtype. */ +#define YYJSON_SUBTYPE_BIT ((uint8_t)2) +/** The mask used to extract the reserved bits of a JSON value. */ +#define YYJSON_RESERVED_MASK ((uint8_t)0xE0) /* 111_____ */ +/** The number of reserved bits. */ +#define YYJSON_RESERVED_BIT ((uint8_t)3) +/** The mask used to extract the tag of a JSON value. */ +#define YYJSON_TAG_MASK ((uint8_t)0xFF) /* 11111111 */ +/** The number of bits used by the tag. */ +#define YYJSON_TAG_BIT ((uint8_t)8) + +/** Padding size for JSON reader. */ +#define YYJSON_PADDING_SIZE 4 + + + +/*============================================================================== + * MARK: - Allocator + *============================================================================*/ + +/** + A memory allocator. + + Typically you don't need to use it, unless you want to customize your own + memory allocator. + */ +typedef struct yyjson_alc { + /** Same as libc's malloc(size), should not be NULL. */ + void *(*malloc)(void *ctx, size_t size); + /** Same as libc's realloc(ptr, size), should not be NULL. */ + void *(*realloc)(void *ctx, void *ptr, size_t old_size, size_t size); + /** Same as libc's free(ptr), should not be NULL. */ + void (*free)(void *ctx, void *ptr); + /** A context for malloc/realloc/free, can be NULL. */ + void *ctx; +} yyjson_alc; + +/** + A pool allocator uses fixed length pre-allocated memory. + + This allocator may be used to avoid malloc/realloc calls. The pre-allocated + memory should be held by the caller. The maximum amount of memory required to + read a JSON can be calculated using the `yyjson_read_max_memory_usage()` + function, but the amount of memory required to write a JSON cannot be directly + calculated. + + This is not a general-purpose allocator. It is designed to handle a single JSON + data at a time. If it is used for overly complex memory tasks, such as parsing + multiple JSON documents using the same allocator but releasing only a few of + them, it may cause memory fragmentation, resulting in performance degradation + and memory waste. + + @param alc The allocator to be initialized. + If this parameter is NULL, the function will fail and return false. + If `buf` or `size` is invalid, this will be set to an empty allocator. + @param buf The buffer memory for this allocator. + If this parameter is NULL, the function will fail and return false. + @param size The size of `buf`, in bytes. + If this parameter is less than 8 words (32/64 bytes on 32/64-bit OS), the + function will fail and return false. + @return true if the `alc` has been successfully initialized. + + @b Example + @code + // parse JSON with stack memory + char buf[1024]; + yyjson_alc alc; + yyjson_alc_pool_init(&alc, buf, 1024); + + const char *json = "{\"name\":\"Helvetica\",\"size\":16}" + yyjson_doc *doc = yyjson_read_opts(json, strlen(json), 0, &alc, NULL); + // the memory of `doc` is on the stack + @endcode + + @warning This Allocator is not thread-safe. + */ +yyjson_api bool yyjson_alc_pool_init(yyjson_alc *alc, void *buf, size_t size); + +/** + A dynamic allocator. + + This allocator has a similar usage to the pool allocator above. However, when + there is not enough memory, this allocator will dynamically request more memory + using libc's `malloc` function, and frees it all at once when it is destroyed. + + @return A new dynamic allocator, or NULL if memory allocation failed. + @note The returned value should be freed with `yyjson_alc_dyn_free()`. + + @warning This Allocator is not thread-safe. + */ +yyjson_api yyjson_alc *yyjson_alc_dyn_new(void); + +/** + Free a dynamic allocator which is created by `yyjson_alc_dyn_new()`. + @param alc The dynamic allocator to be destroyed. + */ +yyjson_api void yyjson_alc_dyn_free(yyjson_alc *alc); + + + +/*============================================================================== + * MARK: - Text Locating + *============================================================================*/ + +/** + Locate the line and column number for a byte position in a string. + This can be used to get better description for error position. + + @param str The input string. + @param len The byte length of the input string. + @param pos The byte position within the input string. + @param line A pointer to receive the line number, starting from 1. + @param col A pointer to receive the column number, starting from 1. + @param chr A pointer to receive the character index, starting from 0. + @return true on success, false if `str` is NULL or `pos` is out of bounds. + @note Line/column/character are calculated based on Unicode characters for + compatibility with text editors. For multi-byte UTF-8 characters, + the returned value may not directly correspond to the byte position. + */ +yyjson_api bool yyjson_locate_pos(const char *str, size_t len, size_t pos, + size_t *line, size_t *col, size_t *chr); + + + +/*============================================================================== + * MARK: - JSON Structure + *============================================================================*/ + +/** + An immutable document for reading JSON. + This document holds memory for all its JSON values and strings. When it is no + longer used, the user should call `yyjson_doc_free()` to free its memory. + */ +typedef struct yyjson_doc yyjson_doc; + +/** + An immutable value for reading JSON. + A JSON Value has the same lifetime as its document. The memory is held by its + document and and cannot be freed alone. + */ +typedef struct yyjson_val yyjson_val; + +/** + A mutable document for building JSON. + This document holds memory for all its JSON values and strings. When it is no + longer used, the user should call `yyjson_mut_doc_free()` to free its memory. + */ +typedef struct yyjson_mut_doc yyjson_mut_doc; + +/** + A mutable value for building JSON. + A JSON Value has the same lifetime as its document. The memory is held by its + document and and cannot be freed alone. + */ +typedef struct yyjson_mut_val yyjson_mut_val; + + + +/*============================================================================== + * MARK: - JSON Reader API + *============================================================================*/ + +/** Run-time options for JSON reader. */ +typedef uint32_t yyjson_read_flag; + +/** Default option (RFC 8259 compliant): + - Read positive integer as uint64_t. + - Read negative integer as int64_t. + - Read floating-point number as double with round-to-nearest mode. + - Read integer which cannot fit in uint64_t or int64_t as double. + - Report error if double number is infinity. + - Report error if string contains invalid UTF-8 character or BOM. + - Report error on trailing commas, comments, inf and nan literals. */ +static const yyjson_read_flag YYJSON_READ_NOFLAG = 0; + +/** Read the input data in-situ. + This option allows the reader to modify and use input data to store string + values, which can increase reading speed slightly. + The caller should hold the input data before free the document. + The input data must be padded by at least `YYJSON_PADDING_SIZE` bytes. + For example: `[1,2]` should be `[1,2]\0\0\0\0`, input length should be 5. */ +static const yyjson_read_flag YYJSON_READ_INSITU = 1 << 0; + +/** Stop when done instead of issuing an error if there's additional content + after a JSON document. This option may be used to parse small pieces of JSON + in larger data, such as `NDJSON`. */ +static const yyjson_read_flag YYJSON_READ_STOP_WHEN_DONE = 1 << 1; + +/** Allow single trailing comma at the end of an object or array, + such as `[1,2,3,]`, `{"a":1,"b":2,}` (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_TRAILING_COMMAS = 1 << 2; + +/** Allow C-style single-line and mult-line comments (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_COMMENTS = 1 << 3; + +/** Allow inf/nan number and literal, case-insensitive, + such as 1e999, NaN, inf, -Infinity (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_INF_AND_NAN = 1 << 4; + +/** Read all numbers as raw strings (value with `YYJSON_TYPE_RAW` type), + inf/nan literal is also read as raw with `ALLOW_INF_AND_NAN` flag. */ +static const yyjson_read_flag YYJSON_READ_NUMBER_AS_RAW = 1 << 5; + +/** Allow reading invalid unicode when parsing string values (non-standard). + Invalid characters will be allowed to appear in the string values, but + invalid escape sequences will still be reported as errors. + This flag does not affect the performance of correctly encoded strings. + + @warning Strings in JSON values may contain incorrect encoding when this + option is used, you need to handle these strings carefully to avoid security + risks. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_INVALID_UNICODE = 1 << 6; + +/** Read big numbers as raw strings. These big numbers include integers that + cannot be represented by `int64_t` and `uint64_t`, and floating-point + numbers that cannot be represented by finite `double`. + The flag will be overridden by `YYJSON_READ_NUMBER_AS_RAW` flag. */ +static const yyjson_read_flag YYJSON_READ_BIGNUM_AS_RAW = 1 << 7; + +/** Allow UTF-8 BOM and skip it before parsing if any (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_BOM = 1 << 8; + +/** Allow extended number formats (non-standard): + - Hexadecimal numbers, such as `0x7B`. + - Numbers with leading or trailing decimal point, such as `.123`, `123.`. + - Numbers with a leading plus sign, such as `+123`. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_EXT_NUMBER = 1 << 9; + +/** Allow extended escape sequences in strings (non-standard): + - Additional escapes: `\a`, `\e`, `\v`, ``\'``, `\?`, `\0`. + - Hex escapes: `\xNN`, such as `\x7B`. + - Line continuation: backslash followed by line terminator sequences. + - Unknown escape: if backslash is followed by an unsupported character, + the backslash will be removed and the character will be kept as-is. + However, `\1`-`\9` will still trigger an error. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_EXT_ESCAPE = 1 << 10; + +/** Allow extended whitespace characters (non-standard): + - Vertical tab `\v` and form feed `\f`. + - Line separator `\u2028` and paragraph separator `\u2029`. + - Non-breaking space `\xA0`. + - Byte order mark: `\uFEFF`. + - Other Unicode characters in the Zs (Separator, space) category. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_EXT_WHITESPACE = 1 << 11; + +/** Allow strings enclosed in single quotes (non-standard), such as ``'ab'``. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_SINGLE_QUOTED_STR = 1 << 12; + +/** Allow object keys without quotes (non-standard), such as `{a:1,b:2}`. + This extends the ECMAScript IdentifierName rule by allowing any + non-whitespace character with code point above `U+007F`. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_UNQUOTED_KEY = 1 << 13; + +/** Allow JSON5 format, see: [https://json5.org]. + This flag supports all JSON5 features with some additional extensions: + - Accepts more escape sequences than JSON5 (e.g. `\a`, `\e`). + - Unquoted keys are not limited to ECMAScript IdentifierName. + - Allow case-insensitive `NaN`, `Inf` and `Infinity` literals. */ +static const yyjson_read_flag YYJSON_READ_JSON5 = + (1 << 2) | /* YYJSON_READ_ALLOW_TRAILING_COMMAS */ + (1 << 3) | /* YYJSON_READ_ALLOW_COMMENTS */ + (1 << 4) | /* YYJSON_READ_ALLOW_INF_AND_NAN */ + (1 << 9) | /* YYJSON_READ_ALLOW_EXT_NUMBER */ + (1 << 10) | /* YYJSON_READ_ALLOW_EXT_ESCAPE */ + (1 << 11) | /* YYJSON_READ_ALLOW_EXT_WHITESPACE */ + (1 << 12) | /* YYJSON_READ_ALLOW_SINGLE_QUOTED_STR */ + (1 << 13); /* YYJSON_READ_ALLOW_UNQUOTED_KEY */ + + + +/** Result code for JSON reader. */ +typedef uint32_t yyjson_read_code; + +/** Success, no error. */ +static const yyjson_read_code YYJSON_READ_SUCCESS = 0; + +/** Invalid parameter, such as NULL input string or 0 input length. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_PARAMETER = 1; + +/** Memory allocation failed. */ +static const yyjson_read_code YYJSON_READ_ERROR_MEMORY_ALLOCATION = 2; + +/** Input JSON string is empty. */ +static const yyjson_read_code YYJSON_READ_ERROR_EMPTY_CONTENT = 3; + +/** Unexpected content after document, such as `[123]abc`. */ +static const yyjson_read_code YYJSON_READ_ERROR_UNEXPECTED_CONTENT = 4; + +/** Unexpected end of input, the parsed part is valid, such as `[123`. */ +static const yyjson_read_code YYJSON_READ_ERROR_UNEXPECTED_END = 5; + +/** Unexpected character inside the document, such as `[abc]`. */ +static const yyjson_read_code YYJSON_READ_ERROR_UNEXPECTED_CHARACTER = 6; + +/** Invalid JSON structure, such as `[1,]`. */ +static const yyjson_read_code YYJSON_READ_ERROR_JSON_STRUCTURE = 7; + +/** Invalid comment, deprecated, use `UNEXPECTED_END` for unclosed comment. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_COMMENT = 8; + +/** Invalid number, such as `123.e12`, `000`. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_NUMBER = 9; + +/** Invalid string, such as invalid escaped character inside a string. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_STRING = 10; + +/** Invalid JSON literal, such as `truu`. */ +static const yyjson_read_code YYJSON_READ_ERROR_LITERAL = 11; + +/** Failed to open a file. */ +static const yyjson_read_code YYJSON_READ_ERROR_FILE_OPEN = 12; + +/** Failed to read a file. */ +static const yyjson_read_code YYJSON_READ_ERROR_FILE_READ = 13; + +/** Incomplete input during incremental parsing; parsing state is preserved. */ +static const yyjson_read_code YYJSON_READ_ERROR_MORE = 14; + +/** Error information for JSON reader. */ +typedef struct yyjson_read_err { + /** Error code, see `yyjson_read_code` for all possible values. */ + yyjson_read_code code; + /** Error message, constant, no need to free (NULL if success). */ + const char *msg; + /** Error byte position for input data (0 if success). */ + size_t pos; +} yyjson_read_err; + + + +#if !defined(YYJSON_DISABLE_READER) || !YYJSON_DISABLE_READER + +/** + Read JSON with options. + + This function is thread-safe when: + 1. The `dat` is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param dat The JSON data (UTF-8 without BOM), null-terminator is not required. + If this parameter is NULL, the function will fail and return NULL. + The `dat` will not be modified without the flag `YYJSON_READ_INSITU`, so you + can pass a `const char *` string and case it to `char *` if you don't use + the `YYJSON_READ_INSITU` flag. + @param len The length of JSON data in bytes. + If this parameter is 0, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + */ +yyjson_api yyjson_doc *yyjson_read_opts(char *dat, + size_t len, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** + Read a JSON file. + + This function is thread-safe when: + 1. The file is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + + @warning On 32-bit operating system, files larger than 2GB may fail to read. + */ +yyjson_api yyjson_doc *yyjson_read_file(const char *path, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** + Read JSON from a file pointer. + + @param fp The file pointer. + The data will be read from the current position of the FILE to the end. + If this fp is NULL or invalid, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + + @warning On 32-bit operating system, files larger than 2GB may fail to read. + */ +yyjson_api yyjson_doc *yyjson_read_fp(FILE *fp, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** + Read a JSON string. + + This function is thread-safe. + + @param dat The JSON data (UTF-8 without BOM), null-terminator is not required. + If this parameter is NULL, the function will fail and return NULL. + @param len The length of JSON data in bytes. + If this parameter is 0, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + */ +yyjson_api_inline yyjson_doc *yyjson_read(const char *dat, + size_t len, + yyjson_read_flag flg) { + flg &= ~YYJSON_READ_INSITU; /* const string cannot be modified */ + return yyjson_read_opts((char *)(void *)(size_t)(const void *)dat, + len, flg, NULL, NULL); +} + + + +#if !defined(YYJSON_DISABLE_INCR_READER) || !YYJSON_DISABLE_INCR_READER + +/** Opaque state for incremental JSON reader. */ +typedef struct yyjson_incr_state yyjson_incr_state; + +/** + Initialize state for incremental read. + + To read a large JSON document incrementally: + 1. Call `yyjson_incr_new()` to create the state for incremental reading. + 2. Call `yyjson_incr_read()` repeatedly. + 3. Call `yyjson_incr_free()` to free the state. + + Note: The incremental JSON reader only supports standard JSON. + Flags for non-standard features (e.g. comments, trailing commas) are ignored. + + @param buf The JSON data, null-terminator is not required. + If this parameter is NULL, the function will fail and return NULL. + @param buf_len The length of the JSON data in `buf`. + If use `YYJSON_READ_INSITU`, `buf_len` should not include the padding size. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @return A state for incremental reading. + It should be freed with `yyjson_incr_free()`. + NULL is returned if memory allocation fails. +*/ +yyjson_api yyjson_incr_state *yyjson_incr_new(char *buf, size_t buf_len, + yyjson_read_flag flg, + const yyjson_alc *alc); + +/** + Performs incremental read of up to `len` bytes. + + If NULL is returned and `err->code` is set to `YYJSON_READ_ERROR_MORE`, it + indicates that more data is required to continue parsing. Then, call this + function again with incremented `len`. Continue until a document is returned or + an error other than `YYJSON_READ_ERROR_MORE` is returned. + + Note: Parsing in very small increments is not efficient. An increment of + several kilobytes or megabytes is recommended. + + @param state The state for incremental reading, created using + `yyjson_incr_new()`. + @param len The number of bytes of JSON data available to parse. + If this parameter is 0, the function will fail and return NULL. + @param err A pointer to receive error information. + @return A new JSON document, or NULL if an error occurs. + When the document is no longer needed, it should be freed with + `yyjson_doc_free()`. +*/ +yyjson_api yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, + yyjson_read_err *err); + +/** Release the incremental read state and free the memory. */ +yyjson_api void yyjson_incr_free(yyjson_incr_state *state); + +#endif /* YYJSON_DISABLE_INCR_READER */ + +/** + Returns the size of maximum memory usage to read a JSON data. + + You may use this value to avoid malloc() or calloc() call inside the reader + to get better performance, or read multiple JSON with one piece of memory. + + @param len The length of JSON data in bytes. + @param flg The JSON read options. + @return The maximum memory size to read this JSON, or 0 if overflow. + + @b Example + @code + // read multiple JSON with same pre-allocated memory + + char *dat1, *dat2, *dat3; // JSON data + size_t len1, len2, len3; // JSON length + size_t max_len = MAX(len1, MAX(len2, len3)); + yyjson_doc *doc; + + // use one allocator for multiple JSON + size_t size = yyjson_read_max_memory_usage(max_len, 0); + void *buf = malloc(size); + yyjson_alc alc; + yyjson_alc_pool_init(&alc, buf, size); + + // no more alloc() or realloc() call during reading + doc = yyjson_read_opts(dat1, len1, 0, &alc, NULL); + yyjson_doc_free(doc); + doc = yyjson_read_opts(dat2, len2, 0, &alc, NULL); + yyjson_doc_free(doc); + doc = yyjson_read_opts(dat3, len3, 0, &alc, NULL); + yyjson_doc_free(doc); + + free(buf); + @endcode + @see yyjson_alc_pool_init() + */ +yyjson_api_inline size_t yyjson_read_max_memory_usage(size_t len, + yyjson_read_flag flg) { + /* + 1. The max value count is (json_size / 2 + 1), + for example: "[1,2,3,4]" size is 9, value count is 5. + 2. Some broken JSON may cost more memory during reading, but fail at end, + for example: "[[[[[[[[". + 3. yyjson use 16 bytes per value, see struct yyjson_val. + 4. yyjson use dynamic memory with a growth factor of 1.5. + + The max memory size is (json_size / 2 * 16 * 1.5 + padding). + */ + size_t mul = (size_t)12 + !(flg & YYJSON_READ_INSITU); + size_t pad = 256; + size_t max = (size_t)(~(size_t)0); + if (flg & YYJSON_READ_STOP_WHEN_DONE) len = len < 256 ? 256 : len; + if (len >= (max - pad - mul) / mul) return 0; + return len * mul + pad; +} + +/** + Read a JSON number. + + This function is thread-safe when data is not modified by other threads. + + @param dat The JSON data (UTF-8 without BOM), null-terminator is required. + If this parameter is NULL, the function will fail and return NULL. + @param val The output value where result is stored. + If this parameter is NULL, the function will fail and return NULL. + The value will hold either UINT or SINT or REAL number; + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + Supports `YYJSON_READ_NUMBER_AS_RAW` and `YYJSON_READ_ALLOW_INF_AND_NAN`. + @param alc The memory allocator used for long number. + It is only used when the built-in floating point reader is disabled. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return If successful, a pointer to the character after the last character + used in the conversion, NULL if an error occurs. + */ +yyjson_api const char *yyjson_read_number(const char *dat, + yyjson_val *val, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** Same as `yyjson_read_number()`. */ +yyjson_api_inline const char *yyjson_mut_read_number(const char *dat, + yyjson_mut_val *val, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err) { + return yyjson_read_number(dat, (yyjson_val *)val, flg, alc, err); +} + +#endif /* YYJSON_DISABLE_READER) */ + + + +/*============================================================================== + * MARK: - JSON Writer API + *============================================================================*/ + +/** Run-time options for JSON writer. */ +typedef uint32_t yyjson_write_flag; + +/** Default option: + - Write JSON minify. + - Report error on inf or nan number. + - Report error on invalid UTF-8 string. + - Do not escape unicode or slash. */ +static const yyjson_write_flag YYJSON_WRITE_NOFLAG = 0; + +/** Write JSON pretty with 4 space indent. */ +static const yyjson_write_flag YYJSON_WRITE_PRETTY = 1 << 0; + +/** Escape unicode as `uXXXX`, make the output ASCII only. */ +static const yyjson_write_flag YYJSON_WRITE_ESCAPE_UNICODE = 1 << 1; + +/** Escape '/' as '\/'. */ +static const yyjson_write_flag YYJSON_WRITE_ESCAPE_SLASHES = 1 << 2; + +/** Write inf and nan number as 'Infinity' and 'NaN' literal (non-standard). */ +static const yyjson_write_flag YYJSON_WRITE_ALLOW_INF_AND_NAN = 1 << 3; + +/** Write inf and nan number as null literal. + This flag will override `YYJSON_WRITE_ALLOW_INF_AND_NAN` flag. */ +static const yyjson_write_flag YYJSON_WRITE_INF_AND_NAN_AS_NULL = 1 << 4; + +/** Allow invalid unicode when encoding string values (non-standard). + Invalid characters in string value will be copied byte by byte. + If `YYJSON_WRITE_ESCAPE_UNICODE` flag is also set, invalid character will be + escaped as `U+FFFD` (replacement character). + This flag does not affect the performance of correctly encoded strings. */ +static const yyjson_write_flag YYJSON_WRITE_ALLOW_INVALID_UNICODE = 1 << 5; + +/** Write JSON pretty with 2 space indent. + This flag will override `YYJSON_WRITE_PRETTY` flag. */ +static const yyjson_write_flag YYJSON_WRITE_PRETTY_TWO_SPACES = 1 << 6; + +/** Adds a newline character `\n` at the end of the JSON. + This can be helpful for text editors or NDJSON. */ +static const yyjson_write_flag YYJSON_WRITE_NEWLINE_AT_END = 1 << 7; + + + +/** The highest 8 bits of `yyjson_write_flag` and real number value's `tag` + are reserved for controlling the output format of floating-point numbers. */ +#define YYJSON_WRITE_FP_FLAG_BITS 8 + +/** The highest 4 bits of flag are reserved for precision value. */ +#define YYJSON_WRITE_FP_PREC_BITS 4 + +/** Write floating-point number using fixed-point notation. + - This is similar to ECMAScript `Number.prototype.toFixed(prec)`, + but with trailing zeros removed. The `prec` ranges from 1 to 15. + - This will produce shorter output but may lose some precision. */ +#define YYJSON_WRITE_FP_TO_FIXED(prec) ((yyjson_write_flag)( \ + (uint32_t)((uint32_t)(prec)) << (32 - 4) )) + +/** Write floating-point numbers using single-precision (float). + - This casts `double` to `float` before serialization. + - This will produce shorter output, but may lose some precision. + - This flag is ignored if `YYJSON_WRITE_FP_TO_FIXED(prec)` is also used. */ +#define YYJSON_WRITE_FP_TO_FLOAT ((yyjson_write_flag)(1 << (32 - 5))) + + + +/** Result code for JSON writer */ +typedef uint32_t yyjson_write_code; + +/** Success, no error. */ +static const yyjson_write_code YYJSON_WRITE_SUCCESS = 0; + +/** Invalid parameter, such as NULL document. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_PARAMETER = 1; + +/** Memory allocation failure occurs. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_MEMORY_ALLOCATION = 2; + +/** Invalid value type in JSON document. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_VALUE_TYPE = 3; + +/** NaN or Infinity number occurs. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_NAN_OR_INF = 4; + +/** Failed to open a file. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_FILE_OPEN = 5; + +/** Failed to write a file. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_FILE_WRITE = 6; + +/** Invalid unicode in string. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_STRING = 7; + +/** Error information for JSON writer. */ +typedef struct yyjson_write_err { + /** Error code, see `yyjson_write_code` for all possible values. */ + yyjson_write_code code; + /** Error message, constant, no need to free (NULL if success). */ + const char *msg; +} yyjson_write_err; + + + +#if !defined(YYJSON_DISABLE_WRITER) || !YYJSON_DISABLE_WRITER + +/*============================================================================== + * MARK: - JSON Document Writer API + *============================================================================*/ + +/** + Write a document to JSON string with options. + + This function is thread-safe when: + The `alc` is thread-safe or NULL. + + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_write_opts(const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a document to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_write_file(const char *path, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to file pointer with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this fp is NULL or invalid, the function will fail and return false. + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_write_fp(FILE *fp, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to JSON string. + + This function is thread-safe. + + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_write(const yyjson_doc *doc, + yyjson_write_flag flg, + size_t *len) { + return yyjson_write_opts(doc, flg, NULL, len, NULL); +} + + + +/** + Write a document to JSON string with options. + + This function is thread-safe when: + 1. The `doc` is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param doc The mutable JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a document to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `doc` is not modified by other threads. + 3. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param doc The mutable JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_write_file(const char *path, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to file pointer with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this fp is NULL or invalid, the function will fail and return false. + @param doc The mutable JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_write_fp(FILE *fp, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to JSON string. + + This function is thread-safe when: + The `doc` is not modified by other threads. + + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_mut_write(const yyjson_mut_doc *doc, + yyjson_write_flag flg, + size_t *len) { + return yyjson_mut_write_opts(doc, flg, NULL, len, NULL); +} + + + +/*============================================================================== + * MARK: - JSON Value Writer API + *============================================================================*/ + +/** + Write a value to JSON string with options. + + This function is thread-safe when: + The `alc` is thread-safe or NULL. + + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_val_write_opts(const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a value to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_val_write_file(const char *path, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to file pointer with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this path is NULL or invalid, the function will fail and return false. + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_val_write_fp(FILE *fp, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to JSON string. + + This function is thread-safe. + + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_val_write(const yyjson_val *val, + yyjson_write_flag flg, + size_t *len) { + return yyjson_val_write_opts(val, flg, NULL, len, NULL); +} + +/** + Write a value to JSON string with options. + + This function is thread-safe when: + 1. The `val` is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param val The mutable JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_mut_val_write_opts(const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a value to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `val` is not modified by other threads. + 3. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param val The mutable JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_val_write_file(const char *path, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to JSON file with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this path is NULL or invalid, the function will fail and return false. + @param val The mutable JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_val_write_fp(FILE *fp, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to JSON string. + + This function is thread-safe when: + The `val` is not modified by other threads. + + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_mut_val_write(const yyjson_mut_val *val, + yyjson_write_flag flg, + size_t *len) { + return yyjson_mut_val_write_opts(val, flg, NULL, len, NULL); +} + +/** + Write a JSON number. + + @param val A JSON number value to be converted to a string. + If this parameter is invalid, the function will fail and return NULL. + @param buf A buffer to store the resulting null-terminated string. + If this parameter is NULL, the function will fail and return NULL. + For integer values, the buffer must be at least 21 bytes. + For floating-point values, the buffer must be at least 40 bytes. + @return On success, returns a pointer to the character after the last + written character. On failure, returns NULL. + @note + - This function is thread-safe and does not allocate memory + (when `YYJSON_DISABLE_FAST_FP_CONV` is not defined). + - This function will fail and return NULL only in the following cases: + 1) `val` or `buf` is NULL; + 2) `val` is not a number type; + 3) `val` is `inf` or `nan`, and non-standard JSON is explicitly disabled + via the `YYJSON_DISABLE_NON_STANDARD` flag. + */ +yyjson_api char *yyjson_write_number(const yyjson_val *val, char *buf); + +/** Same as `yyjson_write_number()`. */ +yyjson_api_inline char *yyjson_mut_write_number(const yyjson_mut_val *val, + char *buf) { + return yyjson_write_number((const yyjson_val *)val, buf); +} + +#endif /* YYJSON_DISABLE_WRITER */ + + + +/*============================================================================== + * MARK: - JSON Document API + *============================================================================*/ + +/** Returns the root value of this JSON document. + Returns NULL if `doc` is NULL. */ +yyjson_api_inline yyjson_val *yyjson_doc_get_root(yyjson_doc *doc); + +/** Returns read size of input JSON data. + Returns 0 if `doc` is NULL. + For example: the read size of `[1,2,3]` is 7 bytes. */ +yyjson_api_inline size_t yyjson_doc_get_read_size(yyjson_doc *doc); + +/** Returns total value count in this JSON document. + Returns 0 if `doc` is NULL. + For example: the value count of `[1,2,3]` is 4. */ +yyjson_api_inline size_t yyjson_doc_get_val_count(yyjson_doc *doc); + +/** Release the JSON document and free the memory. + After calling this function, the `doc` and all values from the `doc` are no + longer available. This function will do nothing if the `doc` is NULL. */ +yyjson_api_inline void yyjson_doc_free(yyjson_doc *doc); + + + +/*============================================================================== + * MARK: - JSON Value Type API + *============================================================================*/ + +/** Returns whether the JSON value is raw. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_raw(yyjson_val *val); + +/** Returns whether the JSON value is `null`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_null(yyjson_val *val); + +/** Returns whether the JSON value is `true`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_true(yyjson_val *val); + +/** Returns whether the JSON value is `false`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_false(yyjson_val *val); + +/** Returns whether the JSON value is bool (true/false). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_bool(yyjson_val *val); + +/** Returns whether the JSON value is unsigned integer (uint64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_uint(yyjson_val *val); + +/** Returns whether the JSON value is signed integer (int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_sint(yyjson_val *val); + +/** Returns whether the JSON value is integer (uint64_t/int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_int(yyjson_val *val); + +/** Returns whether the JSON value is real number (double). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_real(yyjson_val *val); + +/** Returns whether the JSON value is number (uint64_t/int64_t/double). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_num(yyjson_val *val); + +/** Returns whether the JSON value is string. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_str(yyjson_val *val); + +/** Returns whether the JSON value is array. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_arr(yyjson_val *val); + +/** Returns whether the JSON value is object. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_obj(yyjson_val *val); + +/** Returns whether the JSON value is container (array/object). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val); + + + +/*============================================================================== + * MARK: - JSON Value Content API + *============================================================================*/ + +/** Returns the JSON value's type. + Returns YYJSON_TYPE_NONE if `val` is NULL. */ +yyjson_api_inline yyjson_type yyjson_get_type(yyjson_val *val); + +/** Returns the JSON value's subtype. + Returns YYJSON_SUBTYPE_NONE if `val` is NULL. */ +yyjson_api_inline yyjson_subtype yyjson_get_subtype(yyjson_val *val); + +/** Returns the JSON value's tag. + Returns 0 if `val` is NULL. */ +yyjson_api_inline uint8_t yyjson_get_tag(yyjson_val *val); + +/** Returns the JSON value's type description. + The return value should be one of these strings: "raw", "null", "string", + "array", "object", "true", "false", "uint", "sint", "real", "unknown". */ +yyjson_api_inline const char *yyjson_get_type_desc(yyjson_val *val); + +/** Returns the content if the value is raw. + Returns NULL if `val` is NULL or type is not raw. */ +yyjson_api_inline const char *yyjson_get_raw(yyjson_val *val); + +/** Returns the content if the value is bool. + Returns false if `val` is NULL or type is not bool. */ +yyjson_api_inline bool yyjson_get_bool(yyjson_val *val); + +/** Returns the content and cast to uint64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline uint64_t yyjson_get_uint(yyjson_val *val); + +/** Returns the content and cast to int64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int64_t yyjson_get_sint(yyjson_val *val); + +/** Returns the content and cast to int. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int yyjson_get_int(yyjson_val *val); + +/** Returns the content if the value is real number, or 0.0 on error. + Returns 0.0 if `val` is NULL or type is not real(double). */ +yyjson_api_inline double yyjson_get_real(yyjson_val *val); + +/** Returns the content and typecast to `double` if the value is number. + Returns 0.0 if `val` is NULL or type is not number(uint/sint/real). */ +yyjson_api_inline double yyjson_get_num(yyjson_val *val); + +/** Returns the content if the value is string. + Returns NULL if `val` is NULL or type is not string. */ +yyjson_api_inline const char *yyjson_get_str(yyjson_val *val); + +/** Returns the content length (string length, array size, object size. + Returns 0 if `val` is NULL or type is not string/array/object. */ +yyjson_api_inline size_t yyjson_get_len(yyjson_val *val); + +/** Returns whether the JSON value is equals to a string. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_equals_str(yyjson_val *val, const char *str); + +/** Returns whether the JSON value is equals to a string. + The `str` should be a UTF-8 string, null-terminator is not required. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_equals_strn(yyjson_val *val, const char *str, + size_t len); + +/** Returns whether two JSON values are equal (deep compare). + Returns false if input is NULL. + @note the result may be inaccurate if object has duplicate keys. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api_inline bool yyjson_equals(yyjson_val *lhs, yyjson_val *rhs); + +/** Set the value to raw. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_raw(yyjson_val *val, + const char *raw, size_t len); + +/** Set the value to null. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_null(yyjson_val *val); + +/** Set the value to bool. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_bool(yyjson_val *val, bool num); + +/** Set the value to uint. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_uint(yyjson_val *val, uint64_t num); + +/** Set the value to sint. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_sint(yyjson_val *val, int64_t num); + +/** Set the value to int. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int num); + +/** Set the value to float. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_float(yyjson_val *val, float num); + +/** Set the value to double. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_double(yyjson_val *val, double num); + +/** Set the value to real. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_real(yyjson_val *val, double num); + +/** Set the floating-point number's output format to fixed-point notation. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FIXED flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_fp_to_fixed(yyjson_val *val, int prec); + +/** Set the floating-point number's output format to single-precision. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FLOAT flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_fp_to_float(yyjson_val *val, bool flt); + +/** Set the value to string (null-terminated). + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_str(yyjson_val *val, const char *str); + +/** Set the value to string (with length). + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_strn(yyjson_val *val, + const char *str, size_t len); + +/** Marks this string as not needing to be escaped during JSON writing. + This can be used to avoid the overhead of escaping if the string contains + only characters that do not require escaping. + Returns false if input is NULL or `val` is not string. + @see YYJSON_SUBTYPE_NOESC subtype. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_str_noesc(yyjson_val *val, bool noesc); + + + +/*============================================================================== + * MARK: - JSON Array API + *============================================================================*/ + +/** Returns the number of elements in this array. + Returns 0 if `arr` is NULL or type is not array. */ +yyjson_api_inline size_t yyjson_arr_size(yyjson_val *arr); + +/** Returns the element at the specified position in this array. + Returns NULL if array is NULL/empty or the index is out of bounds. + @warning This function takes a linear search time if array is not flat. + For example: `[1,{},3]` is flat, `[1,[2],3]` is not flat. */ +yyjson_api_inline yyjson_val *yyjson_arr_get(yyjson_val *arr, size_t idx); + +/** Returns the first element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. */ +yyjson_api_inline yyjson_val *yyjson_arr_get_first(yyjson_val *arr); + +/** Returns the last element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. + @warning This function takes a linear search time if array is not flat. + For example: `[1,{},3]` is flat, `[1,[2],3]` is not flat.*/ +yyjson_api_inline yyjson_val *yyjson_arr_get_last(yyjson_val *arr); + + + +/*============================================================================== + * MARK: - JSON Array Iterator API + *============================================================================*/ + +/** + A JSON array iterator. + + @b Example + @code + yyjson_val *val; + yyjson_arr_iter iter = yyjson_arr_iter_with(arr); + while ((val = yyjson_arr_iter_next(&iter))) { + your_func(val); + } + @endcode + */ +typedef struct yyjson_arr_iter { + size_t idx; /**< next value's index */ + size_t max; /**< maximum index (arr.size) */ + yyjson_val *cur; /**< next value */ +} yyjson_arr_iter; + +/** + Initialize an iterator for this array. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_arr_iter_init(yyjson_val *arr, + yyjson_arr_iter *iter); + +/** + Create an iterator with an array , same as `yyjson_arr_iter_init()`. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, an empty iterator will returned. + @return A new iterator for the array. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(yyjson_val *arr); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_arr_iter_has_next(yyjson_arr_iter *iter); + +/** + Returns the next element in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_val *yyjson_arr_iter_next(yyjson_arr_iter *iter); + +/** + Macro for iterating over an array. + It works like iterator, but with a more intuitive API. + + @b Example + @code + size_t idx, max; + yyjson_val *val; + yyjson_arr_foreach(arr, idx, max, val) { + your_func(idx, val); + } + @endcode + */ +#define yyjson_arr_foreach(arr, idx, max, val) \ + for ((idx) = 0, \ + (max) = yyjson_arr_size(arr), \ + (val) = yyjson_arr_get_first(arr); \ + (idx) < (max); \ + (idx)++, \ + (val) = unsafe_yyjson_get_next(val)) + + + +/*============================================================================== + * MARK: - JSON Object API + *============================================================================*/ + +/** Returns the number of key-value pairs in this object. + Returns 0 if `obj` is NULL or type is not object. */ +yyjson_api_inline size_t yyjson_obj_size(yyjson_val *obj); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a null-terminated UTF-8 string. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_val *yyjson_obj_get(yyjson_val *obj, const char *key); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a UTF-8 string, null-terminator is not required. + The `key_len` should be the length of the key, in bytes. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_val *yyjson_obj_getn(yyjson_val *obj, const char *key, + size_t key_len); + + + +/*============================================================================== + * MARK: - JSON Object Iterator API + *============================================================================*/ + +/** + A JSON object iterator. + + @b Example + @code + yyjson_val *key, *val; + yyjson_obj_iter iter = yyjson_obj_iter_with(obj); + while ((key = yyjson_obj_iter_next(&iter))) { + val = yyjson_obj_iter_get_val(key); + your_func(key, val); + } + @endcode + + If the ordering of the keys is known at compile-time, you can use this method + to speed up value lookups: + @code + // {"k1":1, "k2": 3, "k3": 3} + yyjson_val *key, *val; + yyjson_obj_iter iter = yyjson_obj_iter_with(obj); + yyjson_val *v1 = yyjson_obj_iter_get(&iter, "k1"); + yyjson_val *v3 = yyjson_obj_iter_get(&iter, "k3"); + @endcode + @see yyjson_obj_iter_get() and yyjson_obj_iter_getn() + */ +typedef struct yyjson_obj_iter { + size_t idx; /**< next key's index */ + size_t max; /**< maximum key index (obj.size) */ + yyjson_val *cur; /**< next key */ + yyjson_val *obj; /**< the object being iterated */ +} yyjson_obj_iter; + +/** + Initialize an iterator for this object. + + @param obj The object to be iterated over. + If this parameter is NULL or not an object, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_obj_iter_init(yyjson_val *obj, + yyjson_obj_iter *iter); + +/** + Create an iterator with an object, same as `yyjson_obj_iter_init()`. + + @param obj The object to be iterated over. + If this parameter is NULL or not an object, an empty iterator will returned. + @return A new iterator for the object. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(yyjson_val *obj); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_obj_iter_has_next(yyjson_obj_iter *iter); + +/** + Returns the next key in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_next(yyjson_obj_iter *iter); + +/** + Returns the value for key inside the iteration. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_get_val(yyjson_val *key); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_obj_get()`, but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string with null-terminator. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_get(yyjson_obj_iter *iter, + const char *key); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_obj_getn()`, but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string, null-terminator is not required. + @param key_len The the length of `key`, in bytes. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_getn(yyjson_obj_iter *iter, + const char *key, + size_t key_len); + +/** + Macro for iterating over an object. + It works like iterator, but with a more intuitive API. + + @b Example + @code + size_t idx, max; + yyjson_val *key, *val; + yyjson_obj_foreach(obj, idx, max, key, val) { + your_func(key, val); + } + @endcode + */ +#define yyjson_obj_foreach(obj, idx, max, key, val) \ + for ((idx) = 0, \ + (max) = yyjson_obj_size(obj), \ + (key) = (obj) ? unsafe_yyjson_get_first(obj) : NULL, \ + (val) = (key) + 1; \ + (idx) < (max); \ + (idx)++, \ + (key) = unsafe_yyjson_get_next(val), \ + (val) = (key) + 1) + + + +/*============================================================================== + * MARK: - Mutable JSON Document API + *============================================================================*/ + +/** Returns the root value of this JSON document. + Returns NULL if `doc` is NULL. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_root(yyjson_mut_doc *doc); + +/** Sets the root value of this JSON document. + Pass NULL to clear root value of the document. */ +yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, + yyjson_mut_val *root); + +/** + Set the string pool size for a mutable document. + This function does not allocate memory immediately, but uses the size when + the next memory allocation is needed. + + If the caller knows the approximate bytes of strings that the document needs to + store (e.g. copy string with `yyjson_mut_strcpy` function), setting a larger + size can avoid multiple memory allocations and improve performance. + + @param doc The mutable document. + @param len The desired string pool size in bytes (total string length). + @return true if successful, false if size is 0 or overflow. + */ +yyjson_api bool yyjson_mut_doc_set_str_pool_size(yyjson_mut_doc *doc, + size_t len); + +/** + Set the value pool size for a mutable document. + This function does not allocate memory immediately, but uses the size when + the next memory allocation is needed. + + If the caller knows the approximate number of values that the document needs to + store (e.g. create new value with `yyjson_mut_xxx` functions), setting a larger + size can avoid multiple memory allocations and improve performance. + + @param doc The mutable document. + @param count The desired value pool size (number of `yyjson_mut_val`). + @return true if successful, false if size is 0 or overflow. + */ +yyjson_api bool yyjson_mut_doc_set_val_pool_size(yyjson_mut_doc *doc, + size_t count); + +/** Release the JSON document and free the memory. + After calling this function, the `doc` and all values from the `doc` are no + longer available. This function will do nothing if the `doc` is NULL. */ +yyjson_api void yyjson_mut_doc_free(yyjson_mut_doc *doc); + +/** Creates and returns a new mutable JSON document, returns NULL on error. + If allocator is NULL, the default allocator will be used. */ +yyjson_api yyjson_mut_doc *yyjson_mut_doc_new(const yyjson_alc *alc); + +/** Copies and returns a new mutable document from input, returns NULL on error. + This makes a `deep-copy` on the immutable document. + If allocator is NULL, the default allocator will be used. + @note `imut_doc` -> `mut_doc`. */ +yyjson_api yyjson_mut_doc *yyjson_doc_mut_copy(yyjson_doc *doc, + const yyjson_alc *alc); + +/** Copies and returns a new mutable document from input, returns NULL on error. + This makes a `deep-copy` on the mutable document. + If allocator is NULL, the default allocator will be used. + @note `mut_doc` -> `mut_doc`. */ +yyjson_api yyjson_mut_doc *yyjson_mut_doc_mut_copy(yyjson_mut_doc *doc, + const yyjson_alc *alc); + +/** Copies and returns a new mutable value from input, returns NULL on error. + This makes a `deep-copy` on the immutable value. + The memory was managed by mutable document. + @note `imut_val` -> `mut_val`. */ +yyjson_api yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *doc, + yyjson_val *val); + +/** Copies and returns a new mutable value from input, returns NULL on error. + This makes a `deep-copy` on the mutable value. + The memory was managed by mutable document. + @note `mut_val` -> `mut_val`. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api yyjson_mut_val *yyjson_mut_val_mut_copy(yyjson_mut_doc *doc, + yyjson_mut_val *val); + +/** Copies and returns a new immutable document from input, + returns NULL on error. This makes a `deep-copy` on the mutable document. + The returned document should be freed with `yyjson_doc_free()`. + @note `mut_doc` -> `imut_doc`. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api yyjson_doc *yyjson_mut_doc_imut_copy(yyjson_mut_doc *doc, + const yyjson_alc *alc); + +/** Copies and returns a new immutable document from input, + returns NULL on error. This makes a `deep-copy` on the mutable value. + The returned document should be freed with `yyjson_doc_free()`. + @note `mut_val` -> `imut_doc`. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *val, + const yyjson_alc *alc); + + + +/*============================================================================== + * MARK: - Mutable JSON Value Type API + *============================================================================*/ + +/** Returns whether the JSON value is raw. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_raw(yyjson_mut_val *val); + +/** Returns whether the JSON value is `null`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_null(yyjson_mut_val *val); + +/** Returns whether the JSON value is `true`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_true(yyjson_mut_val *val); + +/** Returns whether the JSON value is `false`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_false(yyjson_mut_val *val); + +/** Returns whether the JSON value is bool (true/false). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_bool(yyjson_mut_val *val); + +/** Returns whether the JSON value is unsigned integer (uint64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_uint(yyjson_mut_val *val); + +/** Returns whether the JSON value is signed integer (int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_sint(yyjson_mut_val *val); + +/** Returns whether the JSON value is integer (uint64_t/int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_int(yyjson_mut_val *val); + +/** Returns whether the JSON value is real number (double). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_real(yyjson_mut_val *val); + +/** Returns whether the JSON value is number (uint/sint/real). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_num(yyjson_mut_val *val); + +/** Returns whether the JSON value is string. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_str(yyjson_mut_val *val); + +/** Returns whether the JSON value is array. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_arr(yyjson_mut_val *val); + +/** Returns whether the JSON value is object. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_obj(yyjson_mut_val *val); + +/** Returns whether the JSON value is container (array/object). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val); + + + +/*============================================================================== + * MARK: - Mutable JSON Value Content API + *============================================================================*/ + +/** Returns the JSON value's type. + Returns `YYJSON_TYPE_NONE` if `val` is NULL. */ +yyjson_api_inline yyjson_type yyjson_mut_get_type(yyjson_mut_val *val); + +/** Returns the JSON value's subtype. + Returns `YYJSON_SUBTYPE_NONE` if `val` is NULL. */ +yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype(yyjson_mut_val *val); + +/** Returns the JSON value's tag. + Returns 0 if `val` is NULL. */ +yyjson_api_inline uint8_t yyjson_mut_get_tag(yyjson_mut_val *val); + +/** Returns the JSON value's type description. + The return value should be one of these strings: "raw", "null", "string", + "array", "object", "true", "false", "uint", "sint", "real", "unknown". */ +yyjson_api_inline const char *yyjson_mut_get_type_desc(yyjson_mut_val *val); + +/** Returns the content if the value is raw. + Returns NULL if `val` is NULL or type is not raw. */ +yyjson_api_inline const char *yyjson_mut_get_raw(yyjson_mut_val *val); + +/** Returns the content if the value is bool. + Returns NULL if `val` is NULL or type is not bool. */ +yyjson_api_inline bool yyjson_mut_get_bool(yyjson_mut_val *val); + +/** Returns the content and cast to uint64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline uint64_t yyjson_mut_get_uint(yyjson_mut_val *val); + +/** Returns the content and cast to int64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int64_t yyjson_mut_get_sint(yyjson_mut_val *val); + +/** Returns the content and cast to int. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int yyjson_mut_get_int(yyjson_mut_val *val); + +/** Returns the content if the value is real number. + Returns 0.0 if `val` is NULL or type is not real(double). */ +yyjson_api_inline double yyjson_mut_get_real(yyjson_mut_val *val); + +/** Returns the content and typecast to `double` if the value is number. + Returns 0.0 if `val` is NULL or type is not number(uint/sint/real). */ +yyjson_api_inline double yyjson_mut_get_num(yyjson_mut_val *val); + +/** Returns the content if the value is string. + Returns NULL if `val` is NULL or type is not string. */ +yyjson_api_inline const char *yyjson_mut_get_str(yyjson_mut_val *val); + +/** Returns the content length (string length, array size, object size. + Returns 0 if `val` is NULL or type is not string/array/object. */ +yyjson_api_inline size_t yyjson_mut_get_len(yyjson_mut_val *val); + +/** Returns whether the JSON value is equals to a string. + The `str` should be a null-terminated UTF-8 string. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_mut_equals_str(yyjson_mut_val *val, + const char *str); + +/** Returns whether the JSON value is equals to a string. + The `str` should be a UTF-8 string, null-terminator is not required. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_mut_equals_strn(yyjson_mut_val *val, + const char *str, size_t len); + +/** Returns whether two JSON values are equal (deep compare). + Returns false if input is NULL. + @note the result may be inaccurate if object has duplicate keys. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api_inline bool yyjson_mut_equals(yyjson_mut_val *lhs, + yyjson_mut_val *rhs); + +/** Set the value to raw. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_raw(yyjson_mut_val *val, + const char *raw, size_t len); + +/** Set the value to null. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_null(yyjson_mut_val *val); + +/** Set the value to bool. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_bool(yyjson_mut_val *val, bool num); + +/** Set the value to uint. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_uint(yyjson_mut_val *val, uint64_t num); + +/** Set the value to sint. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_sint(yyjson_mut_val *val, int64_t num); + +/** Set the value to int. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int num); + +/** Set the value to float. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_float(yyjson_mut_val *val, float num); + +/** Set the value to double. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_double(yyjson_mut_val *val, double num); + +/** Set the value to real. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_real(yyjson_mut_val *val, double num); + +/** Set the floating-point number's output format to fixed-point notation. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FIXED flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_mut_set_fp_to_fixed(yyjson_mut_val *val, + int prec); + +/** Set the floating-point number's output format to single-precision. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FLOAT flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_mut_set_fp_to_float(yyjson_mut_val *val, + bool flt); + +/** Set the value to string (null-terminated). + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_str(yyjson_mut_val *val, const char *str); + +/** Set the value to string (with length). + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_strn(yyjson_mut_val *val, + const char *str, size_t len); + +/** Marks this string as not needing to be escaped during JSON writing. + This can be used to avoid the overhead of escaping if the string contains + only characters that do not require escaping. + Returns false if input is NULL or `val` is not string. + @see YYJSON_SUBTYPE_NOESC subtype. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_mut_set_str_noesc(yyjson_mut_val *val, + bool noesc); + +/** Set the value to array. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_arr(yyjson_mut_val *val); + +/** Set the value to array. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_obj(yyjson_mut_val *val); + + + +/*============================================================================== + * MARK: - Mutable JSON Value Creation API + *============================================================================*/ + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_raw(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawn(yyjson_mut_doc *doc, + const char *str, + size_t len); + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawcpy(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawncpy(yyjson_mut_doc *doc, + const char *str, + size_t len); + +/** Creates and returns a null value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_null(yyjson_mut_doc *doc); + +/** Creates and returns a true value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_true(yyjson_mut_doc *doc); + +/** Creates and returns a false value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_false(yyjson_mut_doc *doc); + +/** Creates and returns a bool value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_bool(yyjson_mut_doc *doc, + bool val); + +/** Creates and returns an unsigned integer value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_uint(yyjson_mut_doc *doc, + uint64_t num); + +/** Creates and returns a signed integer value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_sint(yyjson_mut_doc *doc, + int64_t num); + +/** Creates and returns a signed integer value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_int(yyjson_mut_doc *doc, + int64_t num); + +/** Creates and returns a float number value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_float(yyjson_mut_doc *doc, + float num); + +/** Creates and returns a double number value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_double(yyjson_mut_doc *doc, + double num); + +/** Creates and returns a real number value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_real(yyjson_mut_doc *doc, + double num); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_str(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_strn(yyjson_mut_doc *doc, + const char *str, + size_t len); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_strcpy(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_strncpy(yyjson_mut_doc *doc, + const char *str, + size_t len); + + + +/*============================================================================== + * MARK: - Mutable JSON Array API + *============================================================================*/ + +/** Returns the number of elements in this array. + Returns 0 if `arr` is NULL or type is not array. */ +yyjson_api_inline size_t yyjson_mut_arr_size(yyjson_mut_val *arr); + +/** Returns the element at the specified position in this array. + Returns NULL if array is NULL/empty or the index is out of bounds. + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(yyjson_mut_val *arr, + size_t idx); + +/** Returns the first element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first(yyjson_mut_val *arr); + +/** Returns the last element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_last(yyjson_mut_val *arr); + + + +/*============================================================================== + * MARK: - Mutable JSON Array Iterator API + *============================================================================*/ + +/** + A mutable JSON array iterator. + + @warning You should not modify the array while iterating over it, but you can + use `yyjson_mut_arr_iter_remove()` to remove current value. + + @b Example + @code + yyjson_mut_val *val; + yyjson_mut_arr_iter iter = yyjson_mut_arr_iter_with(arr); + while ((val = yyjson_mut_arr_iter_next(&iter))) { + your_func(val); + if (your_val_is_unused(val)) { + yyjson_mut_arr_iter_remove(&iter); + } + } + @endcode + */ +typedef struct yyjson_mut_arr_iter { + size_t idx; /**< next value's index */ + size_t max; /**< maximum index (arr.size) */ + yyjson_mut_val *cur; /**< current value */ + yyjson_mut_val *pre; /**< previous value */ + yyjson_mut_val *arr; /**< the array being iterated */ +} yyjson_mut_arr_iter; + +/** + Initialize an iterator for this array. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_mut_arr_iter_init(yyjson_mut_val *arr, + yyjson_mut_arr_iter *iter); + +/** + Create an iterator with an array , same as `yyjson_mut_arr_iter_init()`. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, an empty iterator will returned. + @return A new iterator for the array. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_mut_arr_iter yyjson_mut_arr_iter_with( + yyjson_mut_val *arr); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_mut_arr_iter_has_next( + yyjson_mut_arr_iter *iter); + +/** + Returns the next element in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_next( + yyjson_mut_arr_iter *iter); + +/** + Removes and returns current element in the iteration. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( + yyjson_mut_arr_iter *iter); + +/** + Macro for iterating over an array. + It works like iterator, but with a more intuitive API. + + @warning You should not modify the array while iterating over it. + + @b Example + @code + size_t idx, max; + yyjson_mut_val *val; + yyjson_mut_arr_foreach(arr, idx, max, val) { + your_func(idx, val); + } + @endcode + */ +#define yyjson_mut_arr_foreach(arr, idx, max, val) \ + for ((idx) = 0, \ + (max) = yyjson_mut_arr_size(arr), \ + (val) = yyjson_mut_arr_get_first(arr); \ + (idx) < (max); \ + (idx)++, \ + (val) = (val)->next) + + + +/*============================================================================== + * MARK: - Mutable JSON Array Creation API + *============================================================================*/ + +/** + Creates and returns an empty mutable array. + @param doc A mutable document, used for memory allocation only. + @return The new array. NULL if input is NULL or memory allocation failed. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr(yyjson_mut_doc *doc); + +/** + Creates and returns a new mutable array with the given boolean values. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of boolean values. + @param count The value count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const bool vals[3] = { true, false, true }; + yyjson_mut_val *arr = yyjson_mut_arr_with_bool(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_bool( + yyjson_mut_doc *doc, const bool *vals, size_t count); + +/** + Creates and returns a new mutable array with the given sint numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of sint numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int64_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint64(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint( + yyjson_mut_doc *doc, const int64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint64_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given real numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of real numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const double vals[3] = { 0.1, 0.2, 0.3 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_real(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_real( + yyjson_mut_doc *doc, const double *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int8 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int8 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int8_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint8(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint8( + yyjson_mut_doc *doc, const int8_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int16 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int16 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int16_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint16(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint16( + yyjson_mut_doc *doc, const int16_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int32 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int32 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int32_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint32(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint32( + yyjson_mut_doc *doc, const int32_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int64 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int64 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int64_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint64(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint64( + yyjson_mut_doc *doc, const int64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint8 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint8 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint8_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint8(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint8( + yyjson_mut_doc *doc, const uint8_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint16 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint16 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint16_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint16(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint16( + yyjson_mut_doc *doc, const uint16_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint32 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint32 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint32_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint32(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint32( + yyjson_mut_doc *doc, const uint32_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint64 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint64 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint64_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint64(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint64( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given float numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of float numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const float vals[3] = { -1.0f, 0.0f, 1.0f }; + yyjson_mut_val *arr = yyjson_mut_arr_with_float(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_float( + yyjson_mut_doc *doc, const float *vals, size_t count); + +/** + Creates and returns a new mutable array with the given double numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of double numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const double vals[3] = { -1.0, 0.0, 1.0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_double(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_double( + yyjson_mut_doc *doc, const double *vals, size_t count); + +/** + Creates and returns a new mutable array with the given strings, these strings + will not be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 null-terminator strings. + If this array contains NULL, the function will fail and return NULL. + @param count The number of values in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @warning The input strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. If these strings will be + modified, you should use `yyjson_mut_arr_with_strcpy()` instead. + + @b Example + @code + const char *vals[3] = { "a", "b", "c" }; + yyjson_mut_val *arr = yyjson_mut_arr_with_str(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_str( + yyjson_mut_doc *doc, const char **vals, size_t count); + +/** + Creates and returns a new mutable array with the given strings and string + lengths, these strings will not be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 strings, null-terminator is not required. + If this array contains NULL, the function will fail and return NULL. + @param lens A C array of string lengths, in bytes. + @param count The number of strings in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @warning The input strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. If these strings will be + modified, you should use `yyjson_mut_arr_with_strncpy()` instead. + + @b Example + @code + const char *vals[3] = { "a", "bb", "c" }; + const size_t lens[3] = { 1, 2, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_strn(doc, vals, lens, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strn( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count); + +/** + Creates and returns a new mutable array with the given strings, these strings + will be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 null-terminator strings. + If this array contains NULL, the function will fail and return NULL. + @param count The number of values in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const char *vals[3] = { "a", "b", "c" }; + yyjson_mut_val *arr = yyjson_mut_arr_with_strcpy(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strcpy( + yyjson_mut_doc *doc, const char **vals, size_t count); + +/** + Creates and returns a new mutable array with the given strings and string + lengths, these strings will be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 strings, null-terminator is not required. + If this array contains NULL, the function will fail and return NULL. + @param lens A C array of string lengths, in bytes. + @param count The number of strings in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const char *vals[3] = { "a", "bb", "c" }; + const size_t lens[3] = { 1, 2, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_strn(doc, vals, lens, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strncpy( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count); + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification API + *============================================================================*/ + +/** + Inserts a value into an array at a given index. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @param idx The index to which to insert the new value. + Returns false if the index is out of range. + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_arr_insert(yyjson_mut_val *arr, + yyjson_mut_val *val, size_t idx); + +/** + Inserts a value at the end of the array. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, + yyjson_mut_val *val); + +/** + Inserts a value at the head of the array. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_prepend(yyjson_mut_val *arr, + yyjson_mut_val *val); + +/** + Replaces a value at index and returns old value. + @param arr The array to which the value is to be replaced. + Returns false if it is NULL or not an array. + @param idx The index to which to replace the value. + Returns false if the index is out of range. + @param val The new value to replace. Returns false if it is NULL. + @return Old value, or NULL on error. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_replace(yyjson_mut_val *arr, + size_t idx, + yyjson_mut_val *val); + +/** + Removes and returns a value at index. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @param idx The index from which to remove the value. + Returns false if the index is out of range. + @return Old value, or NULL on error. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove(yyjson_mut_val *arr, + size_t idx); + +/** + Removes and returns the first value in this array. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @return The first value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_first( + yyjson_mut_val *arr); + +/** + Removes and returns the last value in this array. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @return The last value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_last( + yyjson_mut_val *arr); + +/** + Removes all values within a specified range in the array. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @param idx The start index of the range (0 is the first). + @param len The number of items in the range (can be 0). + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_arr_remove_range(yyjson_mut_val *arr, + size_t idx, size_t len); + +/** + Removes all values in this array. + @param arr The array from which all of the values are to be removed. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_clear(yyjson_mut_val *arr); + +/** + Rotates values in this array for the given number of times. + For example: `[1,2,3,4,5]` rotate 2 is `[3,4,5,1,2]`. + @param arr The array to be rotated. + @param idx Index (or times) to rotate. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_arr_rotate(yyjson_mut_val *arr, + size_t idx); + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification Convenience API + *============================================================================*/ + +/** + Adds a value at the end of the array. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_val(yyjson_mut_val *arr, + yyjson_mut_val *val); + +/** + Adds a `null` value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Adds a `true` value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Adds a `false` value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Adds a bool value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The bool value to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + bool val); + +/** + Adds an unsigned integer value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + uint64_t num); + +/** + Adds a signed integer value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num); + +/** + Adds an integer value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num); + +/** + Adds a float value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + float num); + +/** + Adds a double value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num); + +/** + Adds a double value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num); + +/** + Adds a string value at the end of the array (no copy). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A null-terminated UTF-8 string. + @return Whether successful. + @warning The input string is not copied, you should keep this string unmodified + for the lifetime of this JSON document. + */ +yyjson_api_inline bool yyjson_mut_arr_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str); + +/** + Adds a string value at the end of the array (no copy). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A UTF-8 string, null-terminator is not required. + @param len The length of the string, in bytes. + @return Whether successful. + @warning The input string is not copied, you should keep this string unmodified + for the lifetime of this JSON document. + */ +yyjson_api_inline bool yyjson_mut_arr_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, + size_t len); + +/** + Adds a string value at the end of the array (copied). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A null-terminated UTF-8 string. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str); + +/** + Adds a string value at the end of the array (copied). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A UTF-8 string, null-terminator is not required. + @param len The length of the string, in bytes. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, + size_t len); + +/** + Creates and adds a new array at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return The new array, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Creates and adds a new object at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return The new object, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + + + +/*============================================================================== + * MARK: - Mutable JSON Object API + *============================================================================*/ + +/** Returns the number of key-value pairs in this object. + Returns 0 if `obj` is NULL or type is not object. */ +yyjson_api_inline size_t yyjson_mut_obj_size(yyjson_mut_val *obj); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a null-terminated UTF-8 string. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(yyjson_mut_val *obj, + const char *key); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a UTF-8 string, null-terminator is not required. + The `key_len` should be the length of the key, in bytes. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(yyjson_mut_val *obj, + const char *key, + size_t key_len); + + + +/*============================================================================== + * MARK: - Mutable JSON Object Iterator API + *============================================================================*/ + +/** + A mutable JSON object iterator. + + @warning You should not modify the object while iterating over it, but you can + use `yyjson_mut_obj_iter_remove()` to remove current value. + + @b Example + @code + yyjson_mut_val *key, *val; + yyjson_mut_obj_iter iter = yyjson_mut_obj_iter_with(obj); + while ((key = yyjson_mut_obj_iter_next(&iter))) { + val = yyjson_mut_obj_iter_get_val(key); + your_func(key, val); + if (your_val_is_unused(key, val)) { + yyjson_mut_obj_iter_remove(&iter); + } + } + @endcode + + If the ordering of the keys is known at compile-time, you can use this method + to speed up value lookups: + @code + // {"k1":1, "k2": 3, "k3": 3} + yyjson_mut_val *key, *val; + yyjson_mut_obj_iter iter = yyjson_mut_obj_iter_with(obj); + yyjson_mut_val *v1 = yyjson_mut_obj_iter_get(&iter, "k1"); + yyjson_mut_val *v3 = yyjson_mut_obj_iter_get(&iter, "k3"); + @endcode + @see `yyjson_mut_obj_iter_get()` and `yyjson_mut_obj_iter_getn()` + */ +typedef struct yyjson_mut_obj_iter { + size_t idx; /**< next key's index */ + size_t max; /**< maximum key index (obj.size) */ + yyjson_mut_val *cur; /**< current key */ + yyjson_mut_val *pre; /**< previous key */ + yyjson_mut_val *obj; /**< the object being iterated */ +} yyjson_mut_obj_iter; + +/** + Initialize an iterator for this object. + + @param obj The object to be iterated over. + If this parameter is NULL or not an array, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_mut_obj_iter_init(yyjson_mut_val *obj, + yyjson_mut_obj_iter *iter); + +/** + Create an iterator with an object, same as `yyjson_obj_iter_init()`. + + @param obj The object to be iterated over. + If this parameter is NULL or not an object, an empty iterator will returned. + @return A new iterator for the object. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_mut_obj_iter yyjson_mut_obj_iter_with( + yyjson_mut_val *obj); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_mut_obj_iter_has_next( + yyjson_mut_obj_iter *iter); + +/** + Returns the next key in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_next( + yyjson_mut_obj_iter *iter); + +/** + Returns the value for key inside the iteration. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get_val( + yyjson_mut_val *key); + +/** + Removes current key-value pair in the iteration, returns the removed value. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( + yyjson_mut_obj_iter *iter); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_mut_obj_get()`, but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string with null-terminator. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get( + yyjson_mut_obj_iter *iter, const char *key); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_mut_obj_getn()` but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string, null-terminator is not required. + @param key_len The the length of `key`, in bytes. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_getn( + yyjson_mut_obj_iter *iter, const char *key, size_t key_len); + +/** + Macro for iterating over an object. + It works like iterator, but with a more intuitive API. + + @warning You should not modify the object while iterating over it. + + @b Example + @code + size_t idx, max; + yyjson_mut_val *key, *val; + yyjson_mut_obj_foreach(obj, idx, max, key, val) { + your_func(key, val); + } + @endcode + */ +#define yyjson_mut_obj_foreach(obj, idx, max, key, val) \ + for ((idx) = 0, \ + (max) = yyjson_mut_obj_size(obj), \ + (key) = (max) ? ((yyjson_mut_val *)(obj)->uni.ptr)->next->next : NULL, \ + (val) = (key) ? (key)->next : NULL; \ + (idx) < (max); \ + (idx)++, \ + (key) = (val)->next, \ + (val) = (key)->next) + + + +/*============================================================================== + * MARK: - Mutable JSON Object Creation API + *============================================================================*/ + +/** Creates and returns a mutable object, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj(yyjson_mut_doc *doc); + +/** + Creates and returns a mutable object with keys and values, returns NULL on + error. The keys and values are not copied. The strings should be a + null-terminated UTF-8 string. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. + + @b Example + @code + const char *keys[2] = { "id", "name" }; + const char *vals[2] = { "01", "Harry" }; + yyjson_mut_val *obj = yyjson_mut_obj_with_str(doc, keys, vals, 2); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, + const char **keys, + const char **vals, + size_t count); + +/** + Creates and returns a mutable object with key-value pairs and pair count, + returns NULL on error. The keys and values are not copied. The strings should + be a null-terminated UTF-8 string. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. + + @b Example + @code + const char *kv_pairs[4] = { "id", "01", "name", "Harry" }; + yyjson_mut_val *obj = yyjson_mut_obj_with_kv(doc, kv_pairs, 2); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_kv(yyjson_mut_doc *doc, + const char **kv_pairs, + size_t pair_count); + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification API + *============================================================================*/ + +/** + Adds a key-value pair at the end of the object. + This function allows duplicated key in one object. + @param obj The object to which the new key-value pair is to be added. + @param key The key, should be a string which is created by `yyjson_mut_str()`, + `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. + @param val The value to add to the object. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_add(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val); +/** + Sets a key-value pair at the end of the object. + This function may remove all key-value pairs for the given key before add. + @param obj The object to which the new key-value pair is to be added. + @param key The key, should be a string which is created by `yyjson_mut_str()`, + `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. + @param val The value to add to the object. If this value is null, the behavior + is same as `yyjson_mut_obj_remove()`. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val); + +/** + Inserts a key-value pair to the object at the given position. + This function allows duplicated key in one object. + @param obj The object to which the new key-value pair is to be added. + @param key The key, should be a string which is created by `yyjson_mut_str()`, + `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. + @param val The value to add to the object. + @param idx The index to which to insert the new pair. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_insert(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val, + size_t idx); + +/** + Removes all key-value pair from the object with given key. + @param obj The object from which the key-value pair is to be removed. + @param key The key, should be a string value. + @return The first matched value, or NULL if no matched value. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove(yyjson_mut_val *obj, + yyjson_mut_val *key); + +/** + Removes all key-value pair from the object with given key. + @param obj The object from which the key-value pair is to be removed. + @param key The key, should be a UTF-8 string with null-terminator. + @return The first matched value, or NULL if no matched value. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_key( + yyjson_mut_val *obj, const char *key); + +/** + Removes all key-value pair from the object with given key. + @param obj The object from which the key-value pair is to be removed. + @param key The key, should be a UTF-8 string, null-terminator is not required. + @param key_len The length of the key. + @return The first matched value, or NULL if no matched value. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_keyn( + yyjson_mut_val *obj, const char *key, size_t key_len); + +/** + Removes all key-value pairs in this object. + @param obj The object from which all of the values are to be removed. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_clear(yyjson_mut_val *obj); + +/** + Replaces value from the object with given key. + If the key is not exist, or the value is NULL, it will fail. + @param obj The object to which the value is to be replaced. + @param key The key, should be a string value. + @param val The value to replace into the object. + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_obj_replace(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val); + +/** + Rotates key-value pairs in the object for the given number of times. + For example: `{"a":1,"b":2,"c":3,"d":4}` rotate 1 is + `{"b":2,"c":3,"d":4,"a":1}`. + @param obj The object to be rotated. + @param idx Index (or times) to rotate. + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_obj_rotate(yyjson_mut_val *obj, + size_t idx); + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification Convenience API + *============================================================================*/ + +/** Adds a `null` value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a `true` value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a `false` value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a bool value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, bool val); + +/** Adds an unsigned integer value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, uint64_t val); + +/** Adds a signed integer value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, int64_t val); + +/** Adds an int value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, int64_t val); + +/** Adds a float value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, float val); + +/** Adds a double value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, double val); + +/** Adds a real value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, double val); + +/** Adds a string value at the end of the object. + The `key` and `val` should be null-terminated UTF-8 strings. + This function allows duplicated key in one object. + + @warning The key/value strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, const char *val); + +/** Adds a string value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + The `val` should be a UTF-8 string, null-terminator is not required. + The `len` should be the length of the `val`, in bytes. + This function allows duplicated key in one object. + + @warning The key/value strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *val, size_t len); + +/** Adds a string value at the end of the object. + The `key` and `val` should be null-terminated UTF-8 strings. + The value string is copied. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *val); + +/** Adds a string value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + The `val` should be a UTF-8 string, null-terminator is not required. + The `len` should be the length of the `val`, in bytes. + This function allows duplicated key in one object. + + @warning The key strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *val, size_t len); + +/** + Creates and adds a new array to the target object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep these strings + unmodified for the lifetime of this JSON document. + @return The new array, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** + Creates and adds a new object to the target object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep these strings + unmodified for the lifetime of this JSON document. + @return The new object, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a JSON value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_val(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + yyjson_mut_val *val); + +/** Removes all key-value pairs for the given key. + Returns the first value to which the specified key is mapped or NULL if this + object contains no mapping for the key. + The `key` should be a null-terminated UTF-8 string. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_str( + yyjson_mut_val *obj, const char *key); + +/** Removes all key-value pairs for the given key. + Returns the first value to which the specified key is mapped or NULL if this + object contains no mapping for the key. + The `key` should be a UTF-8 string, null-terminator is not required. + The `len` should be the length of the key, in bytes. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_strn( + yyjson_mut_val *obj, const char *key, size_t len); + +/** Replaces all matching keys with the new key. + Returns true if at least one key was renamed. + The `key` and `new_key` should be a null-terminated UTF-8 string. + The `new_key` is copied and held by doc. + + @warning This function takes a linear search time. + If `new_key` already exists, it will cause duplicate keys. + */ +yyjson_api_inline bool yyjson_mut_obj_rename_key(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *new_key); + +/** Replaces all matching keys with the new key. + Returns true if at least one key was renamed. + The `key` and `new_key` should be a UTF-8 string, + null-terminator is not required. The `new_key` is copied and held by doc. + + @warning This function takes a linear search time. + If `new_key` already exists, it will cause duplicate keys. + */ +yyjson_api_inline bool yyjson_mut_obj_rename_keyn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + size_t len, + const char *new_key, + size_t new_len); + + + +#if !defined(YYJSON_DISABLE_UTILS) || !YYJSON_DISABLE_UTILS + +/*============================================================================== + * MARK: - JSON Pointer API (RFC 6901) + * https://tools.ietf.org/html/rfc6901 + *============================================================================*/ + +/** JSON Pointer error code. */ +typedef uint32_t yyjson_ptr_code; + +/** No JSON pointer error. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_NONE = 0; + +/** Invalid input parameter, such as NULL input. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_PARAMETER = 1; + +/** JSON pointer syntax error, such as invalid escape, token no prefix. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_SYNTAX = 2; + +/** JSON pointer resolve failed, such as index out of range, key not found. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_RESOLVE = 3; + +/** Document's root is NULL, but it is required for the function call. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_NULL_ROOT = 4; + +/** Cannot set root as the target is not a document. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_SET_ROOT = 5; + +/** The memory allocation failed and a new value could not be created. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_MEMORY_ALLOCATION = 6; + +/** Error information for JSON pointer. */ +typedef struct yyjson_ptr_err { + /** Error code, see `yyjson_ptr_code` for all possible values. */ + yyjson_ptr_code code; + /** Error message, constant, no need to free (NULL if no error). */ + const char *msg; + /** Error byte position for input JSON pointer (0 if no error). */ + size_t pos; +} yyjson_ptr_err; + +/** + A context for JSON pointer operation. + + This struct stores the context of JSON Pointer operation result. The struct + can be used with three helper functions: `ctx_append()`, `ctx_replace()`, and + `ctx_remove()`, which perform the corresponding operations on the container + without re-parsing the JSON Pointer. + + For example: + @code + // doc before: {"a":[0,1,null]} + // ptr: "/a/2" + val = yyjson_mut_doc_ptr_getx(doc, ptr, strlen(ptr), &ctx, &err); + if (yyjson_is_null(val)) { + yyjson_ptr_ctx_remove(&ctx); + } + // doc after: {"a":[0,1]} + @endcode + */ +typedef struct yyjson_ptr_ctx { + /** + The container (parent) of the target value. It can be either an array or + an object. If the target location has no value, but all its parent + containers exist, and the target location can be used to insert a new + value, then `ctn` is the parent container of the target location. + Otherwise, `ctn` is NULL. + */ + yyjson_mut_val *ctn; + /** + The previous sibling of the target value. It can be either a value in an + array or a key in an object. As the container is a `circular linked list` + of elements, `pre` is the previous node of the target value. If the + operation is `add` or `set`, then `pre` is the previous node of the new + value, not the original target value. If the target value does not exist, + `pre` is NULL. + */ + yyjson_mut_val *pre; + /** + The removed value if the operation is `set`, `replace` or `remove`. It can + be used to restore the original state of the document if needed. + */ + yyjson_mut_val *old; +} yyjson_ptr_ctx; + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(yyjson_doc *doc, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(yyjson_doc *doc, + const char *ptr, size_t len); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, + const char *ptr, size_t len, + yyjson_ptr_err *err); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_ptr_get(yyjson_val *val, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_ptr_getn(yyjson_val *val, + const char *ptr, size_t len); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t len, + yyjson_ptr_err *err); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get(yyjson_mut_doc *doc, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn(yyjson_mut_doc *doc, + const char *ptr, + size_t len); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(yyjson_mut_val *val, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(yyjson_mut_val *val, + const char *ptr, + size_t len); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Add (insert) value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_add(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val); + +/** + Add (insert) value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_addn(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val); + +/** + Add (insert) value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be added. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is added, false otherwise. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_addx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Add (insert) value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param doc Only used to create new values when needed. + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_ptr_add(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Add (insert) value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param doc Only used to create new values when needed. + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_ptr_addn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Add (insert) value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param doc Only used to create new values when needed. + @param new_val The value to be added. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is added, false otherwise. + */ +yyjson_api_inline bool yyjson_mut_ptr_addx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Set value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The value to be set, pass NULL to remove. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_set(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val); + +/** + Set value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_setn(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val); + +/** + Set value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_setx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Set value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The value to be set, pass NULL to remove. + @param doc Only used to create new values when needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_ptr_set(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Set value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @param doc Only used to create new values when needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_ptr_setn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Set value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @param doc Only used to create new values when needed. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_ptr_setx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Replace value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replace( + yyjson_mut_doc *doc, const char *ptr, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacen( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacex( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/** + Replace value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replace( + yyjson_mut_val *val, const char *ptr, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacen( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/** + Remove value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_remove( + yyjson_mut_doc *doc, const char *ptr); + +/** + Remove value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removen( + yyjson_mut_doc *doc, const char *ptr, size_t len); + +/** + Remove value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removex( + yyjson_mut_doc *doc, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/** + Remove value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_remove(yyjson_mut_val *val, + const char *ptr); + +/** + Remove value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removen(yyjson_mut_val *val, + const char *ptr, + size_t len); + +/** + Remove value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removex(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Append value by JSON pointer context. + @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. + @param key New key if `ctx->ctn` is object, or NULL if `ctx->ctn` is array. + @param val New value to be added. + @return true on success or false on fail. + */ +yyjson_api_inline bool yyjson_ptr_ctx_append(yyjson_ptr_ctx *ctx, + yyjson_mut_val *key, + yyjson_mut_val *val); + +/** + Replace value by JSON pointer context. + @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. + @param val New value to be replaced. + @return true on success or false on fail. + @note If success, the old value will be returned via `ctx->old`. + */ +yyjson_api_inline bool yyjson_ptr_ctx_replace(yyjson_ptr_ctx *ctx, + yyjson_mut_val *val); + +/** + Remove value by JSON pointer context. + @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. + @return true on success or false on fail. + @note If success, the old value will be returned via `ctx->old`. + */ +yyjson_api_inline bool yyjson_ptr_ctx_remove(yyjson_ptr_ctx *ctx); + + + +/*============================================================================== + * MARK: - JSON Patch API (RFC 6902) + * https://tools.ietf.org/html/rfc6902 + *============================================================================*/ + +/** Result code for JSON patch. */ +typedef uint32_t yyjson_patch_code; + +/** Success, no error. */ +static const yyjson_patch_code YYJSON_PATCH_SUCCESS = 0; + +/** Invalid parameter, such as NULL input or non-array patch. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_PARAMETER = 1; + +/** Memory allocation failure occurs. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_MEMORY_ALLOCATION = 2; + +/** JSON patch operation is not object type. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_OPERATION = 3; + +/** JSON patch operation is missing a required key. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_MISSING_KEY = 4; + +/** JSON patch operation member is invalid. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_MEMBER = 5; + +/** JSON patch operation `test` not equal. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_EQUAL = 6; + +/** JSON patch operation failed on JSON pointer. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_POINTER = 7; + +/** Error information for JSON patch. */ +typedef struct yyjson_patch_err { + /** Error code, see `yyjson_patch_code` for all possible values. */ + yyjson_patch_code code; + /** Index of the error operation (0 if no error). */ + size_t idx; + /** Error message, constant, no need to free (NULL if no error). */ + const char *msg; + /** JSON pointer error if `code == YYJSON_PATCH_ERROR_POINTER`. */ + yyjson_ptr_err ptr; +} yyjson_patch_err; + +/** + Creates and returns a patched JSON value (RFC 6902). + The memory of the returned value is allocated by the `doc`. + The `err` is used to receive error information, pass NULL if not needed. + Returns NULL if the patch could not be applied. + */ +yyjson_api yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch, + yyjson_patch_err *err); + +/** + Creates and returns a patched JSON value (RFC 6902). + The memory of the returned value is allocated by the `doc`. + The `err` is used to receive error information, pass NULL if not needed. + Returns NULL if the patch could not be applied. + */ +yyjson_api yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch, + yyjson_patch_err *err); + + + +/*============================================================================== + * MARK: - JSON Merge-Patch API (RFC 7386) + * https://tools.ietf.org/html/rfc7386 + *============================================================================*/ + +/** + Creates and returns a merge-patched JSON value (RFC 7386). + The memory of the returned value is allocated by the `doc`. + Returns NULL if the patch could not be applied. + + @warning This function is recursive and may cause a stack overflow if the + object level is too deep. + */ +yyjson_api yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch); + +/** + Creates and returns a merge-patched JSON value (RFC 7386). + The memory of the returned value is allocated by the `doc`. + Returns NULL if the patch could not be applied. + + @warning This function is recursive and may cause a stack overflow if the + object level is too deep. + */ +yyjson_api yyjson_mut_val *yyjson_mut_merge_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch); + +#endif /* YYJSON_DISABLE_UTILS */ + + + +/*============================================================================== + * MARK: - JSON Structure (Implementation) + *============================================================================*/ + +/** Payload of a JSON value (8 bytes). */ +typedef union yyjson_val_uni { + uint64_t u64; + int64_t i64; + double f64; + const char *str; + void *ptr; + size_t ofs; +} yyjson_val_uni; + +/** + Immutable JSON value, 16 bytes. + */ +struct yyjson_val { + uint64_t tag; /**< type, subtype and length */ + yyjson_val_uni uni; /**< payload */ +}; + +struct yyjson_doc { + /** Root value of the document (nonnull). */ + yyjson_val *root; + /** Allocator used by document (nonnull). */ + yyjson_alc alc; + /** The total number of bytes read when parsing JSON (nonzero). */ + size_t dat_read; + /** The total number of value read when parsing JSON (nonzero). */ + size_t val_read; + /** The string pool used by JSON values (nullable). */ + char *str_pool; +}; + + + +/*============================================================================== + * MARK: - Unsafe JSON Value API (Implementation) + *============================================================================*/ + +/* + Whether the string does not need to be escaped for serialization. + This function is used to optimize the writing speed of small constant strings. + This function works only if the compiler can evaluate it at compile time. + + Clang supports it since v8.0, + earlier versions do not support constant_p(strlen) and return false. + GCC supports it since at least v4.4, + earlier versions may compile it as run-time instructions. + ICC supports it since at least v16, + earlier versions are uncertain. + + @param str The C string. + @param len The returnd value from strlen(str). + */ +yyjson_api_inline bool unsafe_yyjson_is_str_noesc(const char *str, size_t len) { +#if YYJSON_HAS_CONSTANT_P && \ + (!YYJSON_IS_REAL_GCC || yyjson_gcc_available(4, 4, 0)) + if (yyjson_constant_p(len) && len <= 32) { + /* + Same as the following loop: + + for (size_t i = 0; i < len; i++) { + char c = str[i]; + if (c < ' ' || c > '~' || c == '"' || c == '\\') return false; + } + + GCC evaluates it at compile time only if the string length is within 17 + and -O3 (which turns on the -fpeel-loops flag) is used. + So the loop is unrolled for GCC. + */ +# define yyjson_repeat32_incr(x) \ + x(0) x(1) x(2) x(3) x(4) x(5) x(6) x(7) \ + x(8) x(9) x(10) x(11) x(12) x(13) x(14) x(15) \ + x(16) x(17) x(18) x(19) x(20) x(21) x(22) x(23) \ + x(24) x(25) x(26) x(27) x(28) x(29) x(30) x(31) +# define yyjson_check_char_noesc(i) \ + if (i < len) { \ + char c = str[i]; \ + if (c < ' ' || c > '~' || c == '"' || c == '\\') return false; } + yyjson_repeat32_incr(yyjson_check_char_noesc) +# undef yyjson_repeat32_incr +# undef yyjson_check_char_noesc + return true; + } +#else + (void)str; + (void)len; +#endif + return false; +} + +yyjson_api_inline double unsafe_yyjson_u64_to_f64(uint64_t num) { +#if YYJSON_U64_TO_F64_NO_IMPL + uint64_t msb = ((uint64_t)1) << 63; + if ((num & msb) == 0) { + return (double)(int64_t)num; + } else { + return ((double)(int64_t)((num >> 1) | (num & 1))) * (double)2.0; + } +#else + return (double)num; +#endif +} + +yyjson_api_inline yyjson_type unsafe_yyjson_get_type(void *val) { + uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; + return (yyjson_type)(tag & YYJSON_TYPE_MASK); +} + +yyjson_api_inline yyjson_subtype unsafe_yyjson_get_subtype(void *val) { + uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; + return (yyjson_subtype)(tag & YYJSON_SUBTYPE_MASK); +} + +yyjson_api_inline uint8_t unsafe_yyjson_get_tag(void *val) { + uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; + return (uint8_t)(tag & YYJSON_TAG_MASK); +} + +yyjson_api_inline bool unsafe_yyjson_is_raw(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_RAW; +} + +yyjson_api_inline bool unsafe_yyjson_is_null(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_NULL; +} + +yyjson_api_inline bool unsafe_yyjson_is_bool(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_BOOL; +} + +yyjson_api_inline bool unsafe_yyjson_is_num(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_NUM; +} + +yyjson_api_inline bool unsafe_yyjson_is_str(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_STR; +} + +yyjson_api_inline bool unsafe_yyjson_is_arr(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_ARR; +} + +yyjson_api_inline bool unsafe_yyjson_is_obj(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_OBJ; +} + +yyjson_api_inline bool unsafe_yyjson_is_ctn(void *val) { + uint8_t mask = YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ; + return (unsafe_yyjson_get_tag(val) & mask) == mask; +} + +yyjson_api_inline bool unsafe_yyjson_is_uint(void *val) { + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_sint(void *val) { + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_int(void *val) { + const uint8_t mask = YYJSON_TAG_MASK & (~YYJSON_SUBTYPE_SINT); + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT; + return (unsafe_yyjson_get_tag(val) & mask) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_real(void *val) { + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_true(void *val) { + const uint8_t patt = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_TRUE; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_false(void *val) { + const uint8_t patt = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_FALSE; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_arr_is_flat(yyjson_val *val) { + size_t ofs = val->uni.ofs; + size_t len = (size_t)(val->tag >> YYJSON_TAG_BIT); + return len * sizeof(yyjson_val) + sizeof(yyjson_val) == ofs; +} + +yyjson_api_inline const char *unsafe_yyjson_get_raw(void *val) { + return ((yyjson_val *)val)->uni.str; +} + +yyjson_api_inline bool unsafe_yyjson_get_bool(void *val) { + uint8_t tag = unsafe_yyjson_get_tag(val); + return (bool)((tag & YYJSON_SUBTYPE_MASK) >> YYJSON_TYPE_BIT); +} + +yyjson_api_inline uint64_t unsafe_yyjson_get_uint(void *val) { + return ((yyjson_val *)val)->uni.u64; +} + +yyjson_api_inline int64_t unsafe_yyjson_get_sint(void *val) { + return ((yyjson_val *)val)->uni.i64; +} + +yyjson_api_inline int unsafe_yyjson_get_int(void *val) { + return (int)((yyjson_val *)val)->uni.i64; +} + +yyjson_api_inline double unsafe_yyjson_get_real(void *val) { + return ((yyjson_val *)val)->uni.f64; +} + +yyjson_api_inline double unsafe_yyjson_get_num(void *val) { + uint8_t tag = unsafe_yyjson_get_tag(val); + if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL)) { + return ((yyjson_val *)val)->uni.f64; + } else if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT)) { + return (double)((yyjson_val *)val)->uni.i64; + } else if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT)) { + return unsafe_yyjson_u64_to_f64(((yyjson_val *)val)->uni.u64); + } + return 0.0; +} + +yyjson_api_inline const char *unsafe_yyjson_get_str(void *val) { + return ((yyjson_val *)val)->uni.str; +} + +yyjson_api_inline size_t unsafe_yyjson_get_len(void *val) { + return (size_t)(((yyjson_val *)val)->tag >> YYJSON_TAG_BIT); +} + +yyjson_api_inline yyjson_val *unsafe_yyjson_get_first(yyjson_val *ctn) { + return ctn + 1; +} + +yyjson_api_inline yyjson_val *unsafe_yyjson_get_next(yyjson_val *val) { + bool is_ctn = unsafe_yyjson_is_ctn(val); + size_t ctn_ofs = val->uni.ofs; + size_t ofs = (is_ctn ? ctn_ofs : sizeof(yyjson_val)); + return (yyjson_val *)(void *)((uint8_t *)val + ofs); +} + +yyjson_api_inline bool unsafe_yyjson_equals_strn(void *val, const char *str, + size_t len) { + return unsafe_yyjson_get_len(val) == len && + memcmp(((yyjson_val *)val)->uni.str, str, len) == 0; +} + +yyjson_api_inline bool unsafe_yyjson_equals_str(void *val, const char *str) { + return unsafe_yyjson_equals_strn(val, str, strlen(str)); +} + +yyjson_api_inline void unsafe_yyjson_set_type(void *val, yyjson_type type, + yyjson_subtype subtype) { + uint8_t tag = (type | subtype); + uint64_t new_tag = ((yyjson_val *)val)->tag; + new_tag = (new_tag & (~(uint64_t)YYJSON_TAG_MASK)) | (uint64_t)tag; + ((yyjson_val *)val)->tag = new_tag; +} + +yyjson_api_inline void unsafe_yyjson_set_len(void *val, size_t len) { + uint64_t tag = ((yyjson_val *)val)->tag & YYJSON_TAG_MASK; + tag |= (uint64_t)len << YYJSON_TAG_BIT; + ((yyjson_val *)val)->tag = tag; +} + +yyjson_api_inline void unsafe_yyjson_set_tag(void *val, yyjson_type type, + yyjson_subtype subtype, + size_t len) { + uint64_t tag = (uint64_t)len << YYJSON_TAG_BIT; + tag |= (type | subtype); + ((yyjson_val *)val)->tag = tag; +} + +yyjson_api_inline void unsafe_yyjson_inc_len(void *val) { + uint64_t tag = ((yyjson_val *)val)->tag; + tag += (uint64_t)(1 << YYJSON_TAG_BIT); + ((yyjson_val *)val)->tag = tag; +} + +yyjson_api_inline void unsafe_yyjson_set_raw(void *val, const char *raw, + size_t len) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_RAW, YYJSON_SUBTYPE_NONE, len); + ((yyjson_val *)val)->uni.str = raw; +} + +yyjson_api_inline void unsafe_yyjson_set_null(void *val) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NULL, YYJSON_SUBTYPE_NONE, 0); +} + +yyjson_api_inline void unsafe_yyjson_set_bool(void *val, bool num) { + yyjson_subtype subtype = num ? YYJSON_SUBTYPE_TRUE : YYJSON_SUBTYPE_FALSE; + unsafe_yyjson_set_tag(val, YYJSON_TYPE_BOOL, subtype, 0); +} + +yyjson_api_inline void unsafe_yyjson_set_uint(void *val, uint64_t num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_UINT, 0); + ((yyjson_val *)val)->uni.u64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_sint(void *val, int64_t num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_SINT, 0); + ((yyjson_val *)val)->uni.i64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_fp_to_fixed(void *val, int prec) { + ((yyjson_val *)val)->tag &= ~((uint64_t)YYJSON_WRITE_FP_TO_FIXED(15) << 32); + ((yyjson_val *)val)->tag |= (uint64_t)YYJSON_WRITE_FP_TO_FIXED(prec) << 32; +} + +yyjson_api_inline void unsafe_yyjson_set_fp_to_float(void *val, bool flt) { + uint64_t flag = (uint64_t)YYJSON_WRITE_FP_TO_FLOAT << 32; + if (flt) ((yyjson_val *)val)->tag |= flag; + else ((yyjson_val *)val)->tag &= ~flag; +} + +yyjson_api_inline void unsafe_yyjson_set_float(void *val, float num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_REAL, 0); + ((yyjson_val *)val)->tag |= (uint64_t)YYJSON_WRITE_FP_TO_FLOAT << 32; + ((yyjson_val *)val)->uni.f64 = (double)num; +} + +yyjson_api_inline void unsafe_yyjson_set_double(void *val, double num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_REAL, 0); + ((yyjson_val *)val)->uni.f64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_real(void *val, double num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_REAL, 0); + ((yyjson_val *)val)->uni.f64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_str_noesc(void *val, bool noesc) { + ((yyjson_val *)val)->tag &= ~(uint64_t)YYJSON_SUBTYPE_MASK; + if (noesc) ((yyjson_val *)val)->tag |= (uint64_t)YYJSON_SUBTYPE_NOESC; +} + +yyjson_api_inline void unsafe_yyjson_set_strn(void *val, const char *str, + size_t len) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_STR, YYJSON_SUBTYPE_NONE, len); + ((yyjson_val *)val)->uni.str = str; +} + +yyjson_api_inline void unsafe_yyjson_set_str(void *val, const char *str) { + size_t len = strlen(str); + bool noesc = unsafe_yyjson_is_str_noesc(str, len); + yyjson_subtype subtype = noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; + unsafe_yyjson_set_tag(val, YYJSON_TYPE_STR, subtype, len); + ((yyjson_val *)val)->uni.str = str; +} + +yyjson_api_inline void unsafe_yyjson_set_arr(void *val, size_t size) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_ARR, YYJSON_SUBTYPE_NONE, size); +} + +yyjson_api_inline void unsafe_yyjson_set_obj(void *val, size_t size) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_OBJ, YYJSON_SUBTYPE_NONE, size); +} + + + +/*============================================================================== + * MARK: - JSON Document API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_val *yyjson_doc_get_root(yyjson_doc *doc) { + return doc ? doc->root : NULL; +} + +yyjson_api_inline size_t yyjson_doc_get_read_size(yyjson_doc *doc) { + return doc ? doc->dat_read : 0; +} + +yyjson_api_inline size_t yyjson_doc_get_val_count(yyjson_doc *doc) { + return doc ? doc->val_read : 0; +} + +yyjson_api_inline void yyjson_doc_free(yyjson_doc *doc) { + if (doc) { + yyjson_alc alc = doc->alc; + memset(&doc->alc, 0, sizeof(alc)); + if (doc->str_pool) alc.free(alc.ctx, doc->str_pool); + alc.free(alc.ctx, doc); + } +} + + + +/*============================================================================== + * MARK: - JSON Value Type API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_is_raw(yyjson_val *val) { + return val ? unsafe_yyjson_is_raw(val) : false; +} + +yyjson_api_inline bool yyjson_is_null(yyjson_val *val) { + return val ? unsafe_yyjson_is_null(val) : false; +} + +yyjson_api_inline bool yyjson_is_true(yyjson_val *val) { + return val ? unsafe_yyjson_is_true(val) : false; +} + +yyjson_api_inline bool yyjson_is_false(yyjson_val *val) { + return val ? unsafe_yyjson_is_false(val) : false; +} + +yyjson_api_inline bool yyjson_is_bool(yyjson_val *val) { + return val ? unsafe_yyjson_is_bool(val) : false; +} + +yyjson_api_inline bool yyjson_is_uint(yyjson_val *val) { + return val ? unsafe_yyjson_is_uint(val) : false; +} + +yyjson_api_inline bool yyjson_is_sint(yyjson_val *val) { + return val ? unsafe_yyjson_is_sint(val) : false; +} + +yyjson_api_inline bool yyjson_is_int(yyjson_val *val) { + return val ? unsafe_yyjson_is_int(val) : false; +} + +yyjson_api_inline bool yyjson_is_real(yyjson_val *val) { + return val ? unsafe_yyjson_is_real(val) : false; +} + +yyjson_api_inline bool yyjson_is_num(yyjson_val *val) { + return val ? unsafe_yyjson_is_num(val) : false; +} + +yyjson_api_inline bool yyjson_is_str(yyjson_val *val) { + return val ? unsafe_yyjson_is_str(val) : false; +} + +yyjson_api_inline bool yyjson_is_arr(yyjson_val *val) { + return val ? unsafe_yyjson_is_arr(val) : false; +} + +yyjson_api_inline bool yyjson_is_obj(yyjson_val *val) { + return val ? unsafe_yyjson_is_obj(val) : false; +} + +yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val) { + return val ? unsafe_yyjson_is_ctn(val) : false; +} + + + +/*============================================================================== + * MARK: - JSON Value Content API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_type yyjson_get_type(yyjson_val *val) { + return val ? unsafe_yyjson_get_type(val) : YYJSON_TYPE_NONE; +} + +yyjson_api_inline yyjson_subtype yyjson_get_subtype(yyjson_val *val) { + return val ? unsafe_yyjson_get_subtype(val) : YYJSON_SUBTYPE_NONE; +} + +yyjson_api_inline uint8_t yyjson_get_tag(yyjson_val *val) { + return val ? unsafe_yyjson_get_tag(val) : 0; +} + +yyjson_api_inline const char *yyjson_get_type_desc(yyjson_val *val) { + switch (yyjson_get_tag(val)) { + case YYJSON_TYPE_RAW | YYJSON_SUBTYPE_NONE: return "raw"; + case YYJSON_TYPE_NULL | YYJSON_SUBTYPE_NONE: return "null"; + case YYJSON_TYPE_STR | YYJSON_SUBTYPE_NONE: return "string"; + case YYJSON_TYPE_STR | YYJSON_SUBTYPE_NOESC: return "string"; + case YYJSON_TYPE_ARR | YYJSON_SUBTYPE_NONE: return "array"; + case YYJSON_TYPE_OBJ | YYJSON_SUBTYPE_NONE: return "object"; + case YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_TRUE: return "true"; + case YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_FALSE: return "false"; + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT: return "uint"; + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT: return "sint"; + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL: return "real"; + default: return "unknown"; + } +} + +yyjson_api_inline const char *yyjson_get_raw(yyjson_val *val) { + return yyjson_is_raw(val) ? unsafe_yyjson_get_raw(val) : NULL; +} + +yyjson_api_inline bool yyjson_get_bool(yyjson_val *val) { + return yyjson_is_bool(val) ? unsafe_yyjson_get_bool(val) : false; +} + +yyjson_api_inline uint64_t yyjson_get_uint(yyjson_val *val) { + return yyjson_is_int(val) ? unsafe_yyjson_get_uint(val) : 0; +} + +yyjson_api_inline int64_t yyjson_get_sint(yyjson_val *val) { + return yyjson_is_int(val) ? unsafe_yyjson_get_sint(val) : 0; +} + +yyjson_api_inline int yyjson_get_int(yyjson_val *val) { + return yyjson_is_int(val) ? unsafe_yyjson_get_int(val) : 0; +} + +yyjson_api_inline double yyjson_get_real(yyjson_val *val) { + return yyjson_is_real(val) ? unsafe_yyjson_get_real(val) : 0.0; +} + +yyjson_api_inline double yyjson_get_num(yyjson_val *val) { + return val ? unsafe_yyjson_get_num(val) : 0.0; +} + +yyjson_api_inline const char *yyjson_get_str(yyjson_val *val) { + return yyjson_is_str(val) ? unsafe_yyjson_get_str(val) : NULL; +} + +yyjson_api_inline size_t yyjson_get_len(yyjson_val *val) { + return val ? unsafe_yyjson_get_len(val) : 0; +} + +yyjson_api_inline bool yyjson_equals_str(yyjson_val *val, const char *str) { + if (yyjson_likely(val && str)) { + return unsafe_yyjson_is_str(val) && + unsafe_yyjson_equals_str(val, str); + } + return false; +} + +yyjson_api_inline bool yyjson_equals_strn(yyjson_val *val, const char *str, + size_t len) { + if (yyjson_likely(val && str)) { + return unsafe_yyjson_is_str(val) && + unsafe_yyjson_equals_strn(val, str, len); + } + return false; +} + +yyjson_api bool unsafe_yyjson_equals(yyjson_val *lhs, yyjson_val *rhs); + +yyjson_api_inline bool yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { + if (yyjson_unlikely(!lhs || !rhs)) return false; + return unsafe_yyjson_equals(lhs, rhs); +} + +yyjson_api_inline bool yyjson_set_raw(yyjson_val *val, + const char *raw, size_t len) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_raw(val, raw, len); + return true; +} + +yyjson_api_inline bool yyjson_set_null(yyjson_val *val) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_null(val); + return true; +} + +yyjson_api_inline bool yyjson_set_bool(yyjson_val *val, bool num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_bool(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_uint(yyjson_val *val, uint64_t num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_uint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_sint(yyjson_val *val, int64_t num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_sint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_sint(val, (int64_t)num); + return true; +} + +yyjson_api_inline bool yyjson_set_float(yyjson_val *val, float num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_float(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_double(yyjson_val *val, double num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_double(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_real(yyjson_val *val, double num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_real(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_fp_to_fixed(yyjson_val *val, int prec) { + if (yyjson_unlikely(!yyjson_is_real(val))) return false; + unsafe_yyjson_set_fp_to_fixed(val, prec); + return true; +} + +yyjson_api_inline bool yyjson_set_fp_to_float(yyjson_val *val, bool flt) { + if (yyjson_unlikely(!yyjson_is_real(val))) return false; + unsafe_yyjson_set_fp_to_float(val, flt); + return true; +} + +yyjson_api_inline bool yyjson_set_str(yyjson_val *val, const char *str) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + if (yyjson_unlikely(!str)) return false; + unsafe_yyjson_set_str(val, str); + return true; +} + +yyjson_api_inline bool yyjson_set_strn(yyjson_val *val, + const char *str, size_t len) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + if (yyjson_unlikely(!str)) return false; + unsafe_yyjson_set_strn(val, str, len); + return true; +} + +yyjson_api_inline bool yyjson_set_str_noesc(yyjson_val *val, bool noesc) { + if (yyjson_unlikely(!yyjson_is_str(val))) return false; + unsafe_yyjson_set_str_noesc(val, noesc); + return true; +} + + + +/*============================================================================== + * MARK: - JSON Array API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_arr_size(yyjson_val *arr) { + return yyjson_is_arr(arr) ? unsafe_yyjson_get_len(arr) : 0; +} + +yyjson_api_inline yyjson_val *yyjson_arr_get(yyjson_val *arr, size_t idx) { + if (yyjson_likely(yyjson_is_arr(arr))) { + if (yyjson_likely(unsafe_yyjson_get_len(arr) > idx)) { + yyjson_val *val = unsafe_yyjson_get_first(arr); + if (unsafe_yyjson_arr_is_flat(arr)) { + return val + idx; + } else { + while (idx-- > 0) val = unsafe_yyjson_get_next(val); + return val; + } + } + } + return NULL; +} + +yyjson_api_inline yyjson_val *yyjson_arr_get_first(yyjson_val *arr) { + if (yyjson_likely(yyjson_is_arr(arr))) { + if (yyjson_likely(unsafe_yyjson_get_len(arr) > 0)) { + return unsafe_yyjson_get_first(arr); + } + } + return NULL; +} + +yyjson_api_inline yyjson_val *yyjson_arr_get_last(yyjson_val *arr) { + if (yyjson_likely(yyjson_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(len > 0)) { + yyjson_val *val = unsafe_yyjson_get_first(arr); + if (unsafe_yyjson_arr_is_flat(arr)) { + return val + (len - 1); + } else { + while (len-- > 1) val = unsafe_yyjson_get_next(val); + return val; + } + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - JSON Array Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_arr_iter_init(yyjson_val *arr, + yyjson_arr_iter *iter) { + if (yyjson_likely(yyjson_is_arr(arr) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(arr); + iter->cur = unsafe_yyjson_get_first(arr); + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_arr_iter)); + return false; +} + +yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(yyjson_val *arr) { + yyjson_arr_iter iter; + yyjson_arr_iter_init(arr, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_arr_iter_has_next(yyjson_arr_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_val *yyjson_arr_iter_next(yyjson_arr_iter *iter) { + yyjson_val *val; + if (iter && iter->idx < iter->max) { + val = iter->cur; + iter->cur = unsafe_yyjson_get_next(val); + iter->idx++; + return val; + } + return NULL; +} + + + +/*============================================================================== + * MARK: - JSON Object API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_obj_size(yyjson_val *obj) { + return yyjson_is_obj(obj) ? unsafe_yyjson_get_len(obj) : 0; +} + +yyjson_api_inline yyjson_val *yyjson_obj_get(yyjson_val *obj, + const char *key) { + return yyjson_obj_getn(obj, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_val *yyjson_obj_getn(yyjson_val *obj, + const char *_key, + size_t key_len) { + if (yyjson_likely(yyjson_is_obj(obj) && _key)) { + size_t len = unsafe_yyjson_get_len(obj); + yyjson_val *key = unsafe_yyjson_get_first(obj); + while (len-- > 0) { + if (unsafe_yyjson_equals_strn(key, _key, key_len)) return key + 1; + key = unsafe_yyjson_get_next(key + 1); + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - JSON Object Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_obj_iter_init(yyjson_val *obj, + yyjson_obj_iter *iter) { + if (yyjson_likely(yyjson_is_obj(obj) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(obj); + iter->cur = unsafe_yyjson_get_first(obj); + iter->obj = obj; + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_obj_iter)); + return false; +} + +yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(yyjson_val *obj) { + yyjson_obj_iter iter; + yyjson_obj_iter_init(obj, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_obj_iter_has_next(yyjson_obj_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_next(yyjson_obj_iter *iter) { + if (iter && iter->idx < iter->max) { + yyjson_val *key = iter->cur; + iter->idx++; + iter->cur = unsafe_yyjson_get_next(key + 1); + return key; + } + return NULL; +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_get_val(yyjson_val *key) { + return key ? key + 1 : NULL; +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_get(yyjson_obj_iter *iter, + const char *key) { + return yyjson_obj_iter_getn(iter, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_getn(yyjson_obj_iter *iter, + const char *key, + size_t key_len) { + if (iter && key) { + size_t idx = iter->idx; + size_t max = iter->max; + yyjson_val *cur = iter->cur; + if (yyjson_unlikely(idx == max)) { + idx = 0; + cur = unsafe_yyjson_get_first(iter->obj); + } + while (idx++ < max) { + yyjson_val *next = unsafe_yyjson_get_next(cur + 1); + if (unsafe_yyjson_equals_strn(cur, key, key_len)) { + iter->idx = idx; + iter->cur = next; + return cur + 1; + } + cur = next; + if (idx == iter->max && iter->idx < iter->max) { + idx = 0; + max = iter->idx; + cur = unsafe_yyjson_get_first(iter->obj); + } + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Structure (Implementation) + *============================================================================*/ + +/** + Mutable JSON value, 24 bytes. + The 'tag' and 'uni' field is same as immutable value. + The 'next' field links all elements inside the container to be a cycle. + */ +struct yyjson_mut_val { + uint64_t tag; /**< type, subtype and length */ + yyjson_val_uni uni; /**< payload */ + yyjson_mut_val *next; /**< the next value in circular linked list */ +}; + +/** + A memory chunk in string memory pool. + */ +typedef struct yyjson_str_chunk { + struct yyjson_str_chunk *next; /* next chunk linked list */ + size_t chunk_size; /* chunk size in bytes */ + /* char str[]; flexible array member */ +} yyjson_str_chunk; + +/** + A memory pool to hold all strings in a mutable document. + */ +typedef struct yyjson_str_pool { + char *cur; /* cursor inside current chunk */ + char *end; /* the end of current chunk */ + size_t chunk_size; /* chunk size in bytes while creating new chunk */ + size_t chunk_size_max; /* maximum chunk size in bytes */ + yyjson_str_chunk *chunks; /* a linked list of chunks, nullable */ +} yyjson_str_pool; + +/** + A memory chunk in value memory pool. + `sizeof(yyjson_val_chunk)` should not larger than `sizeof(yyjson_mut_val)`. + */ +typedef struct yyjson_val_chunk { + struct yyjson_val_chunk *next; /* next chunk linked list */ + size_t chunk_size; /* chunk size in bytes */ + /* char pad[sizeof(yyjson_mut_val) - sizeof(yyjson_val_chunk)]; padding */ + /* yyjson_mut_val vals[]; flexible array member */ +} yyjson_val_chunk; + +/** + A memory pool to hold all values in a mutable document. + */ +typedef struct yyjson_val_pool { + yyjson_mut_val *cur; /* cursor inside current chunk */ + yyjson_mut_val *end; /* the end of current chunk */ + size_t chunk_size; /* chunk size in bytes while creating new chunk */ + size_t chunk_size_max; /* maximum chunk size in bytes */ + yyjson_val_chunk *chunks; /* a linked list of chunks, nullable */ +} yyjson_val_pool; + +struct yyjson_mut_doc { + yyjson_mut_val *root; /**< root value of the JSON document, nullable */ + yyjson_alc alc; /**< a valid allocator, nonnull */ + yyjson_str_pool str_pool; /**< string memory pool */ + yyjson_val_pool val_pool; /**< value memory pool */ +}; + +/* Ensures the capacity to at least equal to the specified byte length. */ +yyjson_api bool unsafe_yyjson_str_pool_grow(yyjson_str_pool *pool, + const yyjson_alc *alc, + size_t len); + +/* Ensures the capacity to at least equal to the specified value count. */ +yyjson_api bool unsafe_yyjson_val_pool_grow(yyjson_val_pool *pool, + const yyjson_alc *alc, + size_t count); + +/* Allocate memory for string. */ +yyjson_api_inline char *unsafe_yyjson_mut_str_alc(yyjson_mut_doc *doc, + size_t len) { + char *mem; + const yyjson_alc *alc = &doc->alc; + yyjson_str_pool *pool = &doc->str_pool; + if (yyjson_unlikely((size_t)(pool->end - pool->cur) <= len)) { + if (yyjson_unlikely(!unsafe_yyjson_str_pool_grow(pool, alc, len + 1))) { + return NULL; + } + } + mem = pool->cur; + pool->cur = mem + len + 1; + return mem; +} + +yyjson_api_inline char *unsafe_yyjson_mut_strncpy(yyjson_mut_doc *doc, + const char *str, size_t len) { + char *mem = unsafe_yyjson_mut_str_alc(doc, len); + if (yyjson_unlikely(!mem)) return NULL; + memcpy((void *)mem, (const void *)str, len); + mem[len] = '\0'; + return mem; +} + +yyjson_api_inline yyjson_mut_val *unsafe_yyjson_mut_val(yyjson_mut_doc *doc, + size_t count) { + yyjson_mut_val *val; + yyjson_alc *alc = &doc->alc; + yyjson_val_pool *pool = &doc->val_pool; + if (yyjson_unlikely((size_t)(pool->end - pool->cur) < count)) { + if (yyjson_unlikely(!unsafe_yyjson_val_pool_grow(pool, alc, count))) { + return NULL; + } + } + val = pool->cur; + pool->cur += count; + return val; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Document API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_root(yyjson_mut_doc *doc) { + return doc ? doc->root : NULL; +} + +yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, + yyjson_mut_val *root) { + if (doc) doc->root = root; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Value Type API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_is_raw(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_raw(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_null(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_null(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_true(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_true(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_false(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_false(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_bool(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_bool(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_uint(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_uint(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_sint(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_sint(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_int(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_int(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_real(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_real(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_num(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_num(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_str(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_str(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_arr(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_arr(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_obj(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_obj(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_ctn(val) : false; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Value Content API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_type yyjson_mut_get_type(yyjson_mut_val *val) { + return yyjson_get_type((yyjson_val *)val); +} + +yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype(yyjson_mut_val *val) { + return yyjson_get_subtype((yyjson_val *)val); +} + +yyjson_api_inline uint8_t yyjson_mut_get_tag(yyjson_mut_val *val) { + return yyjson_get_tag((yyjson_val *)val); +} + +yyjson_api_inline const char *yyjson_mut_get_type_desc(yyjson_mut_val *val) { + return yyjson_get_type_desc((yyjson_val *)val); +} + +yyjson_api_inline const char *yyjson_mut_get_raw(yyjson_mut_val *val) { + return yyjson_get_raw((yyjson_val *)val); +} + +yyjson_api_inline bool yyjson_mut_get_bool(yyjson_mut_val *val) { + return yyjson_get_bool((yyjson_val *)val); +} + +yyjson_api_inline uint64_t yyjson_mut_get_uint(yyjson_mut_val *val) { + return yyjson_get_uint((yyjson_val *)val); +} + +yyjson_api_inline int64_t yyjson_mut_get_sint(yyjson_mut_val *val) { + return yyjson_get_sint((yyjson_val *)val); +} + +yyjson_api_inline int yyjson_mut_get_int(yyjson_mut_val *val) { + return yyjson_get_int((yyjson_val *)val); +} + +yyjson_api_inline double yyjson_mut_get_real(yyjson_mut_val *val) { + return yyjson_get_real((yyjson_val *)val); +} + +yyjson_api_inline double yyjson_mut_get_num(yyjson_mut_val *val) { + return yyjson_get_num((yyjson_val *)val); +} + +yyjson_api_inline const char *yyjson_mut_get_str(yyjson_mut_val *val) { + return yyjson_get_str((yyjson_val *)val); +} + +yyjson_api_inline size_t yyjson_mut_get_len(yyjson_mut_val *val) { + return yyjson_get_len((yyjson_val *)val); +} + +yyjson_api_inline bool yyjson_mut_equals_str(yyjson_mut_val *val, + const char *str) { + return yyjson_equals_str((yyjson_val *)val, str); +} + +yyjson_api_inline bool yyjson_mut_equals_strn(yyjson_mut_val *val, + const char *str, size_t len) { + return yyjson_equals_strn((yyjson_val *)val, str, len); +} + +yyjson_api bool unsafe_yyjson_mut_equals(yyjson_mut_val *lhs, + yyjson_mut_val *rhs); + +yyjson_api_inline bool yyjson_mut_equals(yyjson_mut_val *lhs, + yyjson_mut_val *rhs) { + if (yyjson_unlikely(!lhs || !rhs)) return false; + return unsafe_yyjson_mut_equals(lhs, rhs); +} + +yyjson_api_inline bool yyjson_mut_set_raw(yyjson_mut_val *val, + const char *raw, size_t len) { + if (yyjson_unlikely(!val || !raw)) return false; + unsafe_yyjson_set_raw(val, raw, len); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_null(yyjson_mut_val *val) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_null(val); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_bool(yyjson_mut_val *val, bool num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_bool(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_uint(yyjson_mut_val *val, uint64_t num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_uint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_sint(yyjson_mut_val *val, int64_t num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_sint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_sint(val, (int64_t)num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_float(yyjson_mut_val *val, float num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_float(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_double(yyjson_mut_val *val, double num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_double(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_real(yyjson_mut_val *val, double num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_real(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_fp_to_fixed(yyjson_mut_val *val, + int prec) { + if (yyjson_unlikely(!yyjson_mut_is_real(val))) return false; + unsafe_yyjson_set_fp_to_fixed(val, prec); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_fp_to_float(yyjson_mut_val *val, + bool flt) { + if (yyjson_unlikely(!yyjson_mut_is_real(val))) return false; + unsafe_yyjson_set_fp_to_float(val, flt); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_str(yyjson_mut_val *val, + const char *str) { + if (yyjson_unlikely(!val || !str)) return false; + unsafe_yyjson_set_str(val, str); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_strn(yyjson_mut_val *val, + const char *str, size_t len) { + if (yyjson_unlikely(!val || !str)) return false; + unsafe_yyjson_set_strn(val, str, len); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_str_noesc(yyjson_mut_val *val, + bool noesc) { + if (yyjson_unlikely(!yyjson_mut_is_str(val))) return false; + unsafe_yyjson_set_str_noesc(val, noesc); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_arr(yyjson_mut_val *val) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_arr(val, 0); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_obj(yyjson_mut_val *val) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_obj(val, 0); + return true; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Value Creation API (Implementation) + *============================================================================*/ + +#define yyjson_mut_val_one(func) \ + if (yyjson_likely(doc)) { \ + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); \ + if (yyjson_likely(val)) { \ + func \ + return val; \ + } \ + } \ + return NULL + +#define yyjson_mut_val_one_str(func) \ + if (yyjson_likely(doc && str)) { \ + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); \ + if (yyjson_likely(val)) { \ + func \ + return val; \ + } \ + } \ + return NULL + +yyjson_api_inline yyjson_mut_val *yyjson_mut_raw(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_raw(val, str, strlen(str)); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawn(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_raw(val, str, len); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawcpy(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ + size_t len = strlen(str); + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_raw(val, new_str, len); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawncpy(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_raw(val, new_str, len); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_null(yyjson_mut_doc *doc) { + yyjson_mut_val_one({ unsafe_yyjson_set_null(val); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_true(yyjson_mut_doc *doc) { + yyjson_mut_val_one({ unsafe_yyjson_set_bool(val, true); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_false(yyjson_mut_doc *doc) { + yyjson_mut_val_one({ unsafe_yyjson_set_bool(val, false); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_bool(yyjson_mut_doc *doc, + bool _val) { + yyjson_mut_val_one({ unsafe_yyjson_set_bool(val, _val); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_uint(yyjson_mut_doc *doc, + uint64_t num) { + yyjson_mut_val_one({ unsafe_yyjson_set_uint(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_sint(yyjson_mut_doc *doc, + int64_t num) { + yyjson_mut_val_one({ unsafe_yyjson_set_sint(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_int(yyjson_mut_doc *doc, + int64_t num) { + yyjson_mut_val_one({ unsafe_yyjson_set_sint(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_float(yyjson_mut_doc *doc, + float num) { + yyjson_mut_val_one({ unsafe_yyjson_set_float(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_double(yyjson_mut_doc *doc, + double num) { + yyjson_mut_val_one({ unsafe_yyjson_set_double(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_real(yyjson_mut_doc *doc, + double num) { + yyjson_mut_val_one({ unsafe_yyjson_set_real(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_str(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_str(val, str); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_strn(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_strn(val, str, len); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_strcpy(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ + size_t len = strlen(str); + bool noesc = unsafe_yyjson_is_str_noesc(str, len); + yyjson_subtype sub = noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_tag(val, YYJSON_TYPE_STR, sub, len); + val->uni.str = new_str; + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_strncpy(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_strn(val, new_str, len); + }); +} + +#undef yyjson_mut_val_one +#undef yyjson_mut_val_one_str + + + +/*============================================================================== + * MARK: - Mutable JSON Array API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_mut_arr_size(yyjson_mut_val *arr) { + return yyjson_mut_is_arr(arr) ? unsafe_yyjson_get_len(arr) : 0; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(yyjson_mut_val *arr, + size_t idx) { + if (yyjson_likely(idx < yyjson_mut_arr_size(arr))) { + yyjson_mut_val *val = (yyjson_mut_val *)arr->uni.ptr; + while (idx-- > 0) val = val->next; + return val->next; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_arr_size(arr) > 0)) { + return ((yyjson_mut_val *)arr->uni.ptr)->next; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_last( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_arr_size(arr) > 0)) { + return ((yyjson_mut_val *)arr->uni.ptr); + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Array Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_arr_iter_init(yyjson_mut_val *arr, + yyjson_mut_arr_iter *iter) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(arr); + iter->cur = iter->max ? (yyjson_mut_val *)arr->uni.ptr : NULL; + iter->pre = NULL; + iter->arr = arr; + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_mut_arr_iter)); + return false; +} + +yyjson_api_inline yyjson_mut_arr_iter yyjson_mut_arr_iter_with( + yyjson_mut_val *arr) { + yyjson_mut_arr_iter iter; + yyjson_mut_arr_iter_init(arr, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_mut_arr_iter_has_next(yyjson_mut_arr_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_next( + yyjson_mut_arr_iter *iter) { + if (iter && iter->idx < iter->max) { + yyjson_mut_val *val = iter->cur; + iter->pre = val; + iter->cur = val->next; + iter->idx++; + return iter->cur; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( + yyjson_mut_arr_iter *iter) { + if (yyjson_likely(iter && 0 < iter->idx && iter->idx <= iter->max)) { + yyjson_mut_val *prev = iter->pre; + yyjson_mut_val *cur = iter->cur; + yyjson_mut_val *next = cur->next; + if (yyjson_unlikely(iter->idx == iter->max)) iter->arr->uni.ptr = prev; + iter->idx--; + iter->max--; + unsafe_yyjson_set_len(iter->arr, iter->max); + prev->next = next; + iter->cur = prev; + return cur; + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Array Creation API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr(yyjson_mut_doc *doc) { + if (yyjson_likely(doc)) { + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); + if (yyjson_likely(val)) { + val->tag = YYJSON_TYPE_ARR | YYJSON_SUBTYPE_NONE; + return val; + } + } + return NULL; +} + +#define yyjson_mut_arr_with_func(func) \ + if (yyjson_likely(doc && ((0 < count && count < \ + (~(size_t)0) / sizeof(yyjson_mut_val) && vals) || count == 0))) { \ + yyjson_mut_val *arr = unsafe_yyjson_mut_val(doc, 1 + count); \ + if (yyjson_likely(arr)) { \ + arr->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; \ + if (count > 0) { \ + size_t i; \ + for (i = 0; i < count; i++) { \ + yyjson_mut_val *val = arr + i + 1; \ + func \ + val->next = val + 1; \ + } \ + arr[count].next = arr + 1; \ + arr->uni.ptr = arr + count; \ + } \ + return arr; \ + } \ + } \ + return NULL + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_bool( + yyjson_mut_doc *doc, const bool *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_bool(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint( + yyjson_mut_doc *doc, const int64_t *vals, size_t count) { + return yyjson_mut_arr_with_sint64(doc, vals, count); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count) { + return yyjson_mut_arr_with_uint64(doc, vals, count); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_real( + yyjson_mut_doc *doc, const double *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_real(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint8( + yyjson_mut_doc *doc, const int8_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint16( + yyjson_mut_doc *doc, const int16_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint32( + yyjson_mut_doc *doc, const int32_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint64( + yyjson_mut_doc *doc, const int64_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint8( + yyjson_mut_doc *doc, const uint8_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint16( + yyjson_mut_doc *doc, const uint16_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint32( + yyjson_mut_doc *doc, const uint32_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint64( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_float( + yyjson_mut_doc *doc, const float *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_float(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_double( + yyjson_mut_doc *doc, const double *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_double(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_str( + yyjson_mut_doc *doc, const char **vals, size_t count) { + yyjson_mut_arr_with_func({ + if (yyjson_unlikely(!vals[i])) return NULL; + unsafe_yyjson_set_str(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strn( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count) { + if (yyjson_unlikely(count > 0 && !lens)) return NULL; + yyjson_mut_arr_with_func({ + if (yyjson_unlikely(!vals[i])) return NULL; + unsafe_yyjson_set_strn(val, vals[i], lens[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strcpy( + yyjson_mut_doc *doc, const char **vals, size_t count) { + size_t len; + const char *str, *new_str; + yyjson_mut_arr_with_func({ + str = vals[i]; + if (yyjson_unlikely(!str)) return NULL; + len = strlen(str); + new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_strn(val, new_str, len); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strncpy( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count) { + size_t len; + const char *str, *new_str; + if (yyjson_unlikely(count > 0 && !lens)) return NULL; + yyjson_mut_arr_with_func({ + str = vals[i]; + if (yyjson_unlikely(!str)) return NULL; + len = lens[i]; + new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_strn(val, new_str, len); + }); +} + +#undef yyjson_mut_arr_with_func + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_arr_insert(yyjson_mut_val *arr, + yyjson_mut_val *val, size_t idx) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(idx <= len)) { + unsafe_yyjson_set_len(arr, len + 1); + if (len == 0) { + val->next = val; + arr->uni.ptr = val; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + if (idx == len) { + prev->next = val; + val->next = next; + arr->uni.ptr = val; + } else { + while (idx-- > 0) { + prev = next; + next = next->next; + } + prev->next = val; + val->next = next; + } + } + return true; + } + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + unsafe_yyjson_set_len(arr, len + 1); + if (len == 0) { + val->next = val; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + prev->next = val; + val->next = next; + } + arr->uni.ptr = val; + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_prepend(yyjson_mut_val *arr, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + unsafe_yyjson_set_len(arr, len + 1); + if (len == 0) { + val->next = val; + arr->uni.ptr = val; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + prev->next = val; + val->next = next; + } + return true; + } + return false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_replace(yyjson_mut_val *arr, + size_t idx, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(idx < len)) { + if (yyjson_likely(len > 1)) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + while (idx-- > 0) { + prev = next; + next = next->next; + } + prev->next = val; + val->next = next->next; + if ((void *)next == arr->uni.ptr) arr->uni.ptr = val; + return next; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + val->next = val; + arr->uni.ptr = val; + return prev; + } + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove(yyjson_mut_val *arr, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(idx < len)) { + unsafe_yyjson_set_len(arr, len - 1); + if (yyjson_likely(len > 1)) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + while (idx-- > 0) { + prev = next; + next = next->next; + } + prev->next = next->next; + if ((void *)next == arr->uni.ptr) arr->uni.ptr = prev; + return next; + } else { + return ((yyjson_mut_val *)arr->uni.ptr); + } + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_first( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (len > 1) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + prev->next = next->next; + unsafe_yyjson_set_len(arr, len - 1); + return next; + } else if (len == 1) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + unsafe_yyjson_set_len(arr, 0); + return prev; + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_last( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(len > 1)) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + unsafe_yyjson_set_len(arr, len - 1); + while (--len > 0) prev = prev->next; + prev->next = next; + next = (yyjson_mut_val *)arr->uni.ptr; + arr->uni.ptr = prev; + return next; + } else if (len == 1) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + unsafe_yyjson_set_len(arr, 0); + return prev; + } + } + return NULL; +} + +yyjson_api_inline bool yyjson_mut_arr_remove_range(yyjson_mut_val *arr, + size_t _idx, size_t _len) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + yyjson_mut_val *prev, *next; + bool tail_removed; + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_unlikely(_idx + _len > len)) return false; + if (yyjson_unlikely(_len == 0)) return true; + unsafe_yyjson_set_len(arr, len - _len); + if (yyjson_unlikely(len == _len)) return true; + tail_removed = (_idx + _len == len); + prev = ((yyjson_mut_val *)arr->uni.ptr); + while (_idx-- > 0) prev = prev->next; + next = prev->next; + while (_len-- > 0) next = next->next; + prev->next = next; + if (yyjson_unlikely(tail_removed)) arr->uni.ptr = prev; + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_clear(yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + unsafe_yyjson_set_len(arr, 0); + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_rotate(yyjson_mut_val *arr, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && + unsafe_yyjson_get_len(arr) > idx)) { + yyjson_mut_val *val = (yyjson_mut_val *)arr->uni.ptr; + while (idx-- > 0) val = val->next; + arr->uni.ptr = (void *)val; + return true; + } + return false; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification Convenience API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_arr_add_val(yyjson_mut_val *arr, + yyjson_mut_val *val) { + return yyjson_mut_arr_append(arr, val); +} + +yyjson_api_inline bool yyjson_mut_arr_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_null(doc); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_true(doc); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_false(doc); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + bool _val) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_bool(doc, _val); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + uint64_t num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_uint(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_sint(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_sint(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + float num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_float(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_double(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_real(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_str(doc, str); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, size_t len) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_strn(doc, str, len); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_strcpy(doc, str); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, size_t len) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_strncpy(doc, str, len); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_arr(doc); + return yyjson_mut_arr_append(arr, val) ? val : NULL; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_obj(doc); + return yyjson_mut_arr_append(arr, val) ? val : NULL; + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_mut_obj_size(yyjson_mut_val *obj) { + return yyjson_mut_is_obj(obj) ? unsafe_yyjson_get_len(obj) : 0; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(yyjson_mut_val *obj, + const char *key) { + return yyjson_mut_obj_getn(obj, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(yyjson_mut_val *obj, + const char *_key, + size_t key_len) { + size_t len = yyjson_mut_obj_size(obj); + if (yyjson_likely(len && _key)) { + yyjson_mut_val *key = ((yyjson_mut_val *)obj->uni.ptr)->next->next; + while (len-- > 0) { + if (unsafe_yyjson_equals_strn(key, _key, key_len)) return key->next; + key = key->next->next; + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_obj_iter_init(yyjson_mut_val *obj, + yyjson_mut_obj_iter *iter) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(obj); + iter->cur = iter->max ? (yyjson_mut_val *)obj->uni.ptr : NULL; + iter->pre = NULL; + iter->obj = obj; + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_mut_obj_iter)); + return false; +} + +yyjson_api_inline yyjson_mut_obj_iter yyjson_mut_obj_iter_with( + yyjson_mut_val *obj) { + yyjson_mut_obj_iter iter; + yyjson_mut_obj_iter_init(obj, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_mut_obj_iter_has_next(yyjson_mut_obj_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_next( + yyjson_mut_obj_iter *iter) { + if (iter && iter->idx < iter->max) { + yyjson_mut_val *key = iter->cur; + iter->pre = key; + iter->cur = key->next->next; + iter->idx++; + return iter->cur; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get_val( + yyjson_mut_val *key) { + return key ? key->next : NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( + yyjson_mut_obj_iter *iter) { + if (yyjson_likely(iter && 0 < iter->idx && iter->idx <= iter->max)) { + yyjson_mut_val *prev = iter->pre; + yyjson_mut_val *cur = iter->cur; + yyjson_mut_val *next = cur->next->next; + if (yyjson_unlikely(iter->idx == iter->max)) iter->obj->uni.ptr = prev; + iter->idx--; + iter->max--; + unsafe_yyjson_set_len(iter->obj, iter->max); + prev->next->next = next; + iter->cur = prev; + return cur->next; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get( + yyjson_mut_obj_iter *iter, const char *key) { + return yyjson_mut_obj_iter_getn(iter, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_getn( + yyjson_mut_obj_iter *iter, const char *key, size_t key_len) { + if (iter && key) { + size_t idx = 0; + size_t max = iter->max; + yyjson_mut_val *pre, *cur = iter->cur; + while (idx++ < max) { + pre = cur; + cur = cur->next->next; + if (unsafe_yyjson_equals_strn(cur, key, key_len)) { + iter->idx += idx; + if (iter->idx > max) iter->idx -= max + 1; + iter->pre = pre; + iter->cur = cur; + return cur->next; + } + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Creation API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj(yyjson_mut_doc *doc) { + if (yyjson_likely(doc)) { + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); + if (yyjson_likely(val)) { + val->tag = YYJSON_TYPE_OBJ | YYJSON_SUBTYPE_NONE; + return val; + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, + const char **keys, + const char **vals, + size_t count) { + if (yyjson_likely(doc && ((count > 0 && keys && vals) || (count == 0)))) { + yyjson_mut_val *obj = unsafe_yyjson_mut_val(doc, 1 + count * 2); + if (yyjson_likely(obj)) { + obj->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_OBJ; + if (count > 0) { + size_t i; + for (i = 0; i < count; i++) { + yyjson_mut_val *key = obj + (i * 2 + 1); + yyjson_mut_val *val = obj + (i * 2 + 2); + uint64_t key_len = (uint64_t)strlen(keys[i]); + uint64_t val_len = (uint64_t)strlen(vals[i]); + key->tag = (key_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->tag = (val_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + key->uni.str = keys[i]; + val->uni.str = vals[i]; + key->next = val; + val->next = val + 1; + } + obj[count * 2].next = obj + 1; + obj->uni.ptr = obj + (count * 2 - 1); + } + return obj; + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_kv(yyjson_mut_doc *doc, + const char **pairs, + size_t count) { + if (yyjson_likely(doc && ((count > 0 && pairs) || (count == 0)))) { + yyjson_mut_val *obj = unsafe_yyjson_mut_val(doc, 1 + count * 2); + if (yyjson_likely(obj)) { + obj->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_OBJ; + if (count > 0) { + size_t i; + for (i = 0; i < count; i++) { + yyjson_mut_val *key = obj + (i * 2 + 1); + yyjson_mut_val *val = obj + (i * 2 + 2); + const char *key_str = pairs[i * 2 + 0]; + const char *val_str = pairs[i * 2 + 1]; + uint64_t key_len = (uint64_t)strlen(key_str); + uint64_t val_len = (uint64_t)strlen(val_str); + key->tag = (key_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->tag = (val_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + key->uni.str = key_str; + val->uni.str = val_str; + key->next = val; + val->next = val + 1; + } + obj[count * 2].next = obj + 1; + obj->uni.ptr = obj + (count * 2 - 1); + } + return obj; + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification API (Implementation) + *============================================================================*/ + +yyjson_api_inline void unsafe_yyjson_mut_obj_add(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val, + size_t len) { + if (yyjson_likely(len)) { + yyjson_mut_val *prev_val = ((yyjson_mut_val *)obj->uni.ptr)->next; + yyjson_mut_val *next_key = prev_val->next; + prev_val->next = key; + val->next = next_key; + } else { + val->next = key; + } + key->next = val; + obj->uni.ptr = (void *)key; + unsafe_yyjson_set_len(obj, len + 1); +} + +yyjson_api_inline yyjson_mut_val *unsafe_yyjson_mut_obj_remove( + yyjson_mut_val *obj, const char *key, size_t key_len) { + size_t obj_len = unsafe_yyjson_get_len(obj); + if (obj_len) { + yyjson_mut_val *pre_key = (yyjson_mut_val *)obj->uni.ptr; + yyjson_mut_val *cur_key = pre_key->next->next; + yyjson_mut_val *removed_item = NULL; + size_t i; + for (i = 0; i < obj_len; i++) { + if (unsafe_yyjson_equals_strn(cur_key, key, key_len)) { + if (!removed_item) removed_item = cur_key->next; + cur_key = cur_key->next->next; + pre_key->next->next = cur_key; + if (i + 1 == obj_len) obj->uni.ptr = pre_key; + i--; + obj_len--; + } else { + pre_key = cur_key; + cur_key = cur_key->next->next; + } + } + unsafe_yyjson_set_len(obj, obj_len); + return removed_item; + } else { + return NULL; + } +} + +yyjson_api_inline bool unsafe_yyjson_mut_obj_replace(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + size_t key_len = unsafe_yyjson_get_len(key); + size_t obj_len = unsafe_yyjson_get_len(obj); + if (obj_len) { + yyjson_mut_val *pre_key = (yyjson_mut_val *)obj->uni.ptr; + yyjson_mut_val *cur_key = pre_key->next->next; + size_t i; + for (i = 0; i < obj_len; i++) { + if (unsafe_yyjson_equals_strn(cur_key, key->uni.str, key_len)) { + cur_key->next->tag = val->tag; + cur_key->next->uni.u64 = val->uni.u64; + return true; + } else { + cur_key = cur_key->next->next; + } + } + } + return false; +} + +yyjson_api_inline void unsafe_yyjson_mut_obj_rotate(yyjson_mut_val *obj, + size_t idx) { + yyjson_mut_val *key = (yyjson_mut_val *)obj->uni.ptr; + while (idx-- > 0) key = key->next->next; + obj->uni.ptr = (void *)key; +} + +yyjson_api_inline bool yyjson_mut_obj_add(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + yyjson_mut_is_str(key) && val)) { + unsafe_yyjson_mut_obj_add(obj, key, val, unsafe_yyjson_get_len(obj)); + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + bool replaced = false; + size_t key_len; + yyjson_mut_obj_iter iter; + yyjson_mut_val *cur_key; + if (yyjson_unlikely(!yyjson_mut_is_obj(obj) || + !yyjson_mut_is_str(key))) return false; + key_len = unsafe_yyjson_get_len(key); + yyjson_mut_obj_iter_init(obj, &iter); + while ((cur_key = yyjson_mut_obj_iter_next(&iter)) != 0) { + if (unsafe_yyjson_equals_strn(cur_key, key->uni.str, key_len)) { + if (!replaced && val) { + replaced = true; + val->next = cur_key->next->next; + cur_key->next = val; + } else { + yyjson_mut_obj_iter_remove(&iter); + } + } + } + if (!replaced && val) unsafe_yyjson_mut_obj_add(obj, key, val, iter.max); + return true; +} + +yyjson_api_inline bool yyjson_mut_obj_insert(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + yyjson_mut_is_str(key) && val)) { + size_t len = unsafe_yyjson_get_len(obj); + if (yyjson_likely(len >= idx)) { + if (len > idx) { + void *ptr = obj->uni.ptr; + unsafe_yyjson_mut_obj_rotate(obj, idx); + unsafe_yyjson_mut_obj_add(obj, key, val, len); + obj->uni.ptr = ptr; + } else { + unsafe_yyjson_mut_obj_add(obj, key, val, len); + } + return true; + } + } + return false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove(yyjson_mut_val *obj, + yyjson_mut_val *key) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && yyjson_mut_is_str(key))) { + return unsafe_yyjson_mut_obj_remove(obj, key->uni.str, + unsafe_yyjson_get_len(key)); + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_key( + yyjson_mut_val *obj, const char *key) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && key)) { + size_t key_len = strlen(key); + return unsafe_yyjson_mut_obj_remove(obj, key, key_len); + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_keyn( + yyjson_mut_val *obj, const char *key, size_t key_len) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && key)) { + return unsafe_yyjson_mut_obj_remove(obj, key, key_len); + } + return NULL; +} + +yyjson_api_inline bool yyjson_mut_obj_clear(yyjson_mut_val *obj) { + if (yyjson_likely(yyjson_mut_is_obj(obj))) { + unsafe_yyjson_set_len(obj, 0); + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_obj_replace(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + yyjson_mut_is_str(key) && val)) { + return unsafe_yyjson_mut_obj_replace(obj, key, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_obj_rotate(yyjson_mut_val *obj, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + unsafe_yyjson_get_len(obj) > idx)) { + unsafe_yyjson_mut_obj_rotate(obj, idx); + return true; + } + return false; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification Convenience API (Implementation) + *============================================================================*/ + +#define yyjson_mut_obj_add_func(func) \ + if (yyjson_likely(doc && yyjson_mut_is_obj(obj) && _key)) { \ + yyjson_mut_val *key = unsafe_yyjson_mut_val(doc, 2); \ + if (yyjson_likely(key)) { \ + size_t len = unsafe_yyjson_get_len(obj); \ + yyjson_mut_val *val = key + 1; \ + size_t key_len = strlen(_key); \ + bool noesc = unsafe_yyjson_is_str_noesc(_key, key_len); \ + key->tag = YYJSON_TYPE_STR; \ + key->tag |= noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; \ + key->tag |= (uint64_t)strlen(_key) << YYJSON_TAG_BIT; \ + key->uni.str = _key; \ + func \ + unsafe_yyjson_mut_obj_add(obj, key, val, len); \ + return true; \ + } \ + } \ + return false + +yyjson_api_inline bool yyjson_mut_obj_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_null(val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_bool(val, true); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_bool(val, false); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + bool _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_bool(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + uint64_t _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_uint(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + int64_t _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_sint(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + int64_t _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_sint(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + float _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_float(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + double _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_double(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + double _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_real(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + size_t val_len = strlen(_val); + bool val_noesc = unsafe_yyjson_is_str_noesc(_val, val_len); + val->tag = ((uint64_t)strlen(_val) << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->tag |= val_noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; + val->uni.str = _val; + }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val, + size_t _len) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + val->tag = ((uint64_t)_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->uni.str = _val; + }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + size_t _len = strlen(_val); + val->uni.str = unsafe_yyjson_mut_strncpy(doc, _val, _len); + if (yyjson_unlikely(!val->uni.str)) return false; + val->tag = ((uint64_t)_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val, + size_t _len) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + val->uni.str = unsafe_yyjson_mut_strncpy(doc, _val, _len); + if (yyjson_unlikely(!val->uni.str)) return false; + val->tag = ((uint64_t)_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_val *key = yyjson_mut_str(doc, _key); + yyjson_mut_val *val = yyjson_mut_arr(doc); + return yyjson_mut_obj_add(obj, key, val) ? val : NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_val *key = yyjson_mut_str(doc, _key); + yyjson_mut_val *val = yyjson_mut_obj(doc); + return yyjson_mut_obj_add(obj, key, val) ? val : NULL; +} + +yyjson_api_inline bool yyjson_mut_obj_add_val(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + yyjson_mut_val *_val) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + val = _val; + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_str(yyjson_mut_val *obj, + const char *key) { + return yyjson_mut_obj_remove_strn(obj, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_strn( + yyjson_mut_val *obj, const char *_key, size_t _len) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && _key)) { + yyjson_mut_val *key; + yyjson_mut_obj_iter iter; + yyjson_mut_val *val_removed = NULL; + yyjson_mut_obj_iter_init(obj, &iter); + while ((key = yyjson_mut_obj_iter_next(&iter)) != NULL) { + if (unsafe_yyjson_equals_strn(key, _key, _len)) { + if (!val_removed) val_removed = key->next; + yyjson_mut_obj_iter_remove(&iter); + } + } + return val_removed; + } + return NULL; +} + +yyjson_api_inline bool yyjson_mut_obj_rename_key(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *new_key) { + if (!key || !new_key) return false; + return yyjson_mut_obj_rename_keyn(doc, obj, key, strlen(key), + new_key, strlen(new_key)); +} + +yyjson_api_inline bool yyjson_mut_obj_rename_keyn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + size_t len, + const char *new_key, + size_t new_len) { + char *cpy_key = NULL; + yyjson_mut_val *old_key; + yyjson_mut_obj_iter iter; + if (!doc || !obj || !key || !new_key) return false; + yyjson_mut_obj_iter_init(obj, &iter); + while ((old_key = yyjson_mut_obj_iter_next(&iter))) { + if (unsafe_yyjson_equals_strn((void *)old_key, key, len)) { + if (!cpy_key) { + cpy_key = unsafe_yyjson_mut_strncpy(doc, new_key, new_len); + if (!cpy_key) return false; + } + yyjson_mut_set_strn(old_key, cpy_key, new_len); + } + } + return cpy_key != NULL; +} + + + +#if !defined(YYJSON_DISABLE_UTILS) || !YYJSON_DISABLE_UTILS + +/*============================================================================== + * MARK: - JSON Pointer API (Implementation) + *============================================================================*/ + +#define yyjson_ptr_set_err(_code, _msg) do { \ + if (err) { \ + err->code = YYJSON_PTR_ERR_##_code; \ + err->msg = _msg; \ + err->pos = 0; \ + } \ +} while(false) + +/* require: val != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_val *unsafe_yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t len, + yyjson_ptr_err *err); + +/* require: val != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_getx(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/* require: val/new_val/doc != NULL, *ptr == '/', len > 0 */ +yyjson_api bool unsafe_yyjson_mut_ptr_putx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, bool insert_new, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/* require: val/err != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/* require: val/err != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_removex(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(yyjson_doc *doc, + const char *ptr) { + if (yyjson_unlikely(!ptr)) return NULL; + return yyjson_doc_ptr_getn(doc, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(yyjson_doc *doc, + const char *ptr, size_t len) { + return yyjson_doc_ptr_getx(doc, ptr, len, NULL); +} + +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, + const char *ptr, size_t len, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return doc->root; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_ptr_getx(doc->root, ptr, len, err); +} + +yyjson_api_inline yyjson_val *yyjson_ptr_get(yyjson_val *val, + const char *ptr) { + if (yyjson_unlikely(!ptr)) return NULL; + return yyjson_ptr_getn(val, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_val *yyjson_ptr_getn(yyjson_val *val, + const char *ptr, size_t len) { + return yyjson_ptr_getx(val, ptr, len, NULL); +} + +yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t len, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (yyjson_unlikely(!val || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return val; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_ptr_getx(val, ptr, len, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get(yyjson_mut_doc *doc, + const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_doc_ptr_getn(doc, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn(yyjson_mut_doc *doc, + const char *ptr, + size_t len) { + return yyjson_mut_doc_ptr_getx(doc, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return doc->root; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_getx(doc->root, ptr, len, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(yyjson_mut_val *val, + const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_ptr_getn(val, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(yyjson_mut_val *val, + const char *ptr, + size_t len) { + return yyjson_mut_ptr_getx(val, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return val; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_getx(val, ptr, len, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_add(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_doc_ptr_addn(doc, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_addn(yyjson_mut_doc *doc, + const char *ptr, + size_t len, + yyjson_mut_val *new_val) { + return yyjson_mut_doc_ptr_addx(doc, ptr, len, new_val, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_addx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr || !new_val)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + if (doc->root) { + yyjson_ptr_set_err(SET_ROOT, "cannot set document's root"); + return false; + } else { + doc->root = new_val; + return true; + } + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + if (yyjson_unlikely(!doc->root && !create_parent)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return false; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_mut_val *root = yyjson_mut_obj(doc); + if (yyjson_unlikely(!root)) { + yyjson_ptr_set_err(MEMORY_ALLOCATION, "failed to create value"); + return false; + } + if (unsafe_yyjson_mut_ptr_putx(root, ptr, len, new_val, doc, + create_parent, true, ctx, err)) { + doc->root = root; + return true; + } + return false; + } + return unsafe_yyjson_mut_ptr_putx(doc->root, ptr, len, new_val, doc, + create_parent, true, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_ptr_add(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_ptr_addn(val, ptr, strlen(ptr), new_val, doc); +} + +yyjson_api_inline bool yyjson_mut_ptr_addn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + return yyjson_mut_ptr_addx(val, ptr, len, new_val, doc, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_ptr_addx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr || !new_val || !doc)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return false; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + return unsafe_yyjson_mut_ptr_putx(val, ptr, len, new_val, + doc, create_parent, true, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_set(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_doc_ptr_setn(doc, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_setn(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val) { + return yyjson_mut_doc_ptr_setx(doc, ptr, len, new_val, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_setx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + if (ctx) ctx->old = doc->root; + doc->root = new_val; + return true; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + if (!new_val) { + if (!doc->root) { + yyjson_ptr_set_err(RESOLVE, "JSON pointer cannot be resolved"); + return false; + } + return !!unsafe_yyjson_mut_ptr_removex(doc->root, ptr, len, ctx, err); + } + if (yyjson_unlikely(!doc->root && !create_parent)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return false; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_mut_val *root = yyjson_mut_obj(doc); + if (yyjson_unlikely(!root)) { + yyjson_ptr_set_err(MEMORY_ALLOCATION, "failed to create value"); + return false; + } + if (unsafe_yyjson_mut_ptr_putx(root, ptr, len, new_val, doc, + create_parent, false, ctx, err)) { + doc->root = root; + return true; + } + return false; + } + return unsafe_yyjson_mut_ptr_putx(doc->root, ptr, len, new_val, doc, + create_parent, false, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_ptr_set(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_ptr_setn(val, ptr, strlen(ptr), new_val, doc); +} + +yyjson_api_inline bool yyjson_mut_ptr_setn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + return yyjson_mut_ptr_setx(val, ptr, len, new_val, doc, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_ptr_setx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr || !doc)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return false; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + if (!new_val) { + return !!unsafe_yyjson_mut_ptr_removex(val, ptr, len, ctx, err); + } + return unsafe_yyjson_mut_ptr_putx(val, ptr, len, new_val, doc, + create_parent, false, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replace( + yyjson_mut_doc *doc, const char *ptr, yyjson_mut_val *new_val) { + if (!ptr) return NULL; + return yyjson_mut_doc_ptr_replacen(doc, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacen( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val) { + return yyjson_mut_doc_ptr_replacex(doc, ptr, len, new_val, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacex( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr || !new_val)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_mut_val *root = doc->root; + if (yyjson_unlikely(!root)) { + yyjson_ptr_set_err(RESOLVE, "JSON pointer cannot be resolved"); + return NULL; + } + if (ctx) ctx->old = root; + doc->root = new_val; + return root; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_replacex(doc->root, ptr, len, new_val, + ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replace( + yyjson_mut_val *val, const char *ptr, yyjson_mut_val *new_val) { + if (!ptr) return NULL; + return yyjson_mut_ptr_replacen(val, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacen( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val) { + return yyjson_mut_ptr_replacex(val, ptr, len, new_val, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr || !new_val)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return NULL; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_replacex(val, ptr, len, new_val, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_remove( + yyjson_mut_doc *doc, const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_doc_ptr_removen(doc, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removen( + yyjson_mut_doc *doc, const char *ptr, size_t len) { + return yyjson_mut_doc_ptr_removex(doc, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removex( + yyjson_mut_doc *doc, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_mut_val *root = doc->root; + if (ctx) ctx->old = root; + doc->root = NULL; + return root; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_removex(doc->root, ptr, len, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_remove(yyjson_mut_val *val, + const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_ptr_removen(val, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removen(yyjson_mut_val *val, + const char *ptr, + size_t len) { + return yyjson_mut_ptr_removex(val, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removex(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return NULL; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_removex(val, ptr, len, ctx, err); +} + +yyjson_api_inline bool yyjson_ptr_ctx_append(yyjson_ptr_ctx *ctx, + yyjson_mut_val *key, + yyjson_mut_val *val) { + yyjson_mut_val *ctn, *pre_key, *pre_val, *cur_key, *cur_val; + if (!ctx || !ctx->ctn || !val) return false; + ctn = ctx->ctn; + + if (yyjson_mut_is_obj(ctn)) { + if (!key) return false; + key->next = val; + pre_key = ctx->pre; + if (unsafe_yyjson_get_len(ctn) == 0) { + val->next = key; + ctn->uni.ptr = key; + ctx->pre = key; + } else if (!pre_key) { + pre_key = (yyjson_mut_val *)ctn->uni.ptr; + pre_val = pre_key->next; + val->next = pre_val->next; + pre_val->next = key; + ctn->uni.ptr = key; + ctx->pre = pre_key; + } else { + cur_key = pre_key->next->next; + cur_val = cur_key->next; + val->next = cur_val->next; + cur_val->next = key; + if (ctn->uni.ptr == cur_key) ctn->uni.ptr = key; + ctx->pre = cur_key; + } + } else { + pre_val = ctx->pre; + if (unsafe_yyjson_get_len(ctn) == 0) { + val->next = val; + ctn->uni.ptr = val; + ctx->pre = val; + } else if (!pre_val) { + pre_val = (yyjson_mut_val *)ctn->uni.ptr; + val->next = pre_val->next; + pre_val->next = val; + ctn->uni.ptr = val; + ctx->pre = pre_val; + } else { + cur_val = pre_val->next; + val->next = cur_val->next; + cur_val->next = val; + if (ctn->uni.ptr == cur_val) ctn->uni.ptr = val; + ctx->pre = cur_val; + } + } + unsafe_yyjson_inc_len(ctn); + return true; +} + +yyjson_api_inline bool yyjson_ptr_ctx_replace(yyjson_ptr_ctx *ctx, + yyjson_mut_val *val) { + yyjson_mut_val *ctn, *pre_key, *cur_key, *pre_val, *cur_val; + if (!ctx || !ctx->ctn || !ctx->pre || !val) return false; + ctn = ctx->ctn; + if (yyjson_mut_is_obj(ctn)) { + pre_key = ctx->pre; + pre_val = pre_key->next; + cur_key = pre_val->next; + cur_val = cur_key->next; + /* replace current value */ + cur_key->next = val; + val->next = cur_val->next; + ctx->old = cur_val; + } else { + pre_val = ctx->pre; + cur_val = pre_val->next; + /* replace current value */ + if (pre_val != cur_val) { + val->next = cur_val->next; + pre_val->next = val; + if (ctn->uni.ptr == cur_val) ctn->uni.ptr = val; + } else { + val->next = val; + ctn->uni.ptr = val; + ctx->pre = val; + } + ctx->old = cur_val; + } + return true; +} + +yyjson_api_inline bool yyjson_ptr_ctx_remove(yyjson_ptr_ctx *ctx) { + yyjson_mut_val *ctn, *pre_key, *pre_val, *cur_key, *cur_val; + size_t len; + if (!ctx || !ctx->ctn || !ctx->pre) return false; + ctn = ctx->ctn; + if (yyjson_mut_is_obj(ctn)) { + pre_key = ctx->pre; + pre_val = pre_key->next; + cur_key = pre_val->next; + cur_val = cur_key->next; + /* remove current key-value */ + pre_val->next = cur_val->next; + if (ctn->uni.ptr == cur_key) ctn->uni.ptr = pre_key; + ctx->pre = NULL; + ctx->old = cur_val; + } else { + pre_val = ctx->pre; + cur_val = pre_val->next; + /* remove current key-value */ + pre_val->next = cur_val->next; + if (ctn->uni.ptr == cur_val) ctn->uni.ptr = pre_val; + ctx->pre = NULL; + ctx->old = cur_val; + } + len = unsafe_yyjson_get_len(ctn) - 1; + if (len == 0) ctn->uni.ptr = NULL; + unsafe_yyjson_set_len(ctn, len); + return true; +} + +#undef yyjson_ptr_set_err + + + +/*============================================================================== + * MARK: - JSON Value at Pointer API (Implementation) + *============================================================================*/ + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type bool. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_bool( + yyjson_val *root, const char *ptr, bool *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_bool(val)) { + *value = unsafe_yyjson_get_bool(val); + return true; + } else { + return false; + } +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is an integer + that fits in `uint64_t`. Returns true if successful, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_uint( + yyjson_val *root, const char *ptr, uint64_t *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && val) { + uint64_t ret = val->uni.u64; + if (unsafe_yyjson_is_uint(val) || + (unsafe_yyjson_is_sint(val) && !(ret >> 63))) { + *value = ret; + return true; + } + } + return false; +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is an integer + that fits in `int64_t`. Returns true if successful, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_sint( + yyjson_val *root, const char *ptr, int64_t *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && val) { + int64_t ret = val->uni.i64; + if (unsafe_yyjson_is_sint(val) || + (unsafe_yyjson_is_uint(val) && ret >= 0)) { + *value = ret; + return true; + } + } + return false; +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type real. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_real( + yyjson_val *root, const char *ptr, double *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_real(val)) { + *value = unsafe_yyjson_get_real(val); + return true; + } else { + return false; + } +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type sint, + uint or real. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_num( + yyjson_val *root, const char *ptr, double *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_num(val)) { + *value = unsafe_yyjson_get_num(val); + return true; + } else { + return false; + } +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type string. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_str( + yyjson_val *root, const char *ptr, const char **value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_str(val)) { + *value = unsafe_yyjson_get_str(val); + return true; + } else { + return false; + } +} + + + +/*============================================================================== + * MARK: - Deprecated + *============================================================================*/ + +/** @deprecated renamed to `yyjson_doc_ptr_get` */ +yyjson_deprecated("renamed to yyjson_doc_ptr_get") +yyjson_api_inline yyjson_val *yyjson_doc_get_pointer(yyjson_doc *doc, + const char *ptr) { + return yyjson_doc_ptr_get(doc, ptr); +} + +/** @deprecated renamed to `yyjson_doc_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_doc_ptr_getn") +yyjson_api_inline yyjson_val *yyjson_doc_get_pointern(yyjson_doc *doc, + const char *ptr, + size_t len) { + return yyjson_doc_ptr_getn(doc, ptr, len); +} + +/** @deprecated renamed to `yyjson_mut_doc_ptr_get` */ +yyjson_deprecated("renamed to yyjson_mut_doc_ptr_get") +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_pointer( + yyjson_mut_doc *doc, const char *ptr) { + return yyjson_mut_doc_ptr_get(doc, ptr); +} + +/** @deprecated renamed to `yyjson_mut_doc_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_mut_doc_ptr_getn") +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_pointern( + yyjson_mut_doc *doc, const char *ptr, size_t len) { + return yyjson_mut_doc_ptr_getn(doc, ptr, len); +} + +/** @deprecated renamed to `yyjson_ptr_get` */ +yyjson_deprecated("renamed to yyjson_ptr_get") +yyjson_api_inline yyjson_val *yyjson_get_pointer(yyjson_val *val, + const char *ptr) { + return yyjson_ptr_get(val, ptr); +} + +/** @deprecated renamed to `yyjson_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_ptr_getn") +yyjson_api_inline yyjson_val *yyjson_get_pointern(yyjson_val *val, + const char *ptr, + size_t len) { + return yyjson_ptr_getn(val, ptr, len); +} + +/** @deprecated renamed to `yyjson_mut_ptr_get` */ +yyjson_deprecated("renamed to yyjson_mut_ptr_get") +yyjson_api_inline yyjson_mut_val *yyjson_mut_get_pointer(yyjson_mut_val *val, + const char *ptr) { + return yyjson_mut_ptr_get(val, ptr); +} + +/** @deprecated renamed to `yyjson_mut_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_mut_ptr_getn") +yyjson_api_inline yyjson_mut_val *yyjson_mut_get_pointern(yyjson_mut_val *val, + const char *ptr, + size_t len) { + return yyjson_mut_ptr_getn(val, ptr, len); +} + +/** @deprecated renamed to `yyjson_mut_ptr_getn` */ +yyjson_deprecated("renamed to unsafe_yyjson_ptr_getn") +yyjson_api_inline yyjson_val *unsafe_yyjson_get_pointer(yyjson_val *val, + const char *ptr, + size_t len) { + yyjson_ptr_err err; + return unsafe_yyjson_ptr_getx(val, ptr, len, &err); +} + +/** @deprecated renamed to `unsafe_yyjson_mut_ptr_getx` */ +yyjson_deprecated("renamed to unsafe_yyjson_mut_ptr_getx") +yyjson_api_inline yyjson_mut_val *unsafe_yyjson_mut_get_pointer( + yyjson_mut_val *val, const char *ptr, size_t len) { + yyjson_ptr_err err; + return unsafe_yyjson_mut_ptr_getx(val, ptr, len, NULL, &err); +} + +#endif /* YYJSON_DISABLE_UTILS */ + + + +/*============================================================================== + * MARK: - Compiler Hint End + *============================================================================*/ + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +# pragma GCC diagnostic pop +# endif +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif /* warning suppress end */ + +#ifdef __cplusplus +} +#endif /* extern "C" end */ + +#endif /* YYJSON_H */ diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/summary.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/summary.json new file mode 100644 index 0000000..908043b --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/summary.json @@ -0,0 +1,4006 @@ +[ + { + "case": "metadata_heavy", + "variant": "borrow", + "samples": 5, + "peak_reserved_bytes": { + "median": 1208192, + "min": 1208192, + "max": 1208192 + }, + "source_peak_bytes": { + "median": 1059888, + "min": 1059888, + "max": 1059888 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "rss_peak_bytes": { + "median": 2785280, + "min": 2768896, + "max": 2801664 + }, + "elapsed_ns": { + "median": 66395496, + "min": 66302663, + "max": 66397387 + }, + "cpu_ns": { + "median": 14840713, + "min": 14680433, + "max": 14949295 + }, + "parse_call_ns": { + "median": 10710339, + "min": 10679603, + "max": 10748920 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 6312628, + "min": 6312628, + "max": 6312628 + }, + "parser_realloc_copy_bytes": { + "median": 14086144, + "min": 14086144, + "max": 14086144 + }, + "source_byte_seconds": { + "median": 67622.063537, + "min": 67603.216745, + "max": 67632.156584 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 26533.881, + "latency_p99_upper_ns": 36864 + }, + { + "peak_count": 64, + "latency_mean_ns": 5055556.934, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8039215.434, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "compact", + "samples": 5, + "peak_reserved_bytes": { + "median": 1209200, + "min": 1209200, + "max": 1209200 + }, + "source_peak_bytes": { + "median": 1059888, + "min": 1059888, + "max": 1059888 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "rss_peak_bytes": { + "median": 2916352, + "min": 2899968, + "max": 2932736 + }, + "elapsed_ns": { + "median": 66425702, + "min": 66346982, + "max": 66647996 + }, + "cpu_ns": { + "median": 15174956, + "min": 15115580, + "max": 15645769 + }, + "parse_call_ns": { + "median": 10767457, + "min": 10730381, + "max": 10897093 + }, + "selection_copy_bytes": { + "median": 28672, + "min": 28672, + "max": 28672 + }, + "reader_copy_bytes": { + "median": 6312628, + "min": 6312628, + "max": 6312628 + }, + "parser_realloc_copy_bytes": { + "median": 14086144, + "min": 14086144, + "max": 14086144 + }, + "source_byte_seconds": { + "median": 42524.965388, + "min": 42518.234312, + "max": 42539.689103 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 512, + "min": 512, + "max": 512 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 26833.023, + "latency_p99_upper_ns": 36864 + }, + { + "peak_count": 64, + "latency_mean_ns": 5056115.033, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8039588.77, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 187312, + "min": 187312, + "max": 187312 + }, + "source_peak_bytes": { + "median": 24672, + "min": 24672, + "max": 24672 + }, + "output_peak_bytes": { + "median": 14896, + "min": 14896, + "max": 14896 + }, + "parser_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "rss_peak_bytes": { + "median": 1835008, + "min": 1818624, + "max": 1835008 + }, + "elapsed_ns": { + "median": 66358054, + "min": 66338315, + "max": 66437867 + }, + "cpu_ns": { + "median": 14754424, + "min": 14688841, + "max": 14887131 + }, + "parse_call_ns": { + "median": 10588699, + "min": 10558757, + "max": 10719986 + }, + "selection_copy_bytes": { + "median": 319488, + "min": 319488, + "max": 319488 + }, + "reader_copy_bytes": { + "median": 6312628, + "min": 6312628, + "max": 6312628 + }, + "parser_realloc_copy_bytes": { + "median": 14086144, + "min": 14086144, + "max": 14086144 + }, + "source_byte_seconds": { + "median": 190.767691, + "min": 190.237303, + "max": 192.97609 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 26285.051, + "latency_p99_upper_ns": 36864 + }, + { + "peak_count": 64, + "latency_mean_ns": 5055200.059, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8039957.693, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "copy_no_pool", + "samples": 5, + "peak_reserved_bytes": { + "median": 170672, + "min": 170672, + "max": 170672 + }, + "source_peak_bytes": { + "median": 24672, + "min": 24672, + "max": 24672 + }, + "output_peak_bytes": { + "median": 14896, + "min": 14896, + "max": 14896 + }, + "parser_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "rss_peak_bytes": { + "median": 1835008, + "min": 1835008, + "max": 1851392 + }, + "elapsed_ns": { + "median": 66421906, + "min": 66367572, + "max": 69246534 + }, + "cpu_ns": { + "median": 14937944, + "min": 14844612, + "max": 16610834 + }, + "parse_call_ns": { + "median": 10714630, + "min": 10626817, + "max": 11758115 + }, + "selection_copy_bytes": { + "median": 319488, + "min": 319488, + "max": 319488 + }, + "reader_copy_bytes": { + "median": 6312628, + "min": 6312628, + "max": 6312628 + }, + "parser_realloc_copy_bytes": { + "median": 14086144, + "min": 14086144, + "max": 14086144 + }, + "source_byte_seconds": { + "median": 193.34471, + "min": 191.787957, + "max": 215.365911 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 26630.812, + "latency_p99_upper_ns": 40960 + }, + { + "peak_count": 64, + "latency_mean_ns": 5055539.021, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8039399.533, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "shared_copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 180144, + "min": 180144, + "max": 180144 + }, + "source_peak_bytes": { + "median": 24672, + "min": 24672, + "max": 24672 + }, + "output_peak_bytes": { + "median": 7728, + "min": 7728, + "max": 7728 + }, + "parser_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "rss_peak_bytes": { + "median": 1835008, + "min": 1835008, + "max": 1835008 + }, + "elapsed_ns": { + "median": 66509552, + "min": 66340017, + "max": 67338922 + }, + "cpu_ns": { + "median": 14641398, + "min": 14565611, + "max": 15543047 + }, + "parse_call_ns": { + "median": 10583202, + "min": 10558947, + "max": 10965512 + }, + "selection_copy_bytes": { + "median": 290816, + "min": 290816, + "max": 290816 + }, + "reader_copy_bytes": { + "median": 6312628, + "min": 6312628, + "max": 6312628 + }, + "parser_realloc_copy_bytes": { + "median": 14086144, + "min": 14086144, + "max": 14086144 + }, + "source_byte_seconds": { + "median": 189.850967, + "min": 189.196651, + "max": 198.833427 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 512, + "min": 512, + "max": 512 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 26126.43, + "latency_p99_upper_ns": 32768 + }, + { + "peak_count": 64, + "latency_mean_ns": 5054896.006, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8039936.652, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold2", + "samples": 5, + "peak_reserved_bytes": { + "median": 180144, + "min": 180144, + "max": 180144 + }, + "source_peak_bytes": { + "median": 24672, + "min": 24672, + "max": 24672 + }, + "output_peak_bytes": { + "median": 7728, + "min": 7728, + "max": 7728 + }, + "parser_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "rss_peak_bytes": { + "median": 1835008, + "min": 1835008, + "max": 1851392 + }, + "elapsed_ns": { + "median": 66354498, + "min": 66341459, + "max": 66675868 + }, + "cpu_ns": { + "median": 14610428, + "min": 14573003, + "max": 15617313 + }, + "parse_call_ns": { + "median": 10568715, + "min": 10552983, + "max": 10794500 + }, + "selection_copy_bytes": { + "median": 290816, + "min": 290816, + "max": 290816 + }, + "reader_copy_bytes": { + "median": 6312628, + "min": 6312628, + "max": 6312628 + }, + "parser_realloc_copy_bytes": { + "median": 14086144, + "min": 14086144, + "max": 14086144 + }, + "source_byte_seconds": { + "median": 189.501318, + "min": 189.19959, + "max": 196.368374 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 512, + "min": 512, + "max": 512 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 26103.785, + "latency_p99_upper_ns": 30720 + }, + { + "peak_count": 64, + "latency_mean_ns": 5054791.455, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8037890.609, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "metadata_heavy", + "variant": "threshold8", + "samples": 5, + "peak_reserved_bytes": { + "median": 180144, + "min": 180144, + "max": 180144 + }, + "source_peak_bytes": { + "median": 24672, + "min": 24672, + "max": 24672 + }, + "output_peak_bytes": { + "median": 7728, + "min": 7728, + "max": 7728 + }, + "parser_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "rss_peak_bytes": { + "median": 1835008, + "min": 1835008, + "max": 1851392 + }, + "elapsed_ns": { + "median": 66346091, + "min": 66342183, + "max": 66620533 + }, + "cpu_ns": { + "median": 14678765, + "min": 14624958, + "max": 15009658 + }, + "parse_call_ns": { + "median": 10599246, + "min": 10558072, + "max": 10663838 + }, + "selection_copy_bytes": { + "median": 290816, + "min": 290816, + "max": 290816 + }, + "reader_copy_bytes": { + "median": 6312628, + "min": 6312628, + "max": 6312628 + }, + "parser_realloc_copy_bytes": { + "median": 14086144, + "min": 14086144, + "max": 14086144 + }, + "source_byte_seconds": { + "median": 190.199351, + "min": 189.596919, + "max": 191.566293 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 512, + "min": 512, + "max": 512 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 26178.92, + "latency_p99_upper_ns": 36864 + }, + { + "peak_count": 64, + "latency_mean_ns": 5054797.43, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8038718.686, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "borrow", + "samples": 5, + "peak_reserved_bytes": { + "median": 2097024, + "min": 2097024, + "max": 2097072 + }, + "source_peak_bytes": { + "median": 2047296, + "min": 2047296, + "max": 2047296 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 524336, + "min": 524336, + "max": 524336 + }, + "rss_peak_bytes": { + "median": 3719168, + "min": 3702784, + "max": 3719168 + }, + "elapsed_ns": { + "median": 516876187, + "min": 516415971, + "max": 519737827 + }, + "cpu_ns": { + "median": 58189132, + "min": 56956626, + "max": 63401151 + }, + "parse_call_ns": { + "median": 413462854, + "min": 410873515, + "max": 414175159 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 114814580, + "min": 114814580, + "max": 114814580 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 866155.703795, + "min": 865936.173669, + "max": 869390.688093 + }, + "budget_waits": { + "median": 1258, + "min": 1250, + "max": 1267 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 496219.126, + "latency_p99_upper_ns": 7864320 + }, + { + "peak_count": 30, + "latency_mean_ns": 5528237.536, + "latency_p99_upper_ns": 12582912 + }, + { + "peak_count": 30, + "latency_mean_ns": 8542682.03, + "latency_p99_upper_ns": 15728640 + } + ] + }, + { + "case": "mixed_tight", + "variant": "compact", + "samples": 5, + "peak_reserved_bytes": { + "median": 2096656, + "min": 2095936, + "max": 2097104 + }, + "source_peak_bytes": { + "median": 2047296, + "min": 2047296, + "max": 2047296 + }, + "output_peak_bytes": { + "median": 3360, + "min": 3360, + "max": 3360 + }, + "parser_peak_bytes": { + "median": 524336, + "min": 524336, + "max": 524336 + }, + "rss_peak_bytes": { + "median": 3719168, + "min": 3702784, + "max": 3833856 + }, + "elapsed_ns": { + "median": 330507221, + "min": 330325402, + "max": 332809762 + }, + "cpu_ns": { + "median": 57227425, + "min": 56526552, + "max": 64506702 + }, + "parse_call_ns": { + "median": 257390641, + "min": 255295188, + "max": 258042238 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 114814580, + "min": 114814580, + "max": 114814580 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 542286.835871, + "min": 542057.64214, + "max": 544031.993873 + }, + "budget_waits": { + "median": 1246, + "min": 1227, + "max": 1262 + }, + "compacted": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 314160.033, + "latency_p99_upper_ns": 4718592 + }, + { + "peak_count": 30, + "latency_mean_ns": 5361784.028, + "latency_p99_upper_ns": 10485760 + }, + { + "peak_count": 49, + "latency_mean_ns": 8345801.06, + "latency_p99_upper_ns": 12582912 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 1104848, + "min": 1104736, + "max": 1104848 + }, + "source_peak_bytes": { + "median": 393312, + "min": 393312, + "max": 393312 + }, + "output_peak_bytes": { + "median": 79920, + "min": 79920, + "max": 79920 + }, + "parser_peak_bytes": { + "median": 524336, + "min": 524336, + "max": 524336 + }, + "rss_peak_bytes": { + "median": 1884160, + "min": 1884160, + "max": 1900544 + }, + "elapsed_ns": { + "median": 134798098, + "min": 134667407, + "max": 135397301 + }, + "cpu_ns": { + "median": 52545277, + "min": 52218674, + "max": 55535720 + }, + "parse_call_ns": { + "median": 26282225, + "min": 26241260, + "max": 28191073 + }, + "selection_copy_bytes": { + "median": 26872064, + "min": 26872064, + "max": 26872064 + }, + "reader_copy_bytes": { + "median": 114814580, + "min": 114814580, + "max": 114814580 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 6883.203763, + "min": 6810.984162, + "max": 7296.683374 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 47583.467, + "latency_p99_upper_ns": 147456 + }, + { + "peak_count": 64, + "latency_mean_ns": 5082343.96, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8083520.998, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "copy_no_pool", + "samples": 5, + "peak_reserved_bytes": { + "median": 842368, + "min": 842368, + "max": 842368 + }, + "source_peak_bytes": { + "median": 393312, + "min": 393312, + "max": 393312 + }, + "output_peak_bytes": { + "median": 79920, + "min": 79920, + "max": 79920 + }, + "parser_peak_bytes": { + "median": 524336, + "min": 524336, + "max": 524336 + }, + "rss_peak_bytes": { + "median": 1933312, + "min": 1916928, + "max": 1949696 + }, + "elapsed_ns": { + "median": 135708499, + "min": 135580728, + "max": 135818181 + }, + "cpu_ns": { + "median": 59498427, + "min": 59055276, + "max": 61310764 + }, + "parse_call_ns": { + "median": 27370335, + "min": 27293452, + "max": 27699522 + }, + "selection_copy_bytes": { + "median": 26872064, + "min": 26872064, + "max": 26872064 + }, + "reader_copy_bytes": { + "median": 114814580, + "min": 114814580, + "max": 114814580 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 8320.069469, + "min": 8268.860241, + "max": 8492.13471 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 54118.737, + "latency_p99_upper_ns": 180224 + }, + { + "peak_count": 64, + "latency_mean_ns": 5093193.132, + "latency_p99_upper_ns": 5767168 + }, + { + "peak_count": 64, + "latency_mean_ns": 8094306.969, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "shared_copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 1097792, + "min": 1097792, + "max": 1097792 + }, + "source_peak_bytes": { + "median": 393312, + "min": 393312, + "max": 393312 + }, + "output_peak_bytes": { + "median": 72752, + "min": 72752, + "max": 72752 + }, + "parser_peak_bytes": { + "median": 524336, + "min": 524336, + "max": 524336 + }, + "rss_peak_bytes": { + "median": 1884160, + "min": 1884160, + "max": 1900544 + }, + "elapsed_ns": { + "median": 134698503, + "min": 134660107, + "max": 134733970 + }, + "cpu_ns": { + "median": 52085095, + "min": 51849496, + "max": 52189389 + }, + "parse_call_ns": { + "median": 26235438, + "min": 26215409, + "max": 26273407 + }, + "selection_copy_bytes": { + "median": 26814720, + "min": 26814720, + "max": 26814720 + }, + "reader_copy_bytes": { + "median": 114814580, + "min": 114814580, + "max": 114814580 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 6831.305534, + "min": 6785.330268, + "max": 6865.482399 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 47354.551, + "latency_p99_upper_ns": 147456 + }, + { + "peak_count": 64, + "latency_mean_ns": 5081713.058, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8082113.784, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold2", + "samples": 5, + "peak_reserved_bytes": { + "median": 1097792, + "min": 1097792, + "max": 1097792 + }, + "source_peak_bytes": { + "median": 393312, + "min": 393312, + "max": 393312 + }, + "output_peak_bytes": { + "median": 72752, + "min": 72752, + "max": 72752 + }, + "parser_peak_bytes": { + "median": 524336, + "min": 524336, + "max": 524336 + }, + "rss_peak_bytes": { + "median": 1884160, + "min": 1867776, + "max": 1900544 + }, + "elapsed_ns": { + "median": 134646087, + "min": 134596389, + "max": 134805557 + }, + "cpu_ns": { + "median": 51969989, + "min": 51772899, + "max": 52099602 + }, + "parse_call_ns": { + "median": 26207832, + "min": 26192074, + "max": 26303805 + }, + "selection_copy_bytes": { + "median": 26814720, + "min": 26814720, + "max": 26814720 + }, + "reader_copy_bytes": { + "median": 114814580, + "min": 114814580, + "max": 114814580 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 6812.54134, + "min": 6795.293198, + "max": 6823.249522 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 47208.702, + "latency_p99_upper_ns": 147456 + }, + { + "peak_count": 64, + "latency_mean_ns": 5082291.09, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8082717.212, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "mixed_tight", + "variant": "threshold8", + "samples": 5, + "peak_reserved_bytes": { + "median": 1097792, + "min": 1097792, + "max": 1097792 + }, + "source_peak_bytes": { + "median": 393312, + "min": 393312, + "max": 393312 + }, + "output_peak_bytes": { + "median": 11312, + "min": 11312, + "max": 11312 + }, + "parser_peak_bytes": { + "median": 524336, + "min": 524336, + "max": 524336 + }, + "rss_peak_bytes": { + "median": 2048000, + "min": 2031616, + "max": 2064384 + }, + "elapsed_ns": { + "median": 134627181, + "min": 134514858, + "max": 135143135 + }, + "cpu_ns": { + "median": 51051158, + "min": 50901202, + "max": 54181725 + }, + "parse_call_ns": { + "median": 26232175, + "min": 26138215, + "max": 28073539 + }, + "selection_copy_bytes": { + "median": 1059072, + "min": 1059072, + "max": 1059072 + }, + "reader_copy_bytes": { + "median": 114814580, + "min": 114814580, + "max": 114814580 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 7499.787736, + "min": 7468.893211, + "max": 7971.142415 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 46365.068, + "latency_p99_upper_ns": 147456 + }, + { + "peak_count": 64, + "latency_mean_ns": 5080938.783, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8081990.295, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "borrow", + "samples": 5, + "peak_reserved_bytes": { + "median": 4427696, + "min": 4427696, + "max": 4427696 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 5537792, + "min": 5521408, + "max": 5554176 + }, + "elapsed_ns": { + "median": 132124751, + "min": 131976979, + "max": 132469040 + }, + "cpu_ns": { + "median": 38591495, + "min": 37612015, + "max": 41862764 + }, + "parse_call_ns": { + "median": 22573964, + "min": 22444229, + "max": 22964342 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 540860.949553, + "min": 540831.163899, + "max": 541099.43903 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 64, + "latency_mean_ns": 8058679.423, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "compact", + "samples": 5, + "peak_reserved_bytes": { + "median": 4427696, + "min": 4427696, + "max": 4427696 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 5537792, + "min": 5537792, + "max": 5554176 + }, + "elapsed_ns": { + "median": 132110379, + "min": 131996119, + "max": 132186905 + }, + "cpu_ns": { + "median": 38257181, + "min": 38004572, + "max": 38709650 + }, + "parse_call_ns": { + "median": 22486562, + "min": 22434299, + "max": 22557256 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 540815.06589, + "min": 540791.270719, + "max": 540907.798619 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 64, + "latency_mean_ns": 8057936.641, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 303072, + "min": 303072, + "max": 303072 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1818624, + "min": 1818624, + "max": 1818624 + }, + "elapsed_ns": { + "median": 131689453, + "min": 131600270, + "max": 131752053 + }, + "cpu_ns": { + "median": 34877739, + "min": 34455850, + "max": 35243759 + }, + "parse_call_ns": { + "median": 21993246, + "min": 21964978, + "max": 22030534 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 1845.194241, + "min": 1832.537919, + "max": 1857.757504 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 64, + "latency_mean_ns": 8053071.22, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "copy_no_pool", + "samples": 5, + "peak_reserved_bytes": { + "median": 237296, + "min": 237296, + "max": 237296 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1818624, + "min": 1802240, + "max": 1835008 + }, + "elapsed_ns": { + "median": 132246841, + "min": 132199048, + "max": 137450461 + }, + "cpu_ns": { + "median": 42202499, + "min": 41767998, + "max": 46306762 + }, + "parse_call_ns": { + "median": 24805054, + "min": 24673720, + "max": 25720817 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2084.967873, + "min": 2066.312998, + "max": 2217.245474 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 64, + "latency_mean_ns": 8059680.607, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "shared_copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 303072, + "min": 303072, + "max": 303072 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1818624, + "min": 1802240, + "max": 1835008 + }, + "elapsed_ns": { + "median": 132103712, + "min": 131708165, + "max": 134003964 + }, + "cpu_ns": { + "median": 35248408, + "min": 34456703, + "max": 37157688 + }, + "parse_call_ns": { + "median": 22044118, + "min": 21940218, + "max": 22141327 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 1858.44117, + "min": 1833.007769, + "max": 1917.711679 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 64, + "latency_mean_ns": 8054276.953, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold2", + "samples": 5, + "peak_reserved_bytes": { + "median": 303072, + "min": 303072, + "max": 303072 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1818624, + "min": 1802240, + "max": 1835008 + }, + "elapsed_ns": { + "median": 132093280, + "min": 131703634, + "max": 132588310 + }, + "cpu_ns": { + "median": 36810738, + "min": 34747849, + "max": 37673671 + }, + "parse_call_ns": { + "median": 22338927, + "min": 21989760, + "max": 22518430 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 1914.610493, + "min": 1842.615404, + "max": 1949.11739 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 64, + "latency_mean_ns": 8054865.09, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "one_slow", + "variant": "threshold8", + "samples": 5, + "peak_reserved_bytes": { + "median": 303072, + "min": 303072, + "max": 303072 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1818624, + "min": 1802240, + "max": 1818624 + }, + "elapsed_ns": { + "median": 131798127, + "min": 131620844, + "max": 131976855 + }, + "cpu_ns": { + "median": 34803856, + "min": 34446035, + "max": 36997295 + }, + "parse_call_ns": { + "median": 22050963, + "min": 21905479, + "max": 22094634 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 1846.36305, + "min": 1832.609103, + "max": 1904.528214 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 64, + "latency_mean_ns": 8054286.008, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "borrow", + "samples": 5, + "peak_reserved_bytes": { + "median": 4435888, + "min": 4435888, + "max": 4435888 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 4767744, + "min": 4767744, + "max": 4767744 + }, + "elapsed_ns": { + "median": 133814643, + "min": 133553215, + "max": 134337593 + }, + "cpu_ns": { + "median": 68888479, + "min": 68534602, + "max": 71105404 + }, + "parse_call_ns": { + "median": 22344284, + "min": 22264001, + "max": 22575073 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 541341.207084, + "min": 541236.958782, + "max": 541448.462493 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 52115.836, + "latency_p99_upper_ns": 81920 + }, + { + "peak_count": 64, + "latency_mean_ns": 5078986.712, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8067041.718, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "compact", + "samples": 5, + "peak_reserved_bytes": { + "median": 4730672, + "min": 4730672, + "max": 4730672 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 2001776, + "min": 1936144, + "max": 2034592 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 6340608, + "min": 6340608, + "max": 6340608 + }, + "elapsed_ns": { + "median": 134204038, + "min": 134074359, + "max": 135831514 + }, + "cpu_ns": { + "median": 74886880, + "min": 74679639, + "max": 84355786 + }, + "parse_call_ns": { + "median": 22736073, + "min": 22714288, + "max": 23107923 + }, + "selection_copy_bytes": { + "median": 33554432, + "min": 33554432, + "max": 33554432 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 341191.973388, + "min": 341111.908821, + "max": 341580.411374 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 54208.683, + "latency_p99_upper_ns": 90112 + }, + { + "peak_count": 64, + "latency_mean_ns": 5084673.227, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8069796.873, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 4504544, + "min": 4504544, + "max": 4504544 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 4233264, + "min": 4233264, + "max": 4233264 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 5292032, + "min": 5275648, + "max": 5292032 + }, + "elapsed_ns": { + "median": 134436483, + "min": 134254523, + "max": 139508137 + }, + "cpu_ns": { + "median": 76196602, + "min": 74842901, + "max": 88332555 + }, + "parse_call_ns": { + "median": 22285338, + "min": 22151327, + "max": 22864596 + }, + "selection_copy_bytes": { + "median": 100663296, + "min": 100663296, + "max": 100663296 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2295.795833, + "min": 2277.11069, + "max": 2528.253394 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 58635.959, + "latency_p99_upper_ns": 90112 + }, + { + "peak_count": 64, + "latency_mean_ns": 5084371.237, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8075247.27, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "copy_no_pool", + "samples": 5, + "peak_reserved_bytes": { + "median": 4373248, + "min": 4373248, + "max": 4373248 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 4233264, + "min": 4233264, + "max": 4233264 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 5816320, + "min": 5799936, + "max": 6078464 + }, + "elapsed_ns": { + "median": 134471775, + "min": 134289332, + "max": 216945564 + }, + "cpu_ns": { + "median": 76002725, + "min": 75355273, + "max": 116157398 + }, + "parse_call_ns": { + "median": 22740343, + "min": 22600489, + "max": 25703347 + }, + "selection_copy_bytes": { + "median": 100663296, + "min": 100663296, + "max": 100663296 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2334.193158, + "min": 2317.391148, + "max": 3917.681175 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 59008.762, + "latency_p99_upper_ns": 90112 + }, + { + "peak_count": 64, + "latency_mean_ns": 5084982.783, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8075279.097, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "shared_copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 2371504, + "min": 2371504, + "max": 2371504 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 2100224, + "min": 2100224, + "max": 2100224 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 3293184, + "min": 3178496, + "max": 3571712 + }, + "elapsed_ns": { + "median": 133593447, + "min": 133495777, + "max": 135030934 + }, + "cpu_ns": { + "median": 66640259, + "min": 66172548, + "max": 71236281 + }, + "parse_call_ns": { + "median": 22303696, + "min": 22176603, + "max": 22446961 + }, + "selection_copy_bytes": { + "median": 33554432, + "min": 33554432, + "max": 33554432 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2044.238204, + "min": 2031.917671, + "max": 2100.123381 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 2048, + "min": 2048, + "max": 2048 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 51484.234, + "latency_p99_upper_ns": 81920 + }, + { + "peak_count": 64, + "latency_mean_ns": 5078396.125, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8066089.677, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold2", + "samples": 5, + "peak_reserved_bytes": { + "median": 2371504, + "min": 2371504, + "max": 2371504 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 2100224, + "min": 2100224, + "max": 2100224 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 3194880, + "min": 3194880, + "max": 3244032 + }, + "elapsed_ns": { + "median": 133655494, + "min": 133441147, + "max": 134034428 + }, + "cpu_ns": { + "median": 66434679, + "min": 66234578, + "max": 69077852 + }, + "parse_call_ns": { + "median": 22159339, + "min": 22119254, + "max": 22386881 + }, + "selection_copy_bytes": { + "median": 33554432, + "min": 33554432, + "max": 33554432 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2045.927873, + "min": 2030.079604, + "max": 2071.219143 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 2048, + "min": 2048, + "max": 2048 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 51484.504, + "latency_p99_upper_ns": 73728 + }, + { + "peak_count": 64, + "latency_mean_ns": 5078118.015, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8066441.419, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "same_large_field", + "variant": "threshold8", + "samples": 5, + "peak_reserved_bytes": { + "median": 4435888, + "min": 4435888, + "max": 4435888 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 4767744, + "min": 4767744, + "max": 4833280 + }, + "elapsed_ns": { + "median": 133681327, + "min": 133447386, + "max": 134457305 + }, + "cpu_ns": { + "median": 69361342, + "min": 68765370, + "max": 70251788 + }, + "parse_call_ns": { + "median": 22353361, + "min": 22338685, + "max": 22437830 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 541359.828153, + "min": 541289.267391, + "max": 541408.186663 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 52372.427, + "latency_p99_upper_ns": 81920 + }, + { + "peak_count": 64, + "latency_mean_ns": 5079700.568, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8067374.837, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "borrow", + "samples": 5, + "peak_reserved_bytes": { + "median": 4435888, + "min": 4435888, + "max": 4435888 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 4767744, + "min": 4767744, + "max": 5390336 + }, + "elapsed_ns": { + "median": 133311566, + "min": 133254368, + "max": 133477288 + }, + "cpu_ns": { + "median": 53725285, + "min": 52234245, + "max": 54530428 + }, + "parse_call_ns": { + "median": 22266351, + "min": 22239925, + "max": 22789187 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 541551.170985, + "min": 541367.893955, + "max": 541575.408384 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 46306.509, + "latency_p99_upper_ns": 61440 + }, + { + "peak_count": 64, + "latency_mean_ns": 5067325.337, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8070214.071, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "compact", + "samples": 5, + "peak_reserved_bytes": { + "median": 4730672, + "min": 4730672, + "max": 4730672 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 2100224, + "min": 2067408, + "max": 2100224 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 6340608, + "min": 6340608, + "max": 6406144 + }, + "elapsed_ns": { + "median": 133282278, + "min": 133118662, + "max": 133472857 + }, + "cpu_ns": { + "median": 59607669, + "min": 59503628, + "max": 62602975 + }, + "parse_call_ns": { + "median": 22648269, + "min": 22558632, + "max": 23134595 + }, + "selection_copy_bytes": { + "median": 33554432, + "min": 33554432, + "max": 33554432 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 340524.867797, + "min": 340502.324772, + "max": 340709.17918 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 47236.674, + "latency_p99_upper_ns": 73728 + }, + { + "peak_count": 64, + "latency_mean_ns": 5071269.907, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8068767.35, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 2378784, + "min": 2378784, + "max": 2378784 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 2107504, + "min": 2107504, + "max": 2107504 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 3948544, + "min": 3751936, + "max": 3964928 + }, + "elapsed_ns": { + "median": 133179940, + "min": 133174357, + "max": 133407209 + }, + "cpu_ns": { + "median": 52486508, + "min": 52033688, + "max": 55263103 + }, + "parse_call_ns": { + "median": 22154380, + "min": 22117835, + "max": 22433685 + }, + "selection_copy_bytes": { + "median": 33669120, + "min": 33669120, + "max": 33669120 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2028.917653, + "min": 2021.284206, + "max": 2079.608844 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 46179.91, + "latency_p99_upper_ns": 61440 + }, + { + "peak_count": 64, + "latency_mean_ns": 5068142.38, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8068218.996, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "copy_no_pool", + "samples": 5, + "peak_reserved_bytes": { + "median": 2312896, + "min": 2312896, + "max": 2312896 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 2107504, + "min": 2107504, + "max": 2107504 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 3194880, + "min": 3178496, + "max": 3194880 + }, + "elapsed_ns": { + "median": 133540522, + "min": 133362779, + "max": 134281084 + }, + "cpu_ns": { + "median": 58472614, + "min": 57999768, + "max": 62190824 + }, + "parse_call_ns": { + "median": 24345793, + "min": 24134627, + "max": 25370210 + }, + "selection_copy_bytes": { + "median": 33669120, + "min": 33669120, + "max": 33669120 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2208.417867, + "min": 2191.645151, + "max": 2300.277794 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 51364.197, + "latency_p99_upper_ns": 81920 + }, + { + "peak_count": 64, + "latency_mean_ns": 5074603.185, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8073207.582, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "shared_copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 2378672, + "min": 2378672, + "max": 2378672 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 2107392, + "min": 2107392, + "max": 2107392 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 3899392, + "min": 3293184, + "max": 3948544 + }, + "elapsed_ns": { + "median": 133216315, + "min": 133064931, + "max": 154001095 + }, + "cpu_ns": { + "median": 52117164, + "min": 51631438, + "max": 65368726 + }, + "parse_call_ns": { + "median": 22128273, + "min": 22057182, + "max": 23437273 + }, + "selection_copy_bytes": { + "median": 33611776, + "min": 33611776, + "max": 33611776 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2026.862393, + "min": 2014.496107, + "max": 2683.230182 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 46182.627, + "latency_p99_upper_ns": 61440 + }, + { + "peak_count": 64, + "latency_mean_ns": 5068255.587, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8067189.304, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold2", + "samples": 5, + "peak_reserved_bytes": { + "median": 2378672, + "min": 2378672, + "max": 2378672 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 2107392, + "min": 2107392, + "max": 2107392 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 3751936, + "min": 3342336, + "max": 3932160 + }, + "elapsed_ns": { + "median": 133312135, + "min": 133094723, + "max": 134333542 + }, + "cpu_ns": { + "median": 52726291, + "min": 52362717, + "max": 53734268 + }, + "parse_call_ns": { + "median": 22219255, + "min": 22103023, + "max": 22533891 + }, + "selection_copy_bytes": { + "median": 33611776, + "min": 33611776, + "max": 33611776 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2039.714059, + "min": 2022.420003, + "max": 2071.170012 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 46192.459, + "latency_p99_upper_ns": 65536 + }, + { + "peak_count": 64, + "latency_mean_ns": 5068879.809, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8069081.323, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "slow_large_field", + "variant": "threshold8", + "samples": 5, + "peak_reserved_bytes": { + "median": 4443056, + "min": 4443056, + "max": 4443056 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 5292032, + "min": 4751360, + "max": 5439488 + }, + "elapsed_ns": { + "median": 133341496, + "min": 133297584, + "max": 134650847 + }, + "cpu_ns": { + "median": 54857945, + "min": 53670610, + "max": 58145169 + }, + "parse_call_ns": { + "median": 22618201, + "min": 22263776, + "max": 22801055 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 541518.37496, + "min": 541459.255198, + "max": 542663.104532 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 47550.497, + "latency_p99_upper_ns": 73728 + }, + { + "peak_count": 64, + "latency_mean_ns": 5070242.543, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8069675.14, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "borrow", + "samples": 5, + "peak_reserved_bytes": { + "median": 4435888, + "min": 4435888, + "max": 4435888 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 5373952, + "min": 5373952, + "max": 5373952 + }, + "elapsed_ns": { + "median": 132595906, + "min": 132489898, + "max": 133433884 + }, + "cpu_ns": { + "median": 49691116, + "min": 48808681, + "max": 51462618 + }, + "parse_call_ns": { + "median": 22667622, + "min": 22622066, + "max": 23747967 + }, + "selection_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 540593.356702, + "min": 540484.370521, + "max": 540814.68462 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 45356.652, + "latency_p99_upper_ns": 65536 + }, + { + "peak_count": 64, + "latency_mean_ns": 5069713.765, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8055489.045, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "compact", + "samples": 5, + "peak_reserved_bytes": { + "median": 4436784, + "min": 4436784, + "max": 4436784 + }, + "source_peak_bytes": { + "median": 4230192, + "min": 4230192, + "max": 4230192 + }, + "output_peak_bytes": { + "median": 6944, + "min": 6944, + "max": 7056 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 5537792, + "min": 5488640, + "max": 5603328 + }, + "elapsed_ns": { + "median": 132699783, + "min": 132597323, + "max": 132746292 + }, + "cpu_ns": { + "median": 50225388, + "min": 49739534, + "max": 50870250 + }, + "parse_call_ns": { + "median": 22671992, + "min": 22546544, + "max": 22749218 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 340108.105991, + "min": 340105.112239, + "max": 340246.674827 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 45463.711, + "latency_p99_upper_ns": 65536 + }, + { + "peak_count": 64, + "latency_mean_ns": 5069991.558, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8055038.37, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 318432, + "min": 318432, + "max": 318432 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 47152, + "min": 47152, + "max": 47152 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1851392, + "min": 1851392, + "max": 1867776 + }, + "elapsed_ns": { + "median": 132282510, + "min": 132122049, + "max": 132351078 + }, + "cpu_ns": { + "median": 46122525, + "min": 45960158, + "max": 46415608 + }, + "parse_call_ns": { + "median": 22062914, + "min": 22012014, + "max": 22082400 + }, + "selection_copy_bytes": { + "median": 33669120, + "min": 33669120, + "max": 33669120 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 1991.776071, + "min": 1985.913742, + "max": 1996.444688 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 42165.024, + "latency_p99_upper_ns": 53248 + }, + { + "peak_count": 64, + "latency_mean_ns": 5068352.743, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8050279.443, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "copy_no_pool", + "samples": 5, + "peak_reserved_bytes": { + "median": 252544, + "min": 252544, + "max": 252544 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 47152, + "min": 47152, + "max": 47152 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1900544, + "min": 1884160, + "max": 1933312 + }, + "elapsed_ns": { + "median": 132297747, + "min": 132221655, + "max": 132985160 + }, + "cpu_ns": { + "median": 46669804, + "min": 46345346, + "max": 49981054 + }, + "parse_call_ns": { + "median": 22170762, + "min": 22094470, + "max": 23442854 + }, + "selection_copy_bytes": { + "median": 33669120, + "min": 33669120, + "max": 33669120 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2009.648765, + "min": 1998.568774, + "max": 2129.142526 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 0, + "min": 0, + "max": 0 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 42678.905, + "latency_p99_upper_ns": 61440 + }, + { + "peak_count": 64, + "latency_mean_ns": 5068637.263, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8054142.04, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "shared_copy", + "samples": 5, + "peak_reserved_bytes": { + "median": 311264, + "min": 311264, + "max": 311264 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 39984, + "min": 39984, + "max": 39984 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1851392, + "min": 1835008, + "max": 1851392 + }, + "elapsed_ns": { + "median": 132284819, + "min": 132140503, + "max": 133179387 + }, + "cpu_ns": { + "median": 45991210, + "min": 45823432, + "max": 47009567 + }, + "parse_call_ns": { + "median": 22104890, + "min": 22053622, + "max": 22145612 + }, + "selection_copy_bytes": { + "median": 33611776, + "min": 33611776, + "max": 33611776 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 1991.371251, + "min": 1983.569581, + "max": 2001.098596 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 42139.616, + "latency_p99_upper_ns": 57344 + }, + { + "peak_count": 64, + "latency_mean_ns": 5068344.258, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8052022.729, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold2", + "samples": 5, + "peak_reserved_bytes": { + "median": 311264, + "min": 311264, + "max": 311264 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 39984, + "min": 39984, + "max": 39984 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1851392, + "min": 1835008, + "max": 1867776 + }, + "elapsed_ns": { + "median": 132243573, + "min": 132119197, + "max": 132423215 + }, + "cpu_ns": { + "median": 45811373, + "min": 45674856, + "max": 46202689 + }, + "parse_call_ns": { + "median": 22085581, + "min": 22029775, + "max": 22137673 + }, + "selection_copy_bytes": { + "median": 33611776, + "min": 33611776, + "max": 33611776 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 1984.871517, + "min": 1978.146096, + "max": 1992.776523 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 42043.254, + "latency_p99_upper_ns": 57344 + }, + { + "peak_count": 64, + "latency_mean_ns": 5068193.573, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8050581.195, + "latency_p99_upper_ns": 8388608 + } + ] + }, + { + "case": "three_distinct_speeds", + "variant": "threshold8", + "samples": 5, + "peak_reserved_bytes": { + "median": 311264, + "min": 311264, + "max": 311264 + }, + "source_peak_bytes": { + "median": 98400, + "min": 98400, + "max": 98400 + }, + "output_peak_bytes": { + "median": 7168, + "min": 7168, + "max": 7168 + }, + "parser_peak_bytes": { + "median": 131120, + "min": 131120, + "max": 131120 + }, + "rss_peak_bytes": { + "median": 1851392, + "min": 1851392, + "max": 1851392 + }, + "elapsed_ns": { + "median": 132177385, + "min": 132072752, + "max": 132687220 + }, + "cpu_ns": { + "median": 44977578, + "min": 44815017, + "max": 45610919 + }, + "parse_call_ns": { + "median": 22128368, + "min": 22007505, + "max": 22143479 + }, + "selection_copy_bytes": { + "median": 57344, + "min": 57344, + "max": 57344 + }, + "reader_copy_bytes": { + "median": 75711296, + "min": 75711296, + "max": 75711296 + }, + "parser_realloc_copy_bytes": { + "median": 0, + "min": 0, + "max": 0 + }, + "source_byte_seconds": { + "median": 2381.095196, + "min": 2372.402719, + "max": 2398.303122 + }, + "budget_waits": { + "median": 0, + "min": 0, + "max": 0 + }, + "compacted": { + "median": 0, + "min": 0, + "max": 0 + }, + "dedup_hits": { + "median": 1024, + "min": 1024, + "max": 1024 + }, + "queues": [ + { + "peak_count": 1, + "latency_mean_ns": 41268.295, + "latency_p99_upper_ns": 57344 + }, + { + "peak_count": 64, + "latency_mean_ns": 5067698.961, + "latency_p99_upper_ns": 5242880 + }, + { + "peak_count": 64, + "latency_mean_ns": 8050851.553, + "latency_p99_upper_ns": 8388608 + } + ] + } +] diff --git a/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/verification.json b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/verification.json new file mode 100644 index 0000000..ca94ae7 --- /dev/null +++ b/research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/verification.json @@ -0,0 +1,114 @@ +{ + "native": [ + "borrow: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "borrow: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "borrow: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "borrow: empty input, final JSON without newline, first duplicate member", + "borrow: eight invalid JSON/schema cases with retained owners", + "borrow: cancellation preserves each branch prefix and releases all shared owners", + "borrow: parser allocation charged and refused within total budget", + "copy: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy: empty input, final JSON without newline, first duplicate member", + "copy: eight invalid JSON/schema cases with retained owners", + "copy: cancellation preserves each branch prefix and releases all shared owners", + "copy: parser allocation charged and refused within total budget", + "shared_copy: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "shared_copy: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "shared_copy: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "shared_copy: empty input, final JSON without newline, first duplicate member", + "shared_copy: eight invalid JSON/schema cases with retained owners", + "shared_copy: cancellation preserves each branch prefix and releases all shared owners", + "shared_copy: parser allocation charged and refused within total budget", + "threshold2: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold2: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold2: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold2: empty input, final JSON without newline, first duplicate member", + "threshold2: eight invalid JSON/schema cases with retained owners", + "threshold2: cancellation preserves each branch prefix and releases all shared owners", + "threshold2: parser allocation charged and refused within total budget", + "threshold8: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold8: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold8: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold8: empty input, final JSON without newline, first duplicate member", + "threshold8: eight invalid JSON/schema cases with retained owners", + "threshold8: cancellation preserves each branch prefix and releases all shared owners", + "threshold8: parser allocation charged and refused within total budget", + "compact: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "compact: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "compact: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "compact: empty input, final JSON without newline, first duplicate member", + "compact: eight invalid JSON/schema cases with retained owners", + "compact: cancellation preserves each branch prefix and releases all shared owners", + "compact: parser allocation charged and refused within total budget", + "copy_no_pool: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy_no_pool: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy_no_pool: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy_no_pool: empty input, final JSON without newline, first duplicate member", + "copy_no_pool: eight invalid JSON/schema cases with retained owners", + "copy_no_pool: cancellation preserves each branch prefix and releases all shared owners", + "copy_no_pool: parser allocation charged and refused within total budget", + "identical selections copied once and live until the final consumer", + "last-owner compaction keeps the delayed decoded value valid", + "overlong NDJSON record refused without leaked source", + "actual /dev/full failure releases multiple owners", + "SIGTERM during delayed fan-out releases all owners" + ], + "asan_ubsan": [ + "borrow: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "borrow: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "borrow: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "borrow: empty input, final JSON without newline, first duplicate member", + "borrow: eight invalid JSON/schema cases with retained owners", + "borrow: cancellation preserves each branch prefix and releases all shared owners", + "borrow: parser allocation charged and refused within total budget", + "copy: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy: empty input, final JSON without newline, first duplicate member", + "copy: eight invalid JSON/schema cases with retained owners", + "copy: cancellation preserves each branch prefix and releases all shared owners", + "copy: parser allocation charged and refused within total budget", + "shared_copy: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "shared_copy: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "shared_copy: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "shared_copy: empty input, final JSON without newline, first duplicate member", + "shared_copy: eight invalid JSON/schema cases with retained owners", + "shared_copy: cancellation preserves each branch prefix and releases all shared owners", + "shared_copy: parser allocation charged and refused within total budget", + "threshold2: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold2: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold2: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold2: empty input, final JSON without newline, first duplicate member", + "threshold2: eight invalid JSON/schema cases with retained owners", + "threshold2: cancellation preserves each branch prefix and releases all shared owners", + "threshold2: parser allocation charged and refused within total budget", + "threshold8: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold8: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold8: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "threshold8: empty input, final JSON without newline, first duplicate member", + "threshold8: eight invalid JSON/schema cases with retained owners", + "threshold8: cancellation preserves each branch prefix and releases all shared owners", + "threshold8: parser allocation charged and refused within total budget", + "compact: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "compact: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "compact: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "compact: empty input, final JSON without newline, first duplicate member", + "compact: eight invalid JSON/schema cases with retained owners", + "compact: cancellation preserves each branch prefix and releases all shared owners", + "compact: parser allocation charged and refused within total budget", + "copy_no_pool: 1 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy_no_pool: 2 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy_no_pool: 3 consumer(s), UTF-8/escapes/NUL/empty, slot reuse", + "copy_no_pool: empty input, final JSON without newline, first duplicate member", + "copy_no_pool: eight invalid JSON/schema cases with retained owners", + "copy_no_pool: cancellation preserves each branch prefix and releases all shared owners", + "copy_no_pool: parser allocation charged and refused within total budget", + "identical selections copied once and live until the final consumer", + "last-owner compaction keeps the delayed decoded value valid", + "overlong NDJSON record refused without leaked source", + "actual /dev/full failure releases multiple owners", + "SIGTERM during delayed fan-out releases all owners" + ] +} diff --git a/research/experiments/003-json-fanout/run.py b/research/experiments/003-json-fanout/run.py new file mode 100644 index 0000000..e4848bd --- /dev/null +++ b/research/experiments/003-json-fanout/run.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +"""Verify and measure yyjson parsing with bounded, differently paced consumers.""" +import argparse +import csv +import hashlib +import json +import os +from pathlib import Path +import platform +import random +import shlex +import signal +import statistics +import struct +import subprocess +import tempfile +import time + +ROOT = Path(__file__).resolve().parent +FLAGS = ["-std=c17", "-O2", "-g", "-Wall", "-Wextra", "-Wpedantic", "-Werror"] +FLAGS += ["-DYYJSON_DISABLE_WRITER=1", "-DYYJSON_DISABLE_UTILS=1", "-DYYJSON_DISABLE_INCR_READER=1"] +SOURCE_NAMES = ["benchmark.c", "run.py", "plan.json", "vendor/yyjson/UPSTREAM.json", + "vendor/yyjson/yyjson.c", "vendor/yyjson/yyjson.h", "vendor/yyjson/LICENSE"] + + +def digest(path): + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def dump(path, value): + path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n") + + +def first_members(pairs): + result = {} + for key, value in pairs: + result.setdefault(key, value) + return result + + +def reference(data, keys): + values = [[] for _ in keys] + for line in data.splitlines(): + row = json.loads(line, object_pairs_hook=first_members) + if not isinstance(row, dict): + raise ValueError("object required") + for k, key in enumerate(keys): + if not isinstance(row.get(key), str): + raise ValueError("string field required") + values[k].append(row[key].encode("utf-8")) + return values + + +def decode(data, keys, expected, partial=False): + at = 0 + found = [[] for _ in keys] + while at < len(data): + if len(data) - at < 16: + raise AssertionError("incomplete output header") + k, length, sequence = struct.unpack_from(" + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/research/experiments/003-json-fanout/vendor/yyjson/UPSTREAM.json b/research/experiments/003-json-fanout/vendor/yyjson/UPSTREAM.json new file mode 100644 index 0000000..7de2396 --- /dev/null +++ b/research/experiments/003-json-fanout/vendor/yyjson/UPSTREAM.json @@ -0,0 +1,23 @@ +{ + "project": "yyjson", + "version": "0.12.0", + "commit": "8b4a38dc994a110abaec8a400615567bd996105f", + "license": "MIT", + "files": { + "yyjson.c": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/src/yyjson.c", + "sha256": "ac2e9bbb2e2d9149d90878d40506a1d624fa0b33c979a11b61075c54782c6d6a", + "bytes": 410813 + }, + "yyjson.h": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/src/yyjson.h", + "sha256": "175867c5493a5df648cec566717fa1c29aa2f6096f5f0cf1efad0b65e1f6d7b3", + "bytes": 322407 + }, + "LICENSE": { + "url": "https://raw.githubusercontent.com/ibireme/yyjson/8b4a38dc994a110abaec8a400615567bd996105f/LICENSE", + "sha256": "45e384d3d52c73cba3a64d6e6c25d47cd738cd8a55c30629e3201046eda62947", + "bytes": 1084 + } + } +} diff --git a/research/experiments/003-json-fanout/vendor/yyjson/yyjson.c b/research/experiments/003-json-fanout/vendor/yyjson/yyjson.c new file mode 100644 index 0000000..c16d925 --- /dev/null +++ b/research/experiments/003-json-fanout/vendor/yyjson/yyjson.c @@ -0,0 +1,11065 @@ +/*============================================================================== + Copyright (c) 2020 YaoYuan + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + *============================================================================*/ + +#include "yyjson.h" +#include /* for `HUGE_VAL/INFINIY/NAN` macros, no libm required */ + + + +/*============================================================================== + * MARK: - Warning Suppress (Private) + *============================================================================*/ + +#if defined(__clang__) +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wunused-parameter" +# pragma clang diagnostic ignored "-Wunused-label" +# pragma clang diagnostic ignored "-Wunused-macros" +# pragma clang diagnostic ignored "-Wunused-variable" +#elif defined(__GNUC__) +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wunused-parameter" +# pragma GCC diagnostic ignored "-Wunused-label" +# pragma GCC diagnostic ignored "-Wunused-macros" +# pragma GCC diagnostic ignored "-Wunused-variable" +#elif defined(_MSC_VER) +# pragma warning(disable:4100) /* unreferenced formal parameter */ +# pragma warning(disable:4101) /* unreferenced variable */ +# pragma warning(disable:4102) /* unreferenced label */ +# pragma warning(disable:4127) /* conditional expression is constant */ +# pragma warning(disable:4706) /* assignment within conditional expression */ +#endif + + + +/*============================================================================== + * MARK: - Version (Public) + *============================================================================*/ + +uint32_t yyjson_version(void) { + return YYJSON_VERSION_HEX; +} + + + +/*============================================================================== + * MARK: - Flags (Private) + *============================================================================*/ + +/* msvc intrinsic */ +#if YYJSON_MSC_VER >= 1400 +# include +# if defined(_M_AMD64) || defined(_M_ARM64) +# define MSC_HAS_BIT_SCAN_64 1 +# pragma intrinsic(_BitScanForward64) +# pragma intrinsic(_BitScanReverse64) +# else +# define MSC_HAS_BIT_SCAN_64 0 +# endif +# if defined(_M_AMD64) || defined(_M_ARM64) || \ + defined(_M_IX86) || defined(_M_ARM) +# define MSC_HAS_BIT_SCAN 1 +# pragma intrinsic(_BitScanForward) +# pragma intrinsic(_BitScanReverse) +# else +# define MSC_HAS_BIT_SCAN 0 +# endif +# if defined(_M_AMD64) +# define MSC_HAS_UMUL128 1 +# pragma intrinsic(_umul128) +# else +# define MSC_HAS_UMUL128 0 +# endif +#else +# define MSC_HAS_BIT_SCAN_64 0 +# define MSC_HAS_BIT_SCAN 0 +# define MSC_HAS_UMUL128 0 +#endif + +/* gcc builtin */ +#if yyjson_has_builtin(__builtin_clzll) || yyjson_gcc_available(3, 4, 0) +# define GCC_HAS_CLZLL 1 +#else +# define GCC_HAS_CLZLL 0 +#endif + +#if yyjson_has_builtin(__builtin_ctzll) || yyjson_gcc_available(3, 4, 0) +# define GCC_HAS_CTZLL 1 +#else +# define GCC_HAS_CTZLL 0 +#endif + +/* int128 type */ +#if defined(__SIZEOF_INT128__) && (__SIZEOF_INT128__ == 16) && \ + (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER)) +# define YYJSON_HAS_INT128 1 +#else +# define YYJSON_HAS_INT128 0 +#endif + +/* IEEE 754 floating-point binary representation */ +#if defined(__STDC_IEC_559__) || defined(__STDC_IEC_60559_BFP__) +# define YYJSON_HAS_IEEE_754 1 +#elif FLT_RADIX == 2 && \ + FLT_MANT_DIG == 24 && FLT_DIG == 6 && \ + FLT_MIN_EXP == -125 && FLT_MAX_EXP == 128 && \ + FLT_MIN_10_EXP == -37 && FLT_MAX_10_EXP == 38 && \ + DBL_MANT_DIG == 53 && DBL_DIG == 15 && \ + DBL_MIN_EXP == -1021 && DBL_MAX_EXP == 1024 && \ + DBL_MIN_10_EXP == -307 && DBL_MAX_10_EXP == 308 +# define YYJSON_HAS_IEEE_754 1 +#else +# define YYJSON_HAS_IEEE_754 0 +# undef YYJSON_DISABLE_FAST_FP_CONV +# define YYJSON_DISABLE_FAST_FP_CONV 1 +#endif + +/* + Correct rounding in double number computations. + + On the x86 architecture, some compilers may use x87 FPU instructions for + floating-point arithmetic. The x87 FPU loads all floating point number as + 80-bit double-extended precision internally, then rounds the result to original + precision, which may produce inaccurate results. For a more detailed + explanation, see the paper: https://arxiv.org/abs/cs/0701192 + + Here are some examples of double precision calculation error: + + 2877.0 / 1e6 == 0.002877, but x87 returns 0.0028770000000000002 + 43683.0 * 1e21 == 4.3683e25, but x87 returns 4.3683000000000004e25 + + Here are some examples of compiler flags to generate x87 instructions on x86: + + clang -m32 -mno-sse + gcc/icc -m32 -mfpmath=387 + msvc /arch:SSE or /arch:IA32 + + If we are sure that there's no similar error described above, we can define the + YYJSON_DOUBLE_MATH_CORRECT as 1 to enable the fast path calculation. This is + not an accurate detection, it's just try to avoid the error at compile-time. + An accurate detection can be done at run-time: + + bool is_double_math_correct(void) { + volatile double r = 43683.0; + r *= 1e21; + return r == 4.3683e25; + } + + See also: utils.h in https://github.com/google/double-conversion/ + */ +#if !defined(FLT_EVAL_METHOD) && defined(__FLT_EVAL_METHOD__) +# define FLT_EVAL_METHOD __FLT_EVAL_METHOD__ +#endif + +#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 && FLT_EVAL_METHOD != 1 +# define YYJSON_DOUBLE_MATH_CORRECT 0 +#elif defined(i386) || defined(__i386) || defined(__i386__) || \ + defined(_X86_) || defined(__X86__) || defined(_M_IX86) || \ + defined(__I86__) || defined(__IA32__) || defined(__THW_INTEL) +# if (defined(_MSC_VER) && defined(_M_IX86_FP) && _M_IX86_FP == 2) || \ + (defined(__SSE2_MATH__) && __SSE2_MATH__) +# define YYJSON_DOUBLE_MATH_CORRECT 1 +# else +# define YYJSON_DOUBLE_MATH_CORRECT 0 +# endif +#elif defined(__mc68000__) || defined(__pnacl__) || defined(__native_client__) +# define YYJSON_DOUBLE_MATH_CORRECT 0 +#else +# define YYJSON_DOUBLE_MATH_CORRECT 1 +#endif + +/* + Detect the endianness at compile-time. + YYJSON_ENDIAN == YYJSON_BIG_ENDIAN + YYJSON_ENDIAN == YYJSON_LITTLE_ENDIAN + */ +#define YYJSON_BIG_ENDIAN 4321 +#define YYJSON_LITTLE_ENDIAN 1234 + +#if yyjson_has_include() +# include /* POSIX */ +#endif +#if yyjson_has_include() +# include /* Linux */ +#elif yyjson_has_include() +# include /* BSD, Android */ +#elif yyjson_has_include() +# include /* BSD, Darwin */ +#endif + +#if defined(BYTE_ORDER) && BYTE_ORDER +# if defined(BIG_ENDIAN) && (BYTE_ORDER == BIG_ENDIAN) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +# elif defined(LITTLE_ENDIAN) && (BYTE_ORDER == LITTLE_ENDIAN) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +# endif +#elif defined(__BYTE_ORDER) && __BYTE_ORDER +# if defined(__BIG_ENDIAN) && (__BYTE_ORDER == __BIG_ENDIAN) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +# elif defined(__LITTLE_ENDIAN) && (__BYTE_ORDER == __LITTLE_ENDIAN) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +# endif +#elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ +# if defined(__ORDER_BIG_ENDIAN__) && \ + (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +# elif defined(__ORDER_LITTLE_ENDIAN__) && \ + (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +# endif +#elif (defined(__LITTLE_ENDIAN__) && __LITTLE_ENDIAN__ == 1) || \ + defined(__i386) || defined(__i386__) || \ + defined(_X86_) || defined(__X86__) || \ + defined(_M_IX86) || defined(__THW_INTEL__) || \ + defined(__x86_64) || defined(__x86_64__) || \ + defined(__amd64) || defined(__amd64__) || \ + defined(_M_AMD64) || defined(_M_X64) || \ + defined(_M_ARM) || defined(_M_ARM64) || \ + defined(__ARMEL__) || defined(__THUMBEL__) || defined(__AARCH64EL__) || \ + defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \ + defined(__EMSCRIPTEN__) || defined(__wasm__) || \ + defined(__loongarch__) +# define YYJSON_ENDIAN YYJSON_LITTLE_ENDIAN +#elif (defined(__BIG_ENDIAN__) && __BIG_ENDIAN__ == 1) || \ + defined(__ARMEB__) || defined(__THUMBEB__) || defined(__AARCH64EB__) || \ + defined(_MIPSEB) || defined(__MIPSEB) || defined(__MIPSEB__) || \ + defined(__or1k__) || defined(__OR1K__) +# define YYJSON_ENDIAN YYJSON_BIG_ENDIAN +#else +# define YYJSON_ENDIAN 0 /* unknown endian, detect at run-time */ +#endif + +/* + This macro controls how yyjson handles unaligned memory accesses. + + By default, yyjson uses `memcpy()` for memory copying. This allows the compiler + to optimize the code and emit unaligned memory access instructions when + supported by the target architecture. + + However, on some older compilers or architectures where `memcpy()` is not + well-optimized and may result in unnecessary function calls, defining this + macro as 1 may help. In such cases, yyjson switches to manual byte-by-byte + access, which can potentially improve performance. + + An example of the generated assembly code for ARM can be found here: + https://godbolt.org/z/334jjhxPT + + This flag is already enabled for common architectures in the following code, + so manual configuration is usually unnecessary. If unsure, you can check the + generated assembly or run benchmarks to make an informed decision. + */ +#ifndef YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS +# if defined(__ia64) || defined(_IA64) || defined(__IA64__) || \ + defined(__ia64__) || defined(_M_IA64) || defined(__itanium__) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* Itanium */ +# elif (defined(__arm__) || defined(__arm64__) || defined(__aarch64__)) && \ + (defined(__GNUC__) || defined(__clang__)) && \ + (!defined(__ARM_FEATURE_UNALIGNED) || !__ARM_FEATURE_UNALIGNED) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* ARM */ +# elif defined(__sparc) || defined(__sparc__) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* SPARC */ +# elif defined(__mips) || defined(__mips__) || defined(__MIPS__) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* MIPS */ +# elif defined(__m68k__) || defined(M68000) +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 1 /* M68K */ +# else +# define YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS 0 +# endif +#endif + +/* + Estimated initial ratio of the JSON data (data_size / value_count). + For example: + + data: {"id":12345678,"name":"Harry"} + data_size: 30 + value_count: 5 + ratio: 6 + + yyjson uses dynamic memory with a growth factor of 1.5 when reading and writing + JSON, the ratios below are used to determine the initial memory size. + + A too large ratio will waste memory, and a too small ratio will cause multiple + memory growths and degrade performance. Currently, these ratios are generated + with some commonly used JSON datasets. + */ +#define YYJSON_READER_ESTIMATED_PRETTY_RATIO 16 +#define YYJSON_READER_ESTIMATED_MINIFY_RATIO 6 +#define YYJSON_WRITER_ESTIMATED_PRETTY_RATIO 32 +#define YYJSON_WRITER_ESTIMATED_MINIFY_RATIO 18 + +/* The initial and maximum size of the memory pool's chunk in yyjson_mut_doc. */ +#define YYJSON_MUT_DOC_STR_POOL_INIT_SIZE 0x100 +#define YYJSON_MUT_DOC_STR_POOL_MAX_SIZE 0x10000000 +#define YYJSON_MUT_DOC_VAL_POOL_INIT_SIZE (0x10 * sizeof(yyjson_mut_val)) +#define YYJSON_MUT_DOC_VAL_POOL_MAX_SIZE (0x1000000 * sizeof(yyjson_mut_val)) + +/* The minimum size of the dynamic allocator's chunk. */ +#define YYJSON_ALC_DYN_MIN_SIZE 0x1000 + +/* Default value for compile-time options. */ +#ifndef YYJSON_DISABLE_READER +#define YYJSON_DISABLE_READER 0 +#endif +#ifndef YYJSON_DISABLE_WRITER +#define YYJSON_DISABLE_WRITER 0 +#endif +#ifndef YYJSON_DISABLE_INCR_READER +#define YYJSON_DISABLE_INCR_READER 0 +#endif +#ifndef YYJSON_DISABLE_UTILS +#define YYJSON_DISABLE_UTILS 0 +#endif +#ifndef YYJSON_DISABLE_FAST_FP_CONV +#define YYJSON_DISABLE_FAST_FP_CONV 0 +#endif +#ifndef YYJSON_DISABLE_NON_STANDARD +#define YYJSON_DISABLE_NON_STANDARD 0 +#endif +#ifndef YYJSON_DISABLE_UTF8_VALIDATION +#define YYJSON_DISABLE_UTF8_VALIDATION 0 +#endif + + + +/*============================================================================== + * MARK: - Macros (Private) + *============================================================================*/ + +/* Macros used for loop unrolling and other purpose. */ +#define repeat2(x) { x x } +#define repeat4(x) { x x x x } +#define repeat8(x) { x x x x x x x x } +#define repeat16(x) { x x x x x x x x x x x x x x x x } + +#define repeat2_incr(x) { x(0) x(1) } +#define repeat4_incr(x) { x(0) x(1) x(2) x(3) } +#define repeat8_incr(x) { x(0) x(1) x(2) x(3) x(4) x(5) x(6) x(7) } +#define repeat16_incr(x) { x(0) x(1) x(2) x(3) x(4) x(5) x(6) x(7) \ + x(8) x(9) x(10) x(11) x(12) x(13) x(14) x(15) } +#define repeat_in_1_18(x) { x(1) x(2) x(3) x(4) x(5) x(6) x(7) x(8) \ + x(9) x(10) x(11) x(12) x(13) x(14) x(15) x(16) \ + x(17) x(18) } + +/* Macros used to provide branch prediction information for compiler. */ +#undef likely +#define likely(x) yyjson_likely(x) +#undef unlikely +#define unlikely(x) yyjson_unlikely(x) + +/* Macros used to provide inline information for compiler. */ +#undef static_inline +#define static_inline static yyjson_inline +#undef static_noinline +#define static_noinline static yyjson_noinline + +/* Macros for min and max. */ +#undef yyjson_min +#define yyjson_min(x, y) ((x) < (y) ? (x) : (y)) +#undef yyjson_max +#define yyjson_max(x, y) ((x) > (y) ? (x) : (y)) + +/* Used to write u64 literal for C89 which doesn't support "ULL" suffix. */ +#undef U64 +#define U64(hi, lo) ((((u64)hi##UL) << 32U) + lo##UL) +#undef U32 +#define U32(hi) ((u32)(hi##UL)) + +/* Used to cast away (remove) const qualifier. */ +#define constcast(type) (type)(void *)(size_t)(const void *) + +/* + Compiler barriers for single variables. + + These macros inform GCC that a read or write access to the given memory + location will occur, preventing certain compiler optimizations or reordering + around the access to 'val'. They do not emit any actual instructions. + + This is useful when GCC's default optimization strategies are suboptimal and + precise control over memory access patterns is required. + These barriers are not needed when using Clang or MSVC. + */ +#if YYJSON_IS_REAL_GCC +# define gcc_load_barrier(val) __asm__ volatile(""::"m"(val)) +# define gcc_store_barrier(val) __asm__ volatile("":"=m"(val)) +# define gcc_full_barrier(val) __asm__ volatile("":"=m"(val):"m"(val)) +#else +# define gcc_load_barrier(val) +# define gcc_store_barrier(val) +# define gcc_full_barrier(val) +#endif + + + +/*============================================================================== + * MARK: - Constants (Private) + *============================================================================*/ + +/* Common error messages. */ +#define MSG_FOPEN "failed to open file" +#define MSG_FREAD "failed to read file" +#define MSG_FWRITE "failed to write file" +#define MSG_FCLOSE "failed to close file" +#define MSG_MALLOC "failed to allocate memory" +#define MSG_CHAR_T "invalid literal, expected 'true'" +#define MSG_CHAR_F "invalid literal, expected 'false'" +#define MSG_CHAR_N "invalid literal, expected 'null'" +#define MSG_CHAR "unexpected character, expected a JSON value" +#define MSG_ARR_END "unexpected character, expected ',' or ']'" +#define MSG_OBJ_KEY "unexpected character, expected a string key" +#define MSG_OBJ_SEP "unexpected character, expected ':' after key" +#define MSG_OBJ_END "unexpected character, expected ',' or '}'" +#define MSG_GARBAGE "unexpected content after document" +#define MSG_NOT_END "unexpected end of data" +#define MSG_COMMENT "unclosed multiline comment" +#define MSG_COMMA "trailing comma is not allowed" +#define MSG_NAN_INF "nan or inf number is not allowed" +#define MSG_ERR_TYPE "invalid JSON value type" +#define MSG_ERR_BOM "UTF-8 byte order mark (BOM) is not supported" +#define MSG_ERR_UTF8 "invalid utf-8 encoding in string" +#define MSG_ERR_UTF16 "UTF-16 encoding is not supported" +#define MSG_ERR_UTF32 "UTF-32 encoding is not supported" + +/* U64 constant values */ +#undef U64_MAX +#define U64_MAX U64(0xFFFFFFFF, 0xFFFFFFFF) +#undef I64_MAX +#define I64_MAX U64(0x7FFFFFFF, 0xFFFFFFFF) +#undef USIZE_MAX +#define USIZE_MAX ((usize)(~(usize)0)) + +/* Maximum number of digits for reading u32/u64/usize safety (not overflow). */ +#undef U32_SAFE_DIG +#define U32_SAFE_DIG 9 /* u32 max is 4294967295, 10 digits */ +#undef U64_SAFE_DIG +#define U64_SAFE_DIG 19 /* u64 max is 18446744073709551615, 20 digits */ +#undef USIZE_SAFE_DIG +#define USIZE_SAFE_DIG (sizeof(usize) == 8 ? U64_SAFE_DIG : U32_SAFE_DIG) + +/* Inf bits (positive) */ +#define F64_BITS_INF U64(0x7FF00000, 0x00000000) + +/* NaN bits (quiet NaN, no payload, no sign) */ +#if defined(__hppa__) || (defined(__mips__) && !defined(__mips_nan2008)) +#define F64_BITS_NAN U64(0x7FF7FFFF, 0xFFFFFFFF) +#else +#define F64_BITS_NAN U64(0x7FF80000, 0x00000000) +#endif + +/* maximum significant digits count in decimal when reading double number */ +#define F64_MAX_DEC_DIG 768 + +/* maximum decimal power of double number (1.7976931348623157e308) */ +#define F64_MAX_DEC_EXP 308 + +/* minimum decimal power of double number (4.9406564584124654e-324) */ +#define F64_MIN_DEC_EXP (-324) + +/* maximum binary power of double number */ +#define F64_MAX_BIN_EXP 1024 + +/* minimum binary power of double number */ +#define F64_MIN_BIN_EXP (-1021) + +/* float/double number bits */ +#define F32_BITS 32 +#define F64_BITS 64 + +/* float/double number exponent part bits */ +#define F32_EXP_BITS 8 +#define F64_EXP_BITS 11 + +/* float/double number significand part bits */ +#define F32_SIG_BITS 23 +#define F64_SIG_BITS 52 + +/* float/double number significand part bits (with 1 hidden bit) */ +#define F32_SIG_FULL_BITS 24 +#define F64_SIG_FULL_BITS 53 + +/* float/double number significand bit mask */ +#define F32_SIG_MASK U32(0x007FFFFF) +#define F64_SIG_MASK U64(0x000FFFFF, 0xFFFFFFFF) + +/* float/double number exponent bit mask */ +#define F32_EXP_MASK U32(0x7F800000) +#define F64_EXP_MASK U64(0x7FF00000, 0x00000000) + +/* float/double number exponent bias */ +#define F32_EXP_BIAS 127 +#define F64_EXP_BIAS 1023 + +/* float/double number significant digits count in decimal */ +#define F32_DEC_DIG 9 +#define F64_DEC_DIG 17 + +/* buffer length required for float/double number writer */ +#define FP_BUF_LEN 40 + +/* maximum length of a number in incremental parsing */ +#define INCR_NUM_MAX_LEN 1024 + + + +/*============================================================================== + * MARK: - Types (Private) + *============================================================================*/ + +/** Type define for primitive types. */ +typedef float f32; +typedef double f64; +typedef int8_t i8; +typedef uint8_t u8; +typedef int16_t i16; +typedef uint16_t u16; +typedef int32_t i32; +typedef uint32_t u32; +typedef int64_t i64; +typedef uint64_t u64; +typedef size_t usize; + +/** 128-bit integer, used by floating-point number reader and writer. */ +#if YYJSON_HAS_INT128 +__extension__ typedef __int128 i128; +__extension__ typedef unsigned __int128 u128; +#endif + +/** 16/32/64-bit vector */ +typedef struct v16 { char c[2]; } v16; +typedef struct v32 { char c[4]; } v32; +typedef struct v64 { char c[8]; } v64; + +/** 16/32/64-bit vector union */ +typedef union v16_uni { v16 v; u16 u; } v16_uni; +typedef union v32_uni { v32 v; u32 u; } v32_uni; +typedef union v64_uni { v64 v; u64 u; } v64_uni; + + + +/*============================================================================== + * MARK: - Load/Store Utils (Private) + *============================================================================*/ + +#define byte_move_idx(x) ((char *)dst)[x] = ((const char *)src)[x]; +#define byte_move_src(x) ((char *)tmp)[x] = ((const char *)src)[x]; +#define byte_move_dst(x) ((char *)dst)[x] = ((const char *)tmp)[x]; + +/** Same as `memcpy(dst, src, 2)`, no overlap. */ +static_inline void byte_copy_2(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 2); +#else + repeat2_incr(byte_move_idx) +#endif +} + +/** Same as `memcpy(dst, src, 4)`, no overlap. */ +static_inline void byte_copy_4(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 4); +#else + repeat4_incr(byte_move_idx) +#endif +} + +/** Same as `memcpy(dst, src, 8)`, no overlap. */ +static_inline void byte_copy_8(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 8); +#else + repeat8_incr(byte_move_idx) +#endif +} + +/** Same as `memcpy(dst, src, 16)`, no overlap. */ +static_inline void byte_copy_16(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(dst, src, 16); +#else + repeat16_incr(byte_move_idx) +#endif +} + +/** Same as `memmove(dst, src, 2)`, allows overlap. */ +static_inline void byte_move_2(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + u16 tmp; + memcpy(&tmp, src, 2); + memcpy(dst, &tmp, 2); +#else + char tmp[2]; + repeat2_incr(byte_move_src) + repeat2_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, 4)`, allows overlap. */ +static_inline void byte_move_4(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + u32 tmp; + memcpy(&tmp, src, 4); + memcpy(dst, &tmp, 4); +#else + char tmp[4]; + repeat4_incr(byte_move_src) + repeat4_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, 8)`, allows overlap. */ +static_inline void byte_move_8(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + u64 tmp; + memcpy(&tmp, src, 8); + memcpy(dst, &tmp, 8); +#else + char tmp[8]; + repeat8_incr(byte_move_src) + repeat8_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, 16)`, allows overlap. */ +static_inline void byte_move_16(void *dst, const void *src) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + char *pdst = (char *)dst; + const char *psrc = (const char *)src; + u64 tmp1, tmp2; + memcpy(&tmp1, psrc, 8); + memcpy(&tmp2, psrc + 8, 8); + memcpy(pdst, &tmp1, 8); + memcpy(pdst + 8, &tmp2, 8); +#else + char tmp[16]; + repeat16_incr(byte_move_src) + repeat16_incr(byte_move_dst) +#endif +} + +/** Same as `memmove(dst, src, n)`, but only `dst <= src` and `n <= 16`. */ +static_inline void byte_move_forward(void *dst, void *src, usize n) { + char *d = (char *)dst, *s = (char *)src; + n += (n % 2); /* round up to even */ + if (n == 16) { byte_move_16(d, s); return; } + if (n >= 8) { byte_move_8(d, s); n -= 8; d += 8; s += 8; } + if (n >= 4) { byte_move_4(d, s); n -= 4; d += 4; s += 4; } + if (n >= 2) { byte_move_2(d, s); } +} + +/** Same as `memcmp(buf, pat, 2) == 0`. */ +static_inline bool byte_match_2(void *buf, const char *pat) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + v16_uni u1, u2; + memcpy(&u1, buf, 2); + memcpy(&u2, pat, 2); + return u1.u == u2.u; +#else + return ((char *)buf)[0] == ((const char *)pat)[0] && + ((char *)buf)[1] == ((const char *)pat)[1]; +#endif +} + +/** Same as `memcmp(buf, pat, 4) == 0`. */ +static_inline bool byte_match_4(void *buf, const char *pat) { +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + v32_uni u1, u2; + memcpy(&u1, buf, 4); + memcpy(&u2, pat, 4); + return u1.u == u2.u; +#else + return ((char *)buf)[0] == ((const char *)pat)[0] && + ((char *)buf)[1] == ((const char *)pat)[1] && + ((char *)buf)[2] == ((const char *)pat)[2] && + ((char *)buf)[3] == ((const char *)pat)[3]; +#endif +} + +/** Loads 2 bytes from `src` as a u16 (native-endian). */ +static_inline u16 byte_load_2(const void *src) { + v16_uni uni; +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(&uni, src, 2); +#else + uni.v.c[0] = ((const char *)src)[0]; + uni.v.c[1] = ((const char *)src)[1]; +#endif + return uni.u; +} + +/** Loads 3 bytes from `src` as a u32 (native-endian). */ +static_inline u32 byte_load_3(const void *src) { + v32_uni uni; +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(&uni, src, 2); + uni.v.c[2] = ((const char *)src)[2]; + uni.v.c[3] = 0; +#else + uni.v.c[0] = ((const char *)src)[0]; + uni.v.c[1] = ((const char *)src)[1]; + uni.v.c[2] = ((const char *)src)[2]; + uni.v.c[3] = 0; +#endif + return uni.u; +} + +/** Loads 4 bytes from `src` as a u32 (native-endian). */ +static_inline u32 byte_load_4(const void *src) { + v32_uni uni; +#if !YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS + memcpy(&uni, src, 4); +#else + uni.v.c[0] = ((const char *)src)[0]; + uni.v.c[1] = ((const char *)src)[1]; + uni.v.c[2] = ((const char *)src)[2]; + uni.v.c[3] = ((const char *)src)[3]; +#endif + return uni.u; +} + + + +/*============================================================================== + * MARK: - Character Utils (Private) + * These lookup tables were generated by `misc/make_tables.c`. + *============================================================================*/ + +/* char_table1 */ +#define CHAR_TYPE_ASCII (1 << 0) /* Except: ["\], [0x00-0x1F, 0x80-0xFF] */ +#define CHAR_TYPE_ASCII_SQ (1 << 1) /* Except: ['\], [0x00-0x1F, 0x80-0xFF] */ +#define CHAR_TYPE_SPACE (1 << 2) /* Whitespace: [ \t\n\r] */ +#define CHAR_TYPE_SPACE_EXT (1 << 3) /* Whitespace: [ \t\n\r\v\f], JSON5 */ +#define CHAR_TYPE_NUM (1 << 4) /* Number: [.-+0-9] */ +#define CHAR_TYPE_COMMENT (1 << 5) /* Comment: [/] */ + +/* char_table2 */ +#define CHAR_TYPE_EOL (1 << 0) /* End of line: [\r\n] */ +#define CHAR_TYPE_EOL_EXT (1 << 1) /* End of line: [\r\n], JSON5 */ +#define CHAR_TYPE_ID_START (1 << 2) /* ID start: [_$A-Za-z\], U+0080+ */ +#define CHAR_TYPE_ID_NEXT (1 << 3) /* ID next: [_$A-Za-z0-9\], U+0080+ */ +#define CHAR_TYPE_ID_ASCII (1 << 4) /* ID next ASCII: [_$A-Za-z0-9] */ + +/* char_table3 */ +#define CHAR_TYPE_SIGN (1 << 0) /* [-+] */ +#define CHAR_TYPE_DIGIT (1 << 1) /* [0-9] */ +#define CHAR_TYPE_NONZERO (1 << 2) /* [1-9] */ +#define CHAR_TYPE_EXP (1 << 3) /* [eE] */ +#define CHAR_TYPE_DOT (1 << 4) /* [.] */ + +static const u8 char_table1[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0x0C, 0x08, 0x08, 0x0C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x03, 0x02, 0x03, 0x03, 0x03, 0x03, 0x01, + 0x03, 0x03, 0x03, 0x13, 0x03, 0x13, 0x13, 0x23, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x00, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +static const u8 char_table2[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x00, 0x0C, 0x00, 0x00, 0x1C, + 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C +}; + +static const u8 char_table3[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x00, + 0x02, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/** Match a whitespace: [ \t\n\r]. */ +static_inline bool char_is_space(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_SPACE); +} + +/** Match an extended whitespace: [ \t\n\r\\v\\f], JSON5 whitespace. */ +static_inline bool char_is_space_ext(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_SPACE_EXT); +} + +/** Match a JSON number: [.-+0-9]. */ +static_inline bool char_is_num(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_NUM); +} + +/** Match an ASCII character in string: ["\], [0x00-0x1F, 0x80-0xFF]. */ +static_inline bool char_is_ascii_skip(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_ASCII); +} + +/** Match an ASCII character single-quoted: ['\], [0x00-0x1F, 0x80-0xFF]. */ +static_inline bool char_is_ascii_skip_sq(u8 c) { + return !!(char_table1[c] & CHAR_TYPE_ASCII_SQ); +} + +/** Match a trivia character: extended whitespace or comment. */ +static_inline bool char_is_trivia(u8 c) { + return !!(char_table1[c] & (CHAR_TYPE_SPACE_EXT | CHAR_TYPE_COMMENT)); +} + +/** Match a line end character: [\r\n]. */ +static_inline bool char_is_eol(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_EOL); +} + +/** Match an extended line end character: [\r\n], JSON5 line terminator. */ +static_inline bool char_is_eol_ext(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_EOL_EXT); +} + +/** Match an identifier name start: [_$A-Za-z\], U+0080+. */ +static_inline bool char_is_id_start(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_ID_START); +} + +/** Match an identifier name next: [_$A-Za-z0-9\], U+0080+. */ +static_inline bool char_is_id_next(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_ID_NEXT); +} + +/** Match an identifier name ASCII: [_$A-Za-z0-9]. */ +static_inline bool char_is_id_ascii(u8 c) { + return !!(char_table2[c] & CHAR_TYPE_ID_ASCII); +} + +/** Match a sign: [+-] */ +static_inline bool char_is_sign(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_SIGN); +} + +/** Match a none-zero digit: [1-9] */ +static_inline bool char_is_nonzero(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_NONZERO); +} + +/** Match a digit: [0-9] */ +static_inline bool char_is_digit(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_DIGIT); +} + +/** Match an exponent sign: [eE]. */ +static_inline bool char_is_exp(u8 d) { + return !!(char_table3[d] & CHAR_TYPE_EXP); +} + +/** Match a floating point indicator: [.eE]. */ +static_inline bool char_is_fp(u8 d) { + return !!(char_table3[d] & (CHAR_TYPE_DOT | CHAR_TYPE_EXP)); +} + +/** Match a digit or floating point indicator: [0-9.eE]. */ +static_inline bool char_is_digit_or_fp(u8 d) { + return !!(char_table3[d] & (CHAR_TYPE_DIGIT | CHAR_TYPE_DOT | + CHAR_TYPE_EXP)); +} + +/** Match a JSON container: `{` or `[`. */ +static_inline bool char_is_ctn(u8 c) { + return (c & 0xDF) == 0x5B; /* '[': 0x5B, '{': 0x7B */ +} + +/** Convert ASCII letter to lowercase; valid only for [A-Za-z]. */ +static_inline u8 char_to_lower(u8 c) { + return c | 0x20; +} + +/** Match UTF-8 byte order mask. */ +static_inline bool is_utf8_bom(const u8 *cur) { + return byte_load_3(cur) == byte_load_3("\xEF\xBB\xBF"); +} + +/** Match UTF-16 byte order mask. */ +static_inline bool is_utf16_bom(const u8 *cur) { + return byte_load_2(cur) == byte_load_2("\xFE\xFF") || + byte_load_2(cur) == byte_load_2("\xFF\xFE"); +} + +/** Match UTF-32 byte order mask, need length check to avoid zero padding. */ +static_inline bool is_utf32_bom(const u8 *cur) { + return byte_load_4(cur) == byte_load_4("\x00\x00\xFE\xFF") || + byte_load_4(cur) == byte_load_4("\xFF\xFE\x00\x00"); +} + +/** Get the extended line end length. Used with `char_is_eol_ext`. */ +static_inline usize ext_eol_len(const u8 *cur) { + if (cur[0] < 0x80) return 1; + if (cur[1] == 0x80 && (cur[2] == 0xA8 || cur[2] == 0xA9)) return 3; + return 0; +} + +/** Get the extended whitespace length. Used with `char_is_space_ext`. */ +static_inline usize ext_space_len(const u8 *cur) { + if (cur[0] < 0x80) { + return 1; + } else if (byte_load_2(cur) == byte_load_2("\xC2\xA0")) { + return 2; + } else if (byte_load_2(cur) == byte_load_2("\xE2\x80")) { + if (cur[2] >= 0x80 && cur[2] <= 0x8A) return 3; + if (cur[2] == 0xA8 || cur[2] == 0xA9 || cur[2] == 0xAF) return 3; + } else { + u32 uni = byte_load_3(cur); + if (uni == byte_load_3("\xE1\x9A\x80") || + uni == byte_load_3("\xE2\x81\x9F") || + uni == byte_load_3("\xE3\x80\x80") || + uni == byte_load_3("\xEF\xBB\xBF")) return 3; + } + return 0; +} + + + +/*============================================================================== + * MARK: - Hex Character Reader (Private) + * This function is used by JSON reader to read escaped characters. + *============================================================================*/ + +/** + This table is used to convert 4 hex character sequence to a number. + A valid hex character [0-9A-Fa-f] will mapped to it's raw number [0x00, 0x0F], + an invalid hex character will mapped to [0xF0]. + (generate with misc/make_tables.c) + */ +static const u8 hex_conv_table[256] = { + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0 +}; + +/** Load 4 hex characters to `u16`, return true on valid input. */ +static_inline bool hex_load_4(const u8 *src, u16 *dst) { + u16 c0 = hex_conv_table[src[0]]; + u16 c1 = hex_conv_table[src[1]]; + u16 c2 = hex_conv_table[src[2]]; + u16 c3 = hex_conv_table[src[3]]; + u16 t0 = (u16)((c0 << 8) | c2); + u16 t1 = (u16)((c1 << 8) | c3); + *dst = (u16)((t0 << 4) | t1); + return ((t0 | t1) & (u16)0xF0F0) == 0; +} + +/** Load 2 hex characters to `u8`, return true on valid input. */ +static_inline bool hex_load_2(const u8 *src, u8 *dst) { + u8 c0 = hex_conv_table[src[0]]; + u8 c1 = hex_conv_table[src[1]]; + *dst = (u8)((c0 << 4) | c1); + return ((c0 | c1) & 0xF0) == 0; +} + +/** Match a hexadecimal numeric character: [0-9a-fA-F]. */ +static_inline bool char_is_hex(u8 c) { + return hex_conv_table[c] != 0xF0; +} + + + +/*============================================================================== + * MARK: - UTF8 Validation (Private) + * Each Unicode code point is encoded using 1 to 4 bytes in UTF-8. + * Validation is performed using a 4-byte mask and pattern-based approach, + * which requires the input data to be padded with four zero bytes at the end. + *============================================================================*/ + +/* Macro for concatenating four u8 into a u32 and keeping the byte order. */ +#if YYJSON_ENDIAN == YYJSON_LITTLE_ENDIAN +# define utf8_seq_def(name, a, b, c, d) \ + static const u32 utf8_seq_##name = 0x##d##c##b##a##UL; +# define utf8_seq(name) utf8_seq_##name +#elif YYJSON_ENDIAN == YYJSON_BIG_ENDIAN +# define utf8_seq_def(name, a, b, c, d) \ + static const u32 utf8_seq_##name = 0x##a##b##c##d##UL; +# define utf8_seq(name) utf8_seq_##name +#else +# define utf8_seq_def(name, a, b, c, d) \ + static const v32_uni utf8_uni_##name = {{ 0x##a, 0x##b, 0x##c, 0x##d }}; +# define utf8_seq(name) utf8_uni_##name.u +#endif + +/* + 1-byte sequence (U+0000 to U+007F) + bit min [.......0] (U+0000) + bit max [.1111111] (U+007F) + bit mask [x.......] (80) + bit pattern [0.......] (00) + */ +utf8_seq_def(b1_mask, 80, 00, 00, 00) +utf8_seq_def(b1_patt, 00, 00, 00, 00) +#define is_utf8_seq1(uni) ( \ + ((uni & utf8_seq(b1_mask)) == utf8_seq(b1_patt)) ) + +/* + 2-byte sequence (U+0080 to U+07FF) + bit min [......10 ..000000] (U+0080) + bit max [...11111 ..111111] (U+07FF) + bit mask [xxx..... xx......] (E0 C0) + bit pattern [110..... 10......] (C0 80) + bit require [...xxxx. ........] (1E 00) + */ +utf8_seq_def(b2_mask, E0, C0, 00, 00) +utf8_seq_def(b2_patt, C0, 80, 00, 00) +utf8_seq_def(b2_requ, 1E, 00, 00, 00) +#define is_utf8_seq2(uni) ( \ + ((uni & utf8_seq(b2_mask)) == utf8_seq(b2_patt)) && \ + ((uni & utf8_seq(b2_requ))) ) + +/* + 3-byte sequence (U+0800 to U+FFFF) + bit min [........ ..100000 ..000000] (U+0800) + bit max [....1111 ..111111 ..111111] (U+FFFF) + bit mask [xxxx.... xx...... xx......] (F0 C0 C0) + bit pattern [1110.... 10...... 10......] (E0 80 80) + bit require [....xxxx ..x..... ........] (0F 20 00) + + 3-byte invalid sequence, reserved for surrogate halves (U+D800 to U+DFFF) + bit min [....1101 ..100000 ..000000] (U+D800) + bit max [....1101 ..111111 ..111111] (U+DFFF) + bit mask [....xxxx ..x..... ........] (0F 20 00) + bit pattern [....1101 ..1..... ........] (0D 20 00) + */ +utf8_seq_def(b3_mask, F0, C0, C0, 00) +utf8_seq_def(b3_patt, E0, 80, 80, 00) +utf8_seq_def(b3_requ, 0F, 20, 00, 00) +utf8_seq_def(b3_erro, 0D, 20, 00, 00) +#define is_utf8_seq3(uni) ( \ + ((uni & utf8_seq(b3_mask)) == utf8_seq(b3_patt)) && \ + ((tmp = (uni & utf8_seq(b3_requ)))) && \ + ((tmp != utf8_seq(b3_erro))) ) + +/* + 4-byte sequence (U+10000 to U+10FFFF) + bit min [........ ...10000 ..000000 ..000000] (U+10000) + bit max [.....100 ..001111 ..111111 ..111111] (U+10FFFF) + bit mask [xxxxx... xx...... xx...... xx......] (F8 C0 C0 C0) + bit pattern [11110... 10...... 10...... 10......] (F0 80 80 80) + bit require [.....xxx ..xx.... ........ ........] (07 30 00 00) + bit require 1 [.....x.. ........ ........ ........] (04 00 00 00) + bit require 2 [......xx ..xx.... ........ ........] (03 30 00 00) + */ +utf8_seq_def(b4_mask, F8, C0, C0, C0) +utf8_seq_def(b4_patt, F0, 80, 80, 80) +utf8_seq_def(b4_requ, 07, 30, 00, 00) +utf8_seq_def(b4_req1, 04, 00, 00, 00) +utf8_seq_def(b4_req2, 03, 30, 00, 00) +#define is_utf8_seq4(uni) ( \ + ((uni & utf8_seq(b4_mask)) == utf8_seq(b4_patt)) && \ + ((tmp = (uni & utf8_seq(b4_requ)))) && \ + ((tmp & utf8_seq(b4_req1)) == 0 || (tmp & utf8_seq(b4_req2)) == 0) ) + + + +/*============================================================================== + * MARK: - Power10 Lookup Table (Private) + * These data are used by the floating-point number reader and writer. + *============================================================================*/ + +#if !YYJSON_DISABLE_FAST_FP_CONV + +/** Maximum pow10 exponent that can be represented exactly as a float64. */ +#define F64_POW10_MAX_EXACT_EXP 22 + +/** Cached pow10 table. */ +static const f64 f64_pow10_table[F64_POW10_MAX_EXACT_EXP + 1] = { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, + 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 +}; + +/** Maximum pow10 exponent that can be represented exactly as a uint64. */ +#define U64_POW10_MAX_EXACT_EXP 19 + +/** Table: [ 10^0, ..., 10^19 ] (generate with misc/make_tables.c) */ +static const u64 u64_pow10_table[U64_POW10_MAX_EXACT_EXP + 1] = { + U64(0x00000000, 0x00000001), U64(0x00000000, 0x0000000A), + U64(0x00000000, 0x00000064), U64(0x00000000, 0x000003E8), + U64(0x00000000, 0x00002710), U64(0x00000000, 0x000186A0), + U64(0x00000000, 0x000F4240), U64(0x00000000, 0x00989680), + U64(0x00000000, 0x05F5E100), U64(0x00000000, 0x3B9ACA00), + U64(0x00000002, 0x540BE400), U64(0x00000017, 0x4876E800), + U64(0x000000E8, 0xD4A51000), U64(0x00000918, 0x4E72A000), + U64(0x00005AF3, 0x107A4000), U64(0x00038D7E, 0xA4C68000), + U64(0x002386F2, 0x6FC10000), U64(0x01634578, 0x5D8A0000), + U64(0x0DE0B6B3, 0xA7640000), U64(0x8AC72304, 0x89E80000) +}; + +/** Minimum decimal exponent in pow10_sig_table. */ +#define POW10_SIG_TABLE_MIN_EXP -343 + +/** Maximum decimal exponent in pow10_sig_table. */ +#define POW10_SIG_TABLE_MAX_EXP 324 + +/** Minimum exact decimal exponent in pow10_sig_table */ +#define POW10_SIG_TABLE_MIN_EXACT_EXP 0 + +/** Maximum exact decimal exponent in pow10_sig_table */ +#define POW10_SIG_TABLE_MAX_EXACT_EXP 55 + +/** Normalized significant 128 bits of pow10, no rounded up (size: 10.4KB). + This lookup table is used by both the double number reader and writer. + (generate with misc/make_tables.c) */ +static const u64 pow10_sig_table[] = { + U64(0xBF29DCAB, 0xA82FDEAE), U64(0x7432EE87, 0x3880FC33), /* ~= 10^-343 */ + U64(0xEEF453D6, 0x923BD65A), U64(0x113FAA29, 0x06A13B3F), /* ~= 10^-342 */ + U64(0x9558B466, 0x1B6565F8), U64(0x4AC7CA59, 0xA424C507), /* ~= 10^-341 */ + U64(0xBAAEE17F, 0xA23EBF76), U64(0x5D79BCF0, 0x0D2DF649), /* ~= 10^-340 */ + U64(0xE95A99DF, 0x8ACE6F53), U64(0xF4D82C2C, 0x107973DC), /* ~= 10^-339 */ + U64(0x91D8A02B, 0xB6C10594), U64(0x79071B9B, 0x8A4BE869), /* ~= 10^-338 */ + U64(0xB64EC836, 0xA47146F9), U64(0x9748E282, 0x6CDEE284), /* ~= 10^-337 */ + U64(0xE3E27A44, 0x4D8D98B7), U64(0xFD1B1B23, 0x08169B25), /* ~= 10^-336 */ + U64(0x8E6D8C6A, 0xB0787F72), U64(0xFE30F0F5, 0xE50E20F7), /* ~= 10^-335 */ + U64(0xB208EF85, 0x5C969F4F), U64(0xBDBD2D33, 0x5E51A935), /* ~= 10^-334 */ + U64(0xDE8B2B66, 0xB3BC4723), U64(0xAD2C7880, 0x35E61382), /* ~= 10^-333 */ + U64(0x8B16FB20, 0x3055AC76), U64(0x4C3BCB50, 0x21AFCC31), /* ~= 10^-332 */ + U64(0xADDCB9E8, 0x3C6B1793), U64(0xDF4ABE24, 0x2A1BBF3D), /* ~= 10^-331 */ + U64(0xD953E862, 0x4B85DD78), U64(0xD71D6DAD, 0x34A2AF0D), /* ~= 10^-330 */ + U64(0x87D4713D, 0x6F33AA6B), U64(0x8672648C, 0x40E5AD68), /* ~= 10^-329 */ + U64(0xA9C98D8C, 0xCB009506), U64(0x680EFDAF, 0x511F18C2), /* ~= 10^-328 */ + U64(0xD43BF0EF, 0xFDC0BA48), U64(0x0212BD1B, 0x2566DEF2), /* ~= 10^-327 */ + U64(0x84A57695, 0xFE98746D), U64(0x014BB630, 0xF7604B57), /* ~= 10^-326 */ + U64(0xA5CED43B, 0x7E3E9188), U64(0x419EA3BD, 0x35385E2D), /* ~= 10^-325 */ + U64(0xCF42894A, 0x5DCE35EA), U64(0x52064CAC, 0x828675B9), /* ~= 10^-324 */ + U64(0x818995CE, 0x7AA0E1B2), U64(0x7343EFEB, 0xD1940993), /* ~= 10^-323 */ + U64(0xA1EBFB42, 0x19491A1F), U64(0x1014EBE6, 0xC5F90BF8), /* ~= 10^-322 */ + U64(0xCA66FA12, 0x9F9B60A6), U64(0xD41A26E0, 0x77774EF6), /* ~= 10^-321 */ + U64(0xFD00B897, 0x478238D0), U64(0x8920B098, 0x955522B4), /* ~= 10^-320 */ + U64(0x9E20735E, 0x8CB16382), U64(0x55B46E5F, 0x5D5535B0), /* ~= 10^-319 */ + U64(0xC5A89036, 0x2FDDBC62), U64(0xEB2189F7, 0x34AA831D), /* ~= 10^-318 */ + U64(0xF712B443, 0xBBD52B7B), U64(0xA5E9EC75, 0x01D523E4), /* ~= 10^-317 */ + U64(0x9A6BB0AA, 0x55653B2D), U64(0x47B233C9, 0x2125366E), /* ~= 10^-316 */ + U64(0xC1069CD4, 0xEABE89F8), U64(0x999EC0BB, 0x696E840A), /* ~= 10^-315 */ + U64(0xF148440A, 0x256E2C76), U64(0xC00670EA, 0x43CA250D), /* ~= 10^-314 */ + U64(0x96CD2A86, 0x5764DBCA), U64(0x38040692, 0x6A5E5728), /* ~= 10^-313 */ + U64(0xBC807527, 0xED3E12BC), U64(0xC6050837, 0x04F5ECF2), /* ~= 10^-312 */ + U64(0xEBA09271, 0xE88D976B), U64(0xF7864A44, 0xC633682E), /* ~= 10^-311 */ + U64(0x93445B87, 0x31587EA3), U64(0x7AB3EE6A, 0xFBE0211D), /* ~= 10^-310 */ + U64(0xB8157268, 0xFDAE9E4C), U64(0x5960EA05, 0xBAD82964), /* ~= 10^-309 */ + U64(0xE61ACF03, 0x3D1A45DF), U64(0x6FB92487, 0x298E33BD), /* ~= 10^-308 */ + U64(0x8FD0C162, 0x06306BAB), U64(0xA5D3B6D4, 0x79F8E056), /* ~= 10^-307 */ + U64(0xB3C4F1BA, 0x87BC8696), U64(0x8F48A489, 0x9877186C), /* ~= 10^-306 */ + U64(0xE0B62E29, 0x29ABA83C), U64(0x331ACDAB, 0xFE94DE87), /* ~= 10^-305 */ + U64(0x8C71DCD9, 0xBA0B4925), U64(0x9FF0C08B, 0x7F1D0B14), /* ~= 10^-304 */ + U64(0xAF8E5410, 0x288E1B6F), U64(0x07ECF0AE, 0x5EE44DD9), /* ~= 10^-303 */ + U64(0xDB71E914, 0x32B1A24A), U64(0xC9E82CD9, 0xF69D6150), /* ~= 10^-302 */ + U64(0x892731AC, 0x9FAF056E), U64(0xBE311C08, 0x3A225CD2), /* ~= 10^-301 */ + U64(0xAB70FE17, 0xC79AC6CA), U64(0x6DBD630A, 0x48AAF406), /* ~= 10^-300 */ + U64(0xD64D3D9D, 0xB981787D), U64(0x092CBBCC, 0xDAD5B108), /* ~= 10^-299 */ + U64(0x85F04682, 0x93F0EB4E), U64(0x25BBF560, 0x08C58EA5), /* ~= 10^-298 */ + U64(0xA76C5823, 0x38ED2621), U64(0xAF2AF2B8, 0x0AF6F24E), /* ~= 10^-297 */ + U64(0xD1476E2C, 0x07286FAA), U64(0x1AF5AF66, 0x0DB4AEE1), /* ~= 10^-296 */ + U64(0x82CCA4DB, 0x847945CA), U64(0x50D98D9F, 0xC890ED4D), /* ~= 10^-295 */ + U64(0xA37FCE12, 0x6597973C), U64(0xE50FF107, 0xBAB528A0), /* ~= 10^-294 */ + U64(0xCC5FC196, 0xFEFD7D0C), U64(0x1E53ED49, 0xA96272C8), /* ~= 10^-293 */ + U64(0xFF77B1FC, 0xBEBCDC4F), U64(0x25E8E89C, 0x13BB0F7A), /* ~= 10^-292 */ + U64(0x9FAACF3D, 0xF73609B1), U64(0x77B19161, 0x8C54E9AC), /* ~= 10^-291 */ + U64(0xC795830D, 0x75038C1D), U64(0xD59DF5B9, 0xEF6A2417), /* ~= 10^-290 */ + U64(0xF97AE3D0, 0xD2446F25), U64(0x4B057328, 0x6B44AD1D), /* ~= 10^-289 */ + U64(0x9BECCE62, 0x836AC577), U64(0x4EE367F9, 0x430AEC32), /* ~= 10^-288 */ + U64(0xC2E801FB, 0x244576D5), U64(0x229C41F7, 0x93CDA73F), /* ~= 10^-287 */ + U64(0xF3A20279, 0xED56D48A), U64(0x6B435275, 0x78C1110F), /* ~= 10^-286 */ + U64(0x9845418C, 0x345644D6), U64(0x830A1389, 0x6B78AAA9), /* ~= 10^-285 */ + U64(0xBE5691EF, 0x416BD60C), U64(0x23CC986B, 0xC656D553), /* ~= 10^-284 */ + U64(0xEDEC366B, 0x11C6CB8F), U64(0x2CBFBE86, 0xB7EC8AA8), /* ~= 10^-283 */ + U64(0x94B3A202, 0xEB1C3F39), U64(0x7BF7D714, 0x32F3D6A9), /* ~= 10^-282 */ + U64(0xB9E08A83, 0xA5E34F07), U64(0xDAF5CCD9, 0x3FB0CC53), /* ~= 10^-281 */ + U64(0xE858AD24, 0x8F5C22C9), U64(0xD1B3400F, 0x8F9CFF68), /* ~= 10^-280 */ + U64(0x91376C36, 0xD99995BE), U64(0x23100809, 0xB9C21FA1), /* ~= 10^-279 */ + U64(0xB5854744, 0x8FFFFB2D), U64(0xABD40A0C, 0x2832A78A), /* ~= 10^-278 */ + U64(0xE2E69915, 0xB3FFF9F9), U64(0x16C90C8F, 0x323F516C), /* ~= 10^-277 */ + U64(0x8DD01FAD, 0x907FFC3B), U64(0xAE3DA7D9, 0x7F6792E3), /* ~= 10^-276 */ + U64(0xB1442798, 0xF49FFB4A), U64(0x99CD11CF, 0xDF41779C), /* ~= 10^-275 */ + U64(0xDD95317F, 0x31C7FA1D), U64(0x40405643, 0xD711D583), /* ~= 10^-274 */ + U64(0x8A7D3EEF, 0x7F1CFC52), U64(0x482835EA, 0x666B2572), /* ~= 10^-273 */ + U64(0xAD1C8EAB, 0x5EE43B66), U64(0xDA324365, 0x0005EECF), /* ~= 10^-272 */ + U64(0xD863B256, 0x369D4A40), U64(0x90BED43E, 0x40076A82), /* ~= 10^-271 */ + U64(0x873E4F75, 0xE2224E68), U64(0x5A7744A6, 0xE804A291), /* ~= 10^-270 */ + U64(0xA90DE353, 0x5AAAE202), U64(0x711515D0, 0xA205CB36), /* ~= 10^-269 */ + U64(0xD3515C28, 0x31559A83), U64(0x0D5A5B44, 0xCA873E03), /* ~= 10^-268 */ + U64(0x8412D999, 0x1ED58091), U64(0xE858790A, 0xFE9486C2), /* ~= 10^-267 */ + U64(0xA5178FFF, 0x668AE0B6), U64(0x626E974D, 0xBE39A872), /* ~= 10^-266 */ + U64(0xCE5D73FF, 0x402D98E3), U64(0xFB0A3D21, 0x2DC8128F), /* ~= 10^-265 */ + U64(0x80FA687F, 0x881C7F8E), U64(0x7CE66634, 0xBC9D0B99), /* ~= 10^-264 */ + U64(0xA139029F, 0x6A239F72), U64(0x1C1FFFC1, 0xEBC44E80), /* ~= 10^-263 */ + U64(0xC9874347, 0x44AC874E), U64(0xA327FFB2, 0x66B56220), /* ~= 10^-262 */ + U64(0xFBE91419, 0x15D7A922), U64(0x4BF1FF9F, 0x0062BAA8), /* ~= 10^-261 */ + U64(0x9D71AC8F, 0xADA6C9B5), U64(0x6F773FC3, 0x603DB4A9), /* ~= 10^-260 */ + U64(0xC4CE17B3, 0x99107C22), U64(0xCB550FB4, 0x384D21D3), /* ~= 10^-259 */ + U64(0xF6019DA0, 0x7F549B2B), U64(0x7E2A53A1, 0x46606A48), /* ~= 10^-258 */ + U64(0x99C10284, 0x4F94E0FB), U64(0x2EDA7444, 0xCBFC426D), /* ~= 10^-257 */ + U64(0xC0314325, 0x637A1939), U64(0xFA911155, 0xFEFB5308), /* ~= 10^-256 */ + U64(0xF03D93EE, 0xBC589F88), U64(0x793555AB, 0x7EBA27CA), /* ~= 10^-255 */ + U64(0x96267C75, 0x35B763B5), U64(0x4BC1558B, 0x2F3458DE), /* ~= 10^-254 */ + U64(0xBBB01B92, 0x83253CA2), U64(0x9EB1AAED, 0xFB016F16), /* ~= 10^-253 */ + U64(0xEA9C2277, 0x23EE8BCB), U64(0x465E15A9, 0x79C1CADC), /* ~= 10^-252 */ + U64(0x92A1958A, 0x7675175F), U64(0x0BFACD89, 0xEC191EC9), /* ~= 10^-251 */ + U64(0xB749FAED, 0x14125D36), U64(0xCEF980EC, 0x671F667B), /* ~= 10^-250 */ + U64(0xE51C79A8, 0x5916F484), U64(0x82B7E127, 0x80E7401A), /* ~= 10^-249 */ + U64(0x8F31CC09, 0x37AE58D2), U64(0xD1B2ECB8, 0xB0908810), /* ~= 10^-248 */ + U64(0xB2FE3F0B, 0x8599EF07), U64(0x861FA7E6, 0xDCB4AA15), /* ~= 10^-247 */ + U64(0xDFBDCECE, 0x67006AC9), U64(0x67A791E0, 0x93E1D49A), /* ~= 10^-246 */ + U64(0x8BD6A141, 0x006042BD), U64(0xE0C8BB2C, 0x5C6D24E0), /* ~= 10^-245 */ + U64(0xAECC4991, 0x4078536D), U64(0x58FAE9F7, 0x73886E18), /* ~= 10^-244 */ + U64(0xDA7F5BF5, 0x90966848), U64(0xAF39A475, 0x506A899E), /* ~= 10^-243 */ + U64(0x888F9979, 0x7A5E012D), U64(0x6D8406C9, 0x52429603), /* ~= 10^-242 */ + U64(0xAAB37FD7, 0xD8F58178), U64(0xC8E5087B, 0xA6D33B83), /* ~= 10^-241 */ + U64(0xD5605FCD, 0xCF32E1D6), U64(0xFB1E4A9A, 0x90880A64), /* ~= 10^-240 */ + U64(0x855C3BE0, 0xA17FCD26), U64(0x5CF2EEA0, 0x9A55067F), /* ~= 10^-239 */ + U64(0xA6B34AD8, 0xC9DFC06F), U64(0xF42FAA48, 0xC0EA481E), /* ~= 10^-238 */ + U64(0xD0601D8E, 0xFC57B08B), U64(0xF13B94DA, 0xF124DA26), /* ~= 10^-237 */ + U64(0x823C1279, 0x5DB6CE57), U64(0x76C53D08, 0xD6B70858), /* ~= 10^-236 */ + U64(0xA2CB1717, 0xB52481ED), U64(0x54768C4B, 0x0C64CA6E), /* ~= 10^-235 */ + U64(0xCB7DDCDD, 0xA26DA268), U64(0xA9942F5D, 0xCF7DFD09), /* ~= 10^-234 */ + U64(0xFE5D5415, 0x0B090B02), U64(0xD3F93B35, 0x435D7C4C), /* ~= 10^-233 */ + U64(0x9EFA548D, 0x26E5A6E1), U64(0xC47BC501, 0x4A1A6DAF), /* ~= 10^-232 */ + U64(0xC6B8E9B0, 0x709F109A), U64(0x359AB641, 0x9CA1091B), /* ~= 10^-231 */ + U64(0xF867241C, 0x8CC6D4C0), U64(0xC30163D2, 0x03C94B62), /* ~= 10^-230 */ + U64(0x9B407691, 0xD7FC44F8), U64(0x79E0DE63, 0x425DCF1D), /* ~= 10^-229 */ + U64(0xC2109436, 0x4DFB5636), U64(0x985915FC, 0x12F542E4), /* ~= 10^-228 */ + U64(0xF294B943, 0xE17A2BC4), U64(0x3E6F5B7B, 0x17B2939D), /* ~= 10^-227 */ + U64(0x979CF3CA, 0x6CEC5B5A), U64(0xA705992C, 0xEECF9C42), /* ~= 10^-226 */ + U64(0xBD8430BD, 0x08277231), U64(0x50C6FF78, 0x2A838353), /* ~= 10^-225 */ + U64(0xECE53CEC, 0x4A314EBD), U64(0xA4F8BF56, 0x35246428), /* ~= 10^-224 */ + U64(0x940F4613, 0xAE5ED136), U64(0x871B7795, 0xE136BE99), /* ~= 10^-223 */ + U64(0xB9131798, 0x99F68584), U64(0x28E2557B, 0x59846E3F), /* ~= 10^-222 */ + U64(0xE757DD7E, 0xC07426E5), U64(0x331AEADA, 0x2FE589CF), /* ~= 10^-221 */ + U64(0x9096EA6F, 0x3848984F), U64(0x3FF0D2C8, 0x5DEF7621), /* ~= 10^-220 */ + U64(0xB4BCA50B, 0x065ABE63), U64(0x0FED077A, 0x756B53A9), /* ~= 10^-219 */ + U64(0xE1EBCE4D, 0xC7F16DFB), U64(0xD3E84959, 0x12C62894), /* ~= 10^-218 */ + U64(0x8D3360F0, 0x9CF6E4BD), U64(0x64712DD7, 0xABBBD95C), /* ~= 10^-217 */ + U64(0xB080392C, 0xC4349DEC), U64(0xBD8D794D, 0x96AACFB3), /* ~= 10^-216 */ + U64(0xDCA04777, 0xF541C567), U64(0xECF0D7A0, 0xFC5583A0), /* ~= 10^-215 */ + U64(0x89E42CAA, 0xF9491B60), U64(0xF41686C4, 0x9DB57244), /* ~= 10^-214 */ + U64(0xAC5D37D5, 0xB79B6239), U64(0x311C2875, 0xC522CED5), /* ~= 10^-213 */ + U64(0xD77485CB, 0x25823AC7), U64(0x7D633293, 0x366B828B), /* ~= 10^-212 */ + U64(0x86A8D39E, 0xF77164BC), U64(0xAE5DFF9C, 0x02033197), /* ~= 10^-211 */ + U64(0xA8530886, 0xB54DBDEB), U64(0xD9F57F83, 0x0283FDFC), /* ~= 10^-210 */ + U64(0xD267CAA8, 0x62A12D66), U64(0xD072DF63, 0xC324FD7B), /* ~= 10^-209 */ + U64(0x8380DEA9, 0x3DA4BC60), U64(0x4247CB9E, 0x59F71E6D), /* ~= 10^-208 */ + U64(0xA4611653, 0x8D0DEB78), U64(0x52D9BE85, 0xF074E608), /* ~= 10^-207 */ + U64(0xCD795BE8, 0x70516656), U64(0x67902E27, 0x6C921F8B), /* ~= 10^-206 */ + U64(0x806BD971, 0x4632DFF6), U64(0x00BA1CD8, 0xA3DB53B6), /* ~= 10^-205 */ + U64(0xA086CFCD, 0x97BF97F3), U64(0x80E8A40E, 0xCCD228A4), /* ~= 10^-204 */ + U64(0xC8A883C0, 0xFDAF7DF0), U64(0x6122CD12, 0x8006B2CD), /* ~= 10^-203 */ + U64(0xFAD2A4B1, 0x3D1B5D6C), U64(0x796B8057, 0x20085F81), /* ~= 10^-202 */ + U64(0x9CC3A6EE, 0xC6311A63), U64(0xCBE33036, 0x74053BB0), /* ~= 10^-201 */ + U64(0xC3F490AA, 0x77BD60FC), U64(0xBEDBFC44, 0x11068A9C), /* ~= 10^-200 */ + U64(0xF4F1B4D5, 0x15ACB93B), U64(0xEE92FB55, 0x15482D44), /* ~= 10^-199 */ + U64(0x99171105, 0x2D8BF3C5), U64(0x751BDD15, 0x2D4D1C4A), /* ~= 10^-198 */ + U64(0xBF5CD546, 0x78EEF0B6), U64(0xD262D45A, 0x78A0635D), /* ~= 10^-197 */ + U64(0xEF340A98, 0x172AACE4), U64(0x86FB8971, 0x16C87C34), /* ~= 10^-196 */ + U64(0x9580869F, 0x0E7AAC0E), U64(0xD45D35E6, 0xAE3D4DA0), /* ~= 10^-195 */ + U64(0xBAE0A846, 0xD2195712), U64(0x89748360, 0x59CCA109), /* ~= 10^-194 */ + U64(0xE998D258, 0x869FACD7), U64(0x2BD1A438, 0x703FC94B), /* ~= 10^-193 */ + U64(0x91FF8377, 0x5423CC06), U64(0x7B6306A3, 0x4627DDCF), /* ~= 10^-192 */ + U64(0xB67F6455, 0x292CBF08), U64(0x1A3BC84C, 0x17B1D542), /* ~= 10^-191 */ + U64(0xE41F3D6A, 0x7377EECA), U64(0x20CABA5F, 0x1D9E4A93), /* ~= 10^-190 */ + U64(0x8E938662, 0x882AF53E), U64(0x547EB47B, 0x7282EE9C), /* ~= 10^-189 */ + U64(0xB23867FB, 0x2A35B28D), U64(0xE99E619A, 0x4F23AA43), /* ~= 10^-188 */ + U64(0xDEC681F9, 0xF4C31F31), U64(0x6405FA00, 0xE2EC94D4), /* ~= 10^-187 */ + U64(0x8B3C113C, 0x38F9F37E), U64(0xDE83BC40, 0x8DD3DD04), /* ~= 10^-186 */ + U64(0xAE0B158B, 0x4738705E), U64(0x9624AB50, 0xB148D445), /* ~= 10^-185 */ + U64(0xD98DDAEE, 0x19068C76), U64(0x3BADD624, 0xDD9B0957), /* ~= 10^-184 */ + U64(0x87F8A8D4, 0xCFA417C9), U64(0xE54CA5D7, 0x0A80E5D6), /* ~= 10^-183 */ + U64(0xA9F6D30A, 0x038D1DBC), U64(0x5E9FCF4C, 0xCD211F4C), /* ~= 10^-182 */ + U64(0xD47487CC, 0x8470652B), U64(0x7647C320, 0x0069671F), /* ~= 10^-181 */ + U64(0x84C8D4DF, 0xD2C63F3B), U64(0x29ECD9F4, 0x0041E073), /* ~= 10^-180 */ + U64(0xA5FB0A17, 0xC777CF09), U64(0xF4681071, 0x00525890), /* ~= 10^-179 */ + U64(0xCF79CC9D, 0xB955C2CC), U64(0x7182148D, 0x4066EEB4), /* ~= 10^-178 */ + U64(0x81AC1FE2, 0x93D599BF), U64(0xC6F14CD8, 0x48405530), /* ~= 10^-177 */ + U64(0xA21727DB, 0x38CB002F), U64(0xB8ADA00E, 0x5A506A7C), /* ~= 10^-176 */ + U64(0xCA9CF1D2, 0x06FDC03B), U64(0xA6D90811, 0xF0E4851C), /* ~= 10^-175 */ + U64(0xFD442E46, 0x88BD304A), U64(0x908F4A16, 0x6D1DA663), /* ~= 10^-174 */ + U64(0x9E4A9CEC, 0x15763E2E), U64(0x9A598E4E, 0x043287FE), /* ~= 10^-173 */ + U64(0xC5DD4427, 0x1AD3CDBA), U64(0x40EFF1E1, 0x853F29FD), /* ~= 10^-172 */ + U64(0xF7549530, 0xE188C128), U64(0xD12BEE59, 0xE68EF47C), /* ~= 10^-171 */ + U64(0x9A94DD3E, 0x8CF578B9), U64(0x82BB74F8, 0x301958CE), /* ~= 10^-170 */ + U64(0xC13A148E, 0x3032D6E7), U64(0xE36A5236, 0x3C1FAF01), /* ~= 10^-169 */ + U64(0xF18899B1, 0xBC3F8CA1), U64(0xDC44E6C3, 0xCB279AC1), /* ~= 10^-168 */ + U64(0x96F5600F, 0x15A7B7E5), U64(0x29AB103A, 0x5EF8C0B9), /* ~= 10^-167 */ + U64(0xBCB2B812, 0xDB11A5DE), U64(0x7415D448, 0xF6B6F0E7), /* ~= 10^-166 */ + U64(0xEBDF6617, 0x91D60F56), U64(0x111B495B, 0x3464AD21), /* ~= 10^-165 */ + U64(0x936B9FCE, 0xBB25C995), U64(0xCAB10DD9, 0x00BEEC34), /* ~= 10^-164 */ + U64(0xB84687C2, 0x69EF3BFB), U64(0x3D5D514F, 0x40EEA742), /* ~= 10^-163 */ + U64(0xE65829B3, 0x046B0AFA), U64(0x0CB4A5A3, 0x112A5112), /* ~= 10^-162 */ + U64(0x8FF71A0F, 0xE2C2E6DC), U64(0x47F0E785, 0xEABA72AB), /* ~= 10^-161 */ + U64(0xB3F4E093, 0xDB73A093), U64(0x59ED2167, 0x65690F56), /* ~= 10^-160 */ + U64(0xE0F218B8, 0xD25088B8), U64(0x306869C1, 0x3EC3532C), /* ~= 10^-159 */ + U64(0x8C974F73, 0x83725573), U64(0x1E414218, 0xC73A13FB), /* ~= 10^-158 */ + U64(0xAFBD2350, 0x644EEACF), U64(0xE5D1929E, 0xF90898FA), /* ~= 10^-157 */ + U64(0xDBAC6C24, 0x7D62A583), U64(0xDF45F746, 0xB74ABF39), /* ~= 10^-156 */ + U64(0x894BC396, 0xCE5DA772), U64(0x6B8BBA8C, 0x328EB783), /* ~= 10^-155 */ + U64(0xAB9EB47C, 0x81F5114F), U64(0x066EA92F, 0x3F326564), /* ~= 10^-154 */ + U64(0xD686619B, 0xA27255A2), U64(0xC80A537B, 0x0EFEFEBD), /* ~= 10^-153 */ + U64(0x8613FD01, 0x45877585), U64(0xBD06742C, 0xE95F5F36), /* ~= 10^-152 */ + U64(0xA798FC41, 0x96E952E7), U64(0x2C481138, 0x23B73704), /* ~= 10^-151 */ + U64(0xD17F3B51, 0xFCA3A7A0), U64(0xF75A1586, 0x2CA504C5), /* ~= 10^-150 */ + U64(0x82EF8513, 0x3DE648C4), U64(0x9A984D73, 0xDBE722FB), /* ~= 10^-149 */ + U64(0xA3AB6658, 0x0D5FDAF5), U64(0xC13E60D0, 0xD2E0EBBA), /* ~= 10^-148 */ + U64(0xCC963FEE, 0x10B7D1B3), U64(0x318DF905, 0x079926A8), /* ~= 10^-147 */ + U64(0xFFBBCFE9, 0x94E5C61F), U64(0xFDF17746, 0x497F7052), /* ~= 10^-146 */ + U64(0x9FD561F1, 0xFD0F9BD3), U64(0xFEB6EA8B, 0xEDEFA633), /* ~= 10^-145 */ + U64(0xC7CABA6E, 0x7C5382C8), U64(0xFE64A52E, 0xE96B8FC0), /* ~= 10^-144 */ + U64(0xF9BD690A, 0x1B68637B), U64(0x3DFDCE7A, 0xA3C673B0), /* ~= 10^-143 */ + U64(0x9C1661A6, 0x51213E2D), U64(0x06BEA10C, 0xA65C084E), /* ~= 10^-142 */ + U64(0xC31BFA0F, 0xE5698DB8), U64(0x486E494F, 0xCFF30A62), /* ~= 10^-141 */ + U64(0xF3E2F893, 0xDEC3F126), U64(0x5A89DBA3, 0xC3EFCCFA), /* ~= 10^-140 */ + U64(0x986DDB5C, 0x6B3A76B7), U64(0xF8962946, 0x5A75E01C), /* ~= 10^-139 */ + U64(0xBE895233, 0x86091465), U64(0xF6BBB397, 0xF1135823), /* ~= 10^-138 */ + U64(0xEE2BA6C0, 0x678B597F), U64(0x746AA07D, 0xED582E2C), /* ~= 10^-137 */ + U64(0x94DB4838, 0x40B717EF), U64(0xA8C2A44E, 0xB4571CDC), /* ~= 10^-136 */ + U64(0xBA121A46, 0x50E4DDEB), U64(0x92F34D62, 0x616CE413), /* ~= 10^-135 */ + U64(0xE896A0D7, 0xE51E1566), U64(0x77B020BA, 0xF9C81D17), /* ~= 10^-134 */ + U64(0x915E2486, 0xEF32CD60), U64(0x0ACE1474, 0xDC1D122E), /* ~= 10^-133 */ + U64(0xB5B5ADA8, 0xAAFF80B8), U64(0x0D819992, 0x132456BA), /* ~= 10^-132 */ + U64(0xE3231912, 0xD5BF60E6), U64(0x10E1FFF6, 0x97ED6C69), /* ~= 10^-131 */ + U64(0x8DF5EFAB, 0xC5979C8F), U64(0xCA8D3FFA, 0x1EF463C1), /* ~= 10^-130 */ + U64(0xB1736B96, 0xB6FD83B3), U64(0xBD308FF8, 0xA6B17CB2), /* ~= 10^-129 */ + U64(0xDDD0467C, 0x64BCE4A0), U64(0xAC7CB3F6, 0xD05DDBDE), /* ~= 10^-128 */ + U64(0x8AA22C0D, 0xBEF60EE4), U64(0x6BCDF07A, 0x423AA96B), /* ~= 10^-127 */ + U64(0xAD4AB711, 0x2EB3929D), U64(0x86C16C98, 0xD2C953C6), /* ~= 10^-126 */ + U64(0xD89D64D5, 0x7A607744), U64(0xE871C7BF, 0x077BA8B7), /* ~= 10^-125 */ + U64(0x87625F05, 0x6C7C4A8B), U64(0x11471CD7, 0x64AD4972), /* ~= 10^-124 */ + U64(0xA93AF6C6, 0xC79B5D2D), U64(0xD598E40D, 0x3DD89BCF), /* ~= 10^-123 */ + U64(0xD389B478, 0x79823479), U64(0x4AFF1D10, 0x8D4EC2C3), /* ~= 10^-122 */ + U64(0x843610CB, 0x4BF160CB), U64(0xCEDF722A, 0x585139BA), /* ~= 10^-121 */ + U64(0xA54394FE, 0x1EEDB8FE), U64(0xC2974EB4, 0xEE658828), /* ~= 10^-120 */ + U64(0xCE947A3D, 0xA6A9273E), U64(0x733D2262, 0x29FEEA32), /* ~= 10^-119 */ + U64(0x811CCC66, 0x8829B887), U64(0x0806357D, 0x5A3F525F), /* ~= 10^-118 */ + U64(0xA163FF80, 0x2A3426A8), U64(0xCA07C2DC, 0xB0CF26F7), /* ~= 10^-117 */ + U64(0xC9BCFF60, 0x34C13052), U64(0xFC89B393, 0xDD02F0B5), /* ~= 10^-116 */ + U64(0xFC2C3F38, 0x41F17C67), U64(0xBBAC2078, 0xD443ACE2), /* ~= 10^-115 */ + U64(0x9D9BA783, 0x2936EDC0), U64(0xD54B944B, 0x84AA4C0D), /* ~= 10^-114 */ + U64(0xC5029163, 0xF384A931), U64(0x0A9E795E, 0x65D4DF11), /* ~= 10^-113 */ + U64(0xF64335BC, 0xF065D37D), U64(0x4D4617B5, 0xFF4A16D5), /* ~= 10^-112 */ + U64(0x99EA0196, 0x163FA42E), U64(0x504BCED1, 0xBF8E4E45), /* ~= 10^-111 */ + U64(0xC06481FB, 0x9BCF8D39), U64(0xE45EC286, 0x2F71E1D6), /* ~= 10^-110 */ + U64(0xF07DA27A, 0x82C37088), U64(0x5D767327, 0xBB4E5A4C), /* ~= 10^-109 */ + U64(0x964E858C, 0x91BA2655), U64(0x3A6A07F8, 0xD510F86F), /* ~= 10^-108 */ + U64(0xBBE226EF, 0xB628AFEA), U64(0x890489F7, 0x0A55368B), /* ~= 10^-107 */ + U64(0xEADAB0AB, 0xA3B2DBE5), U64(0x2B45AC74, 0xCCEA842E), /* ~= 10^-106 */ + U64(0x92C8AE6B, 0x464FC96F), U64(0x3B0B8BC9, 0x0012929D), /* ~= 10^-105 */ + U64(0xB77ADA06, 0x17E3BBCB), U64(0x09CE6EBB, 0x40173744), /* ~= 10^-104 */ + U64(0xE5599087, 0x9DDCAABD), U64(0xCC420A6A, 0x101D0515), /* ~= 10^-103 */ + U64(0x8F57FA54, 0xC2A9EAB6), U64(0x9FA94682, 0x4A12232D), /* ~= 10^-102 */ + U64(0xB32DF8E9, 0xF3546564), U64(0x47939822, 0xDC96ABF9), /* ~= 10^-101 */ + U64(0xDFF97724, 0x70297EBD), U64(0x59787E2B, 0x93BC56F7), /* ~= 10^-100 */ + U64(0x8BFBEA76, 0xC619EF36), U64(0x57EB4EDB, 0x3C55B65A), /* ~= 10^-99 */ + U64(0xAEFAE514, 0x77A06B03), U64(0xEDE62292, 0x0B6B23F1), /* ~= 10^-98 */ + U64(0xDAB99E59, 0x958885C4), U64(0xE95FAB36, 0x8E45ECED), /* ~= 10^-97 */ + U64(0x88B402F7, 0xFD75539B), U64(0x11DBCB02, 0x18EBB414), /* ~= 10^-96 */ + U64(0xAAE103B5, 0xFCD2A881), U64(0xD652BDC2, 0x9F26A119), /* ~= 10^-95 */ + U64(0xD59944A3, 0x7C0752A2), U64(0x4BE76D33, 0x46F0495F), /* ~= 10^-94 */ + U64(0x857FCAE6, 0x2D8493A5), U64(0x6F70A440, 0x0C562DDB), /* ~= 10^-93 */ + U64(0xA6DFBD9F, 0xB8E5B88E), U64(0xCB4CCD50, 0x0F6BB952), /* ~= 10^-92 */ + U64(0xD097AD07, 0xA71F26B2), U64(0x7E2000A4, 0x1346A7A7), /* ~= 10^-91 */ + U64(0x825ECC24, 0xC873782F), U64(0x8ED40066, 0x8C0C28C8), /* ~= 10^-90 */ + U64(0xA2F67F2D, 0xFA90563B), U64(0x72890080, 0x2F0F32FA), /* ~= 10^-89 */ + U64(0xCBB41EF9, 0x79346BCA), U64(0x4F2B40A0, 0x3AD2FFB9), /* ~= 10^-88 */ + U64(0xFEA126B7, 0xD78186BC), U64(0xE2F610C8, 0x4987BFA8), /* ~= 10^-87 */ + U64(0x9F24B832, 0xE6B0F436), U64(0x0DD9CA7D, 0x2DF4D7C9), /* ~= 10^-86 */ + U64(0xC6EDE63F, 0xA05D3143), U64(0x91503D1C, 0x79720DBB), /* ~= 10^-85 */ + U64(0xF8A95FCF, 0x88747D94), U64(0x75A44C63, 0x97CE912A), /* ~= 10^-84 */ + U64(0x9B69DBE1, 0xB548CE7C), U64(0xC986AFBE, 0x3EE11ABA), /* ~= 10^-83 */ + U64(0xC24452DA, 0x229B021B), U64(0xFBE85BAD, 0xCE996168), /* ~= 10^-82 */ + U64(0xF2D56790, 0xAB41C2A2), U64(0xFAE27299, 0x423FB9C3), /* ~= 10^-81 */ + U64(0x97C560BA, 0x6B0919A5), U64(0xDCCD879F, 0xC967D41A), /* ~= 10^-80 */ + U64(0xBDB6B8E9, 0x05CB600F), U64(0x5400E987, 0xBBC1C920), /* ~= 10^-79 */ + U64(0xED246723, 0x473E3813), U64(0x290123E9, 0xAAB23B68), /* ~= 10^-78 */ + U64(0x9436C076, 0x0C86E30B), U64(0xF9A0B672, 0x0AAF6521), /* ~= 10^-77 */ + U64(0xB9447093, 0x8FA89BCE), U64(0xF808E40E, 0x8D5B3E69), /* ~= 10^-76 */ + U64(0xE7958CB8, 0x7392C2C2), U64(0xB60B1D12, 0x30B20E04), /* ~= 10^-75 */ + U64(0x90BD77F3, 0x483BB9B9), U64(0xB1C6F22B, 0x5E6F48C2), /* ~= 10^-74 */ + U64(0xB4ECD5F0, 0x1A4AA828), U64(0x1E38AEB6, 0x360B1AF3), /* ~= 10^-73 */ + U64(0xE2280B6C, 0x20DD5232), U64(0x25C6DA63, 0xC38DE1B0), /* ~= 10^-72 */ + U64(0x8D590723, 0x948A535F), U64(0x579C487E, 0x5A38AD0E), /* ~= 10^-71 */ + U64(0xB0AF48EC, 0x79ACE837), U64(0x2D835A9D, 0xF0C6D851), /* ~= 10^-70 */ + U64(0xDCDB1B27, 0x98182244), U64(0xF8E43145, 0x6CF88E65), /* ~= 10^-69 */ + U64(0x8A08F0F8, 0xBF0F156B), U64(0x1B8E9ECB, 0x641B58FF), /* ~= 10^-68 */ + U64(0xAC8B2D36, 0xEED2DAC5), U64(0xE272467E, 0x3D222F3F), /* ~= 10^-67 */ + U64(0xD7ADF884, 0xAA879177), U64(0x5B0ED81D, 0xCC6ABB0F), /* ~= 10^-66 */ + U64(0x86CCBB52, 0xEA94BAEA), U64(0x98E94712, 0x9FC2B4E9), /* ~= 10^-65 */ + U64(0xA87FEA27, 0xA539E9A5), U64(0x3F2398D7, 0x47B36224), /* ~= 10^-64 */ + U64(0xD29FE4B1, 0x8E88640E), U64(0x8EEC7F0D, 0x19A03AAD), /* ~= 10^-63 */ + U64(0x83A3EEEE, 0xF9153E89), U64(0x1953CF68, 0x300424AC), /* ~= 10^-62 */ + U64(0xA48CEAAA, 0xB75A8E2B), U64(0x5FA8C342, 0x3C052DD7), /* ~= 10^-61 */ + U64(0xCDB02555, 0x653131B6), U64(0x3792F412, 0xCB06794D), /* ~= 10^-60 */ + U64(0x808E1755, 0x5F3EBF11), U64(0xE2BBD88B, 0xBEE40BD0), /* ~= 10^-59 */ + U64(0xA0B19D2A, 0xB70E6ED6), U64(0x5B6ACEAE, 0xAE9D0EC4), /* ~= 10^-58 */ + U64(0xC8DE0475, 0x64D20A8B), U64(0xF245825A, 0x5A445275), /* ~= 10^-57 */ + U64(0xFB158592, 0xBE068D2E), U64(0xEED6E2F0, 0xF0D56712), /* ~= 10^-56 */ + U64(0x9CED737B, 0xB6C4183D), U64(0x55464DD6, 0x9685606B), /* ~= 10^-55 */ + U64(0xC428D05A, 0xA4751E4C), U64(0xAA97E14C, 0x3C26B886), /* ~= 10^-54 */ + U64(0xF5330471, 0x4D9265DF), U64(0xD53DD99F, 0x4B3066A8), /* ~= 10^-53 */ + U64(0x993FE2C6, 0xD07B7FAB), U64(0xE546A803, 0x8EFE4029), /* ~= 10^-52 */ + U64(0xBF8FDB78, 0x849A5F96), U64(0xDE985204, 0x72BDD033), /* ~= 10^-51 */ + U64(0xEF73D256, 0xA5C0F77C), U64(0x963E6685, 0x8F6D4440), /* ~= 10^-50 */ + U64(0x95A86376, 0x27989AAD), U64(0xDDE70013, 0x79A44AA8), /* ~= 10^-49 */ + U64(0xBB127C53, 0xB17EC159), U64(0x5560C018, 0x580D5D52), /* ~= 10^-48 */ + U64(0xE9D71B68, 0x9DDE71AF), U64(0xAAB8F01E, 0x6E10B4A6), /* ~= 10^-47 */ + U64(0x92267121, 0x62AB070D), U64(0xCAB39613, 0x04CA70E8), /* ~= 10^-46 */ + U64(0xB6B00D69, 0xBB55C8D1), U64(0x3D607B97, 0xC5FD0D22), /* ~= 10^-45 */ + U64(0xE45C10C4, 0x2A2B3B05), U64(0x8CB89A7D, 0xB77C506A), /* ~= 10^-44 */ + U64(0x8EB98A7A, 0x9A5B04E3), U64(0x77F3608E, 0x92ADB242), /* ~= 10^-43 */ + U64(0xB267ED19, 0x40F1C61C), U64(0x55F038B2, 0x37591ED3), /* ~= 10^-42 */ + U64(0xDF01E85F, 0x912E37A3), U64(0x6B6C46DE, 0xC52F6688), /* ~= 10^-41 */ + U64(0x8B61313B, 0xBABCE2C6), U64(0x2323AC4B, 0x3B3DA015), /* ~= 10^-40 */ + U64(0xAE397D8A, 0xA96C1B77), U64(0xABEC975E, 0x0A0D081A), /* ~= 10^-39 */ + U64(0xD9C7DCED, 0x53C72255), U64(0x96E7BD35, 0x8C904A21), /* ~= 10^-38 */ + U64(0x881CEA14, 0x545C7575), U64(0x7E50D641, 0x77DA2E54), /* ~= 10^-37 */ + U64(0xAA242499, 0x697392D2), U64(0xDDE50BD1, 0xD5D0B9E9), /* ~= 10^-36 */ + U64(0xD4AD2DBF, 0xC3D07787), U64(0x955E4EC6, 0x4B44E864), /* ~= 10^-35 */ + U64(0x84EC3C97, 0xDA624AB4), U64(0xBD5AF13B, 0xEF0B113E), /* ~= 10^-34 */ + U64(0xA6274BBD, 0xD0FADD61), U64(0xECB1AD8A, 0xEACDD58E), /* ~= 10^-33 */ + U64(0xCFB11EAD, 0x453994BA), U64(0x67DE18ED, 0xA5814AF2), /* ~= 10^-32 */ + U64(0x81CEB32C, 0x4B43FCF4), U64(0x80EACF94, 0x8770CED7), /* ~= 10^-31 */ + U64(0xA2425FF7, 0x5E14FC31), U64(0xA1258379, 0xA94D028D), /* ~= 10^-30 */ + U64(0xCAD2F7F5, 0x359A3B3E), U64(0x096EE458, 0x13A04330), /* ~= 10^-29 */ + U64(0xFD87B5F2, 0x8300CA0D), U64(0x8BCA9D6E, 0x188853FC), /* ~= 10^-28 */ + U64(0x9E74D1B7, 0x91E07E48), U64(0x775EA264, 0xCF55347D), /* ~= 10^-27 */ + U64(0xC6120625, 0x76589DDA), U64(0x95364AFE, 0x032A819D), /* ~= 10^-26 */ + U64(0xF79687AE, 0xD3EEC551), U64(0x3A83DDBD, 0x83F52204), /* ~= 10^-25 */ + U64(0x9ABE14CD, 0x44753B52), U64(0xC4926A96, 0x72793542), /* ~= 10^-24 */ + U64(0xC16D9A00, 0x95928A27), U64(0x75B7053C, 0x0F178293), /* ~= 10^-23 */ + U64(0xF1C90080, 0xBAF72CB1), U64(0x5324C68B, 0x12DD6338), /* ~= 10^-22 */ + U64(0x971DA050, 0x74DA7BEE), U64(0xD3F6FC16, 0xEBCA5E03), /* ~= 10^-21 */ + U64(0xBCE50864, 0x92111AEA), U64(0x88F4BB1C, 0xA6BCF584), /* ~= 10^-20 */ + U64(0xEC1E4A7D, 0xB69561A5), U64(0x2B31E9E3, 0xD06C32E5), /* ~= 10^-19 */ + U64(0x9392EE8E, 0x921D5D07), U64(0x3AFF322E, 0x62439FCF), /* ~= 10^-18 */ + U64(0xB877AA32, 0x36A4B449), U64(0x09BEFEB9, 0xFAD487C2), /* ~= 10^-17 */ + U64(0xE69594BE, 0xC44DE15B), U64(0x4C2EBE68, 0x7989A9B3), /* ~= 10^-16 */ + U64(0x901D7CF7, 0x3AB0ACD9), U64(0x0F9D3701, 0x4BF60A10), /* ~= 10^-15 */ + U64(0xB424DC35, 0x095CD80F), U64(0x538484C1, 0x9EF38C94), /* ~= 10^-14 */ + U64(0xE12E1342, 0x4BB40E13), U64(0x2865A5F2, 0x06B06FB9), /* ~= 10^-13 */ + U64(0x8CBCCC09, 0x6F5088CB), U64(0xF93F87B7, 0x442E45D3), /* ~= 10^-12 */ + U64(0xAFEBFF0B, 0xCB24AAFE), U64(0xF78F69A5, 0x1539D748), /* ~= 10^-11 */ + U64(0xDBE6FECE, 0xBDEDD5BE), U64(0xB573440E, 0x5A884D1B), /* ~= 10^-10 */ + U64(0x89705F41, 0x36B4A597), U64(0x31680A88, 0xF8953030), /* ~= 10^-9 */ + U64(0xABCC7711, 0x8461CEFC), U64(0xFDC20D2B, 0x36BA7C3D), /* ~= 10^-8 */ + U64(0xD6BF94D5, 0xE57A42BC), U64(0x3D329076, 0x04691B4C), /* ~= 10^-7 */ + U64(0x8637BD05, 0xAF6C69B5), U64(0xA63F9A49, 0xC2C1B10F), /* ~= 10^-6 */ + U64(0xA7C5AC47, 0x1B478423), U64(0x0FCF80DC, 0x33721D53), /* ~= 10^-5 */ + U64(0xD1B71758, 0xE219652B), U64(0xD3C36113, 0x404EA4A8), /* ~= 10^-4 */ + U64(0x83126E97, 0x8D4FDF3B), U64(0x645A1CAC, 0x083126E9), /* ~= 10^-3 */ + U64(0xA3D70A3D, 0x70A3D70A), U64(0x3D70A3D7, 0x0A3D70A3), /* ~= 10^-2 */ + U64(0xCCCCCCCC, 0xCCCCCCCC), U64(0xCCCCCCCC, 0xCCCCCCCC), /* ~= 10^-1 */ + U64(0x80000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^0 */ + U64(0xA0000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^1 */ + U64(0xC8000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^2 */ + U64(0xFA000000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^3 */ + U64(0x9C400000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^4 */ + U64(0xC3500000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^5 */ + U64(0xF4240000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^6 */ + U64(0x98968000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^7 */ + U64(0xBEBC2000, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^8 */ + U64(0xEE6B2800, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^9 */ + U64(0x9502F900, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^10 */ + U64(0xBA43B740, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^11 */ + U64(0xE8D4A510, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^12 */ + U64(0x9184E72A, 0x00000000), U64(0x00000000, 0x00000000), /* == 10^13 */ + U64(0xB5E620F4, 0x80000000), U64(0x00000000, 0x00000000), /* == 10^14 */ + U64(0xE35FA931, 0xA0000000), U64(0x00000000, 0x00000000), /* == 10^15 */ + U64(0x8E1BC9BF, 0x04000000), U64(0x00000000, 0x00000000), /* == 10^16 */ + U64(0xB1A2BC2E, 0xC5000000), U64(0x00000000, 0x00000000), /* == 10^17 */ + U64(0xDE0B6B3A, 0x76400000), U64(0x00000000, 0x00000000), /* == 10^18 */ + U64(0x8AC72304, 0x89E80000), U64(0x00000000, 0x00000000), /* == 10^19 */ + U64(0xAD78EBC5, 0xAC620000), U64(0x00000000, 0x00000000), /* == 10^20 */ + U64(0xD8D726B7, 0x177A8000), U64(0x00000000, 0x00000000), /* == 10^21 */ + U64(0x87867832, 0x6EAC9000), U64(0x00000000, 0x00000000), /* == 10^22 */ + U64(0xA968163F, 0x0A57B400), U64(0x00000000, 0x00000000), /* == 10^23 */ + U64(0xD3C21BCE, 0xCCEDA100), U64(0x00000000, 0x00000000), /* == 10^24 */ + U64(0x84595161, 0x401484A0), U64(0x00000000, 0x00000000), /* == 10^25 */ + U64(0xA56FA5B9, 0x9019A5C8), U64(0x00000000, 0x00000000), /* == 10^26 */ + U64(0xCECB8F27, 0xF4200F3A), U64(0x00000000, 0x00000000), /* == 10^27 */ + U64(0x813F3978, 0xF8940984), U64(0x40000000, 0x00000000), /* == 10^28 */ + U64(0xA18F07D7, 0x36B90BE5), U64(0x50000000, 0x00000000), /* == 10^29 */ + U64(0xC9F2C9CD, 0x04674EDE), U64(0xA4000000, 0x00000000), /* == 10^30 */ + U64(0xFC6F7C40, 0x45812296), U64(0x4D000000, 0x00000000), /* == 10^31 */ + U64(0x9DC5ADA8, 0x2B70B59D), U64(0xF0200000, 0x00000000), /* == 10^32 */ + U64(0xC5371912, 0x364CE305), U64(0x6C280000, 0x00000000), /* == 10^33 */ + U64(0xF684DF56, 0xC3E01BC6), U64(0xC7320000, 0x00000000), /* == 10^34 */ + U64(0x9A130B96, 0x3A6C115C), U64(0x3C7F4000, 0x00000000), /* == 10^35 */ + U64(0xC097CE7B, 0xC90715B3), U64(0x4B9F1000, 0x00000000), /* == 10^36 */ + U64(0xF0BDC21A, 0xBB48DB20), U64(0x1E86D400, 0x00000000), /* == 10^37 */ + U64(0x96769950, 0xB50D88F4), U64(0x13144480, 0x00000000), /* == 10^38 */ + U64(0xBC143FA4, 0xE250EB31), U64(0x17D955A0, 0x00000000), /* == 10^39 */ + U64(0xEB194F8E, 0x1AE525FD), U64(0x5DCFAB08, 0x00000000), /* == 10^40 */ + U64(0x92EFD1B8, 0xD0CF37BE), U64(0x5AA1CAE5, 0x00000000), /* == 10^41 */ + U64(0xB7ABC627, 0x050305AD), U64(0xF14A3D9E, 0x40000000), /* == 10^42 */ + U64(0xE596B7B0, 0xC643C719), U64(0x6D9CCD05, 0xD0000000), /* == 10^43 */ + U64(0x8F7E32CE, 0x7BEA5C6F), U64(0xE4820023, 0xA2000000), /* == 10^44 */ + U64(0xB35DBF82, 0x1AE4F38B), U64(0xDDA2802C, 0x8A800000), /* == 10^45 */ + U64(0xE0352F62, 0xA19E306E), U64(0xD50B2037, 0xAD200000), /* == 10^46 */ + U64(0x8C213D9D, 0xA502DE45), U64(0x4526F422, 0xCC340000), /* == 10^47 */ + U64(0xAF298D05, 0x0E4395D6), U64(0x9670B12B, 0x7F410000), /* == 10^48 */ + U64(0xDAF3F046, 0x51D47B4C), U64(0x3C0CDD76, 0x5F114000), /* == 10^49 */ + U64(0x88D8762B, 0xF324CD0F), U64(0xA5880A69, 0xFB6AC800), /* == 10^50 */ + U64(0xAB0E93B6, 0xEFEE0053), U64(0x8EEA0D04, 0x7A457A00), /* == 10^51 */ + U64(0xD5D238A4, 0xABE98068), U64(0x72A49045, 0x98D6D880), /* == 10^52 */ + U64(0x85A36366, 0xEB71F041), U64(0x47A6DA2B, 0x7F864750), /* == 10^53 */ + U64(0xA70C3C40, 0xA64E6C51), U64(0x999090B6, 0x5F67D924), /* == 10^54 */ + U64(0xD0CF4B50, 0xCFE20765), U64(0xFFF4B4E3, 0xF741CF6D), /* == 10^55 */ + U64(0x82818F12, 0x81ED449F), U64(0xBFF8F10E, 0x7A8921A4), /* ~= 10^56 */ + U64(0xA321F2D7, 0x226895C7), U64(0xAFF72D52, 0x192B6A0D), /* ~= 10^57 */ + U64(0xCBEA6F8C, 0xEB02BB39), U64(0x9BF4F8A6, 0x9F764490), /* ~= 10^58 */ + U64(0xFEE50B70, 0x25C36A08), U64(0x02F236D0, 0x4753D5B4), /* ~= 10^59 */ + U64(0x9F4F2726, 0x179A2245), U64(0x01D76242, 0x2C946590), /* ~= 10^60 */ + U64(0xC722F0EF, 0x9D80AAD6), U64(0x424D3AD2, 0xB7B97EF5), /* ~= 10^61 */ + U64(0xF8EBAD2B, 0x84E0D58B), U64(0xD2E08987, 0x65A7DEB2), /* ~= 10^62 */ + U64(0x9B934C3B, 0x330C8577), U64(0x63CC55F4, 0x9F88EB2F), /* ~= 10^63 */ + U64(0xC2781F49, 0xFFCFA6D5), U64(0x3CBF6B71, 0xC76B25FB), /* ~= 10^64 */ + U64(0xF316271C, 0x7FC3908A), U64(0x8BEF464E, 0x3945EF7A), /* ~= 10^65 */ + U64(0x97EDD871, 0xCFDA3A56), U64(0x97758BF0, 0xE3CBB5AC), /* ~= 10^66 */ + U64(0xBDE94E8E, 0x43D0C8EC), U64(0x3D52EEED, 0x1CBEA317), /* ~= 10^67 */ + U64(0xED63A231, 0xD4C4FB27), U64(0x4CA7AAA8, 0x63EE4BDD), /* ~= 10^68 */ + U64(0x945E455F, 0x24FB1CF8), U64(0x8FE8CAA9, 0x3E74EF6A), /* ~= 10^69 */ + U64(0xB975D6B6, 0xEE39E436), U64(0xB3E2FD53, 0x8E122B44), /* ~= 10^70 */ + U64(0xE7D34C64, 0xA9C85D44), U64(0x60DBBCA8, 0x7196B616), /* ~= 10^71 */ + U64(0x90E40FBE, 0xEA1D3A4A), U64(0xBC8955E9, 0x46FE31CD), /* ~= 10^72 */ + U64(0xB51D13AE, 0xA4A488DD), U64(0x6BABAB63, 0x98BDBE41), /* ~= 10^73 */ + U64(0xE264589A, 0x4DCDAB14), U64(0xC696963C, 0x7EED2DD1), /* ~= 10^74 */ + U64(0x8D7EB760, 0x70A08AEC), U64(0xFC1E1DE5, 0xCF543CA2), /* ~= 10^75 */ + U64(0xB0DE6538, 0x8CC8ADA8), U64(0x3B25A55F, 0x43294BCB), /* ~= 10^76 */ + U64(0xDD15FE86, 0xAFFAD912), U64(0x49EF0EB7, 0x13F39EBE), /* ~= 10^77 */ + U64(0x8A2DBF14, 0x2DFCC7AB), U64(0x6E356932, 0x6C784337), /* ~= 10^78 */ + U64(0xACB92ED9, 0x397BF996), U64(0x49C2C37F, 0x07965404), /* ~= 10^79 */ + U64(0xD7E77A8F, 0x87DAF7FB), U64(0xDC33745E, 0xC97BE906), /* ~= 10^80 */ + U64(0x86F0AC99, 0xB4E8DAFD), U64(0x69A028BB, 0x3DED71A3), /* ~= 10^81 */ + U64(0xA8ACD7C0, 0x222311BC), U64(0xC40832EA, 0x0D68CE0C), /* ~= 10^82 */ + U64(0xD2D80DB0, 0x2AABD62B), U64(0xF50A3FA4, 0x90C30190), /* ~= 10^83 */ + U64(0x83C7088E, 0x1AAB65DB), U64(0x792667C6, 0xDA79E0FA), /* ~= 10^84 */ + U64(0xA4B8CAB1, 0xA1563F52), U64(0x577001B8, 0x91185938), /* ~= 10^85 */ + U64(0xCDE6FD5E, 0x09ABCF26), U64(0xED4C0226, 0xB55E6F86), /* ~= 10^86 */ + U64(0x80B05E5A, 0xC60B6178), U64(0x544F8158, 0x315B05B4), /* ~= 10^87 */ + U64(0xA0DC75F1, 0x778E39D6), U64(0x696361AE, 0x3DB1C721), /* ~= 10^88 */ + U64(0xC913936D, 0xD571C84C), U64(0x03BC3A19, 0xCD1E38E9), /* ~= 10^89 */ + U64(0xFB587849, 0x4ACE3A5F), U64(0x04AB48A0, 0x4065C723), /* ~= 10^90 */ + U64(0x9D174B2D, 0xCEC0E47B), U64(0x62EB0D64, 0x283F9C76), /* ~= 10^91 */ + U64(0xC45D1DF9, 0x42711D9A), U64(0x3BA5D0BD, 0x324F8394), /* ~= 10^92 */ + U64(0xF5746577, 0x930D6500), U64(0xCA8F44EC, 0x7EE36479), /* ~= 10^93 */ + U64(0x9968BF6A, 0xBBE85F20), U64(0x7E998B13, 0xCF4E1ECB), /* ~= 10^94 */ + U64(0xBFC2EF45, 0x6AE276E8), U64(0x9E3FEDD8, 0xC321A67E), /* ~= 10^95 */ + U64(0xEFB3AB16, 0xC59B14A2), U64(0xC5CFE94E, 0xF3EA101E), /* ~= 10^96 */ + U64(0x95D04AEE, 0x3B80ECE5), U64(0xBBA1F1D1, 0x58724A12), /* ~= 10^97 */ + U64(0xBB445DA9, 0xCA61281F), U64(0x2A8A6E45, 0xAE8EDC97), /* ~= 10^98 */ + U64(0xEA157514, 0x3CF97226), U64(0xF52D09D7, 0x1A3293BD), /* ~= 10^99 */ + U64(0x924D692C, 0xA61BE758), U64(0x593C2626, 0x705F9C56), /* ~= 10^100 */ + U64(0xB6E0C377, 0xCFA2E12E), U64(0x6F8B2FB0, 0x0C77836C), /* ~= 10^101 */ + U64(0xE498F455, 0xC38B997A), U64(0x0B6DFB9C, 0x0F956447), /* ~= 10^102 */ + U64(0x8EDF98B5, 0x9A373FEC), U64(0x4724BD41, 0x89BD5EAC), /* ~= 10^103 */ + U64(0xB2977EE3, 0x00C50FE7), U64(0x58EDEC91, 0xEC2CB657), /* ~= 10^104 */ + U64(0xDF3D5E9B, 0xC0F653E1), U64(0x2F2967B6, 0x6737E3ED), /* ~= 10^105 */ + U64(0x8B865B21, 0x5899F46C), U64(0xBD79E0D2, 0x0082EE74), /* ~= 10^106 */ + U64(0xAE67F1E9, 0xAEC07187), U64(0xECD85906, 0x80A3AA11), /* ~= 10^107 */ + U64(0xDA01EE64, 0x1A708DE9), U64(0xE80E6F48, 0x20CC9495), /* ~= 10^108 */ + U64(0x884134FE, 0x908658B2), U64(0x3109058D, 0x147FDCDD), /* ~= 10^109 */ + U64(0xAA51823E, 0x34A7EEDE), U64(0xBD4B46F0, 0x599FD415), /* ~= 10^110 */ + U64(0xD4E5E2CD, 0xC1D1EA96), U64(0x6C9E18AC, 0x7007C91A), /* ~= 10^111 */ + U64(0x850FADC0, 0x9923329E), U64(0x03E2CF6B, 0xC604DDB0), /* ~= 10^112 */ + U64(0xA6539930, 0xBF6BFF45), U64(0x84DB8346, 0xB786151C), /* ~= 10^113 */ + U64(0xCFE87F7C, 0xEF46FF16), U64(0xE6126418, 0x65679A63), /* ~= 10^114 */ + U64(0x81F14FAE, 0x158C5F6E), U64(0x4FCB7E8F, 0x3F60C07E), /* ~= 10^115 */ + U64(0xA26DA399, 0x9AEF7749), U64(0xE3BE5E33, 0x0F38F09D), /* ~= 10^116 */ + U64(0xCB090C80, 0x01AB551C), U64(0x5CADF5BF, 0xD3072CC5), /* ~= 10^117 */ + U64(0xFDCB4FA0, 0x02162A63), U64(0x73D9732F, 0xC7C8F7F6), /* ~= 10^118 */ + U64(0x9E9F11C4, 0x014DDA7E), U64(0x2867E7FD, 0xDCDD9AFA), /* ~= 10^119 */ + U64(0xC646D635, 0x01A1511D), U64(0xB281E1FD, 0x541501B8), /* ~= 10^120 */ + U64(0xF7D88BC2, 0x4209A565), U64(0x1F225A7C, 0xA91A4226), /* ~= 10^121 */ + U64(0x9AE75759, 0x6946075F), U64(0x3375788D, 0xE9B06958), /* ~= 10^122 */ + U64(0xC1A12D2F, 0xC3978937), U64(0x0052D6B1, 0x641C83AE), /* ~= 10^123 */ + U64(0xF209787B, 0xB47D6B84), U64(0xC0678C5D, 0xBD23A49A), /* ~= 10^124 */ + U64(0x9745EB4D, 0x50CE6332), U64(0xF840B7BA, 0x963646E0), /* ~= 10^125 */ + U64(0xBD176620, 0xA501FBFF), U64(0xB650E5A9, 0x3BC3D898), /* ~= 10^126 */ + U64(0xEC5D3FA8, 0xCE427AFF), U64(0xA3E51F13, 0x8AB4CEBE), /* ~= 10^127 */ + U64(0x93BA47C9, 0x80E98CDF), U64(0xC66F336C, 0x36B10137), /* ~= 10^128 */ + U64(0xB8A8D9BB, 0xE123F017), U64(0xB80B0047, 0x445D4184), /* ~= 10^129 */ + U64(0xE6D3102A, 0xD96CEC1D), U64(0xA60DC059, 0x157491E5), /* ~= 10^130 */ + U64(0x9043EA1A, 0xC7E41392), U64(0x87C89837, 0xAD68DB2F), /* ~= 10^131 */ + U64(0xB454E4A1, 0x79DD1877), U64(0x29BABE45, 0x98C311FB), /* ~= 10^132 */ + U64(0xE16A1DC9, 0xD8545E94), U64(0xF4296DD6, 0xFEF3D67A), /* ~= 10^133 */ + U64(0x8CE2529E, 0x2734BB1D), U64(0x1899E4A6, 0x5F58660C), /* ~= 10^134 */ + U64(0xB01AE745, 0xB101E9E4), U64(0x5EC05DCF, 0xF72E7F8F), /* ~= 10^135 */ + U64(0xDC21A117, 0x1D42645D), U64(0x76707543, 0xF4FA1F73), /* ~= 10^136 */ + U64(0x899504AE, 0x72497EBA), U64(0x6A06494A, 0x791C53A8), /* ~= 10^137 */ + U64(0xABFA45DA, 0x0EDBDE69), U64(0x0487DB9D, 0x17636892), /* ~= 10^138 */ + U64(0xD6F8D750, 0x9292D603), U64(0x45A9D284, 0x5D3C42B6), /* ~= 10^139 */ + U64(0x865B8692, 0x5B9BC5C2), U64(0x0B8A2392, 0xBA45A9B2), /* ~= 10^140 */ + U64(0xA7F26836, 0xF282B732), U64(0x8E6CAC77, 0x68D7141E), /* ~= 10^141 */ + U64(0xD1EF0244, 0xAF2364FF), U64(0x3207D795, 0x430CD926), /* ~= 10^142 */ + U64(0x8335616A, 0xED761F1F), U64(0x7F44E6BD, 0x49E807B8), /* ~= 10^143 */ + U64(0xA402B9C5, 0xA8D3A6E7), U64(0x5F16206C, 0x9C6209A6), /* ~= 10^144 */ + U64(0xCD036837, 0x130890A1), U64(0x36DBA887, 0xC37A8C0F), /* ~= 10^145 */ + U64(0x80222122, 0x6BE55A64), U64(0xC2494954, 0xDA2C9789), /* ~= 10^146 */ + U64(0xA02AA96B, 0x06DEB0FD), U64(0xF2DB9BAA, 0x10B7BD6C), /* ~= 10^147 */ + U64(0xC83553C5, 0xC8965D3D), U64(0x6F928294, 0x94E5ACC7), /* ~= 10^148 */ + U64(0xFA42A8B7, 0x3ABBF48C), U64(0xCB772339, 0xBA1F17F9), /* ~= 10^149 */ + U64(0x9C69A972, 0x84B578D7), U64(0xFF2A7604, 0x14536EFB), /* ~= 10^150 */ + U64(0xC38413CF, 0x25E2D70D), U64(0xFEF51385, 0x19684ABA), /* ~= 10^151 */ + U64(0xF46518C2, 0xEF5B8CD1), U64(0x7EB25866, 0x5FC25D69), /* ~= 10^152 */ + U64(0x98BF2F79, 0xD5993802), U64(0xEF2F773F, 0xFBD97A61), /* ~= 10^153 */ + U64(0xBEEEFB58, 0x4AFF8603), U64(0xAAFB550F, 0xFACFD8FA), /* ~= 10^154 */ + U64(0xEEAABA2E, 0x5DBF6784), U64(0x95BA2A53, 0xF983CF38), /* ~= 10^155 */ + U64(0x952AB45C, 0xFA97A0B2), U64(0xDD945A74, 0x7BF26183), /* ~= 10^156 */ + U64(0xBA756174, 0x393D88DF), U64(0x94F97111, 0x9AEEF9E4), /* ~= 10^157 */ + U64(0xE912B9D1, 0x478CEB17), U64(0x7A37CD56, 0x01AAB85D), /* ~= 10^158 */ + U64(0x91ABB422, 0xCCB812EE), U64(0xAC62E055, 0xC10AB33A), /* ~= 10^159 */ + U64(0xB616A12B, 0x7FE617AA), U64(0x577B986B, 0x314D6009), /* ~= 10^160 */ + U64(0xE39C4976, 0x5FDF9D94), U64(0xED5A7E85, 0xFDA0B80B), /* ~= 10^161 */ + U64(0x8E41ADE9, 0xFBEBC27D), U64(0x14588F13, 0xBE847307), /* ~= 10^162 */ + U64(0xB1D21964, 0x7AE6B31C), U64(0x596EB2D8, 0xAE258FC8), /* ~= 10^163 */ + U64(0xDE469FBD, 0x99A05FE3), U64(0x6FCA5F8E, 0xD9AEF3BB), /* ~= 10^164 */ + U64(0x8AEC23D6, 0x80043BEE), U64(0x25DE7BB9, 0x480D5854), /* ~= 10^165 */ + U64(0xADA72CCC, 0x20054AE9), U64(0xAF561AA7, 0x9A10AE6A), /* ~= 10^166 */ + U64(0xD910F7FF, 0x28069DA4), U64(0x1B2BA151, 0x8094DA04), /* ~= 10^167 */ + U64(0x87AA9AFF, 0x79042286), U64(0x90FB44D2, 0xF05D0842), /* ~= 10^168 */ + U64(0xA99541BF, 0x57452B28), U64(0x353A1607, 0xAC744A53), /* ~= 10^169 */ + U64(0xD3FA922F, 0x2D1675F2), U64(0x42889B89, 0x97915CE8), /* ~= 10^170 */ + U64(0x847C9B5D, 0x7C2E09B7), U64(0x69956135, 0xFEBADA11), /* ~= 10^171 */ + U64(0xA59BC234, 0xDB398C25), U64(0x43FAB983, 0x7E699095), /* ~= 10^172 */ + U64(0xCF02B2C2, 0x1207EF2E), U64(0x94F967E4, 0x5E03F4BB), /* ~= 10^173 */ + U64(0x8161AFB9, 0x4B44F57D), U64(0x1D1BE0EE, 0xBAC278F5), /* ~= 10^174 */ + U64(0xA1BA1BA7, 0x9E1632DC), U64(0x6462D92A, 0x69731732), /* ~= 10^175 */ + U64(0xCA28A291, 0x859BBF93), U64(0x7D7B8F75, 0x03CFDCFE), /* ~= 10^176 */ + U64(0xFCB2CB35, 0xE702AF78), U64(0x5CDA7352, 0x44C3D43E), /* ~= 10^177 */ + U64(0x9DEFBF01, 0xB061ADAB), U64(0x3A088813, 0x6AFA64A7), /* ~= 10^178 */ + U64(0xC56BAEC2, 0x1C7A1916), U64(0x088AAA18, 0x45B8FDD0), /* ~= 10^179 */ + U64(0xF6C69A72, 0xA3989F5B), U64(0x8AAD549E, 0x57273D45), /* ~= 10^180 */ + U64(0x9A3C2087, 0xA63F6399), U64(0x36AC54E2, 0xF678864B), /* ~= 10^181 */ + U64(0xC0CB28A9, 0x8FCF3C7F), U64(0x84576A1B, 0xB416A7DD), /* ~= 10^182 */ + U64(0xF0FDF2D3, 0xF3C30B9F), U64(0x656D44A2, 0xA11C51D5), /* ~= 10^183 */ + U64(0x969EB7C4, 0x7859E743), U64(0x9F644AE5, 0xA4B1B325), /* ~= 10^184 */ + U64(0xBC4665B5, 0x96706114), U64(0x873D5D9F, 0x0DDE1FEE), /* ~= 10^185 */ + U64(0xEB57FF22, 0xFC0C7959), U64(0xA90CB506, 0xD155A7EA), /* ~= 10^186 */ + U64(0x9316FF75, 0xDD87CBD8), U64(0x09A7F124, 0x42D588F2), /* ~= 10^187 */ + U64(0xB7DCBF53, 0x54E9BECE), U64(0x0C11ED6D, 0x538AEB2F), /* ~= 10^188 */ + U64(0xE5D3EF28, 0x2A242E81), U64(0x8F1668C8, 0xA86DA5FA), /* ~= 10^189 */ + U64(0x8FA47579, 0x1A569D10), U64(0xF96E017D, 0x694487BC), /* ~= 10^190 */ + U64(0xB38D92D7, 0x60EC4455), U64(0x37C981DC, 0xC395A9AC), /* ~= 10^191 */ + U64(0xE070F78D, 0x3927556A), U64(0x85BBE253, 0xF47B1417), /* ~= 10^192 */ + U64(0x8C469AB8, 0x43B89562), U64(0x93956D74, 0x78CCEC8E), /* ~= 10^193 */ + U64(0xAF584166, 0x54A6BABB), U64(0x387AC8D1, 0x970027B2), /* ~= 10^194 */ + U64(0xDB2E51BF, 0xE9D0696A), U64(0x06997B05, 0xFCC0319E), /* ~= 10^195 */ + U64(0x88FCF317, 0xF22241E2), U64(0x441FECE3, 0xBDF81F03), /* ~= 10^196 */ + U64(0xAB3C2FDD, 0xEEAAD25A), U64(0xD527E81C, 0xAD7626C3), /* ~= 10^197 */ + U64(0xD60B3BD5, 0x6A5586F1), U64(0x8A71E223, 0xD8D3B074), /* ~= 10^198 */ + U64(0x85C70565, 0x62757456), U64(0xF6872D56, 0x67844E49), /* ~= 10^199 */ + U64(0xA738C6BE, 0xBB12D16C), U64(0xB428F8AC, 0x016561DB), /* ~= 10^200 */ + U64(0xD106F86E, 0x69D785C7), U64(0xE13336D7, 0x01BEBA52), /* ~= 10^201 */ + U64(0x82A45B45, 0x0226B39C), U64(0xECC00246, 0x61173473), /* ~= 10^202 */ + U64(0xA34D7216, 0x42B06084), U64(0x27F002D7, 0xF95D0190), /* ~= 10^203 */ + U64(0xCC20CE9B, 0xD35C78A5), U64(0x31EC038D, 0xF7B441F4), /* ~= 10^204 */ + U64(0xFF290242, 0xC83396CE), U64(0x7E670471, 0x75A15271), /* ~= 10^205 */ + U64(0x9F79A169, 0xBD203E41), U64(0x0F0062C6, 0xE984D386), /* ~= 10^206 */ + U64(0xC75809C4, 0x2C684DD1), U64(0x52C07B78, 0xA3E60868), /* ~= 10^207 */ + U64(0xF92E0C35, 0x37826145), U64(0xA7709A56, 0xCCDF8A82), /* ~= 10^208 */ + U64(0x9BBCC7A1, 0x42B17CCB), U64(0x88A66076, 0x400BB691), /* ~= 10^209 */ + U64(0xC2ABF989, 0x935DDBFE), U64(0x6ACFF893, 0xD00EA435), /* ~= 10^210 */ + U64(0xF356F7EB, 0xF83552FE), U64(0x0583F6B8, 0xC4124D43), /* ~= 10^211 */ + U64(0x98165AF3, 0x7B2153DE), U64(0xC3727A33, 0x7A8B704A), /* ~= 10^212 */ + U64(0xBE1BF1B0, 0x59E9A8D6), U64(0x744F18C0, 0x592E4C5C), /* ~= 10^213 */ + U64(0xEDA2EE1C, 0x7064130C), U64(0x1162DEF0, 0x6F79DF73), /* ~= 10^214 */ + U64(0x9485D4D1, 0xC63E8BE7), U64(0x8ADDCB56, 0x45AC2BA8), /* ~= 10^215 */ + U64(0xB9A74A06, 0x37CE2EE1), U64(0x6D953E2B, 0xD7173692), /* ~= 10^216 */ + U64(0xE8111C87, 0xC5C1BA99), U64(0xC8FA8DB6, 0xCCDD0437), /* ~= 10^217 */ + U64(0x910AB1D4, 0xDB9914A0), U64(0x1D9C9892, 0x400A22A2), /* ~= 10^218 */ + U64(0xB54D5E4A, 0x127F59C8), U64(0x2503BEB6, 0xD00CAB4B), /* ~= 10^219 */ + U64(0xE2A0B5DC, 0x971F303A), U64(0x2E44AE64, 0x840FD61D), /* ~= 10^220 */ + U64(0x8DA471A9, 0xDE737E24), U64(0x5CEAECFE, 0xD289E5D2), /* ~= 10^221 */ + U64(0xB10D8E14, 0x56105DAD), U64(0x7425A83E, 0x872C5F47), /* ~= 10^222 */ + U64(0xDD50F199, 0x6B947518), U64(0xD12F124E, 0x28F77719), /* ~= 10^223 */ + U64(0x8A5296FF, 0xE33CC92F), U64(0x82BD6B70, 0xD99AAA6F), /* ~= 10^224 */ + U64(0xACE73CBF, 0xDC0BFB7B), U64(0x636CC64D, 0x1001550B), /* ~= 10^225 */ + U64(0xD8210BEF, 0xD30EFA5A), U64(0x3C47F7E0, 0x5401AA4E), /* ~= 10^226 */ + U64(0x8714A775, 0xE3E95C78), U64(0x65ACFAEC, 0x34810A71), /* ~= 10^227 */ + U64(0xA8D9D153, 0x5CE3B396), U64(0x7F1839A7, 0x41A14D0D), /* ~= 10^228 */ + U64(0xD31045A8, 0x341CA07C), U64(0x1EDE4811, 0x1209A050), /* ~= 10^229 */ + U64(0x83EA2B89, 0x2091E44D), U64(0x934AED0A, 0xAB460432), /* ~= 10^230 */ + U64(0xA4E4B66B, 0x68B65D60), U64(0xF81DA84D, 0x5617853F), /* ~= 10^231 */ + U64(0xCE1DE406, 0x42E3F4B9), U64(0x36251260, 0xAB9D668E), /* ~= 10^232 */ + U64(0x80D2AE83, 0xE9CE78F3), U64(0xC1D72B7C, 0x6B426019), /* ~= 10^233 */ + U64(0xA1075A24, 0xE4421730), U64(0xB24CF65B, 0x8612F81F), /* ~= 10^234 */ + U64(0xC94930AE, 0x1D529CFC), U64(0xDEE033F2, 0x6797B627), /* ~= 10^235 */ + U64(0xFB9B7CD9, 0xA4A7443C), U64(0x169840EF, 0x017DA3B1), /* ~= 10^236 */ + U64(0x9D412E08, 0x06E88AA5), U64(0x8E1F2895, 0x60EE864E), /* ~= 10^237 */ + U64(0xC491798A, 0x08A2AD4E), U64(0xF1A6F2BA, 0xB92A27E2), /* ~= 10^238 */ + U64(0xF5B5D7EC, 0x8ACB58A2), U64(0xAE10AF69, 0x6774B1DB), /* ~= 10^239 */ + U64(0x9991A6F3, 0xD6BF1765), U64(0xACCA6DA1, 0xE0A8EF29), /* ~= 10^240 */ + U64(0xBFF610B0, 0xCC6EDD3F), U64(0x17FD090A, 0x58D32AF3), /* ~= 10^241 */ + U64(0xEFF394DC, 0xFF8A948E), U64(0xDDFC4B4C, 0xEF07F5B0), /* ~= 10^242 */ + U64(0x95F83D0A, 0x1FB69CD9), U64(0x4ABDAF10, 0x1564F98E), /* ~= 10^243 */ + U64(0xBB764C4C, 0xA7A4440F), U64(0x9D6D1AD4, 0x1ABE37F1), /* ~= 10^244 */ + U64(0xEA53DF5F, 0xD18D5513), U64(0x84C86189, 0x216DC5ED), /* ~= 10^245 */ + U64(0x92746B9B, 0xE2F8552C), U64(0x32FD3CF5, 0xB4E49BB4), /* ~= 10^246 */ + U64(0xB7118682, 0xDBB66A77), U64(0x3FBC8C33, 0x221DC2A1), /* ~= 10^247 */ + U64(0xE4D5E823, 0x92A40515), U64(0x0FABAF3F, 0xEAA5334A), /* ~= 10^248 */ + U64(0x8F05B116, 0x3BA6832D), U64(0x29CB4D87, 0xF2A7400E), /* ~= 10^249 */ + U64(0xB2C71D5B, 0xCA9023F8), U64(0x743E20E9, 0xEF511012), /* ~= 10^250 */ + U64(0xDF78E4B2, 0xBD342CF6), U64(0x914DA924, 0x6B255416), /* ~= 10^251 */ + U64(0x8BAB8EEF, 0xB6409C1A), U64(0x1AD089B6, 0xC2F7548E), /* ~= 10^252 */ + U64(0xAE9672AB, 0xA3D0C320), U64(0xA184AC24, 0x73B529B1), /* ~= 10^253 */ + U64(0xDA3C0F56, 0x8CC4F3E8), U64(0xC9E5D72D, 0x90A2741E), /* ~= 10^254 */ + U64(0x88658996, 0x17FB1871), U64(0x7E2FA67C, 0x7A658892), /* ~= 10^255 */ + U64(0xAA7EEBFB, 0x9DF9DE8D), U64(0xDDBB901B, 0x98FEEAB7), /* ~= 10^256 */ + U64(0xD51EA6FA, 0x85785631), U64(0x552A7422, 0x7F3EA565), /* ~= 10^257 */ + U64(0x8533285C, 0x936B35DE), U64(0xD53A8895, 0x8F87275F), /* ~= 10^258 */ + U64(0xA67FF273, 0xB8460356), U64(0x8A892ABA, 0xF368F137), /* ~= 10^259 */ + U64(0xD01FEF10, 0xA657842C), U64(0x2D2B7569, 0xB0432D85), /* ~= 10^260 */ + U64(0x8213F56A, 0x67F6B29B), U64(0x9C3B2962, 0x0E29FC73), /* ~= 10^261 */ + U64(0xA298F2C5, 0x01F45F42), U64(0x8349F3BA, 0x91B47B8F), /* ~= 10^262 */ + U64(0xCB3F2F76, 0x42717713), U64(0x241C70A9, 0x36219A73), /* ~= 10^263 */ + U64(0xFE0EFB53, 0xD30DD4D7), U64(0xED238CD3, 0x83AA0110), /* ~= 10^264 */ + U64(0x9EC95D14, 0x63E8A506), U64(0xF4363804, 0x324A40AA), /* ~= 10^265 */ + U64(0xC67BB459, 0x7CE2CE48), U64(0xB143C605, 0x3EDCD0D5), /* ~= 10^266 */ + U64(0xF81AA16F, 0xDC1B81DA), U64(0xDD94B786, 0x8E94050A), /* ~= 10^267 */ + U64(0x9B10A4E5, 0xE9913128), U64(0xCA7CF2B4, 0x191C8326), /* ~= 10^268 */ + U64(0xC1D4CE1F, 0x63F57D72), U64(0xFD1C2F61, 0x1F63A3F0), /* ~= 10^269 */ + U64(0xF24A01A7, 0x3CF2DCCF), U64(0xBC633B39, 0x673C8CEC), /* ~= 10^270 */ + U64(0x976E4108, 0x8617CA01), U64(0xD5BE0503, 0xE085D813), /* ~= 10^271 */ + U64(0xBD49D14A, 0xA79DBC82), U64(0x4B2D8644, 0xD8A74E18), /* ~= 10^272 */ + U64(0xEC9C459D, 0x51852BA2), U64(0xDDF8E7D6, 0x0ED1219E), /* ~= 10^273 */ + U64(0x93E1AB82, 0x52F33B45), U64(0xCABB90E5, 0xC942B503), /* ~= 10^274 */ + U64(0xB8DA1662, 0xE7B00A17), U64(0x3D6A751F, 0x3B936243), /* ~= 10^275 */ + U64(0xE7109BFB, 0xA19C0C9D), U64(0x0CC51267, 0x0A783AD4), /* ~= 10^276 */ + U64(0x906A617D, 0x450187E2), U64(0x27FB2B80, 0x668B24C5), /* ~= 10^277 */ + U64(0xB484F9DC, 0x9641E9DA), U64(0xB1F9F660, 0x802DEDF6), /* ~= 10^278 */ + U64(0xE1A63853, 0xBBD26451), U64(0x5E7873F8, 0xA0396973), /* ~= 10^279 */ + U64(0x8D07E334, 0x55637EB2), U64(0xDB0B487B, 0x6423E1E8), /* ~= 10^280 */ + U64(0xB049DC01, 0x6ABC5E5F), U64(0x91CE1A9A, 0x3D2CDA62), /* ~= 10^281 */ + U64(0xDC5C5301, 0xC56B75F7), U64(0x7641A140, 0xCC7810FB), /* ~= 10^282 */ + U64(0x89B9B3E1, 0x1B6329BA), U64(0xA9E904C8, 0x7FCB0A9D), /* ~= 10^283 */ + U64(0xAC2820D9, 0x623BF429), U64(0x546345FA, 0x9FBDCD44), /* ~= 10^284 */ + U64(0xD732290F, 0xBACAF133), U64(0xA97C1779, 0x47AD4095), /* ~= 10^285 */ + U64(0x867F59A9, 0xD4BED6C0), U64(0x49ED8EAB, 0xCCCC485D), /* ~= 10^286 */ + U64(0xA81F3014, 0x49EE8C70), U64(0x5C68F256, 0xBFFF5A74), /* ~= 10^287 */ + U64(0xD226FC19, 0x5C6A2F8C), U64(0x73832EEC, 0x6FFF3111), /* ~= 10^288 */ + U64(0x83585D8F, 0xD9C25DB7), U64(0xC831FD53, 0xC5FF7EAB), /* ~= 10^289 */ + U64(0xA42E74F3, 0xD032F525), U64(0xBA3E7CA8, 0xB77F5E55), /* ~= 10^290 */ + U64(0xCD3A1230, 0xC43FB26F), U64(0x28CE1BD2, 0xE55F35EB), /* ~= 10^291 */ + U64(0x80444B5E, 0x7AA7CF85), U64(0x7980D163, 0xCF5B81B3), /* ~= 10^292 */ + U64(0xA0555E36, 0x1951C366), U64(0xD7E105BC, 0xC332621F), /* ~= 10^293 */ + U64(0xC86AB5C3, 0x9FA63440), U64(0x8DD9472B, 0xF3FEFAA7), /* ~= 10^294 */ + U64(0xFA856334, 0x878FC150), U64(0xB14F98F6, 0xF0FEB951), /* ~= 10^295 */ + U64(0x9C935E00, 0xD4B9D8D2), U64(0x6ED1BF9A, 0x569F33D3), /* ~= 10^296 */ + U64(0xC3B83581, 0x09E84F07), U64(0x0A862F80, 0xEC4700C8), /* ~= 10^297 */ + U64(0xF4A642E1, 0x4C6262C8), U64(0xCD27BB61, 0x2758C0FA), /* ~= 10^298 */ + U64(0x98E7E9CC, 0xCFBD7DBD), U64(0x8038D51C, 0xB897789C), /* ~= 10^299 */ + U64(0xBF21E440, 0x03ACDD2C), U64(0xE0470A63, 0xE6BD56C3), /* ~= 10^300 */ + U64(0xEEEA5D50, 0x04981478), U64(0x1858CCFC, 0xE06CAC74), /* ~= 10^301 */ + U64(0x95527A52, 0x02DF0CCB), U64(0x0F37801E, 0x0C43EBC8), /* ~= 10^302 */ + U64(0xBAA718E6, 0x8396CFFD), U64(0xD3056025, 0x8F54E6BA), /* ~= 10^303 */ + U64(0xE950DF20, 0x247C83FD), U64(0x47C6B82E, 0xF32A2069), /* ~= 10^304 */ + U64(0x91D28B74, 0x16CDD27E), U64(0x4CDC331D, 0x57FA5441), /* ~= 10^305 */ + U64(0xB6472E51, 0x1C81471D), U64(0xE0133FE4, 0xADF8E952), /* ~= 10^306 */ + U64(0xE3D8F9E5, 0x63A198E5), U64(0x58180FDD, 0xD97723A6), /* ~= 10^307 */ + U64(0x8E679C2F, 0x5E44FF8F), U64(0x570F09EA, 0xA7EA7648), /* ~= 10^308 */ + U64(0xB201833B, 0x35D63F73), U64(0x2CD2CC65, 0x51E513DA), /* ~= 10^309 */ + U64(0xDE81E40A, 0x034BCF4F), U64(0xF8077F7E, 0xA65E58D1), /* ~= 10^310 */ + U64(0x8B112E86, 0x420F6191), U64(0xFB04AFAF, 0x27FAF782), /* ~= 10^311 */ + U64(0xADD57A27, 0xD29339F6), U64(0x79C5DB9A, 0xF1F9B563), /* ~= 10^312 */ + U64(0xD94AD8B1, 0xC7380874), U64(0x18375281, 0xAE7822BC), /* ~= 10^313 */ + U64(0x87CEC76F, 0x1C830548), U64(0x8F229391, 0x0D0B15B5), /* ~= 10^314 */ + U64(0xA9C2794A, 0xE3A3C69A), U64(0xB2EB3875, 0x504DDB22), /* ~= 10^315 */ + U64(0xD433179D, 0x9C8CB841), U64(0x5FA60692, 0xA46151EB), /* ~= 10^316 */ + U64(0x849FEEC2, 0x81D7F328), U64(0xDBC7C41B, 0xA6BCD333), /* ~= 10^317 */ + U64(0xA5C7EA73, 0x224DEFF3), U64(0x12B9B522, 0x906C0800), /* ~= 10^318 */ + U64(0xCF39E50F, 0xEAE16BEF), U64(0xD768226B, 0x34870A00), /* ~= 10^319 */ + U64(0x81842F29, 0xF2CCE375), U64(0xE6A11583, 0x00D46640), /* ~= 10^320 */ + U64(0xA1E53AF4, 0x6F801C53), U64(0x60495AE3, 0xC1097FD0), /* ~= 10^321 */ + U64(0xCA5E89B1, 0x8B602368), U64(0x385BB19C, 0xB14BDFC4), /* ~= 10^322 */ + U64(0xFCF62C1D, 0xEE382C42), U64(0x46729E03, 0xDD9ED7B5), /* ~= 10^323 */ + U64(0x9E19DB92, 0xB4E31BA9), U64(0x6C07A2C2, 0x6A8346D1) /* ~= 10^324 */ +}; + +/** + Get the cached pow10 value from `pow10_sig_table`. + @param exp10 The exponent of pow(10, e). This value must in range + `POW10_SIG_TABLE_MIN_EXP` to `POW10_SIG_TABLE_MAX_EXP`. + @param hi The highest 64 bits of pow(10, e). + @param lo The lower 64 bits after `hi`. + */ +static_inline void pow10_table_get_sig(i32 exp10, u64 *hi, u64 *lo) { + i32 idx = exp10 - (POW10_SIG_TABLE_MIN_EXP); + *hi = pow10_sig_table[idx * 2]; + *lo = pow10_sig_table[idx * 2 + 1]; +} + +/** + Get the exponent (base 2) for highest 64 bits significand in `pow10_sig_table`. + */ +static_inline void pow10_table_get_exp(i32 exp10, i32 *exp2) { + /* e2 = floor(log2(pow(10, e))) - 64 + 1 */ + /* = floor(e * log2(10) - 63) */ + *exp2 = (exp10 * 217706 - 4128768) >> 16; +} + +#endif + + + +/*============================================================================== + * MARK: - Number and Bit Utils (Private) + *============================================================================*/ + +/** Convert bits to double. */ +static_inline f64 f64_from_bits(u64 u) { + f64 f; + memcpy(&f, &u, sizeof(u)); + return f; +} + +/** Convert double to bits. */ +static_inline u64 f64_to_bits(f64 f) { + u64 u; + memcpy(&u, &f, sizeof(u)); + return u; +} + +/** Convert double to bits. */ +static_inline u32 f32_to_bits(f32 f) { + u32 u; + memcpy(&u, &f, sizeof(u)); + return u; +} + +/** Get 'infinity' bits with sign. */ +static_inline u64 f64_bits_inf(bool sign) { +#if YYJSON_HAS_IEEE_754 + return F64_BITS_INF | ((u64)sign << 63); +#elif defined(INFINITY) + return f64_to_bits(sign ? -INFINITY : INFINITY); +#else + return f64_to_bits(sign ? -HUGE_VAL : HUGE_VAL); +#endif +} + +/** Get 'nan' bits with sign. */ +static_inline u64 f64_bits_nan(bool sign) { +#if YYJSON_HAS_IEEE_754 + return F64_BITS_NAN | ((u64)sign << 63); +#elif defined(NAN) + return f64_to_bits(sign ? (f64)-NAN : (f64)NAN); +#else + return f64_to_bits((sign ? -0.0 : 0.0) / 0.0); +#endif +} + +/** Casting double to float, allow overflow. */ +#if yyjson_has_attribute(no_sanitize) +__attribute__((no_sanitize("undefined"))) +#elif yyjson_gcc_available(4, 9, 0) +__attribute__((__no_sanitize_undefined__)) +#endif +static_inline f32 f64_to_f32(f64 val) { + return (f32)val; +} + +/** Returns the number of leading 0-bits in value (input should not be 0). */ +static_inline u32 u64_lz_bits(u64 v) { +#if GCC_HAS_CLZLL + return (u32)__builtin_clzll(v); +#elif MSC_HAS_BIT_SCAN_64 + unsigned long r; + _BitScanReverse64(&r, v); + return (u32)63 - (u32)r; +#elif MSC_HAS_BIT_SCAN + unsigned long hi, lo; + bool hi_set = _BitScanReverse(&hi, (u32)(v >> 32)) != 0; + _BitScanReverse(&lo, (u32)v); + hi |= 32; + return (u32)63 - (u32)(hi_set ? hi : lo); +#else + /* branchless, use De Bruijn sequence */ + /* see: https://www.chessprogramming.org/BitScan */ + const u8 table[64] = { + 63, 16, 62, 7, 15, 36, 61, 3, 6, 14, 22, 26, 35, 47, 60, 2, + 9, 5, 28, 11, 13, 21, 42, 19, 25, 31, 34, 40, 46, 52, 59, 1, + 17, 8, 37, 4, 23, 27, 48, 10, 29, 12, 43, 20, 32, 41, 53, 18, + 38, 24, 49, 30, 44, 33, 54, 39, 50, 45, 55, 51, 56, 57, 58, 0 + }; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v |= v >> 32; + return table[(v * U64(0x03F79D71, 0xB4CB0A89)) >> 58]; +#endif +} + +/** Returns the number of trailing 0-bits in value (input should not be 0). */ +static_inline u32 u64_tz_bits(u64 v) { +#if GCC_HAS_CTZLL + return (u32)__builtin_ctzll(v); +#elif MSC_HAS_BIT_SCAN_64 + unsigned long r; + _BitScanForward64(&r, v); + return (u32)r; +#elif MSC_HAS_BIT_SCAN + unsigned long lo, hi; + bool lo_set = _BitScanForward(&lo, (u32)(v)) != 0; + _BitScanForward(&hi, (u32)(v >> 32)); + hi += 32; + return lo_set ? lo : hi; +#else + /* branchless, use De Bruijn sequence */ + /* see: https://www.chessprogramming.org/BitScan */ + const u8 table[64] = { + 0, 1, 2, 53, 3, 7, 54, 27, 4, 38, 41, 8, 34, 55, 48, 28, + 62, 5, 39, 46, 44, 42, 22, 9, 24, 35, 59, 56, 49, 18, 29, 11, + 63, 52, 6, 26, 37, 40, 33, 47, 61, 45, 43, 21, 23, 58, 17, 10, + 51, 25, 36, 32, 60, 20, 57, 16, 50, 31, 19, 15, 30, 14, 13, 12 + }; + return table[((v & (~v + 1)) * U64(0x022FDD63, 0xCC95386D)) >> 58]; +#endif +} + +/** Multiplies two 64-bit unsigned integers (a * b), + returns the 128-bit result as 'hi' and 'lo'. */ +static_inline void u128_mul(u64 a, u64 b, u64 *hi, u64 *lo) { +#if YYJSON_HAS_INT128 + u128 m = (u128)a * b; + *hi = (u64)(m >> 64); + *lo = (u64)(m); +#elif MSC_HAS_UMUL128 + *lo = _umul128(a, b, hi); +#else + u32 a0 = (u32)(a), a1 = (u32)(a >> 32); + u32 b0 = (u32)(b), b1 = (u32)(b >> 32); + u64 p00 = (u64)a0 * b0, p01 = (u64)a0 * b1; + u64 p10 = (u64)a1 * b0, p11 = (u64)a1 * b1; + u64 m0 = p01 + (p00 >> 32); + u32 m00 = (u32)(m0), m01 = (u32)(m0 >> 32); + u64 m1 = p10 + m00; + u32 m10 = (u32)(m1), m11 = (u32)(m1 >> 32); + *hi = p11 + m01 + m11; + *lo = ((u64)m10 << 32) | (u32)p00; +#endif +} + +/** Multiplies two 64-bit unsigned integers and add a value (a * b + c), + returns the 128-bit result as 'hi' and 'lo'. */ +static_inline void u128_mul_add(u64 a, u64 b, u64 c, u64 *hi, u64 *lo) { +#if YYJSON_HAS_INT128 + u128 m = (u128)a * b + c; + *hi = (u64)(m >> 64); + *lo = (u64)(m); +#else + u64 h, l, t; + u128_mul(a, b, &h, &l); + t = l + c; + h += (u64)(((t < l) | (t < c))); + *hi = h; + *lo = t; +#endif +} + + + +/*============================================================================== + * MARK: - File Utils (Private) + * These functions are used to read and write JSON files. + *============================================================================*/ + +#define YYJSON_FOPEN_E +#if !defined(_MSC_VER) && defined(__GLIBC__) && defined(__GLIBC_PREREQ) +# if __GLIBC_PREREQ(2, 7) +# undef YYJSON_FOPEN_E +# define YYJSON_FOPEN_E "e" /* glibc extension to enable O_CLOEXEC */ +# endif +#endif + +static_inline FILE *fopen_safe(const char *path, const char *mode) { +#if YYJSON_MSC_VER >= 1400 + FILE *file = NULL; + if (fopen_s(&file, path, mode) != 0) return NULL; + return file; +#else + return fopen(path, mode); +#endif +} + +static_inline FILE *fopen_readonly(const char *path) { + return fopen_safe(path, "rb" YYJSON_FOPEN_E); +} + +static_inline FILE *fopen_writeonly(const char *path) { + return fopen_safe(path, "wb" YYJSON_FOPEN_E); +} + +static_inline usize fread_safe(void *buf, usize size, FILE *file) { +#if YYJSON_MSC_VER >= 1400 + return fread_s(buf, size, 1, size, file); +#else + return fread(buf, 1, size, file); +#endif +} + + + +/*============================================================================== + * MARK: - Size Utils (Private) + * These functions are used for memory allocation. + *============================================================================*/ + +/** Returns whether the size is overflow after increment. */ +static_inline bool size_add_is_overflow(usize size, usize add) { + return size > (size + add); +} + +/** Returns whether the size is power of 2 (size should not be 0). */ +static_inline bool size_is_pow2(usize size) { + return (size & (size - 1)) == 0; +} + +/** Align size upwards (may overflow). */ +static_inline usize size_align_up(usize size, usize align) { + if (size_is_pow2(align)) { + return (size + (align - 1)) & ~(align - 1); + } else { + return size + align - (size + align - 1) % align - 1; + } +} + +/** Align size downwards. */ +static_inline usize size_align_down(usize size, usize align) { + if (size_is_pow2(align)) { + return size & ~(align - 1); + } else { + return size - (size % align); + } +} + +/** Align address upwards (may overflow). */ +static_inline void *mem_align_up(void *mem, usize align) { + usize size; + memcpy(&size, &mem, sizeof(usize)); + size = size_align_up(size, align); + memcpy(&mem, &size, sizeof(usize)); + return mem; +} + + + +/*============================================================================== + * MARK: - Default Memory Allocator (Private) + * This is a simple libc memory allocator wrapper. + *============================================================================*/ + +static void *default_malloc(void *ctx, usize size) { + return malloc(size); +} + +static void *default_realloc(void *ctx, void *ptr, usize old_size, usize size) { + return realloc(ptr, size); +} + +static void default_free(void *ctx, void *ptr) { + free(ptr); +} + +static const yyjson_alc YYJSON_DEFAULT_ALC = { + default_malloc, default_realloc, default_free, NULL +}; + + + +/*============================================================================== + * MARK: - Null Memory Allocator (Private) + * This allocator is just a placeholder to ensure that the internal + * malloc/realloc/free function pointers are not null. + *============================================================================*/ + +static void *null_malloc(void *ctx, usize size) { + return NULL; +} + +static void *null_realloc(void *ctx, void *ptr, usize old_size, usize size) { + return NULL; +} + +static void null_free(void *ctx, void *ptr) { + return; +} + +static const yyjson_alc YYJSON_NULL_ALC = { + null_malloc, null_realloc, null_free, NULL +}; + + + +/*============================================================================== + * MARK: - Pool Memory Allocator (Public) + * This allocator is initialized with a fixed-size buffer. + * The buffer is split into multiple memory chunks for memory allocation. + *============================================================================*/ + +/** memory chunk header */ +typedef struct pool_chunk { + usize size; /* chunk memory size, include chunk header */ + struct pool_chunk *next; /* linked list, nullable */ + /* char mem[]; flexible array member */ +} pool_chunk; + +/** allocator ctx header */ +typedef struct pool_ctx { + usize size; /* total memory size, include ctx header */ + pool_chunk *free_list; /* linked list, nullable */ + /* pool_chunk chunks[]; flexible array member */ +} pool_ctx; + +/** align up the input size to chunk size */ +static_inline void pool_size_align(usize *size) { + *size = size_align_up(*size, sizeof(pool_chunk)) + sizeof(pool_chunk); +} + +static void *pool_malloc(void *ctx_ptr, usize size) { + /* assert(size != 0) */ + pool_ctx *ctx = (pool_ctx *)ctx_ptr; + pool_chunk *next, *prev = NULL, *cur = ctx->free_list; + + if (unlikely(size >= ctx->size)) return NULL; + pool_size_align(&size); + + while (cur) { + if (cur->size < size) { + /* not enough space, try next chunk */ + prev = cur; + cur = cur->next; + continue; + } + if (cur->size >= size + sizeof(pool_chunk) * 2) { + /* too much space, split this chunk */ + next = (pool_chunk *)(void *)((u8 *)cur + size); + next->size = cur->size - size; + next->next = cur->next; + cur->size = size; + } else { + /* just enough space, use whole chunk */ + next = cur->next; + } + if (prev) prev->next = next; + else ctx->free_list = next; + return (void *)(cur + 1); + } + return NULL; +} + +static void pool_free(void *ctx_ptr, void *ptr) { + /* assert(ptr != NULL) */ + pool_ctx *ctx = (pool_ctx *)ctx_ptr; + pool_chunk *cur = ((pool_chunk *)ptr) - 1; + pool_chunk *prev = NULL, *next = ctx->free_list; + + while (next && next < cur) { + prev = next; + next = next->next; + } + if (prev) prev->next = cur; + else ctx->free_list = cur; + cur->next = next; + + if (next && ((u8 *)cur + cur->size) == (u8 *)next) { + /* merge cur to higher chunk */ + cur->size += next->size; + cur->next = next->next; + } + if (prev && ((u8 *)prev + prev->size) == (u8 *)cur) { + /* merge cur to lower chunk */ + prev->size += cur->size; + prev->next = cur->next; + } +} + +static void *pool_realloc(void *ctx_ptr, void *ptr, + usize old_size, usize size) { + /* assert(ptr != NULL && size != 0 && old_size < size) */ + pool_ctx *ctx = (pool_ctx *)ctx_ptr; + pool_chunk *cur = ((pool_chunk *)ptr) - 1, *prev, *next, *tmp; + + /* check size */ + if (unlikely(size >= ctx->size)) return NULL; + pool_size_align(&old_size); + pool_size_align(&size); + if (unlikely(old_size == size)) return ptr; + + /* find next and prev chunk */ + prev = NULL; + next = ctx->free_list; + while (next && next < cur) { + prev = next; + next = next->next; + } + + if ((u8 *)cur + cur->size == (u8 *)next && cur->size + next->size >= size) { + /* merge to higher chunk if they are contiguous */ + usize free_size = cur->size + next->size - size; + if (free_size > sizeof(pool_chunk) * 2) { + tmp = (pool_chunk *)(void *)((u8 *)cur + size); + if (prev) prev->next = tmp; + else ctx->free_list = tmp; + tmp->next = next->next; + tmp->size = free_size; + cur->size = size; + } else { + if (prev) prev->next = next->next; + else ctx->free_list = next->next; + cur->size += next->size; + } + return ptr; + } else { + /* fallback to malloc and memcpy */ + void *new_ptr = pool_malloc(ctx_ptr, size - sizeof(pool_chunk)); + if (new_ptr) { + memcpy(new_ptr, ptr, cur->size - sizeof(pool_chunk)); + pool_free(ctx_ptr, ptr); + } + return new_ptr; + } +} + +bool yyjson_alc_pool_init(yyjson_alc *alc, void *buf, usize size) { + pool_chunk *chunk; + pool_ctx *ctx; + + if (unlikely(!alc)) return false; + *alc = YYJSON_NULL_ALC; + if (size < sizeof(pool_ctx) * 4) return false; + ctx = (pool_ctx *)mem_align_up(buf, sizeof(pool_ctx)); + if (unlikely(!ctx)) return false; + size -= (usize)((u8 *)ctx - (u8 *)buf); + size = size_align_down(size, sizeof(pool_ctx)); + + chunk = (pool_chunk *)(ctx + 1); + chunk->size = size - sizeof(pool_ctx); + chunk->next = NULL; + ctx->size = size; + ctx->free_list = chunk; + + alc->malloc = pool_malloc; + alc->realloc = pool_realloc; + alc->free = pool_free; + alc->ctx = (void *)ctx; + return true; +} + + + +/*============================================================================== + * MARK: - Dynamic Memory Allocator (Public) + * This allocator allocates memory on demand and does not immediately release + * unused memory. Instead, it places the unused memory into a freelist for + * potential reuse in the future. It is only when the entire allocator is + * destroyed that all previously allocated memory is released at once. + *============================================================================*/ + +/** memory chunk header */ +typedef struct dyn_chunk { + usize size; /* chunk size, include header */ + struct dyn_chunk *next; + /* char mem[]; flexible array member */ +} dyn_chunk; + +/** allocator ctx header */ +typedef struct { + dyn_chunk free_list; /* dummy header, sorted from small to large */ + dyn_chunk used_list; /* dummy header */ +} dyn_ctx; + +/** align up the input size to chunk size */ +static_inline bool dyn_size_align(usize *size) { + usize alc_size = *size + sizeof(dyn_chunk); + alc_size = size_align_up(alc_size, YYJSON_ALC_DYN_MIN_SIZE); + if (unlikely(alc_size < *size)) return false; /* overflow */ + *size = alc_size; + return true; +} + +/** remove a chunk from list (the chunk must already be in the list) */ +static_inline void dyn_chunk_list_remove(dyn_chunk *list, dyn_chunk *chunk) { + dyn_chunk *prev = list, *cur; + for (cur = prev->next; cur; cur = cur->next) { + if (cur == chunk) { + prev->next = cur->next; + cur->next = NULL; + return; + } + prev = cur; + } +} + +/** add a chunk to list header (the chunk must not be in the list) */ +static_inline void dyn_chunk_list_add(dyn_chunk *list, dyn_chunk *chunk) { + chunk->next = list->next; + list->next = chunk; +} + +static void *dyn_malloc(void *ctx_ptr, usize size) { + /* assert(size != 0) */ + const yyjson_alc def = YYJSON_DEFAULT_ALC; + dyn_ctx *ctx = (dyn_ctx *)ctx_ptr; + dyn_chunk *chunk, *prev; + if (unlikely(!dyn_size_align(&size))) return NULL; + + /* freelist is empty, create new chunk */ + if (!ctx->free_list.next) { + chunk = (dyn_chunk *)def.malloc(def.ctx, size); + if (unlikely(!chunk)) return NULL; + chunk->size = size; + chunk->next = NULL; + dyn_chunk_list_add(&ctx->used_list, chunk); + return (void *)(chunk + 1); + } + + /* find a large enough chunk, or resize the largest chunk */ + prev = &ctx->free_list; + while (true) { + chunk = prev->next; + if (chunk->size >= size) { /* enough size, reuse this chunk */ + prev->next = chunk->next; + dyn_chunk_list_add(&ctx->used_list, chunk); + return (void *)(chunk + 1); + } + if (!chunk->next) { /* resize the largest chunk */ + chunk = (dyn_chunk *)def.realloc(def.ctx, chunk, chunk->size, size); + if (unlikely(!chunk)) return NULL; + prev->next = NULL; + chunk->size = size; + dyn_chunk_list_add(&ctx->used_list, chunk); + return (void *)(chunk + 1); + } + prev = chunk; + } +} + +static void *dyn_realloc(void *ctx_ptr, void *ptr, + usize old_size, usize size) { + /* assert(ptr != NULL && size != 0 && old_size < size) */ + const yyjson_alc def = YYJSON_DEFAULT_ALC; + dyn_ctx *ctx = (dyn_ctx *)ctx_ptr; + dyn_chunk *new_chunk, *chunk = (dyn_chunk *)ptr - 1; + if (unlikely(!dyn_size_align(&size))) return NULL; + if (chunk->size >= size) return ptr; + + dyn_chunk_list_remove(&ctx->used_list, chunk); + new_chunk = (dyn_chunk *)def.realloc(def.ctx, chunk, chunk->size, size); + if (likely(new_chunk)) { + new_chunk->size = size; + chunk = new_chunk; + } + dyn_chunk_list_add(&ctx->used_list, chunk); + return new_chunk ? (void *)(new_chunk + 1) : NULL; +} + +static void dyn_free(void *ctx_ptr, void *ptr) { + /* assert(ptr != NULL) */ + dyn_ctx *ctx = (dyn_ctx *)ctx_ptr; + dyn_chunk *chunk = (dyn_chunk *)ptr - 1, *prev; + + dyn_chunk_list_remove(&ctx->used_list, chunk); + for (prev = &ctx->free_list; prev; prev = prev->next) { + if (!prev->next || prev->next->size >= chunk->size) { + chunk->next = prev->next; + prev->next = chunk; + break; + } + } +} + +yyjson_alc *yyjson_alc_dyn_new(void) { + const yyjson_alc def = YYJSON_DEFAULT_ALC; + usize hdr_len = sizeof(yyjson_alc) + sizeof(dyn_ctx); + yyjson_alc *alc = (yyjson_alc *)def.malloc(def.ctx, hdr_len); + dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1); + if (unlikely(!alc)) return NULL; + alc->malloc = dyn_malloc; + alc->realloc = dyn_realloc; + alc->free = dyn_free; + alc->ctx = alc + 1; + memset(ctx, 0, sizeof(*ctx)); + return alc; +} + +void yyjson_alc_dyn_free(yyjson_alc *alc) { + const yyjson_alc def = YYJSON_DEFAULT_ALC; + dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1); + dyn_chunk *chunk, *next; + if (unlikely(!alc)) return; + for (chunk = ctx->free_list.next; chunk; chunk = next) { + next = chunk->next; + def.free(def.ctx, chunk); + } + for (chunk = ctx->used_list.next; chunk; chunk = next) { + next = chunk->next; + def.free(def.ctx, chunk); + } + def.free(def.ctx, alc); +} + + + +/*============================================================================== + * MARK: - JSON Struct Utils (Public) + * These functions are used for creating, copying, releasing, and comparing + * JSON documents and values. They are widely used throughout this library. + *============================================================================*/ + +static_inline void unsafe_yyjson_str_pool_release(yyjson_str_pool *pool, + yyjson_alc *alc) { + yyjson_str_chunk *chunk = pool->chunks, *next; + while (chunk) { + next = chunk->next; + alc->free(alc->ctx, chunk); + chunk = next; + } +} + +static_inline void unsafe_yyjson_val_pool_release(yyjson_val_pool *pool, + yyjson_alc *alc) { + yyjson_val_chunk *chunk = pool->chunks, *next; + while (chunk) { + next = chunk->next; + alc->free(alc->ctx, chunk); + chunk = next; + } +} + +bool unsafe_yyjson_str_pool_grow(yyjson_str_pool *pool, + const yyjson_alc *alc, usize len) { + yyjson_str_chunk *chunk; + usize size, max_len; + + /* create a new chunk */ + max_len = USIZE_MAX - sizeof(yyjson_str_chunk); + if (unlikely(len > max_len)) return false; + size = len + sizeof(yyjson_str_chunk); + size = yyjson_max(pool->chunk_size, size); + chunk = (yyjson_str_chunk *)alc->malloc(alc->ctx, size); + if (unlikely(!chunk)) return false; + + /* insert the new chunk as the head of the linked list */ + chunk->next = pool->chunks; + chunk->chunk_size = size; + pool->chunks = chunk; + pool->cur = (char *)chunk + sizeof(yyjson_str_chunk); + pool->end = (char *)chunk + size; + + /* the next chunk is twice the size of the current one */ + size = yyjson_min(pool->chunk_size * 2, pool->chunk_size_max); + if (size < pool->chunk_size) size = pool->chunk_size_max; /* overflow */ + pool->chunk_size = size; + return true; +} + +bool unsafe_yyjson_val_pool_grow(yyjson_val_pool *pool, + const yyjson_alc *alc, usize count) { + yyjson_val_chunk *chunk; + usize size, max_count; + + /* create a new chunk */ + max_count = USIZE_MAX / sizeof(yyjson_mut_val) - 1; + if (unlikely(count > max_count)) return false; + size = (count + 1) * sizeof(yyjson_mut_val); + size = yyjson_max(pool->chunk_size, size); + chunk = (yyjson_val_chunk *)alc->malloc(alc->ctx, size); + if (unlikely(!chunk)) return false; + + /* insert the new chunk as the head of the linked list */ + chunk->next = pool->chunks; + chunk->chunk_size = size; + pool->chunks = chunk; + pool->cur = (yyjson_mut_val *)(void *)((u8 *)chunk) + 1; + pool->end = (yyjson_mut_val *)(void *)((u8 *)chunk + size); + + /* the next chunk is twice the size of the current one */ + size = yyjson_min(pool->chunk_size * 2, pool->chunk_size_max); + if (size < pool->chunk_size) size = pool->chunk_size_max; /* overflow */ + pool->chunk_size = size; + return true; +} + +bool yyjson_mut_doc_set_str_pool_size(yyjson_mut_doc *doc, size_t len) { + usize max_size = USIZE_MAX - sizeof(yyjson_str_chunk); + if (!doc || !len || len > max_size) return false; + doc->str_pool.chunk_size = len + sizeof(yyjson_str_chunk); + return true; +} + +bool yyjson_mut_doc_set_val_pool_size(yyjson_mut_doc *doc, size_t count) { + usize max_count = USIZE_MAX / sizeof(yyjson_mut_val) - 1; + if (!doc || !count || count > max_count) return false; + doc->val_pool.chunk_size = (count + 1) * sizeof(yyjson_mut_val); + return true; +} + +void yyjson_mut_doc_free(yyjson_mut_doc *doc) { + if (doc) { + yyjson_alc alc = doc->alc; + memset(&doc->alc, 0, sizeof(alc)); + unsafe_yyjson_str_pool_release(&doc->str_pool, &alc); + unsafe_yyjson_val_pool_release(&doc->val_pool, &alc); + alc.free(alc.ctx, doc); + } +} + +yyjson_mut_doc *yyjson_mut_doc_new(const yyjson_alc *alc) { + yyjson_mut_doc *doc; + if (!alc) alc = &YYJSON_DEFAULT_ALC; + doc = (yyjson_mut_doc *)alc->malloc(alc->ctx, sizeof(yyjson_mut_doc)); + if (!doc) return NULL; + memset(doc, 0, sizeof(yyjson_mut_doc)); + + doc->alc = *alc; + doc->str_pool.chunk_size = YYJSON_MUT_DOC_STR_POOL_INIT_SIZE; + doc->str_pool.chunk_size_max = YYJSON_MUT_DOC_STR_POOL_MAX_SIZE; + doc->val_pool.chunk_size = YYJSON_MUT_DOC_VAL_POOL_INIT_SIZE; + doc->val_pool.chunk_size_max = YYJSON_MUT_DOC_VAL_POOL_MAX_SIZE; + return doc; +} + +yyjson_mut_doc *yyjson_doc_mut_copy(yyjson_doc *doc, const yyjson_alc *alc) { + yyjson_mut_doc *m_doc; + yyjson_mut_val *m_val; + + if (!doc || !doc->root) return NULL; + m_doc = yyjson_mut_doc_new(alc); + if (!m_doc) return NULL; + m_val = yyjson_val_mut_copy(m_doc, doc->root); + if (!m_val) { + yyjson_mut_doc_free(m_doc); + return NULL; + } + yyjson_mut_doc_set_root(m_doc, m_val); + return m_doc; +} + +yyjson_mut_doc *yyjson_mut_doc_mut_copy(yyjson_mut_doc *doc, + const yyjson_alc *alc) { + yyjson_mut_doc *m_doc; + yyjson_mut_val *m_val; + + if (!doc) return NULL; + if (!doc->root) return yyjson_mut_doc_new(alc); + + m_doc = yyjson_mut_doc_new(alc); + if (!m_doc) return NULL; + m_val = yyjson_mut_val_mut_copy(m_doc, doc->root); + if (!m_val) { + yyjson_mut_doc_free(m_doc); + return NULL; + } + yyjson_mut_doc_set_root(m_doc, m_val); + return m_doc; +} + +yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *m_doc, + yyjson_val *i_vals) { + /* + The immutable object or array stores all sub-values in a contiguous memory, + We copy them to another contiguous memory as mutable values, + then reconnect the mutable values with the original relationship. + */ + usize i_vals_len; + yyjson_mut_val *m_vals, *m_val; + yyjson_val *i_val, *i_end; + + if (!m_doc || !i_vals) return NULL; + i_end = unsafe_yyjson_get_next(i_vals); + i_vals_len = (usize)(unsafe_yyjson_get_next(i_vals) - i_vals); + m_vals = unsafe_yyjson_mut_val(m_doc, i_vals_len); + if (!m_vals) return NULL; + i_val = i_vals; + m_val = m_vals; + + for (; i_val < i_end; i_val++, m_val++) { + yyjson_type type = unsafe_yyjson_get_type(i_val); + m_val->tag = i_val->tag; + m_val->uni.u64 = i_val->uni.u64; + if (type == YYJSON_TYPE_STR || type == YYJSON_TYPE_RAW) { + const char *str = i_val->uni.str; + usize str_len = unsafe_yyjson_get_len(i_val); + m_val->uni.str = unsafe_yyjson_mut_strncpy(m_doc, str, str_len); + if (!m_val->uni.str) return NULL; + } else if (type == YYJSON_TYPE_ARR) { + usize len = unsafe_yyjson_get_len(i_val); + if (len > 0) { + yyjson_val *ii_val = i_val + 1, *ii_next; + yyjson_mut_val *mm_val = m_val + 1, *mm_ctn = m_val, *mm_next; + while (len-- > 1) { + ii_next = unsafe_yyjson_get_next(ii_val); + mm_next = mm_val + (ii_next - ii_val); + mm_val->next = mm_next; + ii_val = ii_next; + mm_val = mm_next; + } + mm_val->next = mm_ctn + 1; + mm_ctn->uni.ptr = mm_val; + } + } else if (type == YYJSON_TYPE_OBJ) { + usize len = unsafe_yyjson_get_len(i_val); + if (len > 0) { + yyjson_val *ii_key = i_val + 1, *ii_nextkey; + yyjson_mut_val *mm_key = m_val + 1, *mm_ctn = m_val; + yyjson_mut_val *mm_nextkey; + while (len-- > 1) { + ii_nextkey = unsafe_yyjson_get_next(ii_key + 1); + mm_nextkey = mm_key + (ii_nextkey - ii_key); + mm_key->next = mm_key + 1; + mm_key->next->next = mm_nextkey; + ii_key = ii_nextkey; + mm_key = mm_nextkey; + } + mm_key->next = mm_key + 1; + mm_key->next->next = mm_ctn + 1; + mm_ctn->uni.ptr = mm_key; + } + } + } + return m_vals; +} + +static yyjson_mut_val *unsafe_yyjson_mut_val_mut_copy(yyjson_mut_doc *m_doc, + yyjson_mut_val *m_vals) { + /* + The mutable object or array stores all sub-values in a circular linked + list, so we can traverse them in the same loop. The traversal starts from + the last item, continues with the first item in a list, and ends with the + second to last item, which needs to be linked to the last item to close the + circle. + */ + yyjson_mut_val *m_val = unsafe_yyjson_mut_val(m_doc, 1); + if (unlikely(!m_val)) return NULL; + m_val->tag = m_vals->tag; + + switch (unsafe_yyjson_get_type(m_vals)) { + case YYJSON_TYPE_OBJ: + case YYJSON_TYPE_ARR: + if (unsafe_yyjson_get_len(m_vals) > 0) { + yyjson_mut_val *last = (yyjson_mut_val *)m_vals->uni.ptr; + yyjson_mut_val *next = last->next, *prev; + prev = unsafe_yyjson_mut_val_mut_copy(m_doc, last); + if (!prev) return NULL; + m_val->uni.ptr = (void *)prev; + while (next != last) { + prev->next = unsafe_yyjson_mut_val_mut_copy(m_doc, next); + if (!prev->next) return NULL; + prev = prev->next; + next = next->next; + } + prev->next = (yyjson_mut_val *)m_val->uni.ptr; + } + break; + case YYJSON_TYPE_RAW: + case YYJSON_TYPE_STR: { + const char *str = m_vals->uni.str; + usize str_len = unsafe_yyjson_get_len(m_vals); + m_val->uni.str = unsafe_yyjson_mut_strncpy(m_doc, str, str_len); + if (!m_val->uni.str) return NULL; + break; + } + default: + m_val->uni = m_vals->uni; + break; + } + return m_val; +} + +yyjson_mut_val *yyjson_mut_val_mut_copy(yyjson_mut_doc *doc, + yyjson_mut_val *val) { + if (doc && val) return unsafe_yyjson_mut_val_mut_copy(doc, val); + return NULL; +} + +/* Count the number of values and the total length of the strings. */ +static void yyjson_mut_stat(yyjson_mut_val *val, + usize *val_sum, usize *str_sum) { + yyjson_type type = unsafe_yyjson_get_type(val); + *val_sum += 1; + if (type == YYJSON_TYPE_ARR || type == YYJSON_TYPE_OBJ) { + yyjson_mut_val *child = (yyjson_mut_val *)val->uni.ptr; + usize len = unsafe_yyjson_get_len(val), i; + len <<= (u8)(type == YYJSON_TYPE_OBJ); + *val_sum += len; + for (i = 0; i < len; i++) { + yyjson_type stype = unsafe_yyjson_get_type(child); + if (stype == YYJSON_TYPE_STR || stype == YYJSON_TYPE_RAW) { + *str_sum += unsafe_yyjson_get_len(child) + 1; + } else if (stype == YYJSON_TYPE_ARR || stype == YYJSON_TYPE_OBJ) { + yyjson_mut_stat(child, val_sum, str_sum); + *val_sum -= 1; + } + child = child->next; + } + } else if (type == YYJSON_TYPE_STR || type == YYJSON_TYPE_RAW) { + *str_sum += unsafe_yyjson_get_len(val) + 1; + } +} + +/* Copy mutable values to immutable value pool. */ +static usize yyjson_imut_copy(yyjson_val **val_ptr, char **buf_ptr, + yyjson_mut_val *mval) { + yyjson_val *val = *val_ptr; + yyjson_type type = unsafe_yyjson_get_type(mval); + if (type == YYJSON_TYPE_ARR || type == YYJSON_TYPE_OBJ) { + yyjson_mut_val *child = (yyjson_mut_val *)mval->uni.ptr; + usize len = unsafe_yyjson_get_len(mval), i; + usize val_sum = 1; + if (type == YYJSON_TYPE_OBJ) { + if (len) child = child->next->next; + len <<= 1; + } else { + if (len) child = child->next; + } + *val_ptr = val + 1; + for (i = 0; i < len; i++) { + val_sum += yyjson_imut_copy(val_ptr, buf_ptr, child); + child = child->next; + } + val->tag = mval->tag; + val->uni.ofs = val_sum * sizeof(yyjson_val); + return val_sum; + } else if (type == YYJSON_TYPE_STR || type == YYJSON_TYPE_RAW) { + char *buf = *buf_ptr; + usize len = unsafe_yyjson_get_len(mval); + memcpy((void *)buf, (const void *)mval->uni.str, len); + buf[len] = '\0'; + val->tag = mval->tag; + val->uni.str = buf; + *val_ptr = val + 1; + *buf_ptr = buf + len + 1; + return 1; + } else { + val->tag = mval->tag; + val->uni = mval->uni; + *val_ptr = val + 1; + return 1; + } +} + +yyjson_doc *yyjson_mut_doc_imut_copy(yyjson_mut_doc *mdoc, + const yyjson_alc *alc) { + if (!mdoc) return NULL; + return yyjson_mut_val_imut_copy(mdoc->root, alc); +} + +yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *mval, + const yyjson_alc *alc) { + usize val_num = 0, str_sum = 0, hdr_size, buf_size; + yyjson_doc *doc = NULL; + yyjson_val *val_hdr = NULL; + + /* This value should be NULL here. Setting a non-null value suppresses + warning from the clang analyzer. */ + char *str_hdr = (char *)(void *)&str_sum; + if (!mval) return NULL; + if (!alc) alc = &YYJSON_DEFAULT_ALC; + + /* traverse the input value to get pool size */ + yyjson_mut_stat(mval, &val_num, &str_sum); + + /* create doc and val pool */ + hdr_size = size_align_up(sizeof(yyjson_doc), sizeof(yyjson_val)); + buf_size = hdr_size + val_num * sizeof(yyjson_val); + doc = (yyjson_doc *)alc->malloc(alc->ctx, buf_size); + if (!doc) return NULL; + memset(doc, 0, sizeof(yyjson_doc)); + val_hdr = (yyjson_val *)(void *)((char *)(void *)doc + hdr_size); + doc->root = val_hdr; + doc->alc = *alc; + + /* create str pool */ + if (str_sum > 0) { + str_hdr = (char *)alc->malloc(alc->ctx, str_sum); + doc->str_pool = str_hdr; + if (!str_hdr) { + alc->free(alc->ctx, (void *)doc); + return NULL; + } + } + + /* copy vals and strs */ + doc->val_read = yyjson_imut_copy(&val_hdr, &str_hdr, mval); + doc->dat_read = str_sum + 1; + return doc; +} + +static_inline bool unsafe_yyjson_num_equals(void *lhs, void *rhs) { + yyjson_val_uni *luni = &((yyjson_val *)lhs)->uni; + yyjson_val_uni *runi = &((yyjson_val *)rhs)->uni; + yyjson_subtype lt = unsafe_yyjson_get_subtype(lhs); + yyjson_subtype rt = unsafe_yyjson_get_subtype(rhs); + if (lt == rt) return luni->u64 == runi->u64; + if (lt == YYJSON_SUBTYPE_SINT && rt == YYJSON_SUBTYPE_UINT) { + return luni->i64 >= 0 && luni->u64 == runi->u64; + } + if (lt == YYJSON_SUBTYPE_UINT && rt == YYJSON_SUBTYPE_SINT) { + return runi->i64 >= 0 && luni->u64 == runi->u64; + } + return false; +} + +static_inline bool unsafe_yyjson_str_equals(void *lhs, void *rhs) { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + return !memcmp(unsafe_yyjson_get_str(lhs), + unsafe_yyjson_get_str(rhs), len); +} + +bool unsafe_yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { + yyjson_type type = unsafe_yyjson_get_type(lhs); + if (type != unsafe_yyjson_get_type(rhs)) return false; + + switch (type) { + case YYJSON_TYPE_OBJ: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + yyjson_obj_iter iter; + yyjson_obj_iter_init(rhs, &iter); + lhs = unsafe_yyjson_get_first(lhs); + while (len-- > 0) { + rhs = yyjson_obj_iter_getn(&iter, lhs->uni.str, + unsafe_yyjson_get_len(lhs)); + if (!rhs) return false; + if (!unsafe_yyjson_equals(lhs + 1, rhs)) return false; + lhs = unsafe_yyjson_get_next(lhs + 1); + } + } + /* yyjson allows duplicate keys, so the check may be inaccurate */ + return true; + } + + case YYJSON_TYPE_ARR: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + lhs = unsafe_yyjson_get_first(lhs); + rhs = unsafe_yyjson_get_first(rhs); + while (len-- > 0) { + if (!unsafe_yyjson_equals(lhs, rhs)) return false; + lhs = unsafe_yyjson_get_next(lhs); + rhs = unsafe_yyjson_get_next(rhs); + } + } + return true; + } + + case YYJSON_TYPE_NUM: + return unsafe_yyjson_num_equals(lhs, rhs); + + case YYJSON_TYPE_RAW: + case YYJSON_TYPE_STR: + return unsafe_yyjson_str_equals(lhs, rhs); + + case YYJSON_TYPE_NULL: + case YYJSON_TYPE_BOOL: + return lhs->tag == rhs->tag; + + default: + return false; + } +} + +bool unsafe_yyjson_mut_equals(yyjson_mut_val *lhs, yyjson_mut_val *rhs) { + yyjson_type type = unsafe_yyjson_get_type(lhs); + if (type != unsafe_yyjson_get_type(rhs)) return false; + + switch (type) { + case YYJSON_TYPE_OBJ: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + yyjson_mut_obj_iter iter; + yyjson_mut_obj_iter_init(rhs, &iter); + lhs = (yyjson_mut_val *)lhs->uni.ptr; + while (len-- > 0) { + rhs = yyjson_mut_obj_iter_getn(&iter, lhs->uni.str, + unsafe_yyjson_get_len(lhs)); + if (!rhs) return false; + if (!unsafe_yyjson_mut_equals(lhs->next, rhs)) return false; + lhs = lhs->next->next; + } + } + /* yyjson allows duplicate keys, so the check may be inaccurate */ + return true; + } + + case YYJSON_TYPE_ARR: { + usize len = unsafe_yyjson_get_len(lhs); + if (len != unsafe_yyjson_get_len(rhs)) return false; + if (len > 0) { + lhs = (yyjson_mut_val *)lhs->uni.ptr; + rhs = (yyjson_mut_val *)rhs->uni.ptr; + while (len-- > 0) { + if (!unsafe_yyjson_mut_equals(lhs, rhs)) return false; + lhs = lhs->next; + rhs = rhs->next; + } + } + return true; + } + + case YYJSON_TYPE_NUM: + return unsafe_yyjson_num_equals(lhs, rhs); + + case YYJSON_TYPE_RAW: + case YYJSON_TYPE_STR: + return unsafe_yyjson_str_equals(lhs, rhs); + + case YYJSON_TYPE_NULL: + case YYJSON_TYPE_BOOL: + return lhs->tag == rhs->tag; + + default: + return false; + } +} + +bool yyjson_locate_pos(const char *str, size_t len, size_t pos, + size_t *line, size_t *col, size_t *chr) { + usize line_sum = 0, line_pos = 0, chr_sum = 0; + const u8 *cur = (const u8 *)str; + const u8 *end = cur + pos; + + if (!str || pos > len) { + if (line) *line = 0; + if (col) *col = 0; + if (chr) *chr = 0; + return false; + } + + if (pos >= 3 && is_utf8_bom(cur)) cur += 3; /* don't count BOM */ + while (cur < end) { + u8 c = *cur; + chr_sum += 1; + if (likely(c < 0x80)) { /* 0xxxxxxx (0x00-0x7F) ASCII */ + if (c == '\n') { + line_sum += 1; + line_pos = chr_sum; + } + cur += 1; + } + else if (c < 0xC0) cur += 1; /* 10xxxxxx (0x80-0xBF) Invalid */ + else if (c < 0xE0) cur += 2; /* 110xxxxx (0xC0-0xDF) 2-byte UTF-8 */ + else if (c < 0xF0) cur += 3; /* 1110xxxx (0xE0-0xEF) 3-byte UTF-8 */ + else if (c < 0xF8) cur += 4; /* 11110xxx (0xF0-0xF7) 4-byte UTF-8 */ + else cur += 1; /* 11111xxx (0xF8-0xFF) Invalid */ + } + if (line) *line = line_sum + 1; + if (col) *col = chr_sum - line_pos + 1; + if (chr) *chr = chr_sum; + return true; +} + + + +#if !YYJSON_DISABLE_READER /* reader begin */ + +/* Check read flag, avoids `always false` warning when disabled. */ +#define has_flg(_flg) unlikely(has_rflag(flg, YYJSON_READ_##_flg, 0)) +#define has_allow(_flg) unlikely(has_rflag(flg, YYJSON_READ_ALLOW_##_flg, 1)) +#define YYJSON_READ_ALLOW_TRIVIA (YYJSON_READ_ALLOW_COMMENTS | \ + YYJSON_READ_ALLOW_EXT_WHITESPACE) +static_inline bool has_rflag(yyjson_read_flag flg, yyjson_read_flag chk, + bool non_standard) { +#if YYJSON_DISABLE_NON_STANDARD + if (non_standard) return false; +#endif + return (flg & chk) != 0; +} + + + +/*============================================================================== + * MARK: - JSON Reader Utils (Private) + * These functions are used by JSON reader to read literals and comments. + *============================================================================*/ + +/** Read `true` literal, `*ptr[0]` should be `t`. */ +static_inline bool read_true(u8 **ptr, yyjson_val *val) { + u8 *cur = *ptr; + if (likely(byte_match_4(cur, "true"))) { + val->tag = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_TRUE; + *ptr = cur + 4; + return true; + } + return false; +} + +/** Read `false` literal, `*ptr[0]` should be `f`. */ +static_inline bool read_false(u8 **ptr, yyjson_val *val) { + u8 *cur = *ptr; + if (likely(byte_match_4(cur + 1, "alse"))) { + val->tag = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_FALSE; + *ptr = cur + 5; + return true; + } + return false; +} + +/** Read `null` literal, `*ptr[0]` should be `n`. */ +static_inline bool read_null(u8 **ptr, yyjson_val *val) { + u8 *cur = *ptr; + if (likely(byte_match_4(cur, "null"))) { + val->tag = YYJSON_TYPE_NULL; + *ptr = cur + 4; + return true; + } + return false; +} + +/** Read `Inf` or `Infinity` literal (ignoring case). */ +static_inline bool read_inf(u8 **ptr, u8 **pre, + yyjson_read_flag flg, yyjson_val *val) { + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + bool sign = (*cur == '-'); + if (*cur == '+' && !has_allow(EXT_NUMBER)) return false; + cur += char_is_sign(*cur); + if (char_to_lower(cur[0]) == 'i' && + char_to_lower(cur[1]) == 'n' && + char_to_lower(cur[2]) == 'f') { + if (char_to_lower(cur[3]) == 'i') { + if (char_to_lower(cur[4]) == 'n' && + char_to_lower(cur[5]) == 'i' && + char_to_lower(cur[6]) == 't' && + char_to_lower(cur[7]) == 'y') { + cur += 8; + } else { + return false; + } + } else { + cur += 3; + } + *end = cur; + if (has_flg(NUMBER_AS_RAW)) { + **pre = '\0'; /* add null-terminator for previous raw string */ + *pre = cur; /* save end position for current raw string */ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; + val->uni.str = (const char *)hdr; + } else { + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + val->uni.u64 = f64_bits_inf(sign); + } + return true; + } + return false; +} + +/** Read `NaN` literal (ignoring case). */ +static_inline bool read_nan(u8 **ptr, u8 **pre, + yyjson_read_flag flg, yyjson_val *val) { + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + bool sign = (*cur == '-'); + if (*cur == '+' && !has_allow(EXT_NUMBER)) return false; + cur += char_is_sign(*cur); + if (char_to_lower(cur[0]) == 'n' && + char_to_lower(cur[1]) == 'a' && + char_to_lower(cur[2]) == 'n') { + cur += 3; + *end = cur; + if (has_flg(NUMBER_AS_RAW)) { + **pre = '\0'; /* add null-terminator for previous raw string */ + *pre = cur; /* save end position for current raw string */ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; + val->uni.str = (const char *)hdr; + } else { + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + val->uni.u64 = f64_bits_nan(sign); + } + return true; + } + return false; +} + +/** Read `Inf`, `Infinity` or `NaN` literal (ignoring case). */ +static_inline bool read_inf_or_nan(u8 **ptr, u8 **pre, + yyjson_read_flag flg, yyjson_val *val) { + if (read_inf(ptr, pre, flg, val)) return true; + if (read_nan(ptr, pre, flg, val)) return true; + return false; +} + +/** Read a JSON number as raw string. */ +static_noinline bool read_num_raw(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { +#define return_err(_pos, _msg) do { \ + *msg = _msg; *end = _pos; return false; \ +} while (false) + +#define return_raw() do { \ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; \ + val->uni.str = (const char *)hdr; \ + **pre = '\0'; *pre = cur; *end = cur; return true; \ +} while (false) + + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + + /* skip sign */ + cur += (*cur == '-'); + + /* read first digit, check leading zero */ + while (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + if (*cur == '+' && cur == hdr) { /* leading `+` sign */ + cur++; + continue; + } + if (*cur == '.' && char_is_digit(cur[1])) { /* e.g. '.123' */ + goto read_double; + } + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(ptr, pre, flg, val)) return true; + } + return_err(cur, "no digit after sign"); + } + + /* read integral part */ + if (*cur == '0') { + cur++; + if (unlikely(char_is_digit(*cur))) { + return_err(cur - 1, "number with leading zero is not allowed"); + } + if (!char_is_fp(*cur)) { + if (has_allow(EXT_NUMBER) && char_to_lower(*cur) == 'x') { /* hex */ + if (!char_is_hex(*++cur)) return_err(cur, "invalid hex number"); + while(char_is_hex(*cur)) cur++; + } + return_raw(); + } + } else { + while (char_is_digit(*cur)) cur++; + if (!char_is_fp(*cur)) return_raw(); + } + +read_double: + /* read fraction part */ + if (*cur == '.') { + cur++; + if (!char_is_digit(*cur)) { + if (has_allow(EXT_NUMBER)) { + if (!char_is_exp(*cur)) return_raw(); + } else { + return_err(cur, "no digit after decimal point"); + } + } + while (char_is_digit(*cur)) cur++; + } + + /* read exponent part */ + if (char_is_exp(*cur)) { + cur += 1 + char_is_sign(cur[1]); + if (!char_is_digit(*cur++)) { + return_err(cur, "no digit after exponent sign"); + } + while (char_is_digit(*cur)) cur++; + } + + return_raw(); + +#undef return_err +#undef return_raw +} + +/** Read a hex number. */ +static_noinline bool read_num_hex(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + u64 sig = 0, i = 0; + bool sign; + + /* skip sign and '0x' */ + sign = (*cur == '-'); + cur += (*cur == '-' || *cur == '+') + 2; + + /* read hex */ + for(; i < 16; i++) { + u8 c = hex_conv_table[cur[i]]; + if (c == 0xF0) break; + sig <<= 4; + sig |= c; + } + + /* check error */ + if (unlikely(i == 0)) { + *msg = "invalid hex number"; + return false; + } + + /* check overflow */ + if (unlikely(i == 16)) { + if (char_is_hex(cur[16]) || (sign && sig > ((u64)1 << 63))) { + if (!has_flg(BIGNUM_AS_RAW)) { + *msg = "hex number overflow"; + return false; + } + cur += 16; + while (char_is_hex(*cur)) cur++; + **pre = '\0'; + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; + val->uni.str = (const char *)hdr; + *pre = cur; *end = cur; + return true; + } + } + + val->tag = YYJSON_TYPE_NUM | (u64)((u8)sign << 3); + val->uni.u64 = (u64)(sign ? (u64)(~(sig) + 1) : (u64)(sig)); + *end = cur + i; + return true; +} + +/** + Skip trivia (whitespace and comments). + This function should be used only when `char_is_trivia()` returns true. + @param ptr (inout) Input current position, output end position. + @param eof JSON end position. + @param flg JSON read flags. + @return true if at least one character was skipped. + false if no characters were skipped, + or if a multi-line comment is unterminated; + in the latter case, `ptr` will be set to `eof`. + */ +static_noinline bool skip_trivia(u8 **ptr, u8 *eof, yyjson_read_flag flg) { + u8 *hdr = *ptr, *cur = *ptr; + usize len; + + while (cur < eof) { + u8 *loop_begin = cur; + + /* skip standard whitespace */ + while(char_is_space(*cur)) cur++; + + /* skip extended whitespace */ + if (has_allow(EXT_WHITESPACE)) { + while (char_is_space_ext(*cur)) { + cur += (len = ext_space_len(cur)); + if (!len) break; + } + } + + /* skip comment, do not validate encoding */ + if (has_allow(COMMENTS) && cur[0] == '/') { + if (cur[1] == '/') { /* single-line comment */ + cur += 2; + if (has_allow(EXT_WHITESPACE)) { + while (cur < eof) { + if (char_is_eol_ext(*cur)) { + cur += (len = ext_eol_len(cur)); + if (len) break; + } + cur++; + } + } else { + while (cur < eof && !char_is_eol(*cur)) cur++; + } + } else if (cur[1] == '*') { /* multi-line comment */ + cur += 2; + while (!byte_match_2(cur, "*/") && cur < eof) cur++; + if (cur == eof) { + *ptr = eof; + return false; /* unclosed comment */ + } + cur += 2; + } + } + if (cur == loop_begin) break; + } + *ptr = cur; + return cur > hdr; +} + +/** + Check truncated UTF-8 character. + Return true if `cur` starts a valid UTF-8 sequence that is truncated. + */ +static bool is_truncated_utf8(u8 *cur, u8 *eof) { + u8 c0, c1, c2; + usize len = (usize)(eof - cur); + if (cur >= eof || len >= 4) return false; + c0 = cur[0]; c1 = cur[1]; c2 = cur[2]; + /* 1-byte UTF-8, not truncated */ + if (c0 < 0x80) return false; + if (len == 1) { + /* 2-byte UTF-8, truncated */ + if ((c0 & 0xE0) == 0xC0 && (c0 & 0x1E) != 0x00) return true; + /* 3-byte UTF-8, truncated */ + if ((c0 & 0xF0) == 0xE0) return true; + /* 4-byte UTF-8, truncated */ + if ((c0 & 0xF8) == 0xF0 && (c0 & 0x07) <= 0x04) return true; + } else if (len == 2) { + /* 3-byte UTF-8, truncated */ + if ((c0 & 0xF0) == 0xE0 && (c1 & 0xC0) == 0x80) { + u8 t = (u8)(((c0 & 0x0F) << 1) | ((c1 & 0x20) >> 5)); + return 0x01 <= t && t != 0x1B; + } + /* 4-byte UTF-8, truncated */ + if ((c0 & 0xF8) == 0xF0 && (c1 & 0xC0) == 0x80) { + u8 t = (u8)(((c0 & 0x07) << 2) | ((c1 & 0x30) >> 4)); + return 0x01 <= t && t <= 0x10; + } + } else if (len == 3) { + /* 4 bytes UTF-8, truncated */ + if ((c0 & 0xF8) == 0xF0 && (c1 & 0xC0) == 0x80 && (c2 & 0xC0) == 0x80) { + u8 t = (u8)(((c0 & 0x07) << 2) | ((c1 & 0x30) >> 4)); + return 0x01 <= t && t <= 0x10; + } + } + return false; +} + +/** + Check truncated string. + Returns true if `cur` match `str` but is truncated. + The `str` should be lowercase ASCII letters. + */ +static bool is_truncated_str(u8 *cur, u8 *eof, const char *str, + bool case_sensitive) { + usize len = strlen(str); + if (cur + len <= eof || eof <= cur) return false; + if (case_sensitive) { + return memcmp(cur, str, (usize)(eof - cur)) == 0; + } + for (; cur < eof; cur++, str++) { + if (char_to_lower(*cur) != *(const u8 *)str) return false; + } + return true; +} + +/** + Check truncated JSON on parsing errors. + Returns true if the input is valid but truncated. + */ +static_noinline bool is_truncated_end(u8 *hdr, u8 *cur, u8 *eof, + yyjson_read_code code, + yyjson_read_flag flg) { + if (cur >= eof) return true; + if (code == YYJSON_READ_ERROR_LITERAL) { + if (is_truncated_str(cur, eof, "true", true) || + is_truncated_str(cur, eof, "false", true) || + is_truncated_str(cur, eof, "null", true)) { + return true; + } + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CHARACTER || + code == YYJSON_READ_ERROR_INVALID_NUMBER || + code == YYJSON_READ_ERROR_LITERAL) { + if (has_allow(INF_AND_NAN)) { + if (*cur == '-') cur++; + if (is_truncated_str(cur, eof, "infinity", false) || + is_truncated_str(cur, eof, "nan", false)) { + return true; + } + } + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CONTENT) { + if (has_allow(INF_AND_NAN)) { + if (hdr + 3 <= cur && + is_truncated_str(cur - 3, eof, "infinity", false)) { + return true; /* e.g. infin would be read as inf + in */ + } + } + } + if (code == YYJSON_READ_ERROR_INVALID_STRING) { + usize len = (usize)(eof - cur); + + /* unicode escape sequence */ + if (*cur == '\\') { + if (len == 1) return true; + if (len <= 5) { + if (*++cur != 'u') return false; + for (++cur; cur < eof; cur++) { + if (!char_is_hex(*cur)) return false; + } + return true; + } else if (len <= 11) { + /* incomplete surrogate pair? */ + u16 hi; + if (*++cur != 'u') return false; + if (!hex_load_4(++cur, &hi)) return false; + if ((hi & 0xF800) != 0xD800) return false; + cur += 4; + if (cur >= eof) return true; + /* valid low surrogate is DC00...DFFF */ + if (*cur != '\\') return false; + if (++cur >= eof) return true; + if (*cur != 'u') return false; + if (++cur >= eof) return true; + if (*cur != 'd' && *cur != 'D') return false; + if (++cur >= eof) return true; + if ((*cur < 'c' || *cur > 'f') && (*cur < 'C' || *cur > 'F')) + return false; + if (++cur >= eof) return true; + if (!char_is_hex(*cur)) return false; + return true; + } + return false; + } + + /* 2 to 4 bytes UTF-8 */ + if (is_truncated_utf8(cur, eof)) { + return true; + } + } + if (has_allow(COMMENTS)) { + if (code == YYJSON_READ_ERROR_INVALID_COMMENT) { + /* unclosed multiline comment */ + return true; + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CHARACTER && + *cur == '/' && cur + 1 == eof) { + /* truncated beginning of comment */ + return true; + } + } + if (code == YYJSON_READ_ERROR_UNEXPECTED_CHARACTER && + has_allow(BOM)) { + /* truncated UTF-8 BOM */ + usize len = (usize)(eof - cur); + if (cur == hdr && len < 3 && !memcmp(hdr, "\xEF\xBB\xBF", len)) { + return true; + } + } + return false; +} + + + +#if !YYJSON_DISABLE_FAST_FP_CONV /* FP_READER */ + +/*============================================================================== + * MARK: - BigInt For Floating Point Number Reader (Private) + * + * The bigint algorithm is used by floating-point number reader to get correctly + * rounded result for numbers with lots of digits. This part of code is rarely + * used for common numbers. + *============================================================================*/ + +/** Unsigned arbitrarily large integer */ +typedef struct bigint { + u32 used; /* used chunks count, should not be 0 */ + u64 bits[64]; /* chunks (58 is enough here) */ +} bigint; + +/** + Evaluate 'big += val'. + @param big A big number (can be 0). + @param val An unsigned integer (can be 0). + */ +static_inline void bigint_add_u64(bigint *big, u64 val) { + u32 idx, max; + u64 num = big->bits[0]; + u64 add = num + val; + big->bits[0] = add; + if (likely((add >= num) || (add >= val))) return; + for ((void)(idx = 1), max = big->used; idx < max; idx++) { + if (likely(big->bits[idx] != U64_MAX)) { + big->bits[idx] += 1; + return; + } + big->bits[idx] = 0; + } + big->bits[big->used++] = 1; +} + +/** + Evaluate 'big *= val'. + @param big A big number (can be 0). + @param val An unsigned integer (cannot be 0). + */ +static_inline void bigint_mul_u64(bigint *big, u64 val) { + u32 idx = 0, max = big->used; + u64 hi, lo, carry = 0; + for (; idx < max; idx++) { + if (big->bits[idx]) break; + } + for (; idx < max; idx++) { + u128_mul_add(big->bits[idx], val, carry, &hi, &lo); + big->bits[idx] = lo; + carry = hi; + } + if (carry) big->bits[big->used++] = carry; +} + +/** + Evaluate 'big *= 2^exp'. + @param big A big number (can be 0). + @param exp An exponent integer (can be 0). + */ +static_inline void bigint_mul_pow2(bigint *big, u32 exp) { + u32 shft = exp % 64; + u32 move = exp / 64; + u32 idx = big->used; + if (unlikely(shft == 0)) { + for (; idx > 0; idx--) { + big->bits[idx + move - 1] = big->bits[idx - 1]; + } + big->used += move; + while (move) big->bits[--move] = 0; + } else { + big->bits[idx] = 0; + for (; idx > 0; idx--) { + u64 num = big->bits[idx] << shft; + num |= big->bits[idx - 1] >> (64 - shft); + big->bits[idx + move] = num; + } + big->bits[move] = big->bits[0] << shft; + big->used += move + (big->bits[big->used + move] > 0); + while (move) big->bits[--move] = 0; + } +} + +/** + Evaluate 'big *= 10^exp'. + @param big A big number (can be 0). + @param exp An exponent integer (cannot be 0). + */ +static_inline void bigint_mul_pow10(bigint *big, i32 exp) { + for (; exp >= U64_POW10_MAX_EXACT_EXP; exp -= U64_POW10_MAX_EXACT_EXP) { + bigint_mul_u64(big, u64_pow10_table[U64_POW10_MAX_EXACT_EXP]); + } + if (exp) { + bigint_mul_u64(big, u64_pow10_table[exp]); + } +} + +/** + Compare two bigint. + @return -1 if 'a < b', +1 if 'a > b', 0 if 'a == b'. + */ +static_inline i32 bigint_cmp(bigint *a, bigint *b) { + u32 idx = a->used; + if (a->used < b->used) return -1; + if (a->used > b->used) return +1; + while (idx-- > 0) { + u64 av = a->bits[idx]; + u64 bv = b->bits[idx]; + if (av < bv) return -1; + if (av > bv) return +1; + } + return 0; +} + +/** + Evaluate 'big = val'. + @param big A big number (can be 0). + @param val An unsigned integer (can be 0). + */ +static_inline void bigint_set_u64(bigint *big, u64 val) { + big->used = 1; + big->bits[0] = val; +} + +/** Set a bigint with floating point number string. */ +static_noinline void bigint_set_buf(bigint *big, u64 sig, i32 *exp, + u8 *sig_cut, u8 *sig_end, u8 *dot_pos) { + + if (unlikely(!sig_cut)) { + /* no digit cut, set significant part only */ + bigint_set_u64(big, sig); + return; + + } else { + /* some digits were cut, read them from 'sig_cut' to 'sig_end' */ + u8 *hdr = sig_cut; + u8 *cur = hdr; + u32 len = 0; + u64 val = 0; + bool dig_big_cut = false; + bool has_dot = (hdr < dot_pos) & (dot_pos < sig_end); + u32 dig_len_total = U64_SAFE_DIG + (u32)(sig_end - hdr) - has_dot; + + sig -= (*sig_cut >= '5'); /* sig was rounded before */ + if (dig_len_total > F64_MAX_DEC_DIG) { + dig_big_cut = true; + sig_end -= dig_len_total - (F64_MAX_DEC_DIG + 1); + sig_end -= (dot_pos + 1 == sig_end); + dig_len_total = (F64_MAX_DEC_DIG + 1); + } + *exp -= (i32)dig_len_total - U64_SAFE_DIG; + + big->used = 1; + big->bits[0] = sig; + while (cur < sig_end) { + if (likely(cur != dot_pos)) { + val = val * 10 + (u8)(*cur++ - '0'); + len++; + if (unlikely(cur == sig_end && dig_big_cut)) { + /* The last digit must be non-zero, */ + /* set it to '1' for correct rounding. */ + val = val - (val % 10) + 1; + } + if (len == U64_SAFE_DIG || cur == sig_end) { + bigint_mul_pow10(big, (i32)len); + bigint_add_u64(big, val); + val = 0; + len = 0; + } + } else { + cur++; + } + } + } +} + + + +/*============================================================================== + * MARK: - Diy Floating Point (Private) + *============================================================================*/ + +/** "Do It Yourself Floating Point" struct. */ +typedef struct diy_fp { + u64 sig; /* significand */ + i32 exp; /* exponent, base 2 */ + i32 pad; /* padding, useless */ +} diy_fp; + +/** Get cached rounded diy_fp with pow(10, e) The input value must in range + [POW10_SIG_TABLE_MIN_EXP, POW10_SIG_TABLE_MAX_EXP]. */ +static_inline diy_fp diy_fp_get_cached_pow10(i32 exp10) { + diy_fp fp; + u64 sig_ext; + pow10_table_get_sig(exp10, &fp.sig, &sig_ext); + pow10_table_get_exp(exp10, &fp.exp); + fp.sig += (sig_ext >> 63); + return fp; +} + +/** Returns fp * fp2. */ +static_inline diy_fp diy_fp_mul(diy_fp fp, diy_fp fp2) { + u64 hi, lo; + u128_mul(fp.sig, fp2.sig, &hi, &lo); + fp.sig = hi + (lo >> 63); + fp.exp += fp2.exp + 64; + return fp; +} + +/** Convert diy_fp to IEEE-754 raw value. */ +static_inline u64 diy_fp_to_ieee_raw(diy_fp fp) { + u64 sig = fp.sig; + i32 exp = fp.exp; + u32 lz_bits; + if (unlikely(fp.sig == 0)) return 0; + + lz_bits = u64_lz_bits(sig); + sig <<= lz_bits; + sig >>= F64_BITS - F64_SIG_FULL_BITS; + exp -= (i32)lz_bits; + exp += F64_BITS - F64_SIG_FULL_BITS; + exp += F64_SIG_BITS; + + if (unlikely(exp >= F64_MAX_BIN_EXP)) { + /* overflow */ + return F64_BITS_INF; + } else if (likely(exp >= F64_MIN_BIN_EXP - 1)) { + /* normal */ + exp += F64_EXP_BIAS; + return ((u64)exp << F64_SIG_BITS) | (sig & F64_SIG_MASK); + } else if (likely(exp >= F64_MIN_BIN_EXP - F64_SIG_FULL_BITS)) { + /* subnormal */ + return sig >> (F64_MIN_BIN_EXP - exp - 1); + } else { + /* underflow */ + return 0; + } +} + + + +/*============================================================================== + * MARK: - Number Reader (Private) + *============================================================================*/ + +/** + Read a JSON number. + + 1. This function assume that the floating-point number is in IEEE-754 format. + 2. This function support uint64/int64/double number. If an integer number + cannot fit in uint64/int64, it will returns as a double number. If a double + number is infinite, the return value is based on flag. + 3. This function (with inline attribute) may generate a lot of instructions. + */ +static_inline bool read_num(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { +#define return_err(_pos, _msg) do { \ + *msg = _msg; \ + *end = _pos; \ + return false; \ +} while (false) + +#define return_0() do { \ + val->tag = YYJSON_TYPE_NUM | (u8)((u8)sign << 3); \ + val->uni.u64 = 0; \ + *end = cur; return true; \ +} while (false) + +#define return_i64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | (u8)((u8)sign << 3); \ + val->uni.u64 = (u64)(sign ? (u64)(~(_v) + 1) : (u64)(_v)); \ + *end = cur; return true; \ +} while (false) + +#define return_f64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.f64 = sign ? -(f64)(_v) : (f64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_f64_bin(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.u64 = ((u64)sign << 63) | (u64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_inf() do { \ + if (has_flg(BIGNUM_AS_RAW)) return_raw(); \ + if (has_allow(INF_AND_NAN)) return_f64_bin(F64_BITS_INF); \ + else return_err(hdr, "number is infinity when parsed as double"); \ +} while (false) + +#define return_raw() do { \ + **pre = '\0'; /* add null-terminator for previous raw string */ \ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; \ + val->uni.str = (const char *)hdr; \ + *pre = cur; *end = cur; return true; \ +} while (false) + + u8 *sig_cut = NULL; /* significant part cutting position for long number */ + u8 *sig_end = NULL; /* significant part ending position */ + u8 *dot_pos = NULL; /* decimal point position */ + + u64 sig = 0; /* significant part of the number */ + i32 exp = 0; /* exponent part of the number */ + + bool exp_sign; /* temporary exponent sign from literal part */ + i64 exp_sig = 0; /* temporary exponent number from significant part */ + i64 exp_lit = 0; /* temporary exponent number from exponent literal part */ + u64 num; /* temporary number for reading */ + u8 *tmp; /* temporary cursor for reading */ + + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + bool sign; + + /* read number as raw string if has `YYJSON_READ_NUMBER_AS_RAW` flag */ + if (has_flg(NUMBER_AS_RAW)) { + return read_num_raw(ptr, pre, flg, val, msg); + } + + sign = (*hdr == '-'); + cur += sign; + + /* begin with a leading zero or non-digit */ + while (unlikely(!char_is_nonzero(*cur))) { /* 0 or non-digit char */ + if (unlikely(*cur != '0')) { /* non-digit char */ + if (has_allow(EXT_NUMBER)) { + if (*cur == '+' && cur == hdr) { /* leading `+` sign */ + cur++; + continue; + } + if (*cur == '.' && char_is_digit(cur[1])) { /* e.g. '.123' */ + goto leading_dot; + } + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(ptr, pre, flg, val)) return true; + } + return_err(cur, "no digit after sign"); + } + /* begin with 0 */ + if (likely(!char_is_digit_or_fp(*++cur))) { + if (has_allow(EXT_NUMBER) && char_to_lower(*cur) == 'x') { /* hex */ + return read_num_hex(ptr, pre, flg, val, msg); + } + return_0(); + } + if (likely(*cur == '.')) { +leading_dot: + dot_pos = cur++; + if (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + if (char_is_exp(*cur)) { + goto digi_exp_more; + } else { + return_f64_bin(0); + } + } + return_err(cur, "no digit after decimal point"); + } + while (unlikely(*cur == '0')) cur++; + if (likely(char_is_digit(*cur))) { + /* first non-zero digit after decimal point */ + sig = (u64)(*cur - '0'); /* read first digit */ + cur--; + goto digi_frac_1; /* continue read fraction part */ + } + } + if (unlikely(char_is_digit(*cur))) { + return_err(cur - 1, "number with leading zero is not allowed"); + } + if (unlikely(char_is_exp(*cur))) { /* 0 with any exponent is still 0 */ + cur += (usize)1 + char_is_sign(cur[1]); + if (unlikely(!char_is_digit(*cur))) { + return_err(cur, "no digit after exponent sign"); + } + while (char_is_digit(*++cur)); + } + return_f64_bin(0); + } + + /* begin with non-zero digit */ + sig = (u64)(*cur - '0'); + + /* + Read integral part, same as the following code. + + for (int i = 1; i <= 18; i++) { + num = cur[i] - '0'; + if (num <= 9) sig = num + sig * 10; + else goto digi_sepr_i; + } + */ +#define expr_intg(i) \ + if (likely((num = (u64)(cur[i] - (u8)'0')) <= 9)) sig = num + sig * 10; \ + else { goto digi_sepr_##i; } + repeat_in_1_18(expr_intg) +#undef expr_intg + + + cur += 19; /* skip continuous 19 digits */ + if (!char_is_digit_or_fp(*cur)) { + /* this number is an integer consisting of 19 digits */ + if (sign && (sig > ((u64)1 << 63))) { /* overflow */ + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + goto digi_intg_more; /* read more digits in integral part */ + + + /* process first non-digit character */ +#define expr_sepr(i) \ + digi_sepr_##i: \ + if (likely(!char_is_fp(cur[i]))) { cur += i; return_i64(sig); } \ + dot_pos = cur + i; \ + if (likely(cur[i] == '.')) goto digi_frac_##i; \ + cur += i; sig_end = cur; goto digi_exp_more; + repeat_in_1_18(expr_sepr) +#undef expr_sepr + + + /* read fraction part */ +#define expr_frac(i) \ + digi_frac_##i: \ + if (likely((num = (u64)(cur[i + 1] - (u8)'0')) <= 9)) \ + sig = num + sig * 10; \ + else { goto digi_stop_##i; } + repeat_in_1_18(expr_frac) +#undef expr_frac + + cur += 20; /* skip 19 digits and 1 decimal point */ + if (!char_is_digit(*cur)) goto digi_frac_end; /* fraction part end */ + goto digi_frac_more; /* read more digits in fraction part */ + + + /* significant part end */ +#define expr_stop(i) \ + digi_stop_##i: \ + cur += i + 1; \ + goto digi_frac_end; + repeat_in_1_18(expr_stop) +#undef expr_stop + + + /* read more digits in integral part */ +digi_intg_more: + if (char_is_digit(*cur)) { + if (!char_is_digit_or_fp(cur[1])) { + /* this number is an integer consisting of 20 digits */ + num = (u64)(*cur - '0'); + if ((sig < (U64_MAX / 10)) || + (sig == (U64_MAX / 10) && num <= (U64_MAX % 10))) { + sig = num + sig * 10; + cur++; + /* convert to double if overflow */ + if (sign) { + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + } + } + + if (char_is_exp(*cur)) { + dot_pos = cur; + goto digi_exp_more; + } + + if (*cur == '.') { + dot_pos = cur++; + if (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + goto digi_frac_end; + } + return_err(cur, "no digit after decimal point"); + } + } + + + /* read more digits in fraction part */ +digi_frac_more: + sig_cut = cur; /* too large to fit in u64, excess digits need to be cut */ + sig += (*cur >= '5'); /* round */ + while (char_is_digit(*++cur)); + if (!dot_pos) { + if (!char_is_fp(*cur) && has_flg(BIGNUM_AS_RAW)) { + return_raw(); /* it's a large integer */ + } + dot_pos = cur; + if (*cur == '.') { + if (unlikely(!char_is_digit(*++cur))) { + if (!has_allow(EXT_NUMBER)) { + return_err(cur, "no digit after decimal point"); + } + } + while (char_is_digit(*cur)) cur++; + } + } + exp_sig = (i64)(dot_pos - sig_cut); + exp_sig += (dot_pos < sig_cut); + + /* ignore trailing zeros */ + tmp = cur - 1; + while ((*tmp == '0' || *tmp == '.') && tmp > hdr) tmp--; + if (tmp < sig_cut) { + sig_cut = NULL; + } else { + sig_end = cur; + } + + if (char_is_exp(*cur)) goto digi_exp_more; + goto digi_exp_finish; + + + /* fraction part end */ +digi_frac_end: + if (unlikely(dot_pos + 1 == cur)) { + if (!has_allow(EXT_NUMBER)) { + return_err(cur, "no digit after decimal point"); + } + } + sig_end = cur; + exp_sig = -(i64)((u64)(cur - dot_pos) - 1); + if (likely(!char_is_exp(*cur))) { + if (unlikely(exp_sig < F64_MIN_DEC_EXP - 19)) { + return_f64_bin(0); /* underflow */ + } + exp = (i32)exp_sig; + goto digi_finish; + } else { + goto digi_exp_more; + } + + + /* read exponent part */ +digi_exp_more: + exp_sign = (*++cur == '-'); + cur += char_is_sign(*cur); + if (unlikely(!char_is_digit(*cur))) { + return_err(cur, "no digit after exponent sign"); + } + while (*cur == '0') cur++; + + /* read exponent literal */ + tmp = cur; + while (char_is_digit(*cur)) { + exp_lit = (i64)((u8)(*cur++ - '0') + (u64)exp_lit * 10); + } + if (unlikely(cur - tmp >= U64_SAFE_DIG)) { + if (exp_sign) { + return_f64_bin(0); /* underflow */ + } else { + return_inf(); /* overflow */ + } + } + exp_sig += exp_sign ? -exp_lit : exp_lit; + + + /* validate exponent value */ +digi_exp_finish: + if (unlikely(exp_sig < F64_MIN_DEC_EXP - 19)) { + return_f64_bin(0); /* underflow */ + } + if (unlikely(exp_sig > F64_MAX_DEC_EXP)) { + return_inf(); /* overflow */ + } + exp = (i32)exp_sig; + + + /* all digit read finished */ +digi_finish: + + /* + Fast path 1: + + 1. The floating-point number calculation should be accurate, see the + comments of macro `YYJSON_DOUBLE_MATH_CORRECT`. + 2. Correct rounding should be performed (fegetround() == FE_TONEAREST). + 3. The input of floating point number calculation does not lose precision, + which means: 64 - leading_zero(input) - trailing_zero(input) < 53. + + We don't check all available inputs here, because that would make the code + more complicated, and not friendly to branch predictor. + */ +#if YYJSON_DOUBLE_MATH_CORRECT + if (sig < ((u64)1 << 53) && + exp >= -F64_POW10_MAX_EXACT_EXP && + exp <= +F64_POW10_MAX_EXACT_EXP) { + f64 dbl = (f64)sig; + if (exp < 0) { + dbl /= f64_pow10_table[-exp]; + } else { + dbl *= f64_pow10_table[+exp]; + } + return_f64(dbl); + } +#endif + + /* + Fast path 2: + + To keep it simple, we only accept normal number here, + let the slow path to handle subnormal and infinity number. + */ + if (likely(!sig_cut && + exp > -F64_MAX_DEC_EXP + 1 && + exp < +F64_MAX_DEC_EXP - 20)) { + /* + The result value is exactly equal to (sig * 10^exp), + the exponent part (10^exp) can be converted to (sig2 * 2^exp2). + + The sig2 can be an infinite length number, only the highest 128 bits + is cached in the pow10_sig_table. + + Now we have these bits: + sig1 (normalized 64bit) : aaaaaaaa + sig2 (higher 64bit) : bbbbbbbb + sig2_ext (lower 64bit) : cccccccc + sig2_cut (extra unknown bits) : dddddddddddd.... + + And the calculation process is: + ---------------------------------------- + aaaaaaaa * + bbbbbbbbccccccccdddddddddddd.... + ---------------------------------------- + abababababababab + + acacacacacacacac + + adadadadadadadadadad.... + ---------------------------------------- + [hi____][lo____] + + [hi2___][lo2___] + + [unknown___________....] + ---------------------------------------- + + The addition with carry may affect higher bits, but if there is a 0 + in higher bits, the bits higher than 0 will not be affected. + + `lo2` + `unknown` may get a carry bit and may affect `hi2`, the max + value of `hi2` is 0xFFFFFFFFFFFFFFFE, so `hi2` will not overflow. + + `lo` + `hi2` may also get a carry bit and may affect `hi`, but only + the highest significant 53 bits of `hi` is needed. If there is a 0 + in the lower bits of `hi`, then all the following bits can be dropped. + + To convert the result to IEEE-754 double number, we need to perform + correct rounding: + 1. if bit 54 is 0, round down, + 2. if bit 54 is 1 and any bit beyond bit 54 is 1, round up, + 3. if bit 54 is 1 and all bits beyond bit 54 are 0, round to even, + as the extra bits is unknown, this case will not be handled here. + */ + + u64 raw; + u64 sig1, sig2, sig2_ext, hi, lo, hi2, lo2, add, bits; + i32 exp2; + u32 lz; + bool exact = false, carry, round_up; + + /* convert (10^exp) to (sig2 * 2^exp2) */ + pow10_table_get_sig(exp, &sig2, &sig2_ext); + pow10_table_get_exp(exp, &exp2); + + /* normalize and multiply */ + lz = u64_lz_bits(sig); + sig1 = sig << lz; + exp2 -= (i32)lz; + u128_mul(sig1, sig2, &hi, &lo); + + /* + The `hi` is in range [0x4000000000000000, 0xFFFFFFFFFFFFFFFE], + To get normalized value, `hi` should be shifted to the left by 0 or 1. + + The highest significant 53 bits is used by IEEE-754 double number, + and the bit 54 is used to detect rounding direction. + + The lowest (64 - 54 - 1) bits is used to check whether it contains 0. + */ + bits = hi & (((u64)1 << (64 - 54 - 1)) - 1); + if (bits - 1 < (((u64)1 << (64 - 54 - 1)) - 2)) { + /* + (bits != 0 && bits != 0x1FF) => (bits - 1 < 0x1FF - 1) + The `bits` is not zero, so we don't need to check `round to even` + case. The `bits` contains bit `0`, so we can drop the extra bits + after `0`. + */ + exact = true; + + } else { + /* + (bits == 0 || bits == 0x1FF) + The `bits` is filled with all `0` or all `1`, so we need to check + lower bits with another 64-bit multiplication. + */ + u128_mul(sig1, sig2_ext, &hi2, &lo2); + + add = lo + hi2; + if (add + 1 > (u64)1) { + /* + (add != 0 && add != U64_MAX) => (add + 1 > 1) + The `add` is not zero, so we don't need to check `round to + even` case. The `add` contains bit `0`, so we can drop the + extra bits after `0`. The `hi` cannot be U64_MAX, so it will + not overflow. + */ + carry = add < lo || add < hi2; + hi += carry; + exact = true; + } + } + + if (exact) { + /* normalize */ + lz = hi < ((u64)1 << 63); + hi <<= lz; + exp2 -= (i32)lz; + exp2 += 64; + + /* test the bit 54 and get rounding direction */ + round_up = (hi & ((u64)1 << (64 - 54))) > (u64)0; + hi += (round_up ? ((u64)1 << (64 - 54)) : (u64)0); + + /* test overflow */ + if (hi < ((u64)1 << (64 - 54))) { + hi = ((u64)1 << 63); + exp2 += 1; + } + + /* This is a normal number, convert it to IEEE-754 format. */ + hi >>= F64_BITS - F64_SIG_FULL_BITS; + exp2 += F64_BITS - F64_SIG_FULL_BITS + F64_SIG_BITS; + exp2 += F64_EXP_BIAS; + raw = ((u64)exp2 << F64_SIG_BITS) | (hi & F64_SIG_MASK); + return_f64_bin(raw); + } + } + + /* + Slow path: read double number exactly with diyfp. + 1. Use cached diyfp to get an approximation value. + 2. Use bigcomp to check the approximation value if needed. + + This algorithm refers to google's double-conversion project: + https://github.com/google/double-conversion + */ + { + const i32 ERR_ULP_LOG = 3; + const i32 ERR_ULP = 1 << ERR_ULP_LOG; + const i32 ERR_CACHED_POW = ERR_ULP / 2; + const i32 ERR_MUL_FIXED = ERR_ULP / 2; + const i32 DIY_SIG_BITS = 64; + const i32 EXP_BIAS = F64_EXP_BIAS + F64_SIG_BITS; + const i32 EXP_SUBNORMAL = -EXP_BIAS + 1; + + u64 fp_err; + u32 bits; + i32 order_of_magnitude; + i32 effective_significand_size; + i32 precision_digits_count; + u64 precision_bits; + u64 half_way; + + u64 raw; + diy_fp fp, fp_upper; + bigint big_full, big_comp; + i32 cmp; + + fp.sig = sig; + fp.exp = 0; + fp_err = sig_cut ? (u64)(ERR_ULP / 2) : (u64)0; + + /* normalize */ + bits = u64_lz_bits(fp.sig); + fp.sig <<= bits; + fp.exp -= (i32)bits; + fp_err <<= bits; + + /* multiply and add error */ + fp = diy_fp_mul(fp, diy_fp_get_cached_pow10(exp)); + fp_err += (u64)ERR_CACHED_POW + (fp_err != 0) + (u64)ERR_MUL_FIXED; + + /* normalize */ + bits = u64_lz_bits(fp.sig); + fp.sig <<= bits; + fp.exp -= (i32)bits; + fp_err <<= bits; + + /* effective significand */ + order_of_magnitude = DIY_SIG_BITS + fp.exp; + if (likely(order_of_magnitude >= EXP_SUBNORMAL + F64_SIG_FULL_BITS)) { + effective_significand_size = F64_SIG_FULL_BITS; + } else if (order_of_magnitude <= EXP_SUBNORMAL) { + effective_significand_size = 0; + } else { + effective_significand_size = order_of_magnitude - EXP_SUBNORMAL; + } + + /* precision digits count */ + precision_digits_count = DIY_SIG_BITS - effective_significand_size; + if (unlikely(precision_digits_count + ERR_ULP_LOG >= DIY_SIG_BITS)) { + i32 shr = (precision_digits_count + ERR_ULP_LOG) - DIY_SIG_BITS + 1; + fp.sig >>= shr; + fp.exp += shr; + fp_err = (fp_err >> shr) + 1 + (u32)ERR_ULP; + precision_digits_count -= shr; + } + + /* half way */ + precision_bits = fp.sig & (((u64)1 << precision_digits_count) - 1); + precision_bits *= (u32)ERR_ULP; + half_way = (u64)1 << (precision_digits_count - 1); + half_way *= (u32)ERR_ULP; + + /* rounding */ + fp.sig >>= precision_digits_count; + fp.sig += (precision_bits >= half_way + fp_err); + fp.exp += precision_digits_count; + + /* get IEEE double raw value */ + raw = diy_fp_to_ieee_raw(fp); + if (unlikely(raw == F64_BITS_INF)) return_inf(); + if (likely(precision_bits <= half_way - fp_err || + precision_bits >= half_way + fp_err)) { + return_f64_bin(raw); /* number is accurate */ + } + /* now the number is the correct value, or the next lower value */ + + /* upper boundary */ + if (raw & F64_EXP_MASK) { + fp_upper.sig = (raw & F64_SIG_MASK) + ((u64)1 << F64_SIG_BITS); + fp_upper.exp = (i32)((raw & F64_EXP_MASK) >> F64_SIG_BITS); + } else { + fp_upper.sig = (raw & F64_SIG_MASK); + fp_upper.exp = 1; + } + fp_upper.exp -= F64_EXP_BIAS + F64_SIG_BITS; + fp_upper.sig <<= 1; + fp_upper.exp -= 1; + fp_upper.sig += 1; /* add half ulp */ + + /* compare with bigint */ + bigint_set_buf(&big_full, sig, &exp, sig_cut, sig_end, dot_pos); + bigint_set_u64(&big_comp, fp_upper.sig); + if (exp >= 0) { + bigint_mul_pow10(&big_full, +exp); + } else { + bigint_mul_pow10(&big_comp, -exp); + } + if (fp_upper.exp > 0) { + bigint_mul_pow2(&big_comp, (u32)+fp_upper.exp); + } else { + bigint_mul_pow2(&big_full, (u32)-fp_upper.exp); + } + cmp = bigint_cmp(&big_full, &big_comp); + if (likely(cmp != 0)) { + /* round down or round up */ + raw += (cmp > 0); + } else { + /* falls midway, round to even */ + raw += (raw & 1); + } + + if (unlikely(raw == F64_BITS_INF)) return_inf(); + return_f64_bin(raw); + } + +#undef return_err +#undef return_inf +#undef return_0 +#undef return_i64 +#undef return_f64 +#undef return_f64_bin +#undef return_raw +} + + + +#else /* FP_READER */ + +/** + Read a JSON number. + This is a fallback function if the custom number reader is disabled. + This function use libc's strtod() to read floating-point number. + */ +static_inline bool read_num(u8 **ptr, u8 **pre, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { +#define return_err(_pos, _msg) do { \ + *msg = _msg; \ + *end = _pos; \ + return false; \ +} while (false) + +#define return_0() do { \ + val->tag = YYJSON_TYPE_NUM | (u64)((u8)sign << 3); \ + val->uni.u64 = 0; \ + *end = cur; return true; \ +} while (false) + +#define return_i64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | (u64)((u8)sign << 3); \ + val->uni.u64 = (u64)(sign ? (u64)(~(_v) + 1) : (u64)(_v)); \ + *end = cur; return true; \ +} while (false) + +#define return_f64(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.f64 = sign ? -(f64)(_v) : (f64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_f64_bin(_v) do { \ + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; \ + val->uni.u64 = ((u64)sign << 63) | (u64)(_v); \ + *end = cur; return true; \ +} while (false) + +#define return_inf() do { \ + if (has_flg(BIGNUM_AS_RAW)) return_raw(); \ + if (has_allow(INF_AND_NAN)) return_f64_bin(F64_BITS_INF); \ + else return_err(hdr, "number is infinity when parsed as double"); \ +} while (false) + +#define return_raw() do { \ + val->tag = ((u64)(cur - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_RAW; \ + val->uni.str = (const char *)hdr; \ + **pre = '\0'; *pre = cur; *end = cur; return true; \ +} while (false) + + u64 sig, num; + u8 *hdr = *ptr; + u8 *cur = *ptr; + u8 **end = ptr; + u8 *dot = NULL; + u8 *f64_end = NULL; + bool sign; + + /* read number as raw string if has `YYJSON_READ_NUMBER_AS_RAW` flag */ + if (has_flg(NUMBER_AS_RAW)) { + return read_num_raw(ptr, pre, flg, val, msg); + } + + sign = (*hdr == '-'); + cur += sign; + sig = (u8)(*cur - '0'); + + /* read first digit, check leading zero */ + while (unlikely(!char_is_digit(*cur))) { + if (has_allow(EXT_NUMBER)) { + if (*cur == '+' && cur == hdr) { /* leading `+` sign */ + cur++; + sig = (u8)(*cur - '0'); + continue; + } + if (*cur == '.' && char_is_num(cur[1])) { /* no integer part */ + goto read_double; /* e.g. '.123' */ + } + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(ptr, pre, flg, val)) return true; + } + return_err(cur, "no digit after sign"); + } + if (*cur == '0') { + cur++; + if (unlikely(char_is_digit(*cur))) { + return_err(cur - 1, "number with leading zero is not allowed"); + } + if (!char_is_fp(*cur)) { + if (has_allow(EXT_NUMBER) && + (*cur == 'x' || *cur == 'X')) { /* hex integer */ + return read_num_hex(ptr, pre, flg, val, msg); + } + return_0(); + } + goto read_double; + } + + /* read continuous digits, up to 19 characters */ +#define expr_intg(i) \ + if (likely((num = (u64)(cur[i] - (u8)'0')) <= 9)) sig = num + sig * 10; \ + else { cur += i; goto intg_end; } + repeat_in_1_18(expr_intg) +#undef expr_intg + + /* here are 19 continuous digits, skip them */ + cur += 19; + if (char_is_digit(cur[0]) && !char_is_digit_or_fp(cur[1])) { + /* this number is an integer consisting of 20 digits */ + num = (u8)(*cur - '0'); + if ((sig < (U64_MAX / 10)) || + (sig == (U64_MAX / 10) && num <= (U64_MAX % 10))) { + sig = num + sig * 10; + cur++; + if (sign) { + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + } + +intg_end: + /* continuous digits ended */ + if (!char_is_digit_or_fp(*cur)) { + /* this number is an integer consisting of 1 to 19 digits */ + if (sign && (sig > ((u64)1 << 63))) { + if (has_flg(BIGNUM_AS_RAW)) return_raw(); + return_f64(unsafe_yyjson_u64_to_f64(sig)); + } + return_i64(sig); + } + +read_double: + /* this number should be read as double */ + while (char_is_digit(*cur)) cur++; + if (!char_is_fp(*cur) && has_flg(BIGNUM_AS_RAW)) { + return_raw(); /* it's a large integer */ + } + while (*cur == '.') { + /* skip fraction part */ + dot = cur; + cur++; + if (!char_is_digit(*cur)) { + if (has_allow(EXT_NUMBER)) { + break; + } else { + return_err(cur, "no digit after decimal point"); + } + } + cur++; + while (char_is_digit(*cur)) cur++; + break; + } + if (char_is_exp(*cur)) { + /* skip exponent part */ + cur += 1 + char_is_sign(cur[1]); + if (!char_is_digit(*cur)) { + return_err(cur, "no digit after exponent sign"); + } + cur++; + while (char_is_digit(*cur)) cur++; + } + + /* + libc's strtod() is used to parse the floating-point number. + + Note that the decimal point character used by strtod() is locale-dependent, + and the rounding direction may affected by fesetround(). + + For currently known locales, (en, zh, ja, ko, am, he, hi) use '.' as the + decimal point, while other locales use ',' as the decimal point. + + Here strtod() is called twice for different locales, but if another thread + happens calls setlocale() between two strtod(), parsing may still fail. + */ + val->uni.f64 = strtod((const char *)hdr, (char **)&f64_end); + if (unlikely(f64_end != cur)) { + /* replace '.' with ',' for locale */ + bool cut = (*cur == ','); + if (cut) *cur = ' '; + if (dot) *dot = ','; + val->uni.f64 = strtod((const char *)hdr, (char **)&f64_end); + /* restore ',' to '.' */ + if (cut) *cur = ','; + if (dot) *dot = '.'; + if (unlikely(f64_end != cur)) { + return_err(hdr, "strtod() failed to parse the number"); + } + } + if (unlikely(val->uni.f64 >= HUGE_VAL || val->uni.f64 <= -HUGE_VAL)) { + return_inf(); + } + val->tag = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + *end = cur; + return true; + +#undef return_err +#undef return_0 +#undef return_i64 +#undef return_f64 +#undef return_f64_bin +#undef return_inf +#undef return_raw +} + +#endif /* FP_READER */ + + + +/*============================================================================== + * MARK: - String Reader (Private) + *============================================================================*/ + +/** Read unicode escape sequence. */ +static_inline bool read_uni_esc(u8 **src_ptr, u8 **dst_ptr, const char **msg) { +#define return_err(_end, _msg) *msg = _msg; *src_ptr = _end; return false + + u8 *src = *src_ptr; + u8 *dst = *dst_ptr; + u16 hi, lo; + u32 uni; + + src += 2; /* skip `\u` */ + if (unlikely(!hex_load_4(src, &hi))) { + return_err(src - 2, "invalid escaped sequence in string"); + } + src += 4; /* skip hex */ + if (likely((hi & 0xF800) != 0xD800)) { + /* a BMP character */ + if (hi >= 0x800) { + *dst++ = (u8)(0xE0 | (hi >> 12)); + *dst++ = (u8)(0x80 | ((hi >> 6) & 0x3F)); + *dst++ = (u8)(0x80 | (hi & 0x3F)); + } else if (hi >= 0x80) { + *dst++ = (u8)(0xC0 | (hi >> 6)); + *dst++ = (u8)(0x80 | (hi & 0x3F)); + } else { + *dst++ = (u8)hi; + } + } else { + /* a non-BMP character, represented as a surrogate pair */ + if (unlikely((hi & 0xFC00) != 0xD800)) { + return_err(src - 6, "invalid high surrogate in string"); + } + if (unlikely(!byte_match_2(src, "\\u"))) { + return_err(src - 6, "no low surrogate in string"); + } + if (unlikely(!hex_load_4(src + 2, &lo))) { + return_err(src - 6, "invalid escape in string"); + } + if (unlikely((lo & 0xFC00) != 0xDC00)) { + return_err(src - 6, "invalid low surrogate in string"); + } + uni = ((((u32)hi - 0xD800) << 10) | + ((u32)lo - 0xDC00)) + 0x10000; + *dst++ = (u8)(0xF0 | (uni >> 18)); + *dst++ = (u8)(0x80 | ((uni >> 12) & 0x3F)); + *dst++ = (u8)(0x80 | ((uni >> 6) & 0x3F)); + *dst++ = (u8)(0x80 | (uni & 0x3F)); + src += 6; + } + *src_ptr = src; + *dst_ptr = dst; + return true; +#undef return_err +} + +/** + Read a JSON string. + @param quo The quote character (single quote or double quote). + @param ptr The head pointer of string before quote (inout). + @param eof JSON end position. + @param flg JSON read flag. + @param val The string value to be written. + @param msg The error message pointer. + @param con Continuation for incremental parsing. + @return Whether success. + */ +static_inline bool read_str_opt(u8 quo, u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg, u8 *con[2]) { + /* + GCC may sometimes load variables into registers too early, causing + unnecessary instructions and performance degradation. This inline assembly + serves as a hint to GCC: 'This variable will be modified, so avoid loading + it too early.' Other compilers like MSVC, Clang, and ICC can generate the + expected instructions without needing this hint. + + Check out this example: https://godbolt.org/z/YG6a5W5Ec + */ +#define return_err(_end, _msg) do { \ + *msg = _msg; \ + *end = _end; \ + if (con) { con[0] = _end; con[1] = dst; } \ + return false; \ +} while (false) + + u8 *hdr = *ptr + 1; + u8 **end = ptr; + u8 *src = hdr, *dst = NULL, *pos; + u16 hi, lo; + u32 uni, tmp; + + /* Resume incremental parsing. */ + if (con && unlikely(con[0])) { + src = con[0]; + dst = con[1]; + if (dst) goto copy_ascii; + } + +skip_ascii: + /* + Most strings have no escaped characters, so we can jump them quickly. + + We want to make loop unrolling, as shown in the following code. Some + compiler may not generate instructions as expected, so we rewrite it with + explicit goto statements. We hope the compiler can generate instructions + like this: https://godbolt.org/z/8vjsYq + + while (true) repeat16({ + if (likely((char_is_ascii_skip(*src)))) src++; + else break; + }) + */ + if (quo == '"') { +#define expr_jump(i) \ + if (likely(char_is_ascii_skip(src[i]))) {} \ + else goto skip_ascii_stop##i; + +#define expr_stop(i) \ + skip_ascii_stop##i: \ + src += i; \ + goto skip_ascii_end; + + repeat16_incr(expr_jump) + src += 16; + goto skip_ascii; + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + } else { +#define expr_jump(i) \ + if (likely(char_is_ascii_skip_sq(src[i]))) {} \ + else goto skip_ascii_stop_sq##i; + +#define expr_stop(i) \ + skip_ascii_stop_sq##i: \ + src += i; \ + goto skip_ascii_end; + + repeat16_incr(expr_jump) + src += 16; + goto skip_ascii; + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + } + +skip_ascii_end: + gcc_store_barrier(*src); + if (likely(*src == quo)) { + val->tag = ((u64)(src - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_STR | + (quo == '"' ? YYJSON_SUBTYPE_NOESC : 0); + val->uni.str = (const char *)hdr; + *src = '\0'; + *end = src + 1; + if (con) con[0] = con[1] = NULL; + return true; + } + +skip_utf8: + if (*src & 0x80) { /* non-ASCII character */ + /* + Non-ASCII character appears here, which means that the text is likely + to be written in non-English or emoticons. According to some common + data set statistics, byte sequences of the same length may appear + consecutively. We process the byte sequences of the same length in each + loop, which is more friendly to branch prediction. + */ + pos = src; +#if YYJSON_DISABLE_UTF8_VALIDATION + while (true) repeat8({ + if (likely((*src & 0xF0) == 0xE0)) src += 3; + else break; + }) + if (*src < 0x80) goto skip_ascii; + while (true) repeat8({ + if (likely((*src & 0xE0) == 0xC0)) src += 2; + else break; + }) + while (true) repeat8({ + if (likely((*src & 0xF8) == 0xF0)) src += 4; + else break; + }) +#else + uni = byte_load_4(src); + while (is_utf8_seq3(uni)) { + src += 3; + uni = byte_load_4(src); + } + if (is_utf8_seq1(uni)) goto skip_ascii; + while (is_utf8_seq2(uni)) { + src += 2; + uni = byte_load_4(src); + } + while (is_utf8_seq4(uni)) { + src += 4; + uni = byte_load_4(src); + } +#endif + if (unlikely(pos == src)) { + if (has_allow(INVALID_UNICODE)) ++src; + else return_err(src, "invalid UTF-8 encoding in string"); + } + goto skip_ascii; + } + + /* The escape character appears, we need to copy it. */ + dst = src; +copy_escape: + if (likely(*src == '\\')) { + switch (*++src) { + case '"': *dst++ = '"'; src++; break; + case '\\': *dst++ = '\\'; src++; break; + case '/': *dst++ = '/'; src++; break; + case 'b': *dst++ = '\b'; src++; break; + case 'f': *dst++ = '\f'; src++; break; + case 'n': *dst++ = '\n'; src++; break; + case 'r': *dst++ = '\r'; src++; break; + case 't': *dst++ = '\t'; src++; break; + case 'u': + src--; + if (!read_uni_esc(&src, &dst, msg)) return_err(src, *msg); + break; + default: { + if (has_allow(EXT_ESCAPE)) { + /* read extended escape (non-standard) */ + switch (*src) { + case '\'': *dst++ = '\''; src++; break; + case 'a': *dst++ = '\a'; src++; break; + case 'v': *dst++ = '\v'; src++; break; + case '?': *dst++ = '\?'; src++; break; + case 'e': *dst++ = 0x1B; src++; break; + case '0': + if (!char_is_digit(src[1])) { + *dst++ = '\0'; src++; break; + } + return_err(src - 1, "octal escape is not allowed"); + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return_err(src - 1, "invalid number escape"); + case 'x': { + u8 c; + if (hex_load_2(src + 1, &c)) { + src += 3; + if (c <= 0x7F) { /* 1-byte ASCII */ + *dst++ = c; + } else { /* 2-byte UTF-8 */ + *dst++ = (u8)(0xC0 | (c >> 6)); + *dst++ = (u8)(0x80 | (c & 0x3F)); + } + break; + } + return_err(src - 1, "invalid hex escape"); + } + case '\n': src++; break; + case '\r': src++; src += (*src == '\n'); break; + case 0xE2: /* Line terminator: U+2028, U+2029 */ + if ((src[1] == 0x80 && src[2] == 0xA8) || + (src[1] == 0x80 && src[2] == 0xA9)) { + src += 3; + } + break; + default: + break; /* skip */ + } + } else if (quo == '\'' && *src == '\'') { + *dst++ = '\''; src++; break; + } else { + return_err(src - 1, "invalid escaped sequence in string"); + } + } + } + } else if (likely(*src == quo)) { + val->tag = ((u64)(dst - hdr) << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->uni.str = (const char *)hdr; + *dst = '\0'; + *end = src + 1; + if (con) con[0] = con[1] = NULL; + return true; + } else { + if (!has_allow(INVALID_UNICODE)) { + return_err(src, "unexpected control character in string"); + } + if (src >= eof) return_err(src, "unclosed string"); + *dst++ = *src++; + } + +copy_ascii: + /* + Copy continuous ASCII, loop unrolling, same as the following code: + + while (true) repeat16({ + if (char_is_ascii_skip(*src)) *dst++ = *src++; + else break; + }) + */ + if (quo == '"') { +#define expr_jump(i) \ + if (likely((char_is_ascii_skip(src[i])))) {} \ + else { gcc_store_barrier(src[i]); goto copy_ascii_stop_##i; } + repeat16_incr(expr_jump) +#undef expr_jump + } else { +#define expr_jump(i) \ + if (likely((char_is_ascii_skip_sq(src[i])))) {} \ + else { gcc_store_barrier(src[i]); goto copy_ascii_stop_##i; } + repeat16_incr(expr_jump) +#undef expr_jump + } + + byte_move_16(dst, src); + dst += 16; src += 16; + goto copy_ascii; + + /* + The memory is copied forward since `dst < src`. + So it's safe to move one extra byte to reduce instruction count. + */ +#define expr_jump(i) \ + copy_ascii_stop_##i: \ + byte_move_forward(dst, src, i); \ + dst += i; src += i; \ + goto copy_utf8; + repeat16_incr(expr_jump) +#undef expr_jump + +copy_utf8: + if (*src & 0x80) { /* non-ASCII character */ + pos = src; + uni = byte_load_4(src); +#if YYJSON_DISABLE_UTF8_VALIDATION + while (true) repeat4({ + if ((uni & utf8_seq(b3_mask)) == utf8_seq(b3_patt)) { + byte_copy_4(dst, &uni); + dst += 3; src += 3; + uni = byte_load_4(src); + } else break; + }) + if ((uni & utf8_seq(b1_mask)) == utf8_seq(b1_patt)) goto copy_ascii; + while (true) repeat4({ + if ((uni & utf8_seq(b2_mask)) == utf8_seq(b2_patt)) { + byte_copy_2(dst, &uni); + dst += 2; src += 2; + uni = byte_load_4(src); + } else break; + }) + while (true) repeat4({ + if ((uni & utf8_seq(b4_mask)) == utf8_seq(b4_patt)) { + byte_copy_4(dst, &uni); + dst += 4; src += 4; + uni = byte_load_4(src); + } else break; + }) +#else + while (is_utf8_seq3(uni)) { + byte_copy_4(dst, &uni); + dst += 3; src += 3; + uni = byte_load_4(src); + } + if (is_utf8_seq1(uni)) goto copy_ascii; + while (is_utf8_seq2(uni)) { + byte_copy_2(dst, &uni); + dst += 2; src += 2; + uni = byte_load_4(src); + } + while (is_utf8_seq4(uni)) { + byte_copy_4(dst, &uni); + dst += 4; src += 4; + uni = byte_load_4(src); + } +#endif + if (unlikely(pos == src)) { + if (!has_allow(INVALID_UNICODE)) { + return_err(src, MSG_ERR_UTF8); + } + goto copy_ascii_stop_1; + } + goto copy_ascii; + } + goto copy_escape; + +#undef return_err +} + +static_inline bool read_str(u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { + return read_str_opt('\"', ptr, eof, flg, val, msg, NULL); +} + +static_inline bool read_str_con(u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg, u8 **con) { + return read_str_opt('\"', ptr, eof, flg, val, msg, con); +} + +static_noinline bool read_str_sq(u8 **ptr, u8 *eof, yyjson_read_flag flg, + yyjson_val *val, const char **msg) { + return read_str_opt('\'', ptr, eof, flg, val, msg, NULL); +} + +/** Read unquoted key (identifier name). */ +static_noinline bool read_str_id(u8 **ptr, u8 *eof, yyjson_read_flag flg, + u8 **pre, yyjson_val *val, const char **msg) { +#define return_err(_end, _msg) do { \ + *msg = _msg; \ + *end = _end; \ + return false; \ +} while (false) + +#define return_suc(_str_end, _cur_end) do { \ + val->tag = ((u64)(_str_end - hdr) << YYJSON_TAG_BIT) | \ + (u64)(YYJSON_TYPE_STR); \ + val->uni.str = (const char *)hdr; \ + *pre = _str_end; *end = _cur_end; \ + return true; \ +} while (false) + + u8 *hdr = *ptr; + u8 **end = ptr; + u8 *src = hdr, *dst = NULL; + u16 hi, lo; + u32 uni, tmp; + + /* add null-terminator for previous raw string */ + **pre = '\0'; + +skip_ascii: +#define expr_jump(i) \ + if (likely(char_is_id_ascii(src[i]))) {} \ + else goto skip_ascii_stop##i; + +#define expr_stop(i) \ + skip_ascii_stop##i: \ + src += i; \ + goto skip_ascii_end; + + repeat16_incr(expr_jump) + src += 16; + goto skip_ascii; + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + +skip_ascii_end: + gcc_store_barrier(*src); + if (likely(!char_is_id_next(*src))) { + return_suc(src, src); + } + +skip_utf8: + while (*src >= 0x80) { + if (has_allow(EXT_WHITESPACE)) { + if (char_is_space_ext(*src) && ext_space_len(src)) { + return_suc(src, src); + } + } + uni = byte_load_4(src); + if (is_utf8_seq2(uni)) { + src += 2; + } else if (is_utf8_seq3(uni)) { + src += 3; + } else if (is_utf8_seq4(uni)) { + src += 4; + } else { +#if !YYJSON_DISABLE_UTF8_VALIDATION + if (!has_allow(INVALID_UNICODE)) return_err(src, MSG_ERR_UTF8); +#endif + src += 1; + } + } + if (char_is_id_ascii(*src)) goto skip_ascii; + + /* The escape character appears, we need to copy it. */ + dst = src; +copy_escape: + if (byte_match_2(src, "\\u")) { + if (!read_uni_esc(&src, &dst, msg)) return_err(src, *msg); + } else { + if (!char_is_id_next(*src)) return_suc(dst, src); + return_err(src, "unexpected character in key"); + } + +copy_ascii: + /* + Copy continuous ASCII, loop unrolling, same as the following code: + + while (true) repeat16({ + if (char_is_ascii_skip(*src)) *dst++ = *src++; + else break; + }) + */ +#define expr_jump(i) \ + if (likely((char_is_id_ascii(src[i])))) {} \ + else { gcc_store_barrier(src[i]); goto copy_ascii_stop_##i; } + repeat16_incr(expr_jump) +#undef expr_jump + + byte_move_16(dst, src); + dst += 16; src += 16; + goto copy_ascii; + +#define expr_jump(i) \ + copy_ascii_stop_##i: \ + byte_move_forward(dst, src, i); \ + dst += i; src += i; \ + goto copy_utf8; + repeat16_incr(expr_jump) +#undef expr_jump + +copy_utf8: + while (*src >= 0x80) { /* non-ASCII character */ + if (has_allow(EXT_WHITESPACE)) { + if (char_is_space_ext(*src) && ext_space_len(src)) { + return_suc(dst, src); + } + } + uni = byte_load_4(src); + if (is_utf8_seq2(uni)) { + byte_copy_2(dst, &uni); + dst += 2; src += 2; + } else if (is_utf8_seq3(uni)) { + byte_copy_4(dst, &uni); + dst += 3; src += 3; + } else if (is_utf8_seq4(uni)) { + byte_copy_4(dst, &uni); + dst += 4; src += 4; + } else { +#if !YYJSON_DISABLE_UTF8_VALIDATION + if (!has_allow(INVALID_UNICODE)) return_err(src, MSG_ERR_UTF8); +#endif + *dst = *src; + dst += 1; src += 1; + } + } + if (char_is_id_ascii(*src)) goto copy_ascii; + goto copy_escape; + +#undef return_err +#undef return_suc +} + + + +/*============================================================================== + * MARK: - JSON Reader Implementation (Private) + * + * We use goto statements to build the finite state machine (FSM). + * The FSM's state was held by program counter (PC) and the 'goto' make the + * state transitions. + *============================================================================*/ + +/** Read single value JSON document. */ +static_noinline yyjson_doc *read_root_single(u8 *hdr, u8 *cur, u8 *eof, + yyjson_alc alc, + yyjson_read_flag flg, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, eof, YYJSON_READ_ERROR_##_code, flg)) { \ + err->pos = (usize)(eof - hdr); \ + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; \ + err->msg = MSG_NOT_END; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + if (val_hdr) alc.free(alc.ctx, val_hdr); \ + return NULL; \ +} while (false) + + usize hdr_len; /* value count used by doc */ + usize alc_num; /* value count capacity */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val; /* current value */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + alc_num = hdr_len + 1; /* single value */ + + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, alc_num * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val = val_hdr + hdr_len; + + if (char_is_num(*cur)) { + if (likely(read_num(&cur, pre, flg, val, &msg))) goto doc_end; + goto fail_number; + } + if (*cur == '"') { + if (likely(read_str(&cur, eof, flg, val, &msg))) goto doc_end; + goto fail_string; + } + if (*cur == 't') { + if (likely(read_true(&cur, val))) goto doc_end; + goto fail_literal_true; + } + if (*cur == 'f') { + if (likely(read_false(&cur, val))) goto doc_end; + goto fail_literal_false; + } + if (*cur == 'n') { + if (likely(read_null(&cur, val))) goto doc_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto doc_end; + } + goto fail_literal_null; + } + if (has_allow(INF_AND_NAN)) { + if (read_inf_or_nan(&cur, pre, flg, val)) goto doc_end; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto doc_end; + goto fail_string; + } + goto fail_character; + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < eof) && !has_flg(STOP_WHEN_DONE)) { + while (char_is_space(*cur)) cur++; + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + goto fail_comment; + } + } + if (unlikely(cur < eof)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = 1; + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + return doc; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef return_err +} + +/** Read JSON document (accept all style, but optimized for minify). */ +static_inline yyjson_doc *read_root_minify(u8 *hdr, u8 *cur, u8 *eof, + yyjson_alc alc, + yyjson_read_flag flg, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, eof, YYJSON_READ_ERROR_##_code, flg)) { \ + err->pos = (usize)(eof - hdr); \ + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; \ + err->msg = MSG_NOT_END; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + if (val_hdr) alc.free(alc.ctx, val_hdr); \ + return NULL; \ +} while (false) + +#define val_incr() do { \ + val++; \ + if (unlikely(val >= val_end)) { \ + usize alc_old = alc_len; \ + usize val_ofs = (usize)(val - val_hdr); \ + usize ctn_ofs = (usize)(ctn - val_hdr); \ + alc_len += alc_len / 2; \ + if ((sizeof(usize) < 8) && (alc_len >= alc_max)) goto fail_alloc; \ + val_tmp = (yyjson_val *)alc.realloc(alc.ctx, (void *)val_hdr, \ + alc_old * sizeof(yyjson_val), \ + alc_len * sizeof(yyjson_val)); \ + if ((!val_tmp)) goto fail_alloc; \ + val = val_tmp + val_ofs; \ + ctn = val_tmp + ctn_ofs; \ + val_hdr = val_tmp; \ + val_end = val_tmp + (alc_len - 2); \ + } \ +} while (false) + + usize dat_len; /* data length in bytes, hint for allocator */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize alc_max; /* maximum value count for allocator */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val_tmp; /* temporary pointer for realloc */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + yyjson_val *ctn_parent; /* parent of current container */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + + dat_len = has_flg(STOP_WHEN_DONE) ? 256 : (usize)(eof - cur); + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + alc_max = USIZE_MAX / sizeof(yyjson_val); + alc_len = hdr_len + (dat_len / YYJSON_READER_ESTIMATED_MINIFY_RATIO) + 4; + alc_len = yyjson_min(alc_len, alc_max); + + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, alc_len * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val_end = val_hdr + (alc_len - 2); /* padding for key-value pair reading */ + val = val_hdr + hdr_len; + ctn = val; + ctn_len = 0; + + if (*cur++ == '{') { + ctn->tag = YYJSON_TYPE_OBJ; + ctn->uni.ofs = 0; + goto obj_key_begin; + } else { + ctn->tag = YYJSON_TYPE_ARR; + ctn->uni.ofs = 0; + goto arr_val_begin; + } + +arr_begin: + /* save current container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + + /* create a new array value, save parent container offset */ + val_incr(); + val->tag = YYJSON_TYPE_ARR; + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + + /* push the new array value as current container */ + ctn = val; + ctn_len = 0; + +arr_val_begin: + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (char_is_num(*cur)) { + val_incr(); + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto arr_val_end; + goto fail_number; + } + if (*cur == '"') { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (*cur == 't') { + val_incr(); + ctn_len++; + if (likely(read_true(&cur, val))) goto arr_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val_incr(); + ctn_len++; + if (likely(read_false(&cur, val))) goto arr_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val_incr(); + ctn_len++; + if (likely(read_null(&cur, val))) goto arr_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto arr_val_end; + } + goto fail_literal_null; + } + if (*cur == ']') { + cur++; + if (likely(ctn_len == 0)) goto arr_end; + if (has_allow(TRAILING_COMMAS)) goto arr_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val_incr(); + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto arr_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +arr_val_end: + if (*cur == ',') { + cur++; + goto arr_val_begin; + } + if (*cur == ']') { + cur++; + goto arr_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_arr_end; + +arr_end: + /* get parent container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + + /* save the next sibling value offset */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = ((ctn_len) << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; + if (unlikely(ctn == ctn_parent)) goto doc_end; + + /* pop parent as current container */ + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +obj_begin: + /* push container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + val_incr(); + val->tag = YYJSON_TYPE_OBJ; + /* offset to the parent */ + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + ctn = val; + ctn_len = 0; + +obj_key_begin: + if (likely(*cur == '"')) { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (likely(*cur == '}')) { + cur++; + if (likely(ctn_len == 0)) goto obj_end; + if (has_allow(TRAILING_COMMAS)) goto obj_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_begin; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (has_allow(UNQUOTED_KEY) && char_is_id_start(*cur)) { + val_incr(); + ctn_len++; + if (read_str_id(&cur, eof, flg, pre, val, &msg)) goto obj_key_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_key; + +obj_key_end: + if (*cur == ':') { + cur++; + goto obj_val_begin; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_sep; + +obj_val_begin: + if (*cur == '"') { + val++; + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (char_is_num(*cur)) { + val++; + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto obj_val_end; + goto fail_number; + } + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (*cur == 't') { + val++; + ctn_len++; + if (likely(read_true(&cur, val))) goto obj_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val++; + ctn_len++; + if (likely(read_false(&cur, val))) goto obj_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val++; + ctn_len++; + if (likely(read_null(&cur, val))) goto obj_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto obj_val_end; + } + goto fail_literal_null; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val++; + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto obj_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val++; + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +obj_val_end: + if (likely(*cur == ',')) { + cur++; + goto obj_key_begin; + } + if (likely(*cur == '}')) { + cur++; + goto obj_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_end; + +obj_end: + /* pop container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + /* point to the next value */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = (ctn_len << (YYJSON_TAG_BIT - 1)) | YYJSON_TYPE_OBJ; + if (unlikely(ctn == ctn_parent)) goto doc_end; + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < eof) && !has_flg(STOP_WHEN_DONE)) { + while (char_is_space(*cur)) cur++; + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + goto fail_comment; + } + } + if (unlikely(cur < eof)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = (usize)((val - doc->root) + 1); + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + return doc; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_trailing_comma: return_err(cur, JSON_STRUCTURE, MSG_COMMA); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character_val: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_character_arr_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_ARR_END); +fail_character_obj_key: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_KEY); +fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); +fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); +fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef val_incr +#undef return_err +} + +/** Read JSON document (accept all style, but optimized for pretty). */ +static_inline yyjson_doc *read_root_pretty(u8 *hdr, u8 *cur, u8 *eof, + yyjson_alc alc, + yyjson_read_flag flg, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, eof, YYJSON_READ_ERROR_##_code, flg)) { \ + err->pos = (usize)(eof - hdr); \ + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; \ + err->msg = MSG_NOT_END; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + if (val_hdr) alc.free(alc.ctx, val_hdr); \ + return NULL; \ +} while (false) + +#define val_incr() do { \ + val++; \ + if (unlikely(val >= val_end)) { \ + usize alc_old = alc_len; \ + usize val_ofs = (usize)(val - val_hdr); \ + usize ctn_ofs = (usize)(ctn - val_hdr); \ + alc_len += alc_len / 2; \ + if ((sizeof(usize) < 8) && (alc_len >= alc_max)) goto fail_alloc; \ + val_tmp = (yyjson_val *)alc.realloc(alc.ctx, (void *)val_hdr, \ + alc_old * sizeof(yyjson_val), \ + alc_len * sizeof(yyjson_val)); \ + if ((!val_tmp)) goto fail_alloc; \ + val = val_tmp + val_ofs; \ + ctn = val_tmp + ctn_ofs; \ + val_hdr = val_tmp; \ + val_end = val_tmp + (alc_len - 2); \ + } \ +} while (false) + + usize dat_len; /* data length in bytes, hint for allocator */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize alc_max; /* maximum value count for allocator */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val_tmp; /* temporary pointer for realloc */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + yyjson_val *ctn_parent; /* parent of current container */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + + dat_len = has_flg(STOP_WHEN_DONE) ? 256 : (usize)(eof - cur); + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + alc_max = USIZE_MAX / sizeof(yyjson_val); + alc_len = hdr_len + (dat_len / YYJSON_READER_ESTIMATED_PRETTY_RATIO) + 4; + alc_len = yyjson_min(alc_len, alc_max); + + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, alc_len * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val_end = val_hdr + (alc_len - 2); /* padding for key-value pair reading */ + val = val_hdr + hdr_len; + ctn = val; + ctn_len = 0; + + if (*cur++ == '{') { + ctn->tag = YYJSON_TYPE_OBJ; + ctn->uni.ofs = 0; + if (*cur == '\n') cur++; + goto obj_key_begin; + } else { + ctn->tag = YYJSON_TYPE_ARR; + ctn->uni.ofs = 0; + if (*cur == '\n') cur++; + goto arr_val_begin; + } + +arr_begin: + /* save current container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + + /* create a new array value, save parent container offset */ + val_incr(); + val->tag = YYJSON_TYPE_ARR; + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + + /* push the new array value as current container */ + ctn = val; + ctn_len = 0; + if (*cur == '\n') cur++; + +arr_val_begin: +#if YYJSON_IS_REAL_GCC + while (true) repeat16({ + if (byte_match_2(cur, " ")) cur += 2; + else break; + }) +#else + while (true) repeat16({ + if (likely(byte_match_2(cur, " "))) cur += 2; + else break; + }) +#endif + + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (char_is_num(*cur)) { + val_incr(); + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto arr_val_end; + goto fail_number; + } + if (*cur == '"') { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (*cur == 't') { + val_incr(); + ctn_len++; + if (likely(read_true(&cur, val))) goto arr_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val_incr(); + ctn_len++; + if (likely(read_false(&cur, val))) goto arr_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val_incr(); + ctn_len++; + if (likely(read_null(&cur, val))) goto arr_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto arr_val_end; + } + goto fail_literal_null; + } + if (*cur == ']') { + cur++; + if (likely(ctn_len == 0)) goto arr_end; + if (has_allow(TRAILING_COMMAS)) goto arr_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val_incr(); + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto arr_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto arr_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +arr_val_end: + if (byte_match_2(cur, ",\n")) { + cur += 2; + goto arr_val_begin; + } + if (*cur == ',') { + cur++; + goto arr_val_begin; + } + if (*cur == ']') { + cur++; + goto arr_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto arr_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_arr_end; + +arr_end: + /* get parent container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + + /* save the next sibling value offset */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = ((ctn_len) << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; + if (unlikely(ctn == ctn_parent)) goto doc_end; + + /* pop parent as current container */ + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if (*cur == '\n') cur++; + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +obj_begin: + /* push container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + val_incr(); + val->tag = YYJSON_TYPE_OBJ; + /* offset to the parent */ + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + ctn = val; + ctn_len = 0; + if (*cur == '\n') cur++; + +obj_key_begin: +#if YYJSON_IS_REAL_GCC + while (true) repeat16({ + if (byte_match_2(cur, " ")) cur += 2; + else break; + }) +#else + while (true) repeat16({ + if (likely(byte_match_2(cur, " "))) cur += 2; + else break; + }) +#endif + if (likely(*cur == '"')) { + val_incr(); + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (likely(*cur == '}')) { + cur++; + if (likely(ctn_len == 0)) goto obj_end; + if (has_allow(TRAILING_COMMAS)) goto obj_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_begin; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val_incr(); + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_key_end; + goto fail_string; + } + if (has_allow(UNQUOTED_KEY) && char_is_id_start(*cur)) { + val_incr(); + ctn_len++; + if (read_str_id(&cur, eof, flg, pre, val, &msg)) goto obj_key_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_key; + +obj_key_end: + if (byte_match_2(cur, ": ")) { + cur += 2; + goto obj_val_begin; + } + if (*cur == ':') { + cur++; + goto obj_val_begin; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_key_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_sep; + +obj_val_begin: + if (*cur == '"') { + val++; + ctn_len++; + if (likely(read_str(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (char_is_num(*cur)) { + val++; + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto obj_val_end; + goto fail_number; + } + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (*cur == 't') { + val++; + ctn_len++; + if (likely(read_true(&cur, val))) goto obj_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val++; + ctn_len++; + if (likely(read_false(&cur, val))) goto obj_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val++; + ctn_len++; + if (likely(read_null(&cur, val))) goto obj_val_end; + if (has_allow(INF_AND_NAN)) { + if (read_nan(&cur, pre, flg, val)) goto obj_val_end; + } + goto fail_literal_null; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_begin; + } + if (has_allow(INF_AND_NAN) && + (*cur == 'i' || *cur == 'I' || *cur == 'N')) { + val++; + ctn_len++; + if (read_inf_or_nan(&cur, pre, flg, val)) goto obj_val_end; + goto fail_character_val; + } + if (has_allow(SINGLE_QUOTED_STR) && *cur == '\'') { + val++; + ctn_len++; + if (likely(read_str_sq(&cur, eof, flg, val, &msg))) goto obj_val_end; + goto fail_string; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_begin; + if (cur == eof) goto fail_comment; + } + goto fail_character_val; + +obj_val_end: + if (byte_match_2(cur, ",\n")) { + cur += 2; + goto obj_key_begin; + } + if (likely(*cur == ',')) { + cur++; + goto obj_key_begin; + } + if (likely(*cur == '}')) { + cur++; + goto obj_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_end; + } + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (skip_trivia(&cur, eof, flg)) goto obj_val_end; + if (cur == eof) goto fail_comment; + } + goto fail_character_obj_end; + +obj_end: + /* pop container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + /* point to the next value */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = (ctn_len << (YYJSON_TAG_BIT - 1)) | YYJSON_TYPE_OBJ; + if (unlikely(ctn == ctn_parent)) goto doc_end; + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if (*cur == '\n') cur++; + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < eof) && !has_flg(STOP_WHEN_DONE)) { + while (char_is_space(*cur)) cur++; + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + goto fail_comment; + } + } + if (unlikely(cur < eof)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = (usize)((val - doc->root) + 1); + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + return doc; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_trailing_comma: return_err(cur, JSON_STRUCTURE, MSG_COMMA); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character_val: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_character_arr_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_ARR_END); +fail_character_obj_key: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_KEY); +fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); +fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); +fail_comment: return_err(cur, INVALID_COMMENT, MSG_COMMENT); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef val_incr +#undef return_err +} + + + +/*============================================================================== + * MARK: - JSON Reader (Public) + *============================================================================*/ + +yyjson_doc *yyjson_read_opts(char *dat, usize len, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + err->pos = (usize)(_pos); \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + if (!has_flg(INSITU) && hdr) alc.free(alc.ctx, (void *)hdr); \ + return NULL; \ +} while (false) + + yyjson_read_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_doc *doc; + u8 *hdr = NULL, *eof, *cur; + + /* validate input parameters */ + if (!err) err = &tmp_err; + if (unlikely(!dat)) return_err(0, INVALID_PARAMETER, "input data is NULL"); + if (unlikely(!len)) return_err(0, INVALID_PARAMETER, "input length is 0"); + + /* add 4-byte zero padding for input data if necessary */ + if (has_flg(INSITU)) { + hdr = (u8 *)dat; + eof = (u8 *)dat + len; + cur = (u8 *)dat; + } else { + if (unlikely(len >= USIZE_MAX - YYJSON_PADDING_SIZE)) { + return_err(0, MEMORY_ALLOCATION, MSG_MALLOC); + } + hdr = (u8 *)alc.malloc(alc.ctx, len + YYJSON_PADDING_SIZE); + if (unlikely(!hdr)) { + return_err(0, MEMORY_ALLOCATION, MSG_MALLOC); + } + eof = hdr + len; + cur = hdr; + memcpy(hdr, dat, len); + } + memset(eof, 0, YYJSON_PADDING_SIZE); + + if (has_allow(BOM)) { + if (len >= 3 && is_utf8_bom(cur)) cur += 3; + } + + /* skip empty contents before json document */ + if (unlikely(!char_is_ctn(*cur))) { + while (char_is_space(*cur)) cur++; + if (unlikely(!char_is_ctn(*cur))) { + if (has_allow(TRIVIA) && char_is_trivia(*cur)) { + if (!skip_trivia(&cur, eof, flg) && cur == eof) { + return_err(cur - hdr, INVALID_COMMENT, MSG_COMMENT); + } + } + } + if (unlikely(cur >= eof)) { + return_err(0, EMPTY_CONTENT, "input data is empty"); + } + } + + /* read json document */ + if (likely(char_is_ctn(*cur))) { + if (char_is_space(cur[1]) && char_is_space(cur[2])) { + doc = read_root_pretty(hdr, cur, eof, alc, flg, err); + } else { + doc = read_root_minify(hdr, cur, eof, alc, flg, err); + } + } else { + doc = read_root_single(hdr, cur, eof, alc, flg, err); + } + + /* check result */ + if (likely(doc)) { + memset(err, 0, sizeof(yyjson_read_err)); + } else { + /* RFC 8259: JSON text MUST be encoded using UTF-8 */ + if (err->pos == 0 && err->code != YYJSON_READ_ERROR_MEMORY_ALLOCATION) { + if (is_utf8_bom(hdr)) err->msg = MSG_ERR_BOM; + else if (len >= 4 && is_utf32_bom(hdr)) err->msg = MSG_ERR_UTF32; + else if (len >= 2 && is_utf16_bom(hdr)) err->msg = MSG_ERR_UTF16; + } + if (!has_flg(INSITU)) alc.free(alc.ctx, hdr); + } + return doc; + +#undef return_err +} + +yyjson_doc *yyjson_read_file(const char *path, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr, + yyjson_read_err *err) { +#define return_err(_code, _msg) do { \ + err->pos = 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + return NULL; \ +} while (false) + + yyjson_read_err tmp_err; + yyjson_doc *doc; + FILE *file; + + if (!err) err = &tmp_err; + if (unlikely(!path)) return_err(INVALID_PARAMETER, "input path is NULL"); + + file = fopen_readonly(path); + if (unlikely(!file)) return_err(FILE_OPEN, MSG_FREAD); + + doc = yyjson_read_fp(file, flg, alc_ptr, err); + fclose(file); + return doc; + +#undef return_err +} + +yyjson_doc *yyjson_read_fp(FILE *file, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr, + yyjson_read_err *err) { +#define return_err(_code, _msg) do { \ + err->pos = 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + if (buf) alc.free(alc.ctx, buf); \ + return NULL; \ +} while (false) + + yyjson_read_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_doc *doc; + + long file_size = 0, file_pos; + void *buf = NULL; + usize buf_size = 0; + + /* validate input parameters */ + if (!err) err = &tmp_err; + if (unlikely(!file)) return_err(INVALID_PARAMETER, "input file is NULL"); + + /* get current position */ + file_pos = ftell(file); + if (file_pos != -1) { + /* get total file size, may fail */ + if (fseek(file, 0, SEEK_END) == 0) file_size = ftell(file); + /* reset to original position, may fail */ + if (fseek(file, file_pos, SEEK_SET) != 0) file_size = 0; + /* get file size from current postion to end */ + if (file_size > 0) file_size -= file_pos; + } + + /* read file */ + if (file_size > 0) { + /* read the entire file in one call */ + buf_size = (usize)file_size + YYJSON_PADDING_SIZE; + buf = alc.malloc(alc.ctx, buf_size); + if (buf == NULL) { + return_err(MEMORY_ALLOCATION, MSG_MALLOC); + } + if (fread_safe(buf, (usize)file_size, file) != (usize)file_size) { + return_err(FILE_READ, MSG_FREAD); + } + } else { + /* failed to get file size, read it as a stream */ + usize chunk_min = (usize)64; + usize chunk_max = (usize)512 * 1024 * 1024; + usize chunk_now = chunk_min; + usize read_size; + void *tmp; + + buf_size = YYJSON_PADDING_SIZE; + while (true) { + if (buf_size + chunk_now < buf_size) { /* overflow */ + return_err(MEMORY_ALLOCATION, MSG_MALLOC); + } + buf_size += chunk_now; + if (!buf) { + buf = alc.malloc(alc.ctx, buf_size); + if (!buf) return_err(MEMORY_ALLOCATION, MSG_MALLOC); + } else { + tmp = alc.realloc(alc.ctx, buf, buf_size - chunk_now, buf_size); + if (!tmp) return_err(MEMORY_ALLOCATION, MSG_MALLOC); + buf = tmp; + } + tmp = ((u8 *)buf) + buf_size - YYJSON_PADDING_SIZE - chunk_now; + read_size = fread_safe(tmp, chunk_now, file); + file_size += (long)read_size; + if (read_size != chunk_now) break; + + chunk_now *= 2; + if (chunk_now > chunk_max) chunk_now = chunk_max; + } + } + + /* read JSON */ + memset((u8 *)buf + file_size, 0, YYJSON_PADDING_SIZE); + flg |= YYJSON_READ_INSITU; + doc = yyjson_read_opts((char *)buf, (usize)file_size, flg, &alc, err); + if (doc) { + doc->str_pool = (char *)buf; + return doc; + } else { + alc.free(alc.ctx, buf); + return NULL; + } + +#undef return_err +} + +const char *yyjson_read_number(const char *dat, + yyjson_val *val, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err) { +#define return_err(_pos, _code, _msg) do { \ + err->pos = _pos > hdr ? (usize)(_pos - hdr) : 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_##_code; \ + return NULL; \ +} while (false) + + u8 *hdr = constcast(u8 *)dat, *cur = hdr; + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + const char *msg; + yyjson_read_err tmp_err; + +#if YYJSON_DISABLE_FAST_FP_CONV + u8 buf[128]; + usize dat_len; +#endif + + if (!err) err = &tmp_err; + if (unlikely(!dat)) { + return_err(cur, INVALID_PARAMETER, "input data is NULL"); + } + if (unlikely(!val)) { + return_err(cur, INVALID_PARAMETER, "output value is NULL"); + } + +#if YYJSON_DISABLE_FAST_FP_CONV + if (!alc) alc = &YYJSON_DEFAULT_ALC; + dat_len = strlen(dat); + if (dat_len < sizeof(buf)) { + memcpy(buf, dat, dat_len + 1); + hdr = buf; + cur = hdr; + } else { + hdr = (u8 *)alc->malloc(alc->ctx, dat_len + 1); + cur = hdr; + if (unlikely(!hdr)) { + return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); + } + memcpy(hdr, dat, dat_len + 1); + } + hdr[dat_len] = 0; +#endif + +#if YYJSON_DISABLE_FAST_FP_CONV + if (!read_num(&cur, pre, flg, val, &msg)) { + if (dat_len >= sizeof(buf)) alc->free(alc->ctx, hdr); + return_err(cur, INVALID_NUMBER, msg); + } + if (dat_len >= sizeof(buf)) alc->free(alc->ctx, hdr); + if (yyjson_is_raw(val)) val->uni.str = dat; + return dat + (cur - hdr); +#else + if (!read_num(&cur, pre, flg, val, &msg)) { + return_err(cur, INVALID_NUMBER, msg); + } + return (const char *)cur; +#endif + +#undef return_err +} + + + +/*============================================================================== + * MARK: - Incremental JSON Reader (Public) + *============================================================================*/ + +#if !YYJSON_DISABLE_INCR_READER + +/* labels within yyjson_incr_read() to resume incremental parsing */ +#define LABEL_doc_begin 0 +#define LABEL_arr_val_begin 1 +#define LABEL_arr_val_end 2 +#define LABEL_obj_key_begin 3 +#define LABEL_obj_key_end 4 +#define LABEL_obj_val_begin 5 +#define LABEL_obj_val_end 6 +#define LABEL_doc_end 7 + +/** State for incremental JSON reader, opaque in the API. */ +struct yyjson_incr_state { + u32 label; /* current parser goto label */ + yyjson_alc alc; /* allocator */ + yyjson_read_flag flg; /* read flags */ + u8 *hdr; /* JSON data header */ + u8 *cur; /* current position in JSON data */ + usize buf_len; /* total buffer length (without padding) */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + u8 *str_con[2]; /* string parser incremental state */ +}; + +yyjson_incr_state *yyjson_incr_new(char *buf, size_t buf_len, + yyjson_read_flag flg, + const yyjson_alc *alc_ptr) { + yyjson_incr_state *state = NULL; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + + /* remove non-standard flags */ + flg &= ~YYJSON_READ_JSON5; + flg &= ~YYJSON_READ_ALLOW_BOM; + flg &= ~YYJSON_READ_ALLOW_INVALID_UNICODE; + + if (unlikely(!buf)) return NULL; + if (unlikely(buf_len >= USIZE_MAX - YYJSON_PADDING_SIZE)) return NULL; + state = (yyjson_incr_state *)alc.malloc(alc.ctx, sizeof(*state)); + if (!state) return NULL; + memset(state, 0, sizeof(yyjson_incr_state)); + state->alc = alc; + state->flg = flg; + state->buf_len = buf_len; + + /* add 4-byte zero padding for input data if necessary */ + if (has_flg(INSITU)) { + state->hdr = (u8 *)buf; + } else { + state->hdr = (u8 *)alc.malloc(alc.ctx, buf_len + YYJSON_PADDING_SIZE); + if (unlikely(!state->hdr)) { + alc.free(alc.ctx, state); + return NULL; + } + memcpy(state->hdr, buf, buf_len); + } + memset(state->hdr + buf_len, 0, YYJSON_PADDING_SIZE); + state->cur = state->hdr; + state->label = LABEL_doc_begin; + return state; +} + +void yyjson_incr_free(yyjson_incr_state *state) { + if (state) { + yyjson_alc alc = state->alc; + memset(&state->alc, 0, sizeof(alc)); + if (state->val_hdr) { + alc.free(alc.ctx, (void *)state->val_hdr); + } + if (state->hdr && !(state->flg & YYJSON_READ_INSITU)) { + alc.free(alc.ctx, state->hdr); + } + alc.free(alc.ctx, state); + } +} + +yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, + yyjson_read_err *err) { +#define return_err_inv_param(_msg) do { \ + err->pos = 0; \ + err->msg = _msg; \ + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; \ + return NULL; \ +} while (false) + +#define return_err(_pos, _code, _msg) do { \ + if (is_truncated_end(hdr, _pos, end, YYJSON_READ_ERROR_##_code, flg)) { \ + goto unexpected_end; \ + } else { \ + err->pos = (usize)(_pos - hdr); \ + err->code = YYJSON_READ_ERROR_##_code; \ + err->msg = _msg; \ + } \ + return NULL; \ +} while (false) + +#define val_incr() do { \ + val++; \ + if (unlikely(val >= val_end)) { \ + usize alc_old = alc_len; \ + alc_len += alc_len / 2; \ + if ((sizeof(usize) < 8) && (alc_len >= alc_max)) goto fail_alloc; \ + val_tmp = (yyjson_val *)alc.realloc(alc.ctx, (void *)val_hdr, \ + alc_old * sizeof(yyjson_val), \ + alc_len * sizeof(yyjson_val)); \ + if ((!val_tmp)) goto fail_alloc; \ + val = val_tmp + (usize)(val - val_hdr); \ + ctn = val_tmp + (usize)(ctn - val_hdr); \ + state->val = val_tmp + (usize)(state->val - val_hdr); \ + state->val_hdr = val_hdr = val_tmp; \ + val_end = val_tmp + (alc_len - 2); \ + state->val_end = val_end; \ + } \ +} while (false) + + /* save position where it's possible to resume incremental parsing */ +#define save_incr_state(_label) do { \ + state->label = LABEL_##_label; \ + state->cur = cur; \ + state->val = val; \ + state->ctn_len = ctn_len; \ + state->hdr_len = hdr_len; \ + if (unlikely(cur >= end)) goto unexpected_end; \ +} while (false) + +#define check_maybe_truncated_number() do { \ + if (unlikely(cur >= end)) { \ + if (unlikely(cur > state->cur + INCR_NUM_MAX_LEN)) { \ + msg = "number too long"; \ + goto fail_number; \ + } \ + goto unexpected_end; \ + } \ +} while (false) + + u8 *hdr = NULL, *end = NULL, *cur = NULL; + yyjson_read_flag flg; + yyjson_alc alc; + usize dat_len; /* data length in bytes, hint for allocator */ + usize hdr_len; /* value count used by yyjson_doc */ + usize alc_len; /* value count allocated */ + usize alc_max; /* maximum value count for allocator */ + usize ctn_len; /* the number of elements in current container */ + yyjson_val *val_hdr; /* the head of allocated values */ + yyjson_val *val_end; /* the end of allocated values */ + yyjson_val *val_tmp; /* temporary pointer for realloc */ + yyjson_val *val; /* current JSON value */ + yyjson_val *ctn; /* current container */ + yyjson_val *ctn_parent; /* parent of current container */ + yyjson_doc *doc; /* the JSON document, equals to val_hdr */ + const char *msg; /* error message */ + + yyjson_read_err tmp_err; + u8 raw_end[1]; /* raw end for null-terminator */ + u8 *raw_ptr = raw_end; + u8 **pre = &raw_ptr; /* previous raw end pointer */ + u8 **con = NULL; /* for incremental string parsing */ + u8 saved_end = '\0'; /* saved end char */ + + /* validate input parameters */ + if (!err) err = &tmp_err; + if (unlikely(!state)) { + return_err_inv_param("input state is NULL"); + } + if (unlikely(!len)) { + return_err_inv_param("input length is 0"); + } + if (unlikely(len > state->buf_len)) { + return_err_inv_param("length is greater than total input length"); + } + + /* restore state saved from the previous call */ + hdr = state->hdr; + end = state->hdr + len; + cur = state->cur; + flg = state->flg; + alc = state->alc; + ctn_len = state->ctn_len; + hdr_len = state->hdr_len; + alc_len = state->alc_len; + val = state->val; + val_hdr = state->val_hdr; + val_end = state->val_end; + ctn = state->ctn; + con = state->str_con; + alc_max = USIZE_MAX / sizeof(yyjson_val); + + /* insert null terminator to make us stop at the specified end, even if + the data contains more valid JSON */ + saved_end = *end; + *end = '\0'; + + /* resume parsing from the last save point */ + switch (state->label) { + case LABEL_doc_begin: goto doc_begin; + case LABEL_arr_val_begin: goto arr_val_begin; + case LABEL_arr_val_end: goto arr_val_end; + case LABEL_obj_key_begin: goto obj_key_begin; + case LABEL_obj_key_end: goto obj_key_end; + case LABEL_obj_val_begin: goto obj_val_begin; + case LABEL_obj_val_end: goto obj_val_end; + case LABEL_doc_end: goto doc_end; + default: return_err_inv_param("invalid incremental state"); + } + +doc_begin: + /* skip empty contents before json document */ + if (unlikely(!char_is_ctn(*cur))) { + while (char_is_space(*cur)) cur++; + if (unlikely(cur >= end)) goto unexpected_end; /* input data is empty */ + } + + /* allocate memory for document */ + if (!val_hdr) { + hdr_len = sizeof(yyjson_doc) / sizeof(yyjson_val); + hdr_len += (sizeof(yyjson_doc) % sizeof(yyjson_val)) > 0; + if (likely(char_is_ctn(*cur))) { + dat_len = has_flg(STOP_WHEN_DONE) ? 256 : state->buf_len; + alc_len = hdr_len + + (dat_len / YYJSON_READER_ESTIMATED_MINIFY_RATIO) + 4; + alc_len = yyjson_min(alc_len, alc_max); + } else { + alc_len = hdr_len + 1; /* single value */ + } + val_hdr = (yyjson_val *)alc.malloc(alc.ctx, + alc_len * sizeof(yyjson_val)); + if (unlikely(!val_hdr)) goto fail_alloc; + val_end = val_hdr + (alc_len - 2); /* padding for kv pair reading */ + val = val_hdr + hdr_len; + ctn = val; + ctn_len = 0; + state->val_hdr = val_hdr; + state->val_end = val_end; + save_incr_state(doc_begin); + } + + /* read json document */ + if (*cur == '{') { + cur++; + ctn->tag = YYJSON_TYPE_OBJ; + ctn->uni.ofs = 0; + goto obj_key_begin; + } + if (*cur == '[') { + cur++; + ctn->tag = YYJSON_TYPE_ARR; + ctn->uni.ofs = 0; + goto arr_val_begin; + } + if (char_is_num(*cur)) { + if (likely(read_num(&cur, pre, flg, val, &msg))) goto doc_end; + goto fail_number; + } + if (*cur == '"') { + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) goto doc_end; + goto fail_string; + } + if (*cur == 't') { + if (likely(read_true(&cur, val))) goto doc_end; + goto fail_literal_true; + } + if (*cur == 'f') { + if (likely(read_false(&cur, val))) goto doc_end; + goto fail_literal_false; + } + if (*cur == 'n') { + if (likely(read_null(&cur, val))) goto doc_end; + goto fail_literal_null; + } + + msg = "unexpected character, expected a valid root value"; + if (cur == hdr) { + /* RFC 8259: JSON text MUST be encoded using UTF-8 */ + if (is_utf8_bom(hdr)) msg = MSG_ERR_BOM; + else if (len >= 4 && is_utf32_bom(hdr)) msg = MSG_ERR_UTF32; + else if (len >= 2 && is_utf16_bom(hdr)) msg = MSG_ERR_UTF16; + } + return_err(cur, UNEXPECTED_CHARACTER, msg); + +arr_begin: + /* save current container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + + /* create a new array value, save parent container offset */ + val_incr(); + val->tag = YYJSON_TYPE_ARR; + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + + /* push the new array value as current container */ + ctn = val; + ctn_len = 0; + +arr_val_begin: + save_incr_state(arr_val_begin); +arr_val_continue: + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (char_is_num(*cur)) { + val_incr(); + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto arr_val_maybe_end; + goto fail_number; + } + if (*cur == '"') { + val_incr(); + ctn_len++; + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) + goto arr_val_end; + goto fail_string; + } + if (*cur == 't') { + val_incr(); + ctn_len++; + if (likely(read_true(&cur, val))) goto arr_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val_incr(); + ctn_len++; + if (likely(read_false(&cur, val))) goto arr_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val_incr(); + ctn_len++; + if (likely(read_null(&cur, val))) goto arr_val_end; + goto fail_literal_null; + } + if (*cur == ']') { + cur++; + if (likely(ctn_len == 0)) goto arr_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_continue; + } + goto fail_character_val; + +arr_val_maybe_end: + /* if incremental parsing stops in the middle of a number, it may continue + with more digits, so arr val maybe didn't end yet */ + check_maybe_truncated_number(); + +arr_val_end: + save_incr_state(arr_val_end); + if (*cur == ',') { + cur++; + goto arr_val_begin; + } + if (*cur == ']') { + cur++; + goto arr_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto arr_val_end; + } + goto fail_character_arr_end; + +arr_end: + /* get parent container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + + /* save the next sibling value offset */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = ((ctn_len) << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; + if (unlikely(ctn == ctn_parent)) goto doc_end; + + /* pop parent as current container */ + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +obj_begin: + /* push container */ + ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) | + (ctn->tag & YYJSON_TAG_MASK); + val_incr(); + val->tag = YYJSON_TYPE_OBJ; + /* offset to the parent */ + val->uni.ofs = (usize)((u8 *)val - (u8 *)ctn); + ctn = val; + ctn_len = 0; + +obj_key_begin: + save_incr_state(obj_key_begin); +obj_key_continue: + if (likely(*cur == '"')) { + val_incr(); + ctn_len++; + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) + goto obj_key_end; + goto fail_string; + } + if (likely(*cur == '}')) { + cur++; + if (likely(ctn_len == 0)) goto obj_end; + while (*cur != ',') cur--; + goto fail_trailing_comma; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_continue; + } + goto fail_character_obj_key; + +obj_key_end: + save_incr_state(obj_key_end); + if (*cur == ':') { + cur++; + goto obj_val_begin; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_key_end; + } + goto fail_character_obj_sep; + +obj_val_begin: + save_incr_state(obj_val_begin); +obj_val_continue: + if (*cur == '"') { + val++; + ctn_len++; + if (likely(read_str_con(&cur, end, flg, val, &msg, con))) + goto obj_val_end; + goto fail_string; + } + if (char_is_num(*cur)) { + val++; + ctn_len++; + if (likely(read_num(&cur, pre, flg, val, &msg))) goto obj_val_maybe_end; + goto fail_number; + } + if (*cur == '{') { + cur++; + goto obj_begin; + } + if (*cur == '[') { + cur++; + goto arr_begin; + } + if (*cur == 't') { + val++; + ctn_len++; + if (likely(read_true(&cur, val))) goto obj_val_end; + goto fail_literal_true; + } + if (*cur == 'f') { + val++; + ctn_len++; + if (likely(read_false(&cur, val))) goto obj_val_end; + goto fail_literal_false; + } + if (*cur == 'n') { + val++; + ctn_len++; + if (likely(read_null(&cur, val))) goto obj_val_end; + goto fail_literal_null; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_continue; + } + goto fail_character_val; + +obj_val_maybe_end: + /* if incremental parsing stops in the middle of a number, it may continue + with more digits, so obj val maybe didn't end yet */ + check_maybe_truncated_number(); + +obj_val_end: + save_incr_state(obj_val_end); + if (likely(*cur == ',')) { + cur++; + goto obj_key_begin; + } + if (likely(*cur == '}')) { + cur++; + goto obj_end; + } + if (char_is_space(*cur)) { + while (char_is_space(*++cur)); + goto obj_val_end; + } + goto fail_character_obj_end; + +obj_end: + /* pop container */ + ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs); + /* point to the next value */ + ctn->uni.ofs = (usize)((u8 *)val - (u8 *)ctn) + sizeof(yyjson_val); + ctn->tag = (ctn_len << (YYJSON_TAG_BIT - 1)) | YYJSON_TYPE_OBJ; + if (unlikely(ctn == ctn_parent)) goto doc_end; + ctn = ctn_parent; + ctn_len = (usize)(ctn->tag >> YYJSON_TAG_BIT); + if ((ctn->tag & YYJSON_TYPE_MASK) == YYJSON_TYPE_OBJ) { + goto obj_val_end; + } else { + goto arr_val_end; + } + +doc_end: + /* check invalid contents after json document */ + if (unlikely(cur < end) && !has_flg(STOP_WHEN_DONE)) { + save_incr_state(doc_end); + while (char_is_space(*cur)) cur++; + if (unlikely(cur < end)) goto fail_garbage; + } + + **pre = '\0'; + doc = (yyjson_doc *)val_hdr; + doc->root = val_hdr + hdr_len; + doc->alc = alc; + doc->dat_read = (usize)(cur - hdr); + doc->val_read = (usize)((val - doc->root) + 1); + doc->str_pool = has_flg(INSITU) ? NULL : (char *)hdr; + state->hdr = NULL; + state->val_hdr = NULL; + memset(err, 0, sizeof(yyjson_read_err)); + return doc; + +unexpected_end: + err->pos = len; + /* if no nore data, stop the incr read */ + if (unlikely(len >= state->buf_len)) { + err->code = YYJSON_READ_ERROR_UNEXPECTED_END; + err->msg = MSG_NOT_END; + return NULL; + } + /* save parser state in extended error struct, in addition to what was + * stored in the last save_incr_state */ + err->code = YYJSON_READ_ERROR_MORE; + err->msg = "need more data"; + state->val_end = val_end; + state->ctn = ctn; + state->alc_len = alc_len; + /* restore the end where we've inserted a null terminator */ + *end = saved_end; + return NULL; + +fail_string: return_err(cur, INVALID_STRING, msg); +fail_number: return_err(cur, INVALID_NUMBER, msg); +fail_alloc: return_err(cur, MEMORY_ALLOCATION, MSG_MALLOC); +fail_trailing_comma: return_err(cur, JSON_STRUCTURE, MSG_COMMA); +fail_literal_true: return_err(cur, LITERAL, MSG_CHAR_T); +fail_literal_false: return_err(cur, LITERAL, MSG_CHAR_F); +fail_literal_null: return_err(cur, LITERAL, MSG_CHAR_N); +fail_character_val: return_err(cur, UNEXPECTED_CHARACTER, MSG_CHAR); +fail_character_arr_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_ARR_END); +fail_character_obj_key: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_KEY); +fail_character_obj_sep: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_SEP); +fail_character_obj_end: return_err(cur, UNEXPECTED_CHARACTER, MSG_OBJ_END); +fail_garbage: return_err(cur, UNEXPECTED_CONTENT, MSG_GARBAGE); + +#undef val_incr +#undef return_err +#undef return_err_inv_param +#undef save_incr_state +#undef check_maybe_truncated_number +} + +#endif /* YYJSON_DISABLE_INCR_READER */ + +#undef has_flg +#undef has_allow +#endif /* YYJSON_DISABLE_READER */ + + + +#if !YYJSON_DISABLE_WRITER /* writer begin */ + +/* Check write flag, avoids `always false` warning when disabled. */ +#define has_flg(_flg) unlikely(has_wflag(flg, YYJSON_WRITE_##_flg, 0)) +#define has_allow(_flg) unlikely(has_wflag(flg, YYJSON_WRITE_ALLOW_##_flg, 1)) +static_inline bool has_wflag(yyjson_write_flag flg, yyjson_write_flag chk, + bool non_standard) { +#if YYJSON_DISABLE_NON_STANDARD + if (non_standard) return false; +#endif + return (flg & chk) != 0; +} + +/*============================================================================== + * MARK: - Integer Writer (Private) + * + * The maximum value of uint32_t is 4294967295 (10 digits), + * these digits are named as 'aabbccddee' here. + * + * Although most compilers may convert the "division by constant value" into + * "multiply and shift", manual conversion can still help some compilers + * generate fewer and better instructions. + * + * Reference: + * Division by Invariant Integers using Multiplication, 1994. + * https://gmplib.org/~tege/divcnst-pldi94.pdf + * Improved division by invariant integers, 2011. + * https://gmplib.org/~tege/division-paper.pdf + *============================================================================*/ + +/** Digit table from 00 to 99. */ +yyjson_align(2) +static const char digit_table[200] = { + '0', '0', '0', '1', '0', '2', '0', '3', '0', '4', + '0', '5', '0', '6', '0', '7', '0', '8', '0', '9', + '1', '0', '1', '1', '1', '2', '1', '3', '1', '4', + '1', '5', '1', '6', '1', '7', '1', '8', '1', '9', + '2', '0', '2', '1', '2', '2', '2', '3', '2', '4', + '2', '5', '2', '6', '2', '7', '2', '8', '2', '9', + '3', '0', '3', '1', '3', '2', '3', '3', '3', '4', + '3', '5', '3', '6', '3', '7', '3', '8', '3', '9', + '4', '0', '4', '1', '4', '2', '4', '3', '4', '4', + '4', '5', '4', '6', '4', '7', '4', '8', '4', '9', + '5', '0', '5', '1', '5', '2', '5', '3', '5', '4', + '5', '5', '5', '6', '5', '7', '5', '8', '5', '9', + '6', '0', '6', '1', '6', '2', '6', '3', '6', '4', + '6', '5', '6', '6', '6', '7', '6', '8', '6', '9', + '7', '0', '7', '1', '7', '2', '7', '3', '7', '4', + '7', '5', '7', '6', '7', '7', '7', '8', '7', '9', + '8', '0', '8', '1', '8', '2', '8', '3', '8', '4', + '8', '5', '8', '6', '8', '7', '8', '8', '8', '9', + '9', '0', '9', '1', '9', '2', '9', '3', '9', '4', + '9', '5', '9', '6', '9', '7', '9', '8', '9', '9' +}; + +static_inline u8 *write_u32_len_8(u32 val, u8 *buf) { + u32 aa, bb, cc, dd, aabb, ccdd; /* 8 digits: aabbccdd */ + aabb = (u32)(((u64)val * 109951163) >> 40); /* (val / 10000) */ + ccdd = val - aabb * 10000; /* (val % 10000) */ + aa = (aabb * 5243) >> 19; /* (aabb / 100) */ + cc = (ccdd * 5243) >> 19; /* (ccdd / 100) */ + bb = aabb - aa * 100; /* (aabb % 100) */ + dd = ccdd - cc * 100; /* (ccdd % 100) */ + byte_copy_2(buf + 0, digit_table + aa * 2); + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + byte_copy_2(buf + 6, digit_table + dd * 2); + return buf + 8; +} + +static_inline u8 *write_u32_len_4(u32 val, u8 *buf) { + u32 aa, bb; /* 4 digits: aabb */ + aa = (val * 5243) >> 19; /* (val / 100) */ + bb = val - aa * 100; /* (val % 100) */ + byte_copy_2(buf + 0, digit_table + aa * 2); + byte_copy_2(buf + 2, digit_table + bb * 2); + return buf + 4; +} + +static_inline u8 *write_u32_len_1_to_8(u32 val, u8 *buf) { + u32 aa, bb, cc, dd, aabb, bbcc, ccdd, lz; + + if (val < 100) { /* 1-2 digits: aa */ + lz = val < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + val * 2 + lz); + buf -= lz; + return buf + 2; + + } else if (val < 10000) { /* 3-4 digits: aabb */ + aa = (val * 5243) >> 19; /* (val / 100) */ + bb = val - aa * 100; /* (val % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + return buf + 4; + + } else if (val < 1000000) { /* 5-6 digits: aabbcc */ + aa = (u32)(((u64)val * 429497) >> 32); /* (val / 10000) */ + bbcc = val - aa * 10000; /* (val % 10000) */ + bb = (bbcc * 5243) >> 19; /* (bbcc / 100) */ + cc = bbcc - bb * 100; /* (bbcc % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + return buf + 6; + + } else { /* 7-8 digits: aabbccdd */ + aabb = (u32)(((u64)val * 109951163) >> 40); /* (val / 10000) */ + ccdd = val - aabb * 10000; /* (val % 10000) */ + aa = (aabb * 5243) >> 19; /* (aabb / 100) */ + cc = (ccdd * 5243) >> 19; /* (ccdd / 100) */ + bb = aabb - aa * 100; /* (aabb % 100) */ + dd = ccdd - cc * 100; /* (ccdd % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + byte_copy_2(buf + 6, digit_table + dd * 2); + return buf + 8; + } +} + +static_inline u8 *write_u32_len_5_to_8(u32 val, u8 *buf) { + u32 aa, bb, cc, dd, aabb, bbcc, ccdd, lz; + + if (val < 1000000) { /* 5-6 digits: aabbcc */ + aa = (u32)(((u64)val * 429497) >> 32); /* (val / 10000) */ + bbcc = val - aa * 10000; /* (val % 10000) */ + bb = (bbcc * 5243) >> 19; /* (bbcc / 100) */ + cc = bbcc - bb * 100; /* (bbcc % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + return buf + 6; + + } else { /* 7-8 digits: aabbccdd */ + aabb = (u32)(((u64)val * 109951163) >> 40); /* (val / 10000) */ + ccdd = val - aabb * 10000; /* (val % 10000) */ + aa = (aabb * 5243) >> 19; /* (aabb / 100) */ + cc = (ccdd * 5243) >> 19; /* (ccdd / 100) */ + bb = aabb - aa * 100; /* (aabb % 100) */ + dd = ccdd - cc * 100; /* (ccdd % 100) */ + lz = aa < 10; /* leading zero: 0 or 1 */ + byte_copy_2(buf + 0, digit_table + aa * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + bb * 2); + byte_copy_2(buf + 4, digit_table + cc * 2); + byte_copy_2(buf + 6, digit_table + dd * 2); + return buf + 8; + } +} + +static_inline u8 *write_u64(u64 val, u8 *buf) { + u64 tmp, hgh; + u32 mid, low; + + if (val < 100000000) { /* 1-8 digits */ + buf = write_u32_len_1_to_8((u32)val, buf); + return buf; + + } else if (val < (u64)100000000 * 100000000) { /* 9-16 digits */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + buf = write_u32_len_1_to_8((u32)hgh, buf); + buf = write_u32_len_8(low, buf); + return buf; + + } else { /* 17-20 digits */ + tmp = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - tmp * 100000000); /* (val % 100000000) */ + hgh = (u32)(tmp / 10000); /* (tmp / 10000) */ + mid = (u32)(tmp - hgh * 10000); /* (tmp % 10000) */ + buf = write_u32_len_5_to_8((u32)hgh, buf); + buf = write_u32_len_4(mid, buf); + buf = write_u32_len_8(low, buf); + return buf; + } +} + + + +/*============================================================================== + * MARK: - Number Writer (Private) + *============================================================================*/ + +#if !YYJSON_DISABLE_FAST_FP_CONV /* FP_WRITER */ + +/** Trailing zero count table for number 0 to 99. + (generate with misc/make_tables.c) */ +static const u8 dec_trailing_zero_table[] = { + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static_inline u8 *write_u64_len_1_to_16(u64 val, u8 *buf) { + u64 hgh; + u32 low; + if (val < 100000000) { /* 1-8 digits */ + buf = write_u32_len_1_to_8((u32)val, buf); + return buf; + } else { /* 9-16 digits */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + buf = write_u32_len_1_to_8((u32)hgh, buf); + buf = write_u32_len_8(low, buf); + return buf; + } +} + +static_inline u8 *write_u64_len_1_to_17(u64 val, u8 *buf) { + u64 hgh; + u32 mid, low, one; + if (val >= (u64)100000000 * 10000000) { /* len: 16 to 17 */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + one = (u32)(hgh / 100000000); /* (hgh / 100000000) */ + mid = (u32)(hgh - (u64)one * 100000000); /* (hgh % 100000000) */ + *buf = (u8)((u8)one + (u8)'0'); + buf += one > 0; + buf = write_u32_len_8(mid, buf); + buf = write_u32_len_8(low, buf); + return buf; + } else if (val >= (u64)100000000){ /* len: 9 to 15 */ + hgh = val / 100000000; /* (val / 100000000) */ + low = (u32)(val - hgh * 100000000); /* (val % 100000000) */ + buf = write_u32_len_1_to_8((u32)hgh, buf); + buf = write_u32_len_8(low, buf); + return buf; + } else { /* len: 1 to 8 */ + buf = write_u32_len_1_to_8((u32)val, buf); + return buf; + } +} + +/** + Write an unsigned integer with a length of 7 to 9 with trailing zero trimmed. + These digits are named as "abbccddee" here. + For example, input 123456000, output "123456". + */ +static_inline u8 *write_u32_len_7_to_9_trim(u32 val, u8 *buf) { + bool lz; + u32 tz, tz1, tz2; + + u32 abbcc = val / 10000; /* (abbccddee / 10000) */ + u32 ddee = val - abbcc * 10000; /* (abbccddee % 10000) */ + u32 abb = (u32)(((u64)abbcc * 167773) >> 24); /* (abbcc / 100) */ + u32 a = (abb * 41) >> 12; /* (abb / 100) */ + u32 bb = abb - a * 100; /* (abb % 100) */ + u32 cc = abbcc - abb * 100; /* (abbcc % 100) */ + + /* write abbcc */ + buf[0] = (u8)(a + '0'); + buf += a > 0; + lz = bb < 10 && a == 0; + byte_copy_2(buf + 0, digit_table + bb * 2 + lz); + buf -= lz; + byte_copy_2(buf + 2, digit_table + cc * 2); + + if (ddee) { + u32 dd = (ddee * 5243) >> 19; /* (ddee / 100) */ + u32 ee = ddee - dd * 100; /* (ddee % 100) */ + byte_copy_2(buf + 4, digit_table + dd * 2); + byte_copy_2(buf + 6, digit_table + ee * 2); + tz1 = dec_trailing_zero_table[dd]; + tz2 = dec_trailing_zero_table[ee]; + tz = ee ? tz2 : (tz1 + 2); + buf += 8 - tz; + return buf; + } else { + tz1 = dec_trailing_zero_table[bb]; + tz2 = dec_trailing_zero_table[cc]; + tz = cc ? tz2 : (tz1 + tz2); + buf += 4 - tz; + return buf; + } +} + +/** + Write an unsigned integer with a length of 16 or 17 with trailing zero trimmed. + These digits are named as "abbccddeeffgghhii" here. + For example, input 1234567890123000, output "1234567890123". + */ +static_inline u8 *write_u64_len_16_to_17_trim(u64 val, u8 *buf) { + u32 tz, tz1, tz2; + + u32 abbccddee = (u32)(val / 100000000); + u32 ffgghhii = (u32)(val - (u64)abbccddee * 100000000); + u32 abbcc = abbccddee / 10000; + u32 ddee = abbccddee - abbcc * 10000; + u32 abb = (u32)(((u64)abbcc * 167773) >> 24); /* (abbcc / 100) */ + u32 a = (abb * 41) >> 12; /* (abb / 100) */ + u32 bb = abb - a * 100; /* (abb % 100) */ + u32 cc = abbcc - abb * 100; /* (abbcc % 100) */ + buf[0] = (u8)(a + '0'); + buf += a > 0; + byte_copy_2(buf + 0, digit_table + bb * 2); + byte_copy_2(buf + 2, digit_table + cc * 2); + + if (ffgghhii) { + u32 dd = (ddee * 5243) >> 19; /* (ddee / 100) */ + u32 ee = ddee - dd * 100; /* (ddee % 100) */ + u32 ffgg = (u32)(((u64)ffgghhii * 109951163) >> 40); /* (val / 10000) */ + u32 hhii = ffgghhii - ffgg * 10000; /* (val % 10000) */ + u32 ff = (ffgg * 5243) >> 19; /* (aabb / 100) */ + u32 gg = ffgg - ff * 100; /* (aabb % 100) */ + byte_copy_2(buf + 4, digit_table + dd * 2); + byte_copy_2(buf + 6, digit_table + ee * 2); + byte_copy_2(buf + 8, digit_table + ff * 2); + byte_copy_2(buf + 10, digit_table + gg * 2); + if (hhii) { + u32 hh = (hhii * 5243) >> 19; /* (ccdd / 100) */ + u32 ii = hhii - hh * 100; /* (ccdd % 100) */ + byte_copy_2(buf + 12, digit_table + hh * 2); + byte_copy_2(buf + 14, digit_table + ii * 2); + tz1 = dec_trailing_zero_table[hh]; + tz2 = dec_trailing_zero_table[ii]; + tz = ii ? tz2 : (tz1 + 2); + return buf + 16 - tz; + } else { + tz1 = dec_trailing_zero_table[ff]; + tz2 = dec_trailing_zero_table[gg]; + tz = gg ? tz2 : (tz1 + 2); + return buf + 12 - tz; + } + } else { + if (ddee) { + u32 dd = (ddee * 5243) >> 19; /* (ddee / 100) */ + u32 ee = ddee - dd * 100; /* (ddee % 100) */ + byte_copy_2(buf + 4, digit_table + dd * 2); + byte_copy_2(buf + 6, digit_table + ee * 2); + tz1 = dec_trailing_zero_table[dd]; + tz2 = dec_trailing_zero_table[ee]; + tz = ee ? tz2 : (tz1 + 2); + return buf + 8 - tz; + } else { + tz1 = dec_trailing_zero_table[bb]; + tz2 = dec_trailing_zero_table[cc]; + tz = cc ? tz2 : (tz1 + tz2); + return buf + 4 - tz; + } + } +} + +/** Write exponent part in range `e-45` to `e38`. */ +static_inline u8 *write_f32_exp(i32 exp, u8 *buf) { + bool lz; + byte_copy_2(buf, "e-"); + buf += 2 - (exp >= 0); + exp = exp < 0 ? -exp : exp; + lz = exp < 10; + byte_copy_2(buf + 0, digit_table + (u32)exp * 2 + lz); + return buf + 2 - lz; +} + +/** Write exponent part in range `e-324` to `e308`. */ +static_inline u8 *write_f64_exp(i32 exp, u8 *buf) { + byte_copy_2(buf, "e-"); + buf += 2 - (exp >= 0); + exp = exp < 0 ? -exp : exp; + if (exp < 100) { + bool lz = exp < 10; + byte_copy_2(buf + 0, digit_table + (u32)exp * 2 + lz); + return buf + 2 - lz; + } else { + u32 hi = ((u32)exp * 656) >> 16; /* exp / 100 */ + u32 lo = (u32)exp - hi * 100; /* exp % 100 */ + buf[0] = (u8)((u8)hi + (u8)'0'); + byte_copy_2(buf + 1, digit_table + lo * 2); + return buf + 3; + } +} + +/** Magic number for fast `divide by power of 10`. */ +typedef struct { + u64 p10, mul; + u32 shr1, shr2; +} div_pow10_magic; + +/** Generated with llvm, see https://github.com/llvm/llvm-project/ + blob/main/llvm/lib/Support/DivisionByConstantInfo.cpp */ +static const div_pow10_magic div_pow10_table[] = { + { U64(0x00000000, 0x00000001), U64(0x00000000, 0x00000000), 0, 0 }, + { U64(0x00000000, 0x0000000A), U64(0xCCCCCCCC, 0xCCCCCCCD), 0, 3 }, + { U64(0x00000000, 0x00000064), U64(0x28F5C28F, 0x5C28F5C3), 2, 2 }, + { U64(0x00000000, 0x000003E8), U64(0x20C49BA5, 0xE353F7CF), 3, 4 }, + { U64(0x00000000, 0x00002710), U64(0x346DC5D6, 0x3886594B), 0, 11 }, + { U64(0x00000000, 0x000186A0), U64(0x0A7C5AC4, 0x71B47843), 5, 7 }, + { U64(0x00000000, 0x000F4240), U64(0x431BDE82, 0xD7B634DB), 0, 18 }, + { U64(0x00000000, 0x00989680), U64(0xD6BF94D5, 0xE57A42BD), 0, 23 }, + { U64(0x00000000, 0x05F5E100), U64(0xABCC7711, 0x8461CEFD), 0, 26 }, + { U64(0x00000000, 0x3B9ACA00), U64(0x0044B82F, 0xA09B5A53), 9, 11 }, + { U64(0x00000002, 0x540BE400), U64(0xDBE6FECE, 0xBDEDD5BF), 0, 33 }, + { U64(0x00000017, 0x4876E800), U64(0xAFEBFF0B, 0xCB24AAFF), 0, 36 }, + { U64(0x000000E8, 0xD4A51000), U64(0x232F3302, 0x5BD42233), 0, 37 }, + { U64(0x00000918, 0x4E72A000), U64(0x384B84D0, 0x92ED0385), 0, 41 }, + { U64(0x00005AF3, 0x107A4000), U64(0x0B424DC3, 0x5095CD81), 0, 42 }, + { U64(0x00038D7E, 0xA4C68000), U64(0x00024075, 0xF3DCEAC3), 15, 20 }, + { U64(0x002386F2, 0x6FC10000), U64(0x39A5652F, 0xB1137857), 0, 51 }, + { U64(0x01634578, 0x5D8A0000), U64(0x00005C3B, 0xD5191B53), 17, 22 }, + { U64(0x0DE0B6B3, 0xA7640000), U64(0x000049C9, 0x7747490F), 18, 24 }, + { U64(0x8AC72304, 0x89E80000), U64(0x760F253E, 0xDB4AB0d3), 0, 62 }, +}; + +/** Divide a number by power of 10. */ +static_inline void div_pow10(u64 num, u32 exp, u64 *div, u64 *mod, u64 *p10) { + u64 hi, lo; + div_pow10_magic m = div_pow10_table[exp]; + u128_mul(num >> m.shr1, m.mul, &hi, &lo); + *div = hi >> m.shr2; + *mod = num - (*div * m.p10); + *p10 = m.p10; +} + +/** Multiplies 64-bit integer and returns highest 64-bit rounded value. */ +static_inline u32 u64_round_to_odd(u64 u, u32 cp) { + u64 hi, lo; + u32 y_hi, y_lo; + u128_mul(cp, u, &hi, &lo); + y_hi = (u32)hi; + y_lo = (u32)(lo >> 32); + return y_hi | (y_lo > 1); +} + +/** Multiplies 128-bit integer and returns highest 64-bit rounded value. */ +static_inline u64 u128_round_to_odd(u64 hi, u64 lo, u64 cp) { + u64 x_hi, x_lo, y_hi, y_lo; + u128_mul(cp, lo, &x_hi, &x_lo); + u128_mul_add(cp, hi, x_hi, &y_hi, &y_lo); + return y_hi | (y_lo > 1); +} + +/** Convert f32 from binary to decimal (shortest but may have trailing zeros). + The input should not be 0, inf or nan. */ +static_inline void f32_bin_to_dec(u32 sig_raw, u32 exp_raw, + u32 sig_bin, i32 exp_bin, + u32 *sig_dec, i32 *exp_dec) { + + bool is_even, irregular, round_up, trim; + bool u0_inside, u1_inside, w0_inside, w1_inside; + u64 p10_hi, p10_lo, hi, lo; + u32 s, sp, cb, cbl, cbr, vb, vbl, vbr, upper, lower, mid; + i32 k, h; + + /* Fast path, see f64_bin_to_dec(). */ + while (likely(sig_raw)) { + u32 mod, dec, add_1, add_10, s_hi, s_lo; + u32 c, half_ulp, t0, t1; + + /* k = floor(exp_bin * log10(2)); */ + /* h = exp_bin + floor(log2(10) * -k); (h = 0/1/2/3) */ + k = (i32)(exp_bin * 315653) >> 20; + h = exp_bin + ((-k * 217707) >> 16); + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + + /* sig_bin << (1/2/3/4) */ + cb = sig_bin << (h + 1); + u128_mul(cb, p10_hi, &hi, &lo); + s_hi = (u32)(hi); + s_lo = (u32)(lo >> 32); + mod = s_hi % 10; + dec = s_hi - mod; + + /* right shift 4 to fit in u32 */ + c = (mod << (32 - 4)) | (s_lo >> 4); + half_ulp = (u32)(p10_hi >> (32 + 4 - h)); + + /* check w1, u0, w0 range */ + w1_inside = (s_lo >= ((u32)1 << 31)); + if (unlikely(s_lo == ((u32)1 << 31))) break; + u0_inside = (half_ulp >= c); + if (unlikely(half_ulp == c)) break; + t0 = (u32)10 << (32 - 4); + t1 = c + half_ulp; + w0_inside = (t1 >= t0); + if (unlikely(t0 - t1 <= (u32)1)) break; + + trim = (u0_inside | w0_inside); + add_10 = (w0_inside ? 10 : 0); + add_1 = mod + w1_inside; + *sig_dec = dec + (trim ? add_10 : add_1); + *exp_dec = k; + return; + } + + /* Schubfach algorithm, see f64_bin_to_dec(). */ + irregular = (sig_raw == 0 && exp_raw > 1); + is_even = !(sig_bin & 1); + cbl = 4 * sig_bin - 2 + irregular; + cb = 4 * sig_bin; + cbr = 4 * sig_bin + 2; + + /* k = floor(exp_bin * log10(2) + (irregular ? log10(3.0 / 4.0) : 0)); */ + /* h = exp_bin + floor(log2(10) * -k) + 1; (h = 1/2/3/4) */ + k = (i32)(exp_bin * 315653 - (irregular ? 131237 : 0)) >> 20; + h = exp_bin + ((-k * 217707) >> 16) + 1; + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + p10_hi += 1; + + vbl = u64_round_to_odd(p10_hi, cbl << h); + vb = u64_round_to_odd(p10_hi, cb << h); + vbr = u64_round_to_odd(p10_hi, cbr << h); + lower = vbl + !is_even; + upper = vbr - !is_even; + + s = vb / 4; + if (s >= 10) { + sp = s / 10; + u0_inside = (lower <= 40 * sp); + w0_inside = (upper >= 40 * sp + 40); + if (u0_inside != w0_inside) { + *sig_dec = sp * 10 + (w0_inside ? 10 : 0); + *exp_dec = k; + return; + } + } + u1_inside = (lower <= 4 * s); + w1_inside = (upper >= 4 * s + 4); + mid = 4 * s + 2; + round_up = (vb > mid) || (vb == mid && (s & 1) != 0); + *sig_dec = s + ((u1_inside != w1_inside) ? w1_inside : round_up); + *exp_dec = k; +} + +/** Convert f64 from binary to decimal (shortest but may have trailing zeros). + The input should not be 0, inf or nan. */ +static_inline void f64_bin_to_dec(u64 sig_raw, u32 exp_raw, + u64 sig_bin, i32 exp_bin, + u64 *sig_dec, i32 *exp_dec) { + + bool is_even, irregular, round_up, trim; + bool u0_inside, u1_inside, w0_inside, w1_inside; + u64 s, sp, cb, cbl, cbr, vb, vbl, vbr, p10_hi, p10_lo, upper, lower, mid; + i32 k, h; + + /* + Fast path: + For regular spacing significand 'c', there are 4 candidates: + + u0 u1 c w1 w0 + ----|----|----|----|----|-*--|----|----|----|----|----|----|----|---- + 9 0 1 2 3 4 5 6 7 8 9 0 1 + |___________________|___________________| + 1ulp + + The `1ulp` is in the range [1.0, 10.0). + If (c - 0.5ulp < u0), trim the last digit and round down. + If (c + 0.5ulp > w0), trim the last digit and round up. + If (c - 0.5ulp < u1), round down. + If (c + 0.5ulp > w1), round up. + */ + while (likely(sig_raw)) { + u64 mod, dec, add_1, add_10, s_hi, s_lo; + u64 c, half_ulp, t0, t1; + + /* k = floor(exp_bin * log10(2)); */ + /* h = exp_bin + floor(log2(10) * -k); (h = 0/1/2/3) */ + k = (i32)(exp_bin * 315653) >> 20; + h = exp_bin + ((-k * 217707) >> 16); + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + + /* sig_bin << (1/2/3/4) */ + cb = sig_bin << (h + 1); + u128_mul(cb, p10_lo, &s_hi, &s_lo); + u128_mul_add(cb, p10_hi, s_hi, &s_hi, &s_lo); + mod = s_hi % 10; + dec = s_hi - mod; + + /* right shift 4 to fit in u64 */ + c = (mod << (64 - 4)) | (s_lo >> 4); + half_ulp = p10_hi >> (4 - h); + + /* check w1, u0, w0 range */ + w1_inside = (s_lo >= ((u64)1 << 63)); + if (unlikely(s_lo == ((u64)1 << 63))) break; + u0_inside = (half_ulp >= c); + if (unlikely(half_ulp == c)) break; + t0 = ((u64)10 << (64 - 4)); + t1 = c + half_ulp; + w0_inside = (t1 >= t0); + if (unlikely(t0 - t1 <= (u64)1)) break; + + trim = (u0_inside | w0_inside); + add_10 = (w0_inside ? 10 : 0); + add_1 = mod + w1_inside; + *sig_dec = dec + (trim ? add_10 : add_1); + *exp_dec = k; + return; + } + + /* + Schubfach algorithm: + Raffaello Giulietti, The Schubfach way to render doubles, 2022. + https://drive.google.com/file/d/1gp5xv4CAa78SVgCeWfGqqI4FfYYYuNFb (Paper) + https://github.com/openjdk/jdk/pull/3402 (Java implementation) + https://github.com/abolz/Drachennest (C++ implementation) + */ + irregular = (sig_raw == 0 && exp_raw > 1); + is_even = !(sig_bin & 1); + cbl = 4 * sig_bin - 2 + irregular; + cb = 4 * sig_bin; + cbr = 4 * sig_bin + 2; + + /* k = floor(exp_bin * log10(2) + (irregular ? log10(3.0 / 4.0) : 0)); */ + /* h = exp_bin + floor(log2(10) * -k) + 1; (h = 1/2/3/4) */ + k = (i32)(exp_bin * 315653 - (irregular ? 131237 : 0)) >> 20; + h = exp_bin + ((-k * 217707) >> 16) + 1; + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + p10_lo += 1; + + vbl = u128_round_to_odd(p10_hi, p10_lo, cbl << h); + vb = u128_round_to_odd(p10_hi, p10_lo, cb << h); + vbr = u128_round_to_odd(p10_hi, p10_lo, cbr << h); + lower = vbl + !is_even; + upper = vbr - !is_even; + + s = vb / 4; + if (s >= 10) { + sp = s / 10; + u0_inside = (lower <= 40 * sp); + w0_inside = (upper >= 40 * sp + 40); + if (u0_inside != w0_inside) { + *sig_dec = sp * 10 + (w0_inside ? 10 : 0); + *exp_dec = k; + return; + } + } + u1_inside = (lower <= 4 * s); + w1_inside = (upper >= 4 * s + 4); + mid = 4 * s + 2; + round_up = (vb > mid) || (vb == mid && (s & 1) != 0); + *sig_dec = s + ((u1_inside != w1_inside) ? w1_inside : round_up); + *exp_dec = k; +} + +/** Convert f64 from binary to decimal (fast but not the shortest). + The input should not be 0, inf, nan. */ +static_inline void f64_bin_to_dec_fast(u64 sig_raw, u32 exp_raw, + u64 sig_bin, i32 exp_bin, + u64 *sig_dec, i32 *exp_dec, + bool *round_up) { + u64 cb, p10_hi, p10_lo, s_hi, s_lo; + i32 k, h; + bool irregular, u; + + irregular = (sig_raw == 0 && exp_raw > 1); + + /* k = floor(exp_bin * log10(2) + (irregular ? log10(3.0 / 4.0) : 0)); */ + /* h = exp_bin + floor(log2(10) * -k) + 1; (h = 1/2/3/4) */ + k = (i32)(exp_bin * 315653 - (irregular ? 131237 : 0)) >> 20; + h = exp_bin + ((-k * 217707) >> 16); + pow10_table_get_sig(-k, &p10_hi, &p10_lo); + + /* sig_bin << (1/2/3/4) */ + cb = sig_bin << (h + 1); + u128_mul(cb, p10_lo, &s_hi, &s_lo); + u128_mul_add(cb, p10_hi, s_hi, &s_hi, &s_lo); + + /* round up */ + u = s_lo >= (irregular ? U64(0x55555555, 0x55555555) : ((u64)1 << 63)); + + *sig_dec = s_hi + u; + *exp_dec = k; + *round_up = u; + return; +} + +/** Write inf/nan if allowed. */ +static_inline u8 *write_inf_or_nan(u8 *buf, yyjson_write_flag flg, + u64 sig_raw, bool sign) { + if (has_flg(INF_AND_NAN_AS_NULL)) { + byte_copy_4(buf, "null"); + return buf + 4; + } + if (has_allow(INF_AND_NAN)) { + if (sig_raw == 0) { + buf[0] = '-'; + buf += sign; + byte_copy_8(buf, "Infinity"); + return buf + 8; + } else { + byte_copy_4(buf, "NaN"); + return buf + 3; + } + } + return NULL; +} + +/** + Write a float number (requires 40 bytes buffer). + We follow the ECMAScript specification for printing floating-point numbers, + similar to `Number.prototype.toString()`, but with the following changes: + 1. Keep the negative sign of `-0.0` to preserve input information. + 2. Keep decimal point to indicate the number is floating point. + 3. Remove positive sign in the exponent part. + */ +static_noinline u8 *write_f32_raw(u8 *buf, u64 raw_f64, + yyjson_write_flag flg) { + u32 sig_bin, sig_dec, sig_raw; + i32 exp_bin, exp_dec, sig_len, dot_ofs; + u32 exp_raw, raw; + u8 *end; + bool sign; + + /* cast double to float */ + raw = f32_to_bits(f64_to_f32(f64_from_bits(raw_f64))); + + /* decode raw bytes from IEEE-754 double format. */ + sign = (bool)(raw >> (F32_BITS - 1)); + sig_raw = raw & F32_SIG_MASK; + exp_raw = (raw & F32_EXP_MASK) >> F32_SIG_BITS; + + /* return inf or nan */ + if (unlikely(exp_raw == ((u32)1 << F32_EXP_BITS) - 1)) { + return write_inf_or_nan(buf, flg, sig_raw, sign); + } + + /* add sign for all finite number */ + buf[0] = '-'; + buf += sign; + + /* return zero */ + if ((raw << 1) == 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + + if (likely(exp_raw != 0)) { + /* normal number */ + sig_bin = sig_raw | ((u32)1 << F32_SIG_BITS); + exp_bin = (i32)exp_raw - F32_EXP_BIAS - F32_SIG_BITS; + + /* fast path for small integer number without fraction */ + if ((-F32_SIG_BITS <= exp_bin && exp_bin <= 0) && + (u64_tz_bits(sig_bin) >= (u32)-exp_bin)) { + sig_dec = sig_bin >> -exp_bin; /* range: [1, 0xFFFFFF] */ + buf = write_u32_len_1_to_8(sig_dec, buf); + byte_copy_2(buf, ".0"); + return buf + 2; + } + + /* binary to decimal */ + f32_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* the sig length is 7 or 9 */ + sig_len = 7 + (sig_dec >= (u32)10000000) + (sig_dec >= (u32)100000000); + + /* the decimal point offset relative to the first digit */ + dot_ofs = sig_len + exp_dec; + + if (-6 < dot_ofs && dot_ofs <= 21) { + i32 num_sep_pos, dot_set_pos, pre_ofs; + u8 *num_hdr, *num_end, *num_sep, *dot_end; + bool no_pre_zero; + + /* fill zeros */ + memset(buf, '0', 32); + + /* not prefixed with zero, e.g. 1.234, 1234.0 */ + no_pre_zero = (dot_ofs > 0); + + /* write the number as digits */ + pre_ofs = no_pre_zero ? 0 : (2 - dot_ofs); + num_hdr = buf + pre_ofs; + num_end = write_u32_len_7_to_9_trim(sig_dec, num_hdr); + + /* seperate these digits to leave a space for dot */ + num_sep_pos = no_pre_zero ? dot_ofs : 0; + num_sep = num_hdr + num_sep_pos; + byte_move_8(num_sep + no_pre_zero, num_sep); + num_end += no_pre_zero; + + /* write the dot */ + dot_set_pos = yyjson_max(dot_ofs, 1); + buf[dot_set_pos] = '.'; + + /* return the ending */ + dot_end = buf + dot_ofs + 2; + return yyjson_max(dot_end, num_end); + + } else { + /* write with scientific notation, e.g. 1.234e56 */ + end = write_u32_len_7_to_9_trim(sig_dec, buf + 1); + end -= (end == buf + 2); /* remove '.0', e.g. 2.0e34 -> 2e34 */ + exp_dec += sig_len - 1; + buf[0] = buf[1]; + buf[1] = '.'; + return write_f32_exp(exp_dec, end); + } + + } else { + /* subnormal number */ + sig_bin = sig_raw; + exp_bin = 1 - F32_EXP_BIAS - F32_SIG_BITS; + + /* binary to decimal */ + f32_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* write significand part */ + end = write_u32_len_1_to_8(sig_dec, buf + 1); + buf[0] = buf[1]; + buf[1] = '.'; + exp_dec += (i32)(end - buf) - 2; + + /* trim trailing zeros */ + end -= *(end - 1) == '0'; /* branchless for last zero */ + end -= *(end - 1) == '0'; /* branchless for second last zero */ + while (*(end - 1) == '0') end--; /* for unlikely more zeros */ + end -= *(end - 1) == '.'; /* remove dot, e.g. 2.e-321 -> 2e-321 */ + + /* write exponent part */ + return write_f32_exp(exp_dec, end); + } +} + +/** + Write a double number (requires 40 bytes buffer). + We follow the ECMAScript specification for printing floating-point numbers, + similar to `Number.prototype.toString()`, but with the following changes: + 1. Keep the negative sign of `-0.0` to preserve input information. + 2. Keep decimal point to indicate the number is floating point. + 3. Remove positive sign in the exponent part. + */ +static_noinline u8 *write_f64_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { + u64 sig_bin, sig_dec, sig_raw; + i32 exp_bin, exp_dec, sig_len, dot_ofs; + u32 exp_raw; + u8 *end; + bool sign; + + /* decode raw bytes from IEEE-754 double format. */ + sign = (bool)(raw >> (F64_BITS - 1)); + sig_raw = raw & F64_SIG_MASK; + exp_raw = (u32)((raw & F64_EXP_MASK) >> F64_SIG_BITS); + + /* return inf or nan */ + if (unlikely(exp_raw == ((u32)1 << F64_EXP_BITS) - 1)) { + return write_inf_or_nan(buf, flg, sig_raw, sign); + } + + /* add sign for all finite number */ + buf[0] = '-'; + buf += sign; + + /* return zero */ + if ((raw << 1) == 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + + if (likely(exp_raw != 0)) { + /* normal number */ + sig_bin = sig_raw | ((u64)1 << F64_SIG_BITS); + exp_bin = (i32)exp_raw - F64_EXP_BIAS - F64_SIG_BITS; + + /* fast path for small integer number without fraction */ + if ((-F64_SIG_BITS <= exp_bin && exp_bin <= 0) && + (u64_tz_bits(sig_bin) >= (u32)-exp_bin)) { + sig_dec = sig_bin >> -exp_bin; /* range: [1, 0x1FFFFFFFFFFFFF] */ + buf = write_u64_len_1_to_16(sig_dec, buf); + byte_copy_2(buf, ".0"); + return buf + 2; + } + + /* binary to decimal */ + f64_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* the sig length is 16 or 17 */ + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* the decimal point offset relative to the first digit */ + dot_ofs = sig_len + exp_dec; + + if (-6 < dot_ofs && dot_ofs <= 21) { + i32 num_sep_pos, dot_set_pos, pre_ofs; + u8 *num_hdr, *num_end, *num_sep, *dot_end; + bool no_pre_zero; + + /* fill zeros */ + memset(buf, '0', 32); + + /* not prefixed with zero, e.g. 1.234, 1234.0 */ + no_pre_zero = (dot_ofs > 0); + + /* write the number as digits */ + pre_ofs = no_pre_zero ? 0 : (2 - dot_ofs); + num_hdr = buf + pre_ofs; + num_end = write_u64_len_16_to_17_trim(sig_dec, num_hdr); + + /* seperate these digits to leave a space for dot */ + num_sep_pos = no_pre_zero ? dot_ofs : 0; + num_sep = num_hdr + num_sep_pos; + byte_move_16(num_sep + no_pre_zero, num_sep); + num_end += no_pre_zero; + + /* write the dot */ + dot_set_pos = yyjson_max(dot_ofs, 1); + buf[dot_set_pos] = '.'; + + /* return the ending */ + dot_end = buf + dot_ofs + 2; + return yyjson_max(dot_end, num_end); + + } else { + /* write with scientific notation, e.g. 1.234e56 */ + end = write_u64_len_16_to_17_trim(sig_dec, buf + 1); + end -= (end == buf + 2); /* remove '.0', e.g. 2.0e34 -> 2e34 */ + exp_dec += sig_len - 1; + buf[0] = buf[1]; + buf[1] = '.'; + return write_f64_exp(exp_dec, end); + } + + } else { + /* subnormal number */ + sig_bin = sig_raw; + exp_bin = 1 - F64_EXP_BIAS - F64_SIG_BITS; + + /* binary to decimal */ + f64_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, &sig_dec, &exp_dec); + + /* write significand part */ + end = write_u64_len_1_to_17(sig_dec, buf + 1); + buf[0] = buf[1]; + buf[1] = '.'; + exp_dec += (i32)(end - buf) - 2; + + /* trim trailing zeros */ + end -= *(end - 1) == '0'; /* branchless for last zero */ + end -= *(end - 1) == '0'; /* branchless for second last zero */ + while (*(end - 1) == '0') end--; /* for unlikely more zeros */ + end -= *(end - 1) == '.'; /* remove dot, e.g. 2.e-321 -> 2e-321 */ + + /* write exponent part */ + return write_f64_exp(exp_dec, end); + } +} + +/** + Write a double number using fixed-point notation (requires 40 bytes buffer). + + We follow the ECMAScript specification for printing floating-point numbers, + similar to `Number.prototype.toFixed(prec)`, but with the following changes: + 1. Keep the negative sign of `-0.0` to preserve input information. + 2. Keep decimal point to indicate the number is floating point. + 3. Remove positive sign in the exponent part. + 4. Remove trailing zeros and reduce unnecessary precision. + */ +static_noinline u8 *write_f64_raw_fixed(u8 *buf, u64 raw, yyjson_write_flag flg, + u32 prec) { + u64 sig_bin, sig_dec, sig_raw; + i32 exp_bin, exp_dec, sig_len, dot_ofs; + u32 exp_raw; + u8 *end; + bool sign; + + /* decode raw bytes from IEEE-754 double format. */ + sign = (bool)(raw >> (F64_BITS - 1)); + sig_raw = raw & F64_SIG_MASK; + exp_raw = (u32)((raw & F64_EXP_MASK) >> F64_SIG_BITS); + + /* return inf or nan */ + if (unlikely(exp_raw == ((u32)1 << F64_EXP_BITS) - 1)) { + return write_inf_or_nan(buf, flg, sig_raw, sign); + } + + /* add sign for all finite number */ + buf[0] = '-'; + buf += sign; + + /* return zero */ + if ((raw << 1) == 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + + if (likely(exp_raw != 0)) { + /* normal number */ + sig_bin = sig_raw | ((u64)1 << F64_SIG_BITS); + exp_bin = (i32)exp_raw - F64_EXP_BIAS - F64_SIG_BITS; + + /* fast path for small integer number without fraction */ + if ((-F64_SIG_BITS <= exp_bin && exp_bin <= 0) && + (u64_tz_bits(sig_bin) >= (u32)-exp_bin)) { + sig_dec = sig_bin >> -exp_bin; /* range: [1, 0x1FFFFFFFFFFFFF] */ + buf = write_u64_len_1_to_16(sig_dec, buf); + byte_copy_2(buf, ".0"); + return buf + 2; + } + + /* only `fabs(num) < 1e21` are processed here. */ + if ((raw << 1) < (U64(0x444B1AE4, 0xD6E2EF50) << 1)) { + i32 num_sep_pos, dot_set_pos, pre_ofs; + u8 *num_hdr, *num_end, *num_sep; + bool round_up, no_pre_zero; + + /* binary to decimal */ + f64_bin_to_dec_fast(sig_raw, exp_raw, sig_bin, exp_bin, + &sig_dec, &exp_dec, &round_up); + + /* the sig length is 16 or 17 */ + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* limit the length of digits after the decimal point */ + if (exp_dec < -1) { + i32 sig_len_cut = -exp_dec - (i32)prec; + if (sig_len_cut > sig_len) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + if (sig_len_cut > 0) { + u64 div, mod, p10; + + /* remove round up */ + sig_dec -= round_up; + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* cut off some digits */ + div_pow10(sig_dec, (u32)sig_len_cut, &div, &mod, &p10); + + /* add round up */ + sig_dec = div + (mod >= p10 / 2); + + /* update exp and sig length */ + exp_dec += sig_len_cut; + sig_len -= sig_len_cut; + sig_len += (sig_len >= 0) && + (sig_dec >= div_pow10_table[sig_len].p10); + } + if (sig_len <= 0) { + byte_copy_4(buf, "0.0"); + return buf + 3; + } + } + + /* fill zeros */ + memset(buf, '0', 32); + + /* the decimal point offset relative to the first digit */ + dot_ofs = sig_len + exp_dec; + + /* not prefixed with zero, e.g. 1.234, 1234.0 */ + no_pre_zero = (dot_ofs > 0); + + /* write the number as digits */ + pre_ofs = no_pre_zero ? 0 : (1 - dot_ofs); + num_hdr = buf + pre_ofs; + num_end = write_u64_len_1_to_17(sig_dec, num_hdr); + + /* seperate these digits to leave a space for dot */ + num_sep_pos = no_pre_zero ? dot_ofs : -dot_ofs; + num_sep = buf + num_sep_pos; + byte_move_16(num_sep + 1, num_sep); + num_end += (exp_dec < 0); + + /* write the dot */ + dot_set_pos = yyjson_max(dot_ofs, 1); + buf[dot_set_pos] = '.'; + + /* remove trailing zeros */ + buf += dot_set_pos + 2; + buf = yyjson_max(buf, num_end); + buf -= *(buf - 1) == '0'; /* branchless for last zero */ + buf -= *(buf - 1) == '0'; /* branchless for second last zero */ + while (*(buf - 1) == '0') buf--; /* for unlikely more zeros */ + buf += *(buf - 1) == '.'; /* keep a zero after dot */ + return buf; + + } else { + /* binary to decimal */ + f64_bin_to_dec(sig_raw, exp_raw, sig_bin, exp_bin, + &sig_dec, &exp_dec); + + /* the sig length is 16 or 17 */ + sig_len = 16 + (sig_dec >= (u64)100000000 * 100000000); + + /* write with scientific notation, e.g. 1.234e56 */ + end = write_u64_len_16_to_17_trim(sig_dec, buf + 1); + end -= (end == buf + 2); /* remove '.0', e.g. 2.0e34 -> 2e34 */ + exp_dec += sig_len - 1; + buf[0] = buf[1]; + buf[1] = '.'; + return write_f64_exp(exp_dec, end); + } + } else { + /* subnormal number */ + byte_copy_4(buf, "0.0"); + return buf + 3; + } +} + +#else /* FP_WRITER */ + +#if YYJSON_MSC_VER >= 1400 +#define snprintf_num(buf, len, fmt, dig, val) \ + sprintf_s((char *)buf, len, fmt, dig, val) +#elif defined(snprintf) || (YYJSON_STDC_VER >= 199901L) +#define snprintf_num(buf, len, fmt, dig, val) \ + snprintf((char *)buf, len, fmt, dig, val) +#else +#define snprintf_num(buf, len, fmt, dig, val) \ + sprintf((char *)buf, fmt, dig, val) +#endif + +static_noinline u8 *write_fp_reformat(u8 *buf, int len, + yyjson_write_flag flg, bool fixed) { + u8 *cur = buf; + if (unlikely(len < 1)) return NULL; + cur += (*cur == '-'); + if (unlikely(!char_is_digit(*cur))) { + /* nan, inf, or bad output */ + if (has_flg(INF_AND_NAN_AS_NULL)) { + byte_copy_4(buf, "null"); + return buf + 4; + } else if (has_allow(INF_AND_NAN)) { + if (*cur == 'i') { + byte_copy_8(cur, "Infinity"); + return cur + 8; + } else if (*cur == 'n') { + byte_copy_4(buf, "NaN"); + return buf + 3; + } + } + return NULL; + } else { + /* finite number */ + u8 *end = buf + len, *dot = NULL, *exp = NULL; + + /* + The snprintf() function is locale-dependent. For currently known + locales, (en, zh, ja, ko, am, he, hi) use '.' as the decimal point, + while other locales use ',' as the decimal point. we need to replace + ',' with '.' to avoid the locale setting. + */ + for (; cur < end; cur++) { + switch (*cur) { + case ',': *cur = '.'; /* fallthrough */ + case '.': dot = cur; break; + case 'e': exp = cur; break; + default: break; + } + } + if (fixed) { + /* remove trailing zeros */ + while (*(end - 1) == '0') end--; + end += *(end - 1) == '.'; + } else { + if (!dot && !exp) { + /* add decimal point, e.g. 123 -> 123.0 */ + byte_copy_2(end, ".0"); + end += 2; + } else if (exp) { + cur = exp + 1; + /* remove positive sign in the exponent part */ + if (*cur == '+') { + memmove(cur, cur + 1, (usize)(end - cur - 1)); + end--; + } + cur += (*cur == '-'); + /* remove leading zeros in the exponent part */ + if (*cur == '0') { + u8 *hdr = cur++; + while (*cur == '0') cur++; + memmove(hdr, cur, (usize)(end - cur)); + end -= (usize)(cur - hdr); + } + } + } + return end; + } +} + +/** Write a double number (requires 40 bytes buffer). */ +static_noinline u8 *write_f64_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { +#if defined(DBL_DECIMAL_DIG) && DBL_DECIMAL_DIG < F64_DEC_DIG + int dig = DBL_DECIMAL_DIG; +#else + int dig = F64_DEC_DIG; +#endif + f64 val = f64_from_bits(raw); + int len = snprintf_num(buf, FP_BUF_LEN, "%.*g", dig, val); + return write_fp_reformat(buf, len, flg, false); +} + +/** Write a double number (requires 40 bytes buffer). */ +static_noinline u8 *write_f32_raw(u8 *buf, u64 raw, yyjson_write_flag flg) { +#if defined(FLT_DECIMAL_DIG) && FLT_DECIMAL_DIG < F32_DEC_DIG + int dig = FLT_DECIMAL_DIG; +#else + int dig = F32_DEC_DIG; +#endif + f64 val = (f64)f64_to_f32(f64_from_bits(raw)); + int len = snprintf_num(buf, FP_BUF_LEN, "%.*g", dig, val); + return write_fp_reformat(buf, len, flg, false); +} + +/** Write a double number (requires 40 bytes buffer). */ +static_noinline u8 *write_f64_raw_fixed(u8 *buf, u64 raw, + yyjson_write_flag flg, u32 prec) { + f64 val = (f64)f64_from_bits(raw); + if (-1e21 < val && val < 1e21) { + int len = snprintf_num(buf, FP_BUF_LEN, "%.*f", (int)prec, val); + return write_fp_reformat(buf, len, flg, true); + } else { + return write_f64_raw(buf, raw, flg); + } +} + +#endif /* FP_WRITER */ + +/** Write a JSON number (requires 40 bytes buffer). */ +static_inline u8 *write_num(u8 *cur, yyjson_val *val, yyjson_write_flag flg) { + if (!(val->tag & YYJSON_SUBTYPE_REAL)) { + u64 pos = val->uni.u64; + u64 neg = ~pos + 1; + usize sign = ((val->tag & YYJSON_SUBTYPE_SINT) > 0) & ((i64)pos < 0); + *cur = '-'; + return write_u64(sign ? neg : pos, cur + sign); + } else { + u64 raw = val->uni.u64; + u32 val_fmt = (u32)(val->tag >> 32); + u32 all_fmt = flg; + u32 fmt = val_fmt | all_fmt; + if (likely(!(fmt >> (32 - YYJSON_WRITE_FP_FLAG_BITS)))) { + /* double to shortest */ + return write_f64_raw(cur, raw, flg); + } else if (fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS)) { + /* double to fixed */ + u32 val_prec = val_fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS); + u32 all_prec = all_fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS); + u32 prec = val_prec ? val_prec : all_prec; + return write_f64_raw_fixed(cur, raw, flg, prec); + } else { + if (fmt & YYJSON_WRITE_FP_TO_FLOAT) { + /* float to shortest */ + return write_f32_raw(cur, raw, flg); + } else { + /* double to shortest */ + return write_f64_raw(cur, raw, flg); + } + } + } +} + +char *yyjson_write_number(const yyjson_val *val, char *buf) { + if (unlikely(!val || !buf)) return NULL; + switch (val->tag & YYJSON_TAG_MASK) { + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT: { + buf = (char *)write_u64(val->uni.u64, (u8 *)buf); + *buf = '\0'; + return buf; + } + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT: { + u64 pos = val->uni.u64; + u64 neg = ~pos + 1; + usize sign = ((i64)pos < 0); + *buf = '-'; + buf = (char *)write_u64(sign ? neg : pos, (u8 *)buf + sign); + *buf = '\0'; + return buf; + } + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL: { + u64 raw = val->uni.u64; + u32 fmt = (u32)(val->tag >> 32); + u32 flg = YYJSON_WRITE_ALLOW_INF_AND_NAN; + if (likely(!(fmt >> (32 - YYJSON_WRITE_FP_FLAG_BITS)))) { + buf = (char *)write_f64_raw((u8 *)buf, raw, flg); + } else if (fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS)) { + u32 prec = fmt >> (32 - YYJSON_WRITE_FP_PREC_BITS); + buf = (char *)write_f64_raw_fixed((u8 *)buf, raw, flg, prec); + } else { + if (fmt & YYJSON_WRITE_FP_TO_FLOAT) { + buf = (char *)write_f32_raw((u8 *)buf, raw, flg); + } else { + buf = (char *)write_f64_raw((u8 *)buf, raw, flg); + } + } + if (buf) *buf = '\0'; + return buf; + } + default: return NULL; + } +} + + + +/*============================================================================== + * MARK: - String Writer (Private) + *============================================================================*/ + +/** Character encode type, if (type > CHAR_ENC_ERR_1) bytes = type / 2; */ +typedef u8 char_enc_type; +#define CHAR_ENC_CPY_1 0 /* 1-byte UTF-8, copy. */ +#define CHAR_ENC_ERR_1 1 /* 1-byte UTF-8, error. */ +#define CHAR_ENC_ESC_A 2 /* 1-byte ASCII, escaped as '\x'. */ +#define CHAR_ENC_ESC_1 3 /* 1-byte UTF-8, escaped as '\uXXXX'. */ +#define CHAR_ENC_CPY_2 4 /* 2-byte UTF-8, copy. */ +#define CHAR_ENC_ESC_2 5 /* 2-byte UTF-8, escaped as '\uXXXX'. */ +#define CHAR_ENC_CPY_3 6 /* 3-byte UTF-8, copy. */ +#define CHAR_ENC_ESC_3 7 /* 3-byte UTF-8, escaped as '\uXXXX'. */ +#define CHAR_ENC_CPY_4 8 /* 4-byte UTF-8, copy. */ +#define CHAR_ENC_ESC_4 9 /* 4-byte UTF-8, escaped as '\uXXXX\uXXXX'. */ + +/** Character encode type table: don't escape unicode, don't escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_cpy[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Character encode type table: don't escape unicode, escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_cpy_slash[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Character encode type table: escape unicode, don't escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_esc[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 9, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Character encode type table: escape unicode, escape '/'. + (generate with misc/make_tables.c) */ +static const char_enc_type enc_table_esc_slash[256] = { + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 9, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/** Escaped hex character table: ["00" "01" "02" ... "FD" "FE" "FF"]. + (generate with misc/make_tables.c) */ +yyjson_align(2) +static const u8 esc_hex_char_table[512] = { + '0', '0', '0', '1', '0', '2', '0', '3', + '0', '4', '0', '5', '0', '6', '0', '7', + '0', '8', '0', '9', '0', 'A', '0', 'B', + '0', 'C', '0', 'D', '0', 'E', '0', 'F', + '1', '0', '1', '1', '1', '2', '1', '3', + '1', '4', '1', '5', '1', '6', '1', '7', + '1', '8', '1', '9', '1', 'A', '1', 'B', + '1', 'C', '1', 'D', '1', 'E', '1', 'F', + '2', '0', '2', '1', '2', '2', '2', '3', + '2', '4', '2', '5', '2', '6', '2', '7', + '2', '8', '2', '9', '2', 'A', '2', 'B', + '2', 'C', '2', 'D', '2', 'E', '2', 'F', + '3', '0', '3', '1', '3', '2', '3', '3', + '3', '4', '3', '5', '3', '6', '3', '7', + '3', '8', '3', '9', '3', 'A', '3', 'B', + '3', 'C', '3', 'D', '3', 'E', '3', 'F', + '4', '0', '4', '1', '4', '2', '4', '3', + '4', '4', '4', '5', '4', '6', '4', '7', + '4', '8', '4', '9', '4', 'A', '4', 'B', + '4', 'C', '4', 'D', '4', 'E', '4', 'F', + '5', '0', '5', '1', '5', '2', '5', '3', + '5', '4', '5', '5', '5', '6', '5', '7', + '5', '8', '5', '9', '5', 'A', '5', 'B', + '5', 'C', '5', 'D', '5', 'E', '5', 'F', + '6', '0', '6', '1', '6', '2', '6', '3', + '6', '4', '6', '5', '6', '6', '6', '7', + '6', '8', '6', '9', '6', 'A', '6', 'B', + '6', 'C', '6', 'D', '6', 'E', '6', 'F', + '7', '0', '7', '1', '7', '2', '7', '3', + '7', '4', '7', '5', '7', '6', '7', '7', + '7', '8', '7', '9', '7', 'A', '7', 'B', + '7', 'C', '7', 'D', '7', 'E', '7', 'F', + '8', '0', '8', '1', '8', '2', '8', '3', + '8', '4', '8', '5', '8', '6', '8', '7', + '8', '8', '8', '9', '8', 'A', '8', 'B', + '8', 'C', '8', 'D', '8', 'E', '8', 'F', + '9', '0', '9', '1', '9', '2', '9', '3', + '9', '4', '9', '5', '9', '6', '9', '7', + '9', '8', '9', '9', '9', 'A', '9', 'B', + '9', 'C', '9', 'D', '9', 'E', '9', 'F', + 'A', '0', 'A', '1', 'A', '2', 'A', '3', + 'A', '4', 'A', '5', 'A', '6', 'A', '7', + 'A', '8', 'A', '9', 'A', 'A', 'A', 'B', + 'A', 'C', 'A', 'D', 'A', 'E', 'A', 'F', + 'B', '0', 'B', '1', 'B', '2', 'B', '3', + 'B', '4', 'B', '5', 'B', '6', 'B', '7', + 'B', '8', 'B', '9', 'B', 'A', 'B', 'B', + 'B', 'C', 'B', 'D', 'B', 'E', 'B', 'F', + 'C', '0', 'C', '1', 'C', '2', 'C', '3', + 'C', '4', 'C', '5', 'C', '6', 'C', '7', + 'C', '8', 'C', '9', 'C', 'A', 'C', 'B', + 'C', 'C', 'C', 'D', 'C', 'E', 'C', 'F', + 'D', '0', 'D', '1', 'D', '2', 'D', '3', + 'D', '4', 'D', '5', 'D', '6', 'D', '7', + 'D', '8', 'D', '9', 'D', 'A', 'D', 'B', + 'D', 'C', 'D', 'D', 'D', 'E', 'D', 'F', + 'E', '0', 'E', '1', 'E', '2', 'E', '3', + 'E', '4', 'E', '5', 'E', '6', 'E', '7', + 'E', '8', 'E', '9', 'E', 'A', 'E', 'B', + 'E', 'C', 'E', 'D', 'E', 'E', 'E', 'F', + 'F', '0', 'F', '1', 'F', '2', 'F', '3', + 'F', '4', 'F', '5', 'F', '6', 'F', '7', + 'F', '8', 'F', '9', 'F', 'A', 'F', 'B', + 'F', 'C', 'F', 'D', 'F', 'E', 'F', 'F' +}; + +/** Escaped single character table. (generate with misc/make_tables.c) */ +yyjson_align(2) +static const u8 esc_single_char_table[512] = { + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + '\\', 'b', '\\', 't', '\\', 'n', ' ', ' ', + '\\', 'f', '\\', 'r', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', '\\', '"', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', '\\', '/', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + '\\', '\\', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' +}; + +/** Returns the encode table with options. */ +static_inline const char_enc_type *get_enc_table_with_flag( + yyjson_write_flag flg) { + if (has_flg(ESCAPE_UNICODE)) { + if (has_flg(ESCAPE_SLASHES)) { + return enc_table_esc_slash; + } else { + return enc_table_esc; + } + } else { + if (has_flg(ESCAPE_SLASHES)) { + return enc_table_cpy_slash; + } else { + return enc_table_cpy; + } + } +} + +/** Write raw string. */ +static_inline u8 *write_raw(u8 *cur, const u8 *raw, usize raw_len) { + memcpy(cur, raw, raw_len); + return cur + raw_len; +} + +/** + Write string no-escape. + @param cur Buffer cursor. + @param str A UTF-8 string, null-terminator is not required. + @param str_len Length of string in bytes. + @return The buffer cursor after string. + */ +static_inline u8 *write_str_noesc(u8 *cur, const u8 *str, usize str_len) { + *cur++ = '"'; + while (str_len >= 16) { + byte_copy_16(cur, str); + cur += 16; + str += 16; + str_len -= 16; + } + while (str_len >= 4) { + byte_copy_4(cur, str); + cur += 4; + str += 4; + str_len -= 4; + } + while (str_len) { + *cur++ = *str++; + str_len -= 1; + } + *cur++ = '"'; + return cur; +} + +/** + Write UTF-8 string (requires len * 6 + 2 bytes buffer). + @param cur Buffer cursor. + @param esc Escape unicode. + @param inv Allow invalid unicode. + @param str A UTF-8 string, null-terminator is not required. + @param str_len Length of string in bytes. + @param enc_table Encode type table for character. + @return The buffer cursor after string, or NULL on invalid unicode. + */ +static_inline u8 *write_str(u8 *cur, bool esc, bool inv, + const u8 *str, usize str_len, + const char_enc_type *enc_table) { + /* The replacement character U+FFFD, used to indicate invalid character. */ + const v32 rep = {{ 'F', 'F', 'F', 'D' }}; + const v32 pre = {{ '\\', 'u', '0', '0' }}; + + const u8 *src = str; + const u8 *end = str + str_len; + *cur++ = '"'; + +copy_ascii: + /* + Copy continuous ASCII, loop unrolling, same as the following code: + + while (end > src) ( + if (unlikely(enc_table[*src])) break; + *cur++ = *src++; + ); + */ +#define expr_jump(i) \ + if (unlikely(enc_table[src[i]])) goto stop_char_##i; + +#define expr_stop(i) \ + stop_char_##i: \ + memcpy(cur, src, i); \ + cur += i; src += i; goto copy_utf8; + + while (end - src >= 16) { + repeat16_incr(expr_jump) + byte_copy_16(cur, src); + cur += 16; src += 16; + } + + while (end - src >= 4) { + repeat4_incr(expr_jump) + byte_copy_4(cur, src); + cur += 4; src += 4; + } + + while (end > src) { + expr_jump(0) + *cur++ = *src++; + } + + *cur++ = '"'; + return cur; + + repeat16_incr(expr_stop) + +#undef expr_jump +#undef expr_stop + +copy_utf8: + if (unlikely(src + 4 > end)) { + if (end == src) goto copy_end; + if (end - src < enc_table[*src] / 2) goto err_one; + } + switch (enc_table[*src]) { + case CHAR_ENC_CPY_1: { + *cur++ = *src++; + goto copy_ascii; + } + case CHAR_ENC_CPY_2: { +#if YYJSON_DISABLE_UTF8_VALIDATION + byte_copy_2(cur, src); +#else + u32 uni = 0; + byte_copy_2(&uni, src); + if (unlikely(!is_utf8_seq2(uni))) goto err_cpy; + byte_copy_2(cur, &uni); +#endif + cur += 2; + src += 2; + goto copy_utf8; + } + case CHAR_ENC_CPY_3: { +#if YYJSON_DISABLE_UTF8_VALIDATION + if (likely(src + 4 <= end)) { + byte_copy_4(cur, src); + } else { + byte_copy_2(cur, src); + cur[2] = src[2]; + } +#else + u32 uni, tmp; + if (likely(src + 4 <= end)) { + uni = byte_load_4(src); + if (unlikely(!is_utf8_seq3(uni))) goto err_cpy; + byte_copy_4(cur, src); + } else { + uni = byte_load_3(src); + if (unlikely(!is_utf8_seq3(uni))) goto err_cpy; + byte_copy_4(cur, &uni); + } +#endif + cur += 3; + src += 3; + goto copy_utf8; + } + case CHAR_ENC_CPY_4: { +#if YYJSON_DISABLE_UTF8_VALIDATION + byte_copy_4(cur, src); +#else + u32 uni, tmp; + uni = byte_load_4(src); + if (unlikely(!is_utf8_seq4(uni))) goto err_cpy; + byte_copy_4(cur, src); +#endif + cur += 4; + src += 4; + goto copy_utf8; + } + case CHAR_ENC_ESC_A: { + byte_copy_2(cur, &esc_single_char_table[*src * 2]); + cur += 2; + src += 1; + goto copy_utf8; + } + case CHAR_ENC_ESC_1: { + byte_copy_4(cur + 0, &pre); + byte_copy_2(cur + 4, &esc_hex_char_table[*src * 2]); + cur += 6; + src += 1; + goto copy_utf8; + } + case CHAR_ENC_ESC_2: { + u16 u; +#if !YYJSON_DISABLE_UTF8_VALIDATION + u32 v4 = 0; + u16 v2 = byte_load_2(src); + byte_copy_2(&v4, &v2); + if (unlikely(!is_utf8_seq2(v4))) goto err_esc; +#endif + u = (u16)(((u16)(src[0] & 0x1F) << 6) | + ((u16)(src[1] & 0x3F) << 0)); + byte_copy_2(cur + 0, &pre); + byte_copy_2(cur + 2, &esc_hex_char_table[(u >> 8) * 2]); + byte_copy_2(cur + 4, &esc_hex_char_table[(u & 0xFF) * 2]); + cur += 6; + src += 2; + goto copy_utf8; + } + case CHAR_ENC_ESC_3: { + u16 u; + u32 v, tmp; +#if !YYJSON_DISABLE_UTF8_VALIDATION + v = byte_load_3(src); + if (unlikely(!is_utf8_seq3(v))) goto err_esc; +#endif + u = (u16)(((u16)(src[0] & 0x0F) << 12) | + ((u16)(src[1] & 0x3F) << 6) | + ((u16)(src[2] & 0x3F) << 0)); + byte_copy_2(cur + 0, &pre); + byte_copy_2(cur + 2, &esc_hex_char_table[(u >> 8) * 2]); + byte_copy_2(cur + 4, &esc_hex_char_table[(u & 0xFF) * 2]); + cur += 6; + src += 3; + goto copy_utf8; + } + case CHAR_ENC_ESC_4: { + u32 hi, lo, u, v, tmp; +#if !YYJSON_DISABLE_UTF8_VALIDATION + v = byte_load_4(src); + if (unlikely(!is_utf8_seq4(v))) goto err_esc; +#endif + u = ((u32)(src[0] & 0x07) << 18) | + ((u32)(src[1] & 0x3F) << 12) | + ((u32)(src[2] & 0x3F) << 6) | + ((u32)(src[3] & 0x3F) << 0); + u -= 0x10000; + hi = (u >> 10) + 0xD800; + lo = (u & 0x3FF) + 0xDC00; + byte_copy_2(cur + 0, &pre); + byte_copy_2(cur + 2, &esc_hex_char_table[(hi >> 8) * 2]); + byte_copy_2(cur + 4, &esc_hex_char_table[(hi & 0xFF) * 2]); + byte_copy_2(cur + 6, &pre); + byte_copy_2(cur + 8, &esc_hex_char_table[(lo >> 8) * 2]); + byte_copy_2(cur + 10, &esc_hex_char_table[(lo & 0xFF) * 2]); + cur += 12; + src += 4; + goto copy_utf8; + } + case CHAR_ENC_ERR_1: { + goto err_one; + } + default: break; /* unreachable */ + } + +copy_end: + *cur++ = '"'; + return cur; + +err_one: + if (esc) goto err_esc; + else goto err_cpy; + +err_cpy: + if (!inv) return NULL; + *cur++ = *src++; + goto copy_utf8; + +err_esc: + if (!inv) return NULL; + byte_copy_2(cur + 0, &pre); + byte_copy_4(cur + 2, &rep); + cur += 6; + src += 1; + goto copy_utf8; +} + + + +/*============================================================================== + * MARK: - JSON Writer Utilities (Private) + *============================================================================*/ + +/** Write null (requires 8 bytes buffer). */ +static_inline u8 *write_null(u8 *cur) { + v64 v = {{ 'n', 'u', 'l', 'l', ',', '\n', 0, 0 }}; + byte_copy_8(cur, &v); + return cur + 4; +} + +/** Write bool (requires 8 bytes buffer). */ +static_inline u8 *write_bool(u8 *cur, bool val) { + v64 v0 = {{ 'f', 'a', 'l', 's', 'e', ',', '\n', 0 }}; + v64 v1 = {{ 't', 'r', 'u', 'e', ',', '\n', 0, 0 }}; + if (val) { + byte_copy_8(cur, &v1); + } else { + byte_copy_8(cur, &v0); + } + return cur + 5 - val; +} + +/** Write indent (requires level x 4 bytes buffer). + Param spaces should not larger than 4. */ +static_inline u8 *write_indent(u8 *cur, usize level, usize spaces) { + while (level-- > 0) { + byte_copy_4(cur, " "); + cur += spaces; + } + return cur; +} + +/** Write data to file pointer. */ +static bool write_dat_to_fp(FILE *fp, u8 *dat, usize len, + yyjson_write_err *err) { + if (fwrite(dat, len, 1, fp) != 1) { + err->msg = "file writing failed"; + err->code = YYJSON_WRITE_ERROR_FILE_WRITE; + return false; + } + return true; +} + +/** Write data to file. */ +static bool write_dat_to_file(const char *path, u8 *dat, usize len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + err->msg = _msg; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + if (file) fclose(file); \ + return false; \ +} while (false) + + FILE *file = fopen_writeonly(path); + if (file == NULL) { + return_err(FILE_OPEN, MSG_FOPEN); + } + if (fwrite(dat, len, 1, file) != 1) { + return_err(FILE_WRITE, MSG_FWRITE); + } + if (fclose(file) != 0) { + file = NULL; + return_err(FILE_WRITE, MSG_FCLOSE); + } + return true; + +#undef return_err +} + + + +/*============================================================================== + * MARK: - JSON Writer Implementation (Private) + *============================================================================*/ + +typedef struct yyjson_write_ctx { + usize tag; +} yyjson_write_ctx; + +static_inline void yyjson_write_ctx_set(yyjson_write_ctx *ctx, + usize size, bool is_obj) { + ctx->tag = (size << 1) | (usize)is_obj; +} + +static_inline void yyjson_write_ctx_get(yyjson_write_ctx *ctx, + usize *size, bool *is_obj) { + usize tag = ctx->tag; + *size = tag >> 1; + *is_obj = (bool)(tag & 1); +} + +/** Write single JSON value. */ +static_inline u8 *yyjson_write_single(yyjson_val *val, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + if (hdr) alc.free(alc.ctx, (void *)hdr); \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + hdr = (u8 *)alc.malloc(alc.ctx, _len); \ + if (!hdr) goto fail_alloc; \ + cur = hdr; \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + u8 *hdr = NULL, *cur; + usize str_len; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + bool newline = has_flg(NEWLINE_AT_END) != 0; + const usize end_len = 2; /* '\n' and '\0' */ + + switch (unsafe_yyjson_get_type(val)) { + case YYJSON_TYPE_RAW: + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + end_len); + cur = write_raw(cur, str_ptr, str_len); + break; + + case YYJSON_TYPE_STR: + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 2 + end_len); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + break; + + case YYJSON_TYPE_NUM: + incr_len(FP_BUF_LEN + end_len); + cur = write_num(cur, val, flg); + if (unlikely(!cur)) goto fail_num; + break; + + case YYJSON_TYPE_BOOL: + incr_len(8); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + break; + + case YYJSON_TYPE_NULL: + incr_len(8); + cur = write_null(cur); + break; + + case YYJSON_TYPE_ARR: + incr_len(2 + end_len); + byte_copy_2(cur, "[]"); + cur += 2; + break; + + case YYJSON_TYPE_OBJ: + incr_len(2 + end_len); + byte_copy_2(cur, "{}"); + cur += 2; + break; + + default: + goto fail_type; + } + + if (newline) *cur++ = '\n'; + *cur = '\0'; + *dat_len = (usize)(cur - hdr); + memset(err, 0, sizeof(yyjson_write_err)); + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef check_str_len +#undef incr_len +} + +/** Write JSON document minify. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_write_minify(const yyjson_val *root, + const yyjson_write_flag flg, + const yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_val *val; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key; + u8 *hdr, *cur, *end, *tmp; + yyjson_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = root->uni.ofs / sizeof(yyjson_val); + alc_len = alc_len * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + val++; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = ((u8)ctn_obj & (u8)~ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + incr_len(FP_BUF_LEN); + cur = write_num(cur, val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + incr_len(16); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + goto val_end; + } else { + /* push context, setup new container */ + yyjson_write_ctx_set(--ctx, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + val++; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + incr_len(16); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + incr_len(16); + cur = write_null(cur); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 2); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + goto val_end; + } + goto fail_type; + +val_end: + val++; + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + goto val_begin; + +ctn_end: + cur--; + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + *cur++ = ','; + if (unlikely((u8 *)ctx >= end)) goto doc_end; + yyjson_write_ctx_get(ctx++, &ctn_len, &ctn_obj); + ctn_len--; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *(cur - 1) = '\n'; + cur++; + } + *--cur = '\0'; + *dat_len = (usize)(cur - hdr); + memset(err, 0, sizeof(yyjson_write_err)); + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + +/** Write JSON document pretty. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_write_pretty(const yyjson_val *root, + const yyjson_write_flag flg, + const yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_val *val; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key, no_indent; + u8 *hdr, *cur, *end, *tmp; + yyjson_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len, level; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + usize spaces = has_flg(PRETTY_TWO_SPACES) ? 2 : 4; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = root->uni.ofs / sizeof(yyjson_val); + alc_len = alc_len * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + val++; + level = 1; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = (bool)((u8)ctn_obj & (u8)~ctn_len); + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + *cur++ = is_key ? ' ' : '\n'; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(FP_BUF_LEN + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_num(cur, val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } else { + /* push context, setup new container */ + incr_len(32 + (no_indent ? 0 : level * 4)); + yyjson_write_ctx_set(--ctx, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + cur = write_indent(cur, no_indent ? 0 : level, spaces); + level++; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + val++; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_null(cur); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 3 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + goto fail_type; + +val_end: + val++; + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + goto val_begin; + +ctn_end: + cur -= 2; + *cur++ = '\n'; + incr_len(level * 4); + cur = write_indent(cur, --level, spaces); + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + if (unlikely((u8 *)ctx >= end)) goto doc_end; + yyjson_write_ctx_get(ctx++, &ctn_len, &ctn_obj); + ctn_len--; + *cur++ = ','; + *cur++ = '\n'; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *cur++ = '\n'; + } + *cur = '\0'; + *dat_len = (usize)(cur - hdr); + memset(err, 0, sizeof(yyjson_write_err)); + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + + + +/*============================================================================== + * MARK: - JSON Writer (Public) + *============================================================================*/ + +char *yyjson_val_write_opts(const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + usize tmp_dat_len; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_val *root = constcast(yyjson_val *)val; + + if (!err) err = &tmp_err; + if (!dat_len) dat_len = &tmp_dat_len; + + if (unlikely(!root)) { + *dat_len = 0; + err->msg = "input JSON is NULL"; + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; + return NULL; + } + + if (!unsafe_yyjson_is_ctn(root) || unsafe_yyjson_get_len(root) == 0) { + return (char *)yyjson_write_single(root, flg, alc, dat_len, err); + } else if (flg & (YYJSON_WRITE_PRETTY | YYJSON_WRITE_PRETTY_TWO_SPACES)) { + return (char *)yyjson_write_pretty(root, flg, alc, dat_len, err); + } else { + return (char *)yyjson_write_minify(root, flg, alc, dat_len, err); + } +} + +char *yyjson_write_opts(const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_val *root = doc ? doc->root : NULL; + return yyjson_val_write_opts(root, flg, alc_ptr, dat_len, err); +} + +bool yyjson_val_write_file(const char *path, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_val *root = constcast(yyjson_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!path || !*path)) { + err->msg = "input path is invalid"; + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_file(path, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_val_write_fp(FILE *fp, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_val *root = constcast(yyjson_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!fp)) { + err->msg = "input fp is invalid"; + err->code = YYJSON_READ_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_fp(fp, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_write_file(const char *path, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_val *root = doc ? doc->root : NULL; + return yyjson_val_write_file(path, root, flg, alc_ptr, err); +} + +bool yyjson_write_fp(FILE *fp, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_val *root = doc ? doc->root : NULL; + return yyjson_val_write_fp(fp, root, flg, alc_ptr, err); +} + + + +/*============================================================================== + * MARK: - Mutable JSON Writer Implementation (Private) + *============================================================================*/ + +typedef struct yyjson_mut_write_ctx { + usize tag; + yyjson_mut_val *ctn; +} yyjson_mut_write_ctx; + +static_inline void yyjson_mut_write_ctx_set(yyjson_mut_write_ctx *ctx, + yyjson_mut_val *ctn, + usize size, bool is_obj) { + ctx->tag = (size << 1) | (usize)is_obj; + ctx->ctn = ctn; +} + +static_inline void yyjson_mut_write_ctx_get(yyjson_mut_write_ctx *ctx, + yyjson_mut_val **ctn, + usize *size, bool *is_obj) { + usize tag = ctx->tag; + *size = tag >> 1; + *is_obj = (bool)(tag & 1); + *ctn = ctx->ctn; +} + +/** Get the estimated number of values for the mutable JSON document. */ +static_inline usize yyjson_mut_doc_estimated_val_num( + const yyjson_mut_doc *doc) { + usize sum = 0; + yyjson_val_chunk *chunk = doc->val_pool.chunks; + while (chunk) { + sum += chunk->chunk_size / sizeof(yyjson_mut_val) - 1; + if (chunk == doc->val_pool.chunks) { + sum -= (usize)(doc->val_pool.end - doc->val_pool.cur); + } + chunk = chunk->next; + } + return sum; +} + +/** Write single JSON value. */ +static_inline u8 *yyjson_mut_write_single(yyjson_mut_val *val, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { + return yyjson_write_single((yyjson_val *)val, flg, alc, dat_len, err); +} + +/** Write JSON document minify. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_mut_write_minify(const yyjson_mut_val *root, + usize estimated_val_num, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_mut_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_mut_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_mut_val *val, *ctn; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key; + u8 *hdr, *cur, *end, *tmp; + yyjson_mut_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_MINIFY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_mut_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_mut_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + ctn = val; + val = (yyjson_mut_val *)val->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = ((u8)ctn_obj & (u8)~ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + incr_len(FP_BUF_LEN); + cur = write_num(cur, (yyjson_val *)val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + incr_len(16); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + goto val_end; + } else { + /* push context, setup new container */ + yyjson_mut_write_ctx_set(--ctx, ctn, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + ctn = val; + val = (yyjson_mut_val *)ctn->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + incr_len(16); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + incr_len(16); + cur = write_null(cur); + cur++; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 2); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + goto val_end; + } + goto fail_type; + +val_end: + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + val = val->next; + goto val_begin; + +ctn_end: + cur--; + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + *cur++ = ','; + if (unlikely((u8 *)ctx >= end)) goto doc_end; + val = ctn->next; + yyjson_mut_write_ctx_get(ctx++, &ctn, &ctn_len, &ctn_obj); + ctn_len--; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *(cur - 1) = '\n'; + cur++; + } + *--cur = '\0'; + *dat_len = (usize)(cur - hdr); + err->code = YYJSON_WRITE_SUCCESS; + err->msg = NULL; + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + +/** Write JSON document pretty. + The root of this document should be a non-empty container. */ +static_inline u8 *yyjson_mut_write_pretty(const yyjson_mut_val *root, + usize estimated_val_num, + yyjson_write_flag flg, + yyjson_alc alc, + usize *dat_len, + yyjson_write_err *err) { +#define return_err(_code, _msg) do { \ + *dat_len = 0; \ + err->code = YYJSON_WRITE_ERROR_##_code; \ + err->msg = _msg; \ + if (hdr) alc.free(alc.ctx, hdr); \ + return NULL; \ +} while (false) + +#define incr_len(_len) do { \ + ext_len = (usize)(_len); \ + if (unlikely((u8 *)(cur + ext_len) >= (u8 *)ctx)) { \ + usize ctx_pos = (usize)((u8 *)ctx - hdr); \ + usize cur_pos = (usize)(cur - hdr); \ + ctx_len = (usize)(end - (u8 *)ctx); \ + alc_inc = yyjson_max(alc_len / 2, ext_len); \ + alc_inc = size_align_up(alc_inc, sizeof(yyjson_mut_write_ctx)); \ + if ((sizeof(usize) < 8) && size_add_is_overflow(alc_len, alc_inc)) \ + goto fail_alloc; \ + alc_len += alc_inc; \ + tmp = (u8 *)alc.realloc(alc.ctx, hdr, alc_len - alc_inc, alc_len); \ + if (unlikely(!tmp)) goto fail_alloc; \ + ctx_tmp = (yyjson_mut_write_ctx *)(void *)(tmp + (alc_len - ctx_len)); \ + memmove((void *)ctx_tmp, (void *)(tmp + ctx_pos), ctx_len); \ + ctx = ctx_tmp; \ + cur = tmp + cur_pos; \ + end = tmp + alc_len; \ + hdr = tmp; \ + } \ +} while (false) + +#define check_str_len(_len) do { \ + if ((sizeof(usize) < 8) && (_len >= (USIZE_MAX - 16) / 6)) \ + goto fail_alloc; \ +} while (false) + + yyjson_mut_val *val, *ctn; + yyjson_type val_type; + usize ctn_len, ctn_len_tmp; + bool ctn_obj, ctn_obj_tmp, is_key, no_indent; + u8 *hdr, *cur, *end, *tmp; + yyjson_mut_write_ctx *ctx, *ctx_tmp; + usize alc_len, alc_inc, ctx_len, ext_len, str_len, level; + const u8 *str_ptr; + const char_enc_type *enc_table = get_enc_table_with_flag(flg); + bool cpy = (enc_table == enc_table_cpy); + bool esc = has_flg(ESCAPE_UNICODE) != 0; + bool inv = has_allow(INVALID_UNICODE) != 0; + usize spaces = has_flg(PRETTY_TWO_SPACES) ? 2 : 4; + bool newline = has_flg(NEWLINE_AT_END) != 0; + + alc_len = estimated_val_num * YYJSON_WRITER_ESTIMATED_PRETTY_RATIO + 64; + alc_len = size_align_up(alc_len, sizeof(yyjson_mut_write_ctx)); + hdr = (u8 *)alc.malloc(alc.ctx, alc_len); + if (!hdr) goto fail_alloc; + cur = hdr; + end = hdr + alc_len; + ctx = (yyjson_mut_write_ctx *)(void *)end; + +doc_begin: + val = constcast(yyjson_mut_val *)root; + val_type = unsafe_yyjson_get_type(val); + ctn_obj = (val_type == YYJSON_TYPE_OBJ); + ctn_len = unsafe_yyjson_get_len(val) << (u8)ctn_obj; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + ctn = val; + val = (yyjson_mut_val *)val->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + level = 1; + +val_begin: + val_type = unsafe_yyjson_get_type(val); + if (val_type == YYJSON_TYPE_STR) { + is_key = (bool)((u8)ctn_obj & (u8)~ctn_len); + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len * 6 + 16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + if (likely(cpy) && unsafe_yyjson_get_subtype(val)) { + cur = write_str_noesc(cur, str_ptr, str_len); + } else { + cur = write_str(cur, esc, inv, str_ptr, str_len, enc_table); + if (unlikely(!cur)) goto fail_str; + } + *cur++ = is_key ? ':' : ','; + *cur++ = is_key ? ' ' : '\n'; + goto val_end; + } + if (val_type == YYJSON_TYPE_NUM) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(FP_BUF_LEN + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_num(cur, (yyjson_val *)val, flg); + if (unlikely(!cur)) goto fail_num; + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + if ((val_type & (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) == + (YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ)) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + ctn_len_tmp = unsafe_yyjson_get_len(val); + ctn_obj_tmp = (val_type == YYJSON_TYPE_OBJ); + if (unlikely(ctn_len_tmp == 0)) { + /* write empty container */ + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + *cur++ = (u8)('[' | ((u8)ctn_obj_tmp << 5)); + *cur++ = (u8)(']' | ((u8)ctn_obj_tmp << 5)); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } else { + /* push context, setup new container */ + incr_len(32 + (no_indent ? 0 : level * 4)); + yyjson_mut_write_ctx_set(--ctx, ctn, ctn_len, ctn_obj); + ctn_len = ctn_len_tmp << (u8)ctn_obj_tmp; + ctn_obj = ctn_obj_tmp; + cur = write_indent(cur, no_indent ? 0 : level, spaces); + level++; + *cur++ = (u8)('[' | ((u8)ctn_obj << 5)); + *cur++ = '\n'; + ctn = val; + val = (yyjson_mut_val *)ctn->uni.ptr; /* tail */ + val = ctn_obj ? val->next->next : val->next; + goto val_begin; + } + } + if (val_type == YYJSON_TYPE_BOOL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_bool(cur, unsafe_yyjson_get_bool(val)); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_NULL) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + incr_len(16 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_null(cur); + cur += 2; + goto val_end; + } + if (val_type == YYJSON_TYPE_RAW) { + no_indent = (bool)((u8)ctn_obj & (u8)ctn_len); + str_len = unsafe_yyjson_get_len(val); + str_ptr = (const u8 *)unsafe_yyjson_get_str(val); + check_str_len(str_len); + incr_len(str_len + 3 + (no_indent ? 0 : level * 4)); + cur = write_indent(cur, no_indent ? 0 : level, spaces); + cur = write_raw(cur, str_ptr, str_len); + *cur++ = ','; + *cur++ = '\n'; + goto val_end; + } + goto fail_type; + +val_end: + ctn_len--; + if (unlikely(ctn_len == 0)) goto ctn_end; + val = val->next; + goto val_begin; + +ctn_end: + cur -= 2; + *cur++ = '\n'; + incr_len(level * 4); + cur = write_indent(cur, --level, spaces); + *cur++ = (u8)(']' | ((u8)ctn_obj << 5)); + if (unlikely((u8 *)ctx >= end)) goto doc_end; + val = ctn->next; + yyjson_mut_write_ctx_get(ctx++, &ctn, &ctn_len, &ctn_obj); + ctn_len--; + *cur++ = ','; + *cur++ = '\n'; + if (likely(ctn_len > 0)) { + goto val_begin; + } else { + goto ctn_end; + } + +doc_end: + if (newline) { + incr_len(2); + *cur++ = '\n'; + } + *cur = '\0'; + *dat_len = (usize)(cur - hdr); + err->code = YYJSON_WRITE_SUCCESS; + err->msg = NULL; + return hdr; + +fail_alloc: return_err(MEMORY_ALLOCATION, MSG_MALLOC); +fail_type: return_err(INVALID_VALUE_TYPE, MSG_ERR_TYPE); +fail_num: return_err(NAN_OR_INF, MSG_NAN_INF); +fail_str: return_err(INVALID_STRING, MSG_ERR_UTF8); + +#undef return_err +#undef incr_len +#undef check_str_len +} + +static char *yyjson_mut_write_opts_impl(const yyjson_mut_val *val, + usize estimated_val_num, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + usize tmp_dat_len; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + yyjson_mut_val *root = constcast(yyjson_mut_val *)val; + + if (!err) err = &tmp_err; + if (!dat_len) dat_len = &tmp_dat_len; + + if (unlikely(!root)) { + *dat_len = 0; + err->msg = "input JSON is NULL"; + err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + return NULL; + } + + if (!unsafe_yyjson_is_ctn(root) || unsafe_yyjson_get_len(root) == 0) { + return (char *)yyjson_mut_write_single(root, flg, alc, dat_len, err); + } else if (flg & (YYJSON_WRITE_PRETTY | YYJSON_WRITE_PRETTY_TWO_SPACES)) { + return (char *)yyjson_mut_write_pretty(root, estimated_val_num, + flg, alc, dat_len, err); + } else { + return (char *)yyjson_mut_write_minify(root, estimated_val_num, + flg, alc, dat_len, err); + } +} + + + +/*============================================================================== + * MARK: - Mutable JSON Writer (Public) + *============================================================================*/ + +char *yyjson_mut_val_write_opts(const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + return yyjson_mut_write_opts_impl(val, 0, flg, alc_ptr, dat_len, err); +} + +char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + usize *dat_len, + yyjson_write_err *err) { + yyjson_mut_val *root; + usize estimated_val_num; + if (likely(doc)) { + root = doc->root; + estimated_val_num = yyjson_mut_doc_estimated_val_num(doc); + } else { + root = NULL; + estimated_val_num = 0; + } + return yyjson_mut_write_opts_impl(root, estimated_val_num, + flg, alc_ptr, dat_len, err); +} + +bool yyjson_mut_val_write_file(const char *path, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_mut_val *root = constcast(yyjson_mut_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!path || !*path)) { + err->msg = "input path is invalid"; + err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_mut_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_file(path, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_mut_val_write_fp(FILE *fp, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_write_err tmp_err; + yyjson_alc alc = alc_ptr ? *alc_ptr : YYJSON_DEFAULT_ALC; + u8 *dat; + usize dat_len = 0; + yyjson_mut_val *root = constcast(yyjson_mut_val *)val; + bool suc; + + if (!err) err = &tmp_err; + if (unlikely(!fp)) { + err->msg = "input fp is invalid"; + err->code = YYJSON_WRITE_ERROR_INVALID_PARAMETER; + return false; + } + + dat = (u8 *)yyjson_mut_val_write_opts(root, flg, &alc, &dat_len, err); + if (unlikely(!dat)) return false; + suc = write_dat_to_fp(fp, dat, dat_len, err); + alc.free(alc.ctx, dat); + return suc; +} + +bool yyjson_mut_write_file(const char *path, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_mut_val *root = doc ? doc->root : NULL; + return yyjson_mut_val_write_file(path, root, flg, alc_ptr, err); +} + +bool yyjson_mut_write_fp(FILE *fp, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc_ptr, + yyjson_write_err *err) { + yyjson_mut_val *root = doc ? doc->root : NULL; + return yyjson_mut_val_write_fp(fp, root, flg, alc_ptr, err); +} + +#undef has_flg +#undef has_allow +#endif /* YYJSON_DISABLE_WRITER */ + + + +#if !YYJSON_DISABLE_UTILS + +/*============================================================================== + * MARK: - JSON Pointer API (RFC 6901) (Public) + *============================================================================*/ + +/** + Get a token from JSON pointer string. + @param ptr [in] string that points to current token prefix `/` + [out] string that points to next token prefix `/`, or string end + @param end [in] end of the entire JSON Pointer string + @param len [out] unescaped token length + @param esc [out] number of escaped characters in this token + @return head of the token, or NULL if syntax error + */ +static_inline const char *ptr_next_token(const char **ptr, const char *end, + usize *len, usize *esc) { + const char *hdr = *ptr + 1; + const char *cur = hdr; + /* skip unescaped characters */ + while (cur < end && *cur != '/' && *cur != '~') cur++; + if (likely(cur == end || *cur != '~')) { + /* no escaped characters, return */ + *ptr = cur; + *len = (usize)(cur - hdr); + *esc = 0; + return hdr; + } else { + /* handle escaped characters */ + usize esc_num = 0; + while (cur < end && *cur != '/') { + if (*cur++ == '~') { + if (cur == end || (*cur != '0' && *cur != '1')) { + *ptr = cur - 1; + return NULL; + } + esc_num++; + } + } + *ptr = cur; + *len = (usize)(cur - hdr) - esc_num; + *esc = esc_num; + return hdr; + } +} + +/** + Convert token string to index. + @param cur [in] token head + @param len [in] token length + @param idx [out] the index number, or USIZE_MAX if token is '-' + @return true if token is a valid array index + */ +static_inline bool ptr_token_to_idx(const char *cur, usize len, usize *idx) { + const char *end = cur + len; + usize num = 0, add; + if (unlikely(len == 0 || len > USIZE_SAFE_DIG)) return false; + if (*cur == '0') { + if (unlikely(len > 1)) return false; + *idx = 0; + return true; + } + if (*cur == '-') { + if (unlikely(len > 1)) return false; + *idx = USIZE_MAX; + return true; + } + for (; cur < end && (add = (usize)((u8)*cur - (u8)'0')) <= 9; cur++) { + num = num * 10 + add; + } + if (unlikely(num == 0 || cur < end)) return false; + *idx = num; + return true; +} + +/** + Compare JSON key with token. + @param key a string key (yyjson_val or yyjson_mut_val) + @param token a JSON pointer token + @param len unescaped token length + @param esc number of escaped characters in this token + @return true if `str` is equals to `token` + */ +static_inline bool ptr_token_eq(void *key, + const char *token, usize len, usize esc) { + yyjson_val *val = (yyjson_val *)key; + if (unsafe_yyjson_get_len(val) != len) return false; + if (likely(!esc)) { + return memcmp(val->uni.str, token, len) == 0; + } else { + const char *str = val->uni.str; + for (; len-- > 0; token++, str++) { + if (*token == '~') { + if (*str != (*++token == '0' ? '~' : '/')) return false; + } else { + if (*str != *token) return false; + } + } + return true; + } +} + +/** + Get a value from array by token. + @param arr an array, should not be NULL or non-array type + @param token a JSON pointer token + @param len unescaped token length + @param esc number of escaped characters in this token + @return value at index, or NULL if token is not index or index is out of range + */ +static_inline yyjson_val *ptr_arr_get(yyjson_val *arr, const char *token, + usize len, usize esc) { + yyjson_val *val = unsafe_yyjson_get_first(arr); + usize num = unsafe_yyjson_get_len(arr), idx = 0; + if (unlikely(num == 0)) return NULL; + if (unlikely(!ptr_token_to_idx(token, len, &idx))) return NULL; + if (unlikely(idx >= num)) return NULL; + if (unsafe_yyjson_arr_is_flat(arr)) { + return val + idx; + } else { + while (idx-- > 0) val = unsafe_yyjson_get_next(val); + return val; + } +} + +/** + Get a value from object by token. + @param obj [in] an object, should not be NULL or non-object type + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @return value associated with the token, or NULL if no value + */ +static_inline yyjson_val *ptr_obj_get(yyjson_val *obj, const char *token, + usize len, usize esc) { + yyjson_val *key = unsafe_yyjson_get_first(obj); + usize num = unsafe_yyjson_get_len(obj); + if (unlikely(num == 0)) return NULL; + for (; num > 0; num--, key = unsafe_yyjson_get_next(key + 1)) { + if (ptr_token_eq(key, token, len, esc)) return key + 1; + } + return NULL; +} + +/** + Get a value from array by token. + @param arr [in] an array, should not be NULL or non-array type + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @param pre [out] previous (sibling) value of the returned value + @param last [out] whether index is last + @return value at index, or NULL if token is not index or index is out of range + */ +static_inline yyjson_mut_val *ptr_mut_arr_get(yyjson_mut_val *arr, + const char *token, + usize len, usize esc, + yyjson_mut_val **pre, + bool *last) { + yyjson_mut_val *val = (yyjson_mut_val *)arr->uni.ptr; /* last (tail) */ + usize num = unsafe_yyjson_get_len(arr), idx; + if (last) *last = false; + if (pre) *pre = NULL; + if (unlikely(num == 0)) { + if (last && len == 1 && (*token == '0' || *token == '-')) *last = true; + return NULL; + } + if (unlikely(!ptr_token_to_idx(token, len, &idx))) return NULL; + if (last) *last = (idx == num || idx == USIZE_MAX); + if (unlikely(idx >= num)) return NULL; + while (idx-- > 0) val = val->next; + if (pre) *pre = val; + return val->next; +} + +/** + Get a value from object by token. + @param obj [in] an object, should not be NULL or non-object type + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @param pre [out] previous (sibling) key of the returned value's key + @return value associated with the token, or NULL if no value + */ +static_inline yyjson_mut_val *ptr_mut_obj_get(yyjson_mut_val *obj, + const char *token, + usize len, usize esc, + yyjson_mut_val **pre) { + yyjson_mut_val *pre_key = (yyjson_mut_val *)obj->uni.ptr, *key; + usize num = unsafe_yyjson_get_len(obj); + if (pre) *pre = NULL; + if (unlikely(num == 0)) return NULL; + for (; num > 0; num--, pre_key = key) { + key = pre_key->next->next; + if (ptr_token_eq(key, token, len, esc)) { + if (pre) *pre = pre_key; + return key->next; + } + } + return NULL; +} + +/** + Create a string value with JSON pointer token. + @param token [in] a JSON pointer token + @param len [in] unescaped token length + @param esc [in] number of escaped characters in this token + @param doc [in] used for memory allocation when creating value + @return new string value, or NULL if memory allocation failed + */ +static_inline yyjson_mut_val *ptr_new_key(const char *token, + usize len, usize esc, + yyjson_mut_doc *doc) { + const char *src = token; + if (likely(!esc)) { + return yyjson_mut_strncpy(doc, src, len); + } else { + const char *end = src + len + esc; + char *dst = unsafe_yyjson_mut_str_alc(doc, len + esc); + char *str = dst; + if (unlikely(!dst)) return NULL; + for (; src < end; src++, dst++) { + if (*src != '~') *dst = *src; + else *dst = (*++src == '0' ? '~' : '/'); + } + *dst = '\0'; + return yyjson_mut_strn(doc, str, len); + } +} + +/* macros for yyjson_ptr */ +#define return_err(_ret, _code, _pos, _msg) do { \ + if (err) { \ + err->code = YYJSON_PTR_ERR_##_code; \ + err->msg = _msg; \ + err->pos = (usize)(_pos); \ + } \ + return _ret; \ +} while (false) + +#define return_err_resolve(_ret, _pos) \ + return_err(_ret, RESOLVE, _pos, "JSON pointer cannot be resolved") +#define return_err_syntax(_ret, _pos) \ + return_err(_ret, SYNTAX, _pos, "invalid escaped character") +#define return_err_alloc(_ret) \ + return_err(_ret, MEMORY_ALLOCATION, 0, "failed to create value") + +yyjson_val *unsafe_yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t ptr_len, + yyjson_ptr_err *err) { + + const char *hdr = ptr, *end = ptr + ptr_len, *token; + usize len, esc; + yyjson_type type; + + while (true) { + token = ptr_next_token(&ptr, end, &len, &esc); + if (unlikely(!token)) return_err_syntax(NULL, ptr - hdr); + type = unsafe_yyjson_get_type(val); + if (type == YYJSON_TYPE_OBJ) { + val = ptr_obj_get(val, token, len, esc); + } else if (type == YYJSON_TYPE_ARR) { + val = ptr_arr_get(val, token, len, esc); + } else { + val = NULL; + } + if (!val) return_err_resolve(NULL, token - hdr); + if (ptr == end) return val; + } +} + +yyjson_mut_val *unsafe_yyjson_mut_ptr_getx( + yyjson_mut_val *val, const char *ptr, size_t ptr_len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + const char *hdr = ptr, *end = ptr + ptr_len, *token; + usize len, esc; + yyjson_mut_val *ctn, *pre = NULL; + yyjson_type type; + bool idx_is_last = false; + + while (true) { + token = ptr_next_token(&ptr, end, &len, &esc); + if (unlikely(!token)) return_err_syntax(NULL, ptr - hdr); + ctn = val; + type = unsafe_yyjson_get_type(val); + if (type == YYJSON_TYPE_OBJ) { + val = ptr_mut_obj_get(val, token, len, esc, &pre); + } else if (type == YYJSON_TYPE_ARR) { + val = ptr_mut_arr_get(val, token, len, esc, &pre, &idx_is_last); + } else { + val = NULL; + } + if (ctx && (ptr == end)) { + if (type == YYJSON_TYPE_OBJ || + (type == YYJSON_TYPE_ARR && (val || idx_is_last))) { + ctx->ctn = ctn; + ctx->pre = pre; + } + } + if (!val) return_err_resolve(NULL, token - hdr); + if (ptr == end) return val; + } +} + +bool unsafe_yyjson_mut_ptr_putx( + yyjson_mut_val *val, const char *ptr, size_t ptr_len, + yyjson_mut_val *new_val, yyjson_mut_doc *doc, bool create_parent, + bool insert_new, yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + const char *hdr = ptr, *end = ptr + ptr_len, *token; + usize token_len, esc, ctn_len; + yyjson_mut_val *ctn, *key, *pre = NULL; + yyjson_mut_val *sep_ctn = NULL, *sep_key = NULL, *sep_val = NULL; + yyjson_type ctn_type; + bool idx_is_last = false; + + /* skip exist parent nodes */ + while (true) { + token = ptr_next_token(&ptr, end, &token_len, &esc); + if (unlikely(!token)) return_err_syntax(false, ptr - hdr); + ctn = val; + ctn_type = unsafe_yyjson_get_type(ctn); + if (ctn_type == YYJSON_TYPE_OBJ) { + val = ptr_mut_obj_get(ctn, token, token_len, esc, &pre); + } else if (ctn_type == YYJSON_TYPE_ARR) { + val = ptr_mut_arr_get(ctn, token, token_len, esc, &pre, + &idx_is_last); + } else return_err_resolve(false, token - hdr); + if (!val) break; + if (ptr == end) break; /* is last token */ + } + + /* create parent nodes if not exist */ + if (unlikely(ptr != end)) { /* not last token */ + if (!create_parent) return_err_resolve(false, token - hdr); + + /* add value at last index if container is array */ + if (ctn_type == YYJSON_TYPE_ARR) { + if (!idx_is_last || !insert_new) { + return_err_resolve(false, token - hdr); + } + val = yyjson_mut_obj(doc); + if (!val) return_err_alloc(false); + + /* delay attaching until all operations are completed */ + sep_ctn = ctn; + sep_key = NULL; + sep_val = val; + + /* move to next token */ + ctn = val; + val = NULL; + ctn_type = YYJSON_TYPE_OBJ; + token = ptr_next_token(&ptr, end, &token_len, &esc); + if (unlikely(!token)) return_err_resolve(false, token - hdr); + } + + /* container is object, create parent nodes */ + while (ptr != end) { /* not last token */ + key = ptr_new_key(token, token_len, esc, doc); + if (!key) return_err_alloc(false); + val = yyjson_mut_obj(doc); + if (!val) return_err_alloc(false); + + /* delay attaching until all operations are completed */ + if (!sep_ctn) { + sep_ctn = ctn; + sep_key = key; + sep_val = val; + } else { + yyjson_mut_obj_add(ctn, key, val); + } + + /* move to next token */ + ctn = val; + val = NULL; + token = ptr_next_token(&ptr, end, &token_len, &esc); + if (unlikely(!token)) return_err_syntax(false, ptr - hdr); + } + } + + /* JSON pointer is resolved, insert or replace target value */ + ctn_len = unsafe_yyjson_get_len(ctn); + if (ctn_type == YYJSON_TYPE_OBJ) { + if (ctx) ctx->ctn = ctn; + if (!val || insert_new) { + /* insert new key-value pair */ + key = ptr_new_key(token, token_len, esc, doc); + if (unlikely(!key)) return_err_alloc(false); + if (ctx) ctx->pre = ctn_len ? (yyjson_mut_val *)ctn->uni.ptr : key; + unsafe_yyjson_mut_obj_add(ctn, key, new_val, ctn_len); + } else { + /* replace exist value */ + key = pre->next->next; + if (ctx) ctx->pre = pre; + if (ctx) ctx->old = val; + yyjson_mut_obj_put(ctn, key, new_val); + } + } else { + /* array */ + if (ctx && (val || idx_is_last)) ctx->ctn = ctn; + if (insert_new) { + /* append new value */ + if (val) { + pre->next = new_val; + new_val->next = val; + if (ctx) ctx->pre = pre; + unsafe_yyjson_set_len(ctn, ctn_len + 1); + } else if (idx_is_last) { + if (ctx) ctx->pre = ctn_len ? + (yyjson_mut_val *)ctn->uni.ptr : new_val; + yyjson_mut_arr_append(ctn, new_val); + } else { + return_err_resolve(false, token - hdr); + } + } else { + /* replace exist value */ + if (!val) return_err_resolve(false, token - hdr); + if (ctn_len > 1) { + new_val->next = val->next; + pre->next = new_val; + if (ctn->uni.ptr == val) ctn->uni.ptr = new_val; + } else { + new_val->next = new_val; + ctn->uni.ptr = new_val; + pre = new_val; + } + if (ctx) ctx->pre = pre; + if (ctx) ctx->old = val; + } + } + + /* all operations are completed, attach the new components to the target */ + if (unlikely(sep_ctn)) { + if (sep_key) yyjson_mut_obj_add(sep_ctn, sep_key, sep_val); + else yyjson_mut_arr_append(sep_ctn, sep_val); + } + return true; +} + +yyjson_mut_val *unsafe_yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_mut_val *cur_val; + yyjson_ptr_ctx cur_ctx; + memset(&cur_ctx, 0, sizeof(cur_ctx)); + if (!ctx) ctx = &cur_ctx; + cur_val = unsafe_yyjson_mut_ptr_getx(val, ptr, len, ctx, err); + if (!cur_val) return NULL; + + if (yyjson_mut_is_obj(ctx->ctn)) { + yyjson_mut_val *key = ctx->pre->next->next; + yyjson_mut_obj_put(ctx->ctn, key, new_val); + } else { + yyjson_ptr_ctx_replace(ctx, new_val); + } + ctx->old = cur_val; + return cur_val; +} + +yyjson_mut_val *unsafe_yyjson_mut_ptr_removex( + yyjson_mut_val *val, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_mut_val *cur_val; + yyjson_ptr_ctx cur_ctx; + memset(&cur_ctx, 0, sizeof(cur_ctx)); + if (!ctx) ctx = &cur_ctx; + cur_val = unsafe_yyjson_mut_ptr_getx(val, ptr, len, ctx, err); + if (cur_val) { + if (yyjson_mut_is_obj(ctx->ctn)) { + yyjson_mut_val *key = ctx->pre->next->next; + yyjson_mut_obj_put(ctx->ctn, key, NULL); + } else { + yyjson_ptr_ctx_remove(ctx); + } + ctx->pre = NULL; + ctx->old = cur_val; + } + return cur_val; +} + +/* macros for yyjson_ptr */ +#undef return_err +#undef return_err_resolve +#undef return_err_syntax +#undef return_err_alloc + + + +/*============================================================================== + * MARK: - JSON Patch API (RFC 6902) (Public) + *============================================================================*/ + +/* JSON Patch operation */ +typedef enum patch_op { + PATCH_OP_ADD, /* path, value */ + PATCH_OP_REMOVE, /* path */ + PATCH_OP_REPLACE, /* path, value */ + PATCH_OP_MOVE, /* from, path */ + PATCH_OP_COPY, /* from, path */ + PATCH_OP_TEST, /* path, value */ + PATCH_OP_NONE /* invalid */ +} patch_op; + +static patch_op patch_op_get(yyjson_val *op) { + const char *str = op->uni.str; + switch (unsafe_yyjson_get_len(op)) { + case 3: + if (!memcmp(str, "add", 3)) return PATCH_OP_ADD; + return PATCH_OP_NONE; + case 4: + if (!memcmp(str, "move", 4)) return PATCH_OP_MOVE; + if (!memcmp(str, "copy", 4)) return PATCH_OP_COPY; + if (!memcmp(str, "test", 4)) return PATCH_OP_TEST; + return PATCH_OP_NONE; + case 6: + if (!memcmp(str, "remove", 6)) return PATCH_OP_REMOVE; + return PATCH_OP_NONE; + case 7: + if (!memcmp(str, "replace", 7)) return PATCH_OP_REPLACE; + return PATCH_OP_NONE; + default: + return PATCH_OP_NONE; + } +} + +/* macros for yyjson_patch */ +#define return_err(_code, _msg) do { \ + if (err->ptr.code == YYJSON_PTR_ERR_MEMORY_ALLOCATION) { \ + err->code = YYJSON_PATCH_ERROR_MEMORY_ALLOCATION; \ + err->msg = _msg; \ + memset(&err->ptr, 0, sizeof(yyjson_ptr_err)); \ + } else { \ + err->code = YYJSON_PATCH_ERROR_##_code; \ + err->msg = _msg; \ + err->idx = iter.idx ? iter.idx - 1 : 0; \ + } \ + return NULL; \ +} while (false) + +#define return_err_copy() \ + return_err(MEMORY_ALLOCATION, "failed to copy value") +#define return_err_key(_key) \ + return_err(MISSING_KEY, "missing key " _key) +#define return_err_val(_key) \ + return_err(INVALID_MEMBER, "invalid member " _key) + +#define ptr_get(_ptr) yyjson_mut_ptr_getx( \ + root, _ptr->uni.str, _ptr##_len, NULL, &err->ptr) +#define ptr_add(_ptr, _val) yyjson_mut_ptr_addx( \ + root, _ptr->uni.str, _ptr##_len, _val, doc, false, NULL, &err->ptr) +#define ptr_remove(_ptr) yyjson_mut_ptr_removex( \ + root, _ptr->uni.str, _ptr##_len, NULL, &err->ptr) +#define ptr_replace(_ptr, _val)yyjson_mut_ptr_replacex( \ + root, _ptr->uni.str, _ptr##_len, _val, NULL, &err->ptr) + +yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch, + yyjson_patch_err *err) { + + yyjson_mut_val *root; + yyjson_val *obj; + yyjson_arr_iter iter; + yyjson_patch_err err_tmp; + if (!err) err = &err_tmp; + memset(err, 0, sizeof(*err)); + memset(&iter, 0, sizeof(iter)); + + if (unlikely(!doc || !orig || !patch)) { + return_err(INVALID_PARAMETER, "input parameter is NULL"); + } + if (unlikely(!yyjson_is_arr(patch))) { + return_err(INVALID_PARAMETER, "input patch is not array"); + } + root = yyjson_val_mut_copy(doc, orig); + if (unlikely(!root)) return_err_copy(); + + /* iterate through the patch array */ + yyjson_arr_iter_init(patch, &iter); + while ((obj = yyjson_arr_iter_next(&iter))) { + patch_op op_enum; + yyjson_val *op, *path, *from = NULL, *value; + yyjson_mut_val *val = NULL, *test; + usize path_len, from_len = 0; + if (unlikely(!unsafe_yyjson_is_obj(obj))) { + return_err(INVALID_OPERATION, "JSON patch operation is not object"); + } + + /* get required member: op */ + op = yyjson_obj_get(obj, "op"); + if (unlikely(!op)) return_err_key("`op`"); + if (unlikely(!yyjson_is_str(op))) return_err_val("`op`"); + op_enum = patch_op_get(op); + + /* get required member: path */ + path = yyjson_obj_get(obj, "path"); + if (unlikely(!path)) return_err_key("`path`"); + if (unlikely(!yyjson_is_str(path))) return_err_val("`path`"); + path_len = unsafe_yyjson_get_len(path); + + /* get required member: value, from */ + switch ((int)op_enum) { + case PATCH_OP_ADD: case PATCH_OP_REPLACE: case PATCH_OP_TEST: + value = yyjson_obj_get(obj, "value"); + if (unlikely(!value)) return_err_key("`value`"); + val = yyjson_val_mut_copy(doc, value); + if (unlikely(!val)) return_err_copy(); + break; + case PATCH_OP_MOVE: case PATCH_OP_COPY: + from = yyjson_obj_get(obj, "from"); + if (unlikely(!from)) return_err_key("`from`"); + if (unlikely(!yyjson_is_str(from))) return_err_val("`from`"); + from_len = unsafe_yyjson_get_len(from); + break; + default: + break; + } + + /* perform an operation */ + switch ((int)op_enum) { + case PATCH_OP_ADD: /* add(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_REMOVE: /* remove(path) */ + if (unlikely(!ptr_remove(path))) { + return_err(POINTER, "failed to remove `path`"); + } + break; + case PATCH_OP_REPLACE: /* replace(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_replace(path, val))) { + return_err(POINTER, "failed to replace `path`"); + } + break; + case PATCH_OP_MOVE: /* val = remove(from), add(path, val) */ + if (unlikely(from_len == 0 && path_len == 0)) break; + val = ptr_remove(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to remove `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_COPY: /* val = get(from).copy, add(path, val) */ + val = ptr_get(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to get `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + val = yyjson_mut_val_mut_copy(doc, val); + if (unlikely(!val)) return_err_copy(); + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_TEST: /* test = get(path), test.eq(val) */ + test = ptr_get(path); + if (unlikely(!test)) { + return_err(POINTER, "failed to get `path`"); + } + if (unlikely(!yyjson_mut_equals(val, test))) { + return_err(EQUAL, "failed to test equal"); + } + break; + default: + return_err(INVALID_MEMBER, "unsupported `op`"); + } + } + return root; +} + +yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch, + yyjson_patch_err *err) { + yyjson_mut_val *root, *obj; + yyjson_mut_arr_iter iter; + yyjson_patch_err err_tmp; + if (!err) err = &err_tmp; + memset(err, 0, sizeof(*err)); + memset(&iter, 0, sizeof(iter)); + + if (unlikely(!doc || !orig || !patch)) { + return_err(INVALID_PARAMETER, "input parameter is NULL"); + } + if (unlikely(!yyjson_mut_is_arr(patch))) { + return_err(INVALID_PARAMETER, "input patch is not array"); + } + root = yyjson_mut_val_mut_copy(doc, orig); + if (unlikely(!root)) return_err_copy(); + + /* iterate through the patch array */ + yyjson_mut_arr_iter_init(patch, &iter); + while ((obj = yyjson_mut_arr_iter_next(&iter))) { + patch_op op_enum; + yyjson_mut_val *op, *path, *from = NULL, *value; + yyjson_mut_val *val = NULL, *test; + usize path_len, from_len = 0; + if (!unsafe_yyjson_is_obj(obj)) { + return_err(INVALID_OPERATION, "JSON patch operation is not object"); + } + + /* get required member: op */ + op = yyjson_mut_obj_get(obj, "op"); + if (unlikely(!op)) return_err_key("`op`"); + if (unlikely(!yyjson_mut_is_str(op))) return_err_val("`op`"); + op_enum = patch_op_get((yyjson_val *)(void *)op); + + /* get required member: path */ + path = yyjson_mut_obj_get(obj, "path"); + if (unlikely(!path)) return_err_key("`path`"); + if (unlikely(!yyjson_mut_is_str(path))) return_err_val("`path`"); + path_len = unsafe_yyjson_get_len(path); + + /* get required member: value, from */ + switch ((int)op_enum) { + case PATCH_OP_ADD: case PATCH_OP_REPLACE: case PATCH_OP_TEST: + value = yyjson_mut_obj_get(obj, "value"); + if (unlikely(!value)) return_err_key("`value`"); + val = yyjson_mut_val_mut_copy(doc, value); + if (unlikely(!val)) return_err_copy(); + break; + case PATCH_OP_MOVE: case PATCH_OP_COPY: + from = yyjson_mut_obj_get(obj, "from"); + if (unlikely(!from)) return_err_key("`from`"); + if (unlikely(!yyjson_mut_is_str(from))) { + return_err_val("`from`"); + } + from_len = unsafe_yyjson_get_len(from); + break; + default: + break; + } + + /* perform an operation */ + switch ((int)op_enum) { + case PATCH_OP_ADD: /* add(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_REMOVE: /* remove(path) */ + if (unlikely(!ptr_remove(path))) { + return_err(POINTER, "failed to remove `path`"); + } + break; + case PATCH_OP_REPLACE: /* replace(path, val) */ + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_replace(path, val))) { + return_err(POINTER, "failed to replace `path`"); + } + break; + case PATCH_OP_MOVE: /* val = remove(from), add(path, val) */ + if (unlikely(from_len == 0 && path_len == 0)) break; + val = ptr_remove(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to remove `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_COPY: /* val = get(from).copy, add(path, val) */ + val = ptr_get(from); + if (unlikely(!val)) { + return_err(POINTER, "failed to get `from`"); + } + if (unlikely(path_len == 0)) { root = val; break; } + val = yyjson_mut_val_mut_copy(doc, val); + if (unlikely(!val)) return_err_copy(); + if (unlikely(!ptr_add(path, val))) { + return_err(POINTER, "failed to add `path`"); + } + break; + case PATCH_OP_TEST: /* test = get(path), test.eq(val) */ + test = ptr_get(path); + if (unlikely(!test)) { + return_err(POINTER, "failed to get `path`"); + } + if (unlikely(!yyjson_mut_equals(val, test))) { + return_err(EQUAL, "failed to test equal"); + } + break; + default: + return_err(INVALID_MEMBER, "unsupported `op`"); + } + } + return root; +} + +/* macros for yyjson_patch */ +#undef return_err +#undef return_err_copy +#undef return_err_key +#undef return_err_val +#undef ptr_get +#undef ptr_add +#undef ptr_remove +#undef ptr_replace + + + +/*============================================================================== + * MARK: - JSON Merge-Patch API (RFC 7386) (Public) + *============================================================================*/ + +yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch) { + usize idx, max; + yyjson_val *key, *orig_val, *patch_val, local_orig; + yyjson_mut_val *builder, *mut_key, *mut_val, *merged_val; + + if (unlikely(!yyjson_is_obj(patch))) { + return yyjson_val_mut_copy(doc, patch); + } + + builder = yyjson_mut_obj(doc); + if (unlikely(!builder)) return NULL; + + memset(&local_orig, 0, sizeof(local_orig)); + if (!yyjson_is_obj(orig)) { + orig = &local_orig; + orig->tag = builder->tag; + orig->uni = builder->uni; + } + + /* If orig is contributing, copy any items not modified by the patch */ + if (orig != &local_orig) { + yyjson_obj_foreach(orig, idx, max, key, orig_val) { + patch_val = yyjson_obj_getn(patch, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + if (!patch_val) { + mut_key = yyjson_val_mut_copy(doc, key); + mut_val = yyjson_val_mut_copy(doc, orig_val); + if (!yyjson_mut_obj_add(builder, mut_key, mut_val)) return NULL; + } + } + } + + /* Merge items modified by the patch. */ + yyjson_obj_foreach(patch, idx, max, key, patch_val) { + /* null indicates the field is removed. */ + if (unsafe_yyjson_is_null(patch_val)) { + continue; + } + mut_key = yyjson_val_mut_copy(doc, key); + orig_val = yyjson_obj_getn(orig, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + merged_val = yyjson_merge_patch(doc, orig_val, patch_val); + if (!yyjson_mut_obj_add(builder, mut_key, merged_val)) return NULL; + } + + return builder; +} + +yyjson_mut_val *yyjson_mut_merge_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch) { + usize idx, max; + yyjson_mut_val *key, *orig_val, *patch_val, local_orig; + yyjson_mut_val *builder, *mut_key, *mut_val, *merged_val; + + if (unlikely(!yyjson_mut_is_obj(patch))) { + return yyjson_mut_val_mut_copy(doc, patch); + } + + builder = yyjson_mut_obj(doc); + if (unlikely(!builder)) return NULL; + + memset(&local_orig, 0, sizeof(local_orig)); + if (!yyjson_mut_is_obj(orig)) { + orig = &local_orig; + orig->tag = builder->tag; + orig->uni = builder->uni; + } + + /* If orig is contributing, copy any items not modified by the patch */ + if (orig != &local_orig) { + yyjson_mut_obj_foreach(orig, idx, max, key, orig_val) { + patch_val = yyjson_mut_obj_getn(patch, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + if (!patch_val) { + mut_key = yyjson_mut_val_mut_copy(doc, key); + mut_val = yyjson_mut_val_mut_copy(doc, orig_val); + if (!yyjson_mut_obj_add(builder, mut_key, mut_val)) return NULL; + } + } + } + + /* Merge items modified by the patch. */ + yyjson_mut_obj_foreach(patch, idx, max, key, patch_val) { + /* null indicates the field is removed. */ + if (unsafe_yyjson_is_null(patch_val)) { + continue; + } + mut_key = yyjson_mut_val_mut_copy(doc, key); + orig_val = yyjson_mut_obj_getn(orig, + unsafe_yyjson_get_str(key), + unsafe_yyjson_get_len(key)); + merged_val = yyjson_mut_merge_patch(doc, orig_val, patch_val); + if (!yyjson_mut_obj_add(builder, mut_key, merged_val)) return NULL; + } + + return builder; +} + +#endif /* YYJSON_DISABLE_UTILS */ diff --git a/research/experiments/003-json-fanout/vendor/yyjson/yyjson.h b/research/experiments/003-json-fanout/vendor/yyjson/yyjson.h new file mode 100644 index 0000000..5eb6d46 --- /dev/null +++ b/research/experiments/003-json-fanout/vendor/yyjson/yyjson.h @@ -0,0 +1,8230 @@ +/*============================================================================== + Copyright (c) 2020 YaoYuan + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + *============================================================================*/ + +/** + @file yyjson.h + @date 2019-03-09 + @author YaoYuan + */ + +#ifndef YYJSON_H +#define YYJSON_H + + + +/*============================================================================== + * MARK: - Header Files + *============================================================================*/ + +#include +#include +#include +#include +#include +#include + + + +/*============================================================================== + * MARK: - Compile-time Options + *============================================================================*/ + +/* + Define as 1 to disable JSON reader at compile-time. + This disables functions with "read" in their name. + Reduces binary size by about 60%. + */ +#ifndef YYJSON_DISABLE_READER +#endif + +/* + Define as 1 to disable JSON writer at compile-time. + This disables functions with "write" in their name. + Reduces binary size by about 30%. + */ +#ifndef YYJSON_DISABLE_WRITER +#endif + +/* + Define as 1 to disable JSON incremental reader at compile-time. + This disables functions with "incr" in their name. + */ +#ifndef YYJSON_DISABLE_INCR_READER +#endif + +/* + Define as 1 to disable JSON Pointer, JSON Patch and JSON Merge Patch supports. + This disables functions with "ptr" or "patch" in their name. + */ +#ifndef YYJSON_DISABLE_UTILS +#endif + +/* + Define as 1 to disable the fast floating-point number conversion in yyjson. + Libc's `strtod/snprintf` will be used instead. + + This reduces binary size by about 30%, but significantly slows down the + floating-point read/write speed. + */ +#ifndef YYJSON_DISABLE_FAST_FP_CONV +#endif + +/* + Define as 1 to disable non-standard JSON features support at compile-time, + such as YYJSON_READ_ALLOW_XXX and YYJSON_WRITE_ALLOW_XXX. + + This reduces binary size by about 10%, and slightly improves performance. + */ +#ifndef YYJSON_DISABLE_NON_STANDARD +#endif + +/* + Define as 1 to disable UTF-8 validation at compile-time. + + Use this if all input strings are guaranteed to be valid UTF-8 + (e.g. language-level String types are already validated). + + Disabling UTF-8 validation improves performance for non-ASCII strings by about + 3% to 7%. + + Note: If this flag is enabled while passing illegal UTF-8 strings, + the following errors may occur: + - Escaped characters may be ignored when parsing JSON strings. + - Ending quotes may be ignored when parsing JSON strings, causing the + string to merge with the next value. + - When serializing with `yyjson_mut_val`, the string's end may be accessed + out of bounds, potentially causing a segmentation fault. + */ +#ifndef YYJSON_DISABLE_UTF8_VALIDATION +#endif + +/* + Define as 1 to improve performance on architectures that do not support + unaligned memory access. + + Normally, this does not need to be set manually. See the C file for details. + */ +#ifndef YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS +#endif + +/* Define as 1 to export symbols when building this library as a Windows DLL. */ +#ifndef YYJSON_EXPORTS +#endif + +/* Define as 1 to import symbols when using this library as a Windows DLL. */ +#ifndef YYJSON_IMPORTS +#endif + +/* Define as 1 to include for compilers without C99 support. */ +#ifndef YYJSON_HAS_STDINT_H +#endif + +/* Define as 1 to include for compilers without C99 support. */ +#ifndef YYJSON_HAS_STDBOOL_H +#endif + + + +/*============================================================================== + * MARK: - Compiler Macros + *============================================================================*/ + +/** compiler version (MSVC) */ +#ifdef _MSC_VER +# define YYJSON_MSC_VER _MSC_VER +#else +# define YYJSON_MSC_VER 0 +#endif + +/** compiler version (GCC) */ +#ifdef __GNUC__ +# define YYJSON_GCC_VER __GNUC__ +# if defined(__GNUC_PATCHLEVEL__) +# define yyjson_gcc_available(major, minor, patch) \ + ((__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) \ + >= (major * 10000 + minor * 100 + patch)) +# else +# define yyjson_gcc_available(major, minor, patch) \ + ((__GNUC__ * 10000 + __GNUC_MINOR__ * 100) \ + >= (major * 10000 + minor * 100 + patch)) +# endif +#else +# define YYJSON_GCC_VER 0 +# define yyjson_gcc_available(major, minor, patch) 0 +#endif + +/** real gcc check */ +#if defined(__GNUC__) && defined(__GNUC_MINOR__) && \ + !defined(__clang__) && !defined(__llvm__) && \ + !defined(__INTEL_COMPILER) && !defined(__ICC) && \ + !defined(__NVCC__) && !defined(__PGI) && !defined(__TINYC__) +# define YYJSON_IS_REAL_GCC 1 +#else +# define YYJSON_IS_REAL_GCC 0 +#endif + +/** C version (STDC) */ +#if defined(__STDC__) && (__STDC__ >= 1) && defined(__STDC_VERSION__) +# define YYJSON_STDC_VER __STDC_VERSION__ +#else +# define YYJSON_STDC_VER 0 +#endif + +/** C++ version */ +#if defined(__cplusplus) +# define YYJSON_CPP_VER __cplusplus +#else +# define YYJSON_CPP_VER 0 +#endif + +/** compiler builtin check (since gcc 10.0, clang 2.6, icc 2021) */ +#ifndef yyjson_has_builtin +# ifdef __has_builtin +# define yyjson_has_builtin(x) __has_builtin(x) +# else +# define yyjson_has_builtin(x) 0 +# endif +#endif + +/** compiler attribute check (since gcc 5.0, clang 2.9, icc 17) */ +#ifndef yyjson_has_attribute +# ifdef __has_attribute +# define yyjson_has_attribute(x) __has_attribute(x) +# else +# define yyjson_has_attribute(x) 0 +# endif +#endif + +/** compiler feature check (since clang 2.6, icc 17) */ +#ifndef yyjson_has_feature +# ifdef __has_feature +# define yyjson_has_feature(x) __has_feature(x) +# else +# define yyjson_has_feature(x) 0 +# endif +#endif + +/** include check (since gcc 5.0, clang 2.7, icc 16, msvc 2017 15.3) */ +#ifndef yyjson_has_include +# ifdef __has_include +# define yyjson_has_include(x) __has_include(x) +# else +# define yyjson_has_include(x) 0 +# endif +#endif + +/** inline for compiler */ +#ifndef yyjson_inline +# if YYJSON_MSC_VER >= 1200 +# define yyjson_inline __forceinline +# elif defined(_MSC_VER) +# define yyjson_inline __inline +# elif yyjson_has_attribute(always_inline) || YYJSON_GCC_VER >= 4 +# define yyjson_inline __inline__ __attribute__((always_inline)) +# elif defined(__clang__) || defined(__GNUC__) +# define yyjson_inline __inline__ +# elif defined(__cplusplus) || YYJSON_STDC_VER >= 199901L +# define yyjson_inline inline +# else +# define yyjson_inline +# endif +#endif + +/** noinline for compiler */ +#ifndef yyjson_noinline +# if YYJSON_MSC_VER >= 1400 +# define yyjson_noinline __declspec(noinline) +# elif yyjson_has_attribute(noinline) || YYJSON_GCC_VER >= 4 +# define yyjson_noinline __attribute__((noinline)) +# else +# define yyjson_noinline +# endif +#endif + +/** align for compiler */ +#ifndef yyjson_align +# if YYJSON_MSC_VER >= 1300 +# define yyjson_align(x) __declspec(align(x)) +# elif yyjson_has_attribute(aligned) || defined(__GNUC__) +# define yyjson_align(x) __attribute__((aligned(x))) +# elif YYJSON_CPP_VER >= 201103L +# define yyjson_align(x) alignas(x) +# else +# define yyjson_align(x) +# endif +#endif + +/** likely for compiler */ +#ifndef yyjson_likely +# if yyjson_has_builtin(__builtin_expect) || \ + (YYJSON_GCC_VER >= 4 && YYJSON_GCC_VER != 5) +# define yyjson_likely(expr) __builtin_expect(!!(expr), 1) +# else +# define yyjson_likely(expr) (expr) +# endif +#endif + +/** unlikely for compiler */ +#ifndef yyjson_unlikely +# if yyjson_has_builtin(__builtin_expect) || \ + (YYJSON_GCC_VER >= 4 && YYJSON_GCC_VER != 5) +# define yyjson_unlikely(expr) __builtin_expect(!!(expr), 0) +# else +# define yyjson_unlikely(expr) (expr) +# endif +#endif + +/** compile-time constant check for compiler */ +#ifndef yyjson_constant_p +# if yyjson_has_builtin(__builtin_constant_p) || (YYJSON_GCC_VER >= 3) +# define YYJSON_HAS_CONSTANT_P 1 +# define yyjson_constant_p(value) __builtin_constant_p(value) +# else +# define YYJSON_HAS_CONSTANT_P 0 +# define yyjson_constant_p(value) 0 +# endif +#endif + +/** deprecate warning */ +#ifndef yyjson_deprecated +# if YYJSON_MSC_VER >= 1400 +# define yyjson_deprecated(msg) __declspec(deprecated(msg)) +# elif yyjson_has_feature(attribute_deprecated_with_message) || \ + (YYJSON_GCC_VER > 4 || (YYJSON_GCC_VER == 4 && __GNUC_MINOR__ >= 5)) +# define yyjson_deprecated(msg) __attribute__((deprecated(msg))) +# elif YYJSON_GCC_VER >= 3 +# define yyjson_deprecated(msg) __attribute__((deprecated)) +# else +# define yyjson_deprecated(msg) +# endif +#endif + +/** function export */ +#ifndef yyjson_api +# if defined(_WIN32) +# if defined(YYJSON_EXPORTS) && YYJSON_EXPORTS +# define yyjson_api __declspec(dllexport) +# elif defined(YYJSON_IMPORTS) && YYJSON_IMPORTS +# define yyjson_api __declspec(dllimport) +# else +# define yyjson_api +# endif +# elif yyjson_has_attribute(visibility) || YYJSON_GCC_VER >= 4 +# define yyjson_api __attribute__((visibility("default"))) +# else +# define yyjson_api +# endif +#endif + +/** inline function export */ +#ifndef yyjson_api_inline +# define yyjson_api_inline static yyjson_inline +#endif + +/** stdint (C89 compatible) */ +#if (defined(YYJSON_HAS_STDINT_H) && YYJSON_HAS_STDINT_H) || \ + YYJSON_MSC_VER >= 1600 || YYJSON_STDC_VER >= 199901L || \ + defined(_STDINT_H) || defined(_STDINT_H_) || \ + defined(__CLANG_STDINT_H) || defined(_STDINT_H_INCLUDED) || \ + yyjson_has_include() +# include +#elif defined(_MSC_VER) +# if _MSC_VER < 1300 + typedef signed char int8_t; + typedef signed short int16_t; + typedef signed int int32_t; + typedef unsigned char uint8_t; + typedef unsigned short uint16_t; + typedef unsigned int uint32_t; + typedef signed __int64 int64_t; + typedef unsigned __int64 uint64_t; +# else + typedef signed __int8 int8_t; + typedef signed __int16 int16_t; + typedef signed __int32 int32_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef signed __int64 int64_t; + typedef unsigned __int64 uint64_t; +# endif +#else +# if UCHAR_MAX == 0xFFU + typedef signed char int8_t; + typedef unsigned char uint8_t; +# else +# error cannot find 8-bit integer type +# endif +# if USHRT_MAX == 0xFFFFU + typedef unsigned short uint16_t; + typedef signed short int16_t; +# elif UINT_MAX == 0xFFFFU + typedef unsigned int uint16_t; + typedef signed int int16_t; +# else +# error cannot find 16-bit integer type +# endif +# if UINT_MAX == 0xFFFFFFFFUL + typedef unsigned int uint32_t; + typedef signed int int32_t; +# elif ULONG_MAX == 0xFFFFFFFFUL + typedef unsigned long uint32_t; + typedef signed long int32_t; +# elif USHRT_MAX == 0xFFFFFFFFUL + typedef unsigned short uint32_t; + typedef signed short int32_t; +# else +# error cannot find 32-bit integer type +# endif +# if defined(__INT64_TYPE__) && defined(__UINT64_TYPE__) + typedef __INT64_TYPE__ int64_t; + typedef __UINT64_TYPE__ uint64_t; +# elif defined(__GNUC__) || defined(__clang__) +# if !defined(_SYS_TYPES_H) && !defined(__int8_t_defined) + __extension__ typedef long long int64_t; +# endif + __extension__ typedef unsigned long long uint64_t; +# elif defined(_LONG_LONG) || defined(__MWERKS__) || defined(_CRAYC) || \ + defined(__SUNPRO_C) || defined(__SUNPRO_CC) + typedef long long int64_t; + typedef unsigned long long uint64_t; +# elif (defined(__BORLANDC__) && __BORLANDC__ > 0x460) || \ + defined(__WATCOM_INT64__) || defined (__alpha) || defined (__DECC) + typedef __int64 int64_t; + typedef unsigned __int64 uint64_t; +# else +# error cannot find 64-bit integer type +# endif +#endif + +/** stdbool (C89 compatible) */ +#if (defined(YYJSON_HAS_STDBOOL_H) && YYJSON_HAS_STDBOOL_H) || \ + (yyjson_has_include() && !defined(__STRICT_ANSI__)) || \ + YYJSON_MSC_VER >= 1800 || YYJSON_STDC_VER >= 199901L +# include +#elif !defined(__bool_true_false_are_defined) +# define __bool_true_false_are_defined 1 +# if defined(__cplusplus) +# if defined(__GNUC__) && !defined(__STRICT_ANSI__) +# define _Bool bool +# if __cplusplus < 201103L +# define bool bool +# define false false +# define true true +# endif +# endif +# else +# define bool unsigned char +# define true 1 +# define false 0 +# endif +#endif + +/** char bit check */ +#if defined(CHAR_BIT) +# if CHAR_BIT != 8 +# error non 8-bit char is not supported +# endif +#endif + +/** + Microsoft Visual C++ 6.0 doesn't support converting number from u64 to f64: + error C2520: conversion from unsigned __int64 to double not implemented. + */ +#ifndef YYJSON_U64_TO_F64_NO_IMPL +# if (0 < YYJSON_MSC_VER) && (YYJSON_MSC_VER <= 1200) +# define YYJSON_U64_TO_F64_NO_IMPL 1 +# else +# define YYJSON_U64_TO_F64_NO_IMPL 0 +# endif +#endif + + + +/*============================================================================== + * MARK: - Compile Hint Begin + *============================================================================*/ + +/* extern "C" begin */ +#ifdef __cplusplus +extern "C" { +#endif + +/* warning suppress begin */ +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wunused-parameter" +#elif defined(__GNUC__) +# if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +# pragma GCC diagnostic push +# endif +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wunused-parameter" +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable:4800) /* 'int': forcing value to 'true' or 'false' */ +#endif + + + +/*============================================================================== + * MARK: - Version + *============================================================================*/ + +/** The major version of yyjson. */ +#define YYJSON_VERSION_MAJOR 0 + +/** The minor version of yyjson. */ +#define YYJSON_VERSION_MINOR 12 + +/** The patch version of yyjson. */ +#define YYJSON_VERSION_PATCH 0 + +/** The version of yyjson in hex: `(major << 16) | (minor << 8) | (patch)`. */ +#define YYJSON_VERSION_HEX 0x000C00 + +/** The version string of yyjson. */ +#define YYJSON_VERSION_STRING "0.12.0" + +/** The version of yyjson in hex, same as `YYJSON_VERSION_HEX`. */ +yyjson_api uint32_t yyjson_version(void); + + + +/*============================================================================== + * MARK: - JSON Types + *============================================================================*/ + +/** Type of a JSON value (3 bit). */ +typedef uint8_t yyjson_type; +/** No type, invalid. */ +#define YYJSON_TYPE_NONE ((uint8_t)0) /* _____000 */ +/** Raw string type, no subtype. */ +#define YYJSON_TYPE_RAW ((uint8_t)1) /* _____001 */ +/** Null type: `null` literal, no subtype. */ +#define YYJSON_TYPE_NULL ((uint8_t)2) /* _____010 */ +/** Boolean type, subtype: TRUE, FALSE. */ +#define YYJSON_TYPE_BOOL ((uint8_t)3) /* _____011 */ +/** Number type, subtype: UINT, SINT, REAL. */ +#define YYJSON_TYPE_NUM ((uint8_t)4) /* _____100 */ +/** String type, subtype: NONE, NOESC. */ +#define YYJSON_TYPE_STR ((uint8_t)5) /* _____101 */ +/** Array type, no subtype. */ +#define YYJSON_TYPE_ARR ((uint8_t)6) /* _____110 */ +/** Object type, no subtype. */ +#define YYJSON_TYPE_OBJ ((uint8_t)7) /* _____111 */ + +/** Subtype of a JSON value (2 bit). */ +typedef uint8_t yyjson_subtype; +/** No subtype. */ +#define YYJSON_SUBTYPE_NONE ((uint8_t)(0 << 3)) /* ___00___ */ +/** False subtype: `false` literal. */ +#define YYJSON_SUBTYPE_FALSE ((uint8_t)(0 << 3)) /* ___00___ */ +/** True subtype: `true` literal. */ +#define YYJSON_SUBTYPE_TRUE ((uint8_t)(1 << 3)) /* ___01___ */ +/** Unsigned integer subtype: `uint64_t`. */ +#define YYJSON_SUBTYPE_UINT ((uint8_t)(0 << 3)) /* ___00___ */ +/** Signed integer subtype: `int64_t`. */ +#define YYJSON_SUBTYPE_SINT ((uint8_t)(1 << 3)) /* ___01___ */ +/** Real number subtype: `double`. */ +#define YYJSON_SUBTYPE_REAL ((uint8_t)(2 << 3)) /* ___10___ */ +/** String that do not need to be escaped for writing (internal use). */ +#define YYJSON_SUBTYPE_NOESC ((uint8_t)(1 << 3)) /* ___01___ */ + +/** The mask used to extract the type of a JSON value. */ +#define YYJSON_TYPE_MASK ((uint8_t)0x07) /* _____111 */ +/** The number of bits used by the type. */ +#define YYJSON_TYPE_BIT ((uint8_t)3) +/** The mask used to extract the subtype of a JSON value. */ +#define YYJSON_SUBTYPE_MASK ((uint8_t)0x18) /* ___11___ */ +/** The number of bits used by the subtype. */ +#define YYJSON_SUBTYPE_BIT ((uint8_t)2) +/** The mask used to extract the reserved bits of a JSON value. */ +#define YYJSON_RESERVED_MASK ((uint8_t)0xE0) /* 111_____ */ +/** The number of reserved bits. */ +#define YYJSON_RESERVED_BIT ((uint8_t)3) +/** The mask used to extract the tag of a JSON value. */ +#define YYJSON_TAG_MASK ((uint8_t)0xFF) /* 11111111 */ +/** The number of bits used by the tag. */ +#define YYJSON_TAG_BIT ((uint8_t)8) + +/** Padding size for JSON reader. */ +#define YYJSON_PADDING_SIZE 4 + + + +/*============================================================================== + * MARK: - Allocator + *============================================================================*/ + +/** + A memory allocator. + + Typically you don't need to use it, unless you want to customize your own + memory allocator. + */ +typedef struct yyjson_alc { + /** Same as libc's malloc(size), should not be NULL. */ + void *(*malloc)(void *ctx, size_t size); + /** Same as libc's realloc(ptr, size), should not be NULL. */ + void *(*realloc)(void *ctx, void *ptr, size_t old_size, size_t size); + /** Same as libc's free(ptr), should not be NULL. */ + void (*free)(void *ctx, void *ptr); + /** A context for malloc/realloc/free, can be NULL. */ + void *ctx; +} yyjson_alc; + +/** + A pool allocator uses fixed length pre-allocated memory. + + This allocator may be used to avoid malloc/realloc calls. The pre-allocated + memory should be held by the caller. The maximum amount of memory required to + read a JSON can be calculated using the `yyjson_read_max_memory_usage()` + function, but the amount of memory required to write a JSON cannot be directly + calculated. + + This is not a general-purpose allocator. It is designed to handle a single JSON + data at a time. If it is used for overly complex memory tasks, such as parsing + multiple JSON documents using the same allocator but releasing only a few of + them, it may cause memory fragmentation, resulting in performance degradation + and memory waste. + + @param alc The allocator to be initialized. + If this parameter is NULL, the function will fail and return false. + If `buf` or `size` is invalid, this will be set to an empty allocator. + @param buf The buffer memory for this allocator. + If this parameter is NULL, the function will fail and return false. + @param size The size of `buf`, in bytes. + If this parameter is less than 8 words (32/64 bytes on 32/64-bit OS), the + function will fail and return false. + @return true if the `alc` has been successfully initialized. + + @b Example + @code + // parse JSON with stack memory + char buf[1024]; + yyjson_alc alc; + yyjson_alc_pool_init(&alc, buf, 1024); + + const char *json = "{\"name\":\"Helvetica\",\"size\":16}" + yyjson_doc *doc = yyjson_read_opts(json, strlen(json), 0, &alc, NULL); + // the memory of `doc` is on the stack + @endcode + + @warning This Allocator is not thread-safe. + */ +yyjson_api bool yyjson_alc_pool_init(yyjson_alc *alc, void *buf, size_t size); + +/** + A dynamic allocator. + + This allocator has a similar usage to the pool allocator above. However, when + there is not enough memory, this allocator will dynamically request more memory + using libc's `malloc` function, and frees it all at once when it is destroyed. + + @return A new dynamic allocator, or NULL if memory allocation failed. + @note The returned value should be freed with `yyjson_alc_dyn_free()`. + + @warning This Allocator is not thread-safe. + */ +yyjson_api yyjson_alc *yyjson_alc_dyn_new(void); + +/** + Free a dynamic allocator which is created by `yyjson_alc_dyn_new()`. + @param alc The dynamic allocator to be destroyed. + */ +yyjson_api void yyjson_alc_dyn_free(yyjson_alc *alc); + + + +/*============================================================================== + * MARK: - Text Locating + *============================================================================*/ + +/** + Locate the line and column number for a byte position in a string. + This can be used to get better description for error position. + + @param str The input string. + @param len The byte length of the input string. + @param pos The byte position within the input string. + @param line A pointer to receive the line number, starting from 1. + @param col A pointer to receive the column number, starting from 1. + @param chr A pointer to receive the character index, starting from 0. + @return true on success, false if `str` is NULL or `pos` is out of bounds. + @note Line/column/character are calculated based on Unicode characters for + compatibility with text editors. For multi-byte UTF-8 characters, + the returned value may not directly correspond to the byte position. + */ +yyjson_api bool yyjson_locate_pos(const char *str, size_t len, size_t pos, + size_t *line, size_t *col, size_t *chr); + + + +/*============================================================================== + * MARK: - JSON Structure + *============================================================================*/ + +/** + An immutable document for reading JSON. + This document holds memory for all its JSON values and strings. When it is no + longer used, the user should call `yyjson_doc_free()` to free its memory. + */ +typedef struct yyjson_doc yyjson_doc; + +/** + An immutable value for reading JSON. + A JSON Value has the same lifetime as its document. The memory is held by its + document and and cannot be freed alone. + */ +typedef struct yyjson_val yyjson_val; + +/** + A mutable document for building JSON. + This document holds memory for all its JSON values and strings. When it is no + longer used, the user should call `yyjson_mut_doc_free()` to free its memory. + */ +typedef struct yyjson_mut_doc yyjson_mut_doc; + +/** + A mutable value for building JSON. + A JSON Value has the same lifetime as its document. The memory is held by its + document and and cannot be freed alone. + */ +typedef struct yyjson_mut_val yyjson_mut_val; + + + +/*============================================================================== + * MARK: - JSON Reader API + *============================================================================*/ + +/** Run-time options for JSON reader. */ +typedef uint32_t yyjson_read_flag; + +/** Default option (RFC 8259 compliant): + - Read positive integer as uint64_t. + - Read negative integer as int64_t. + - Read floating-point number as double with round-to-nearest mode. + - Read integer which cannot fit in uint64_t or int64_t as double. + - Report error if double number is infinity. + - Report error if string contains invalid UTF-8 character or BOM. + - Report error on trailing commas, comments, inf and nan literals. */ +static const yyjson_read_flag YYJSON_READ_NOFLAG = 0; + +/** Read the input data in-situ. + This option allows the reader to modify and use input data to store string + values, which can increase reading speed slightly. + The caller should hold the input data before free the document. + The input data must be padded by at least `YYJSON_PADDING_SIZE` bytes. + For example: `[1,2]` should be `[1,2]\0\0\0\0`, input length should be 5. */ +static const yyjson_read_flag YYJSON_READ_INSITU = 1 << 0; + +/** Stop when done instead of issuing an error if there's additional content + after a JSON document. This option may be used to parse small pieces of JSON + in larger data, such as `NDJSON`. */ +static const yyjson_read_flag YYJSON_READ_STOP_WHEN_DONE = 1 << 1; + +/** Allow single trailing comma at the end of an object or array, + such as `[1,2,3,]`, `{"a":1,"b":2,}` (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_TRAILING_COMMAS = 1 << 2; + +/** Allow C-style single-line and mult-line comments (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_COMMENTS = 1 << 3; + +/** Allow inf/nan number and literal, case-insensitive, + such as 1e999, NaN, inf, -Infinity (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_INF_AND_NAN = 1 << 4; + +/** Read all numbers as raw strings (value with `YYJSON_TYPE_RAW` type), + inf/nan literal is also read as raw with `ALLOW_INF_AND_NAN` flag. */ +static const yyjson_read_flag YYJSON_READ_NUMBER_AS_RAW = 1 << 5; + +/** Allow reading invalid unicode when parsing string values (non-standard). + Invalid characters will be allowed to appear in the string values, but + invalid escape sequences will still be reported as errors. + This flag does not affect the performance of correctly encoded strings. + + @warning Strings in JSON values may contain incorrect encoding when this + option is used, you need to handle these strings carefully to avoid security + risks. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_INVALID_UNICODE = 1 << 6; + +/** Read big numbers as raw strings. These big numbers include integers that + cannot be represented by `int64_t` and `uint64_t`, and floating-point + numbers that cannot be represented by finite `double`. + The flag will be overridden by `YYJSON_READ_NUMBER_AS_RAW` flag. */ +static const yyjson_read_flag YYJSON_READ_BIGNUM_AS_RAW = 1 << 7; + +/** Allow UTF-8 BOM and skip it before parsing if any (non-standard). */ +static const yyjson_read_flag YYJSON_READ_ALLOW_BOM = 1 << 8; + +/** Allow extended number formats (non-standard): + - Hexadecimal numbers, such as `0x7B`. + - Numbers with leading or trailing decimal point, such as `.123`, `123.`. + - Numbers with a leading plus sign, such as `+123`. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_EXT_NUMBER = 1 << 9; + +/** Allow extended escape sequences in strings (non-standard): + - Additional escapes: `\a`, `\e`, `\v`, ``\'``, `\?`, `\0`. + - Hex escapes: `\xNN`, such as `\x7B`. + - Line continuation: backslash followed by line terminator sequences. + - Unknown escape: if backslash is followed by an unsupported character, + the backslash will be removed and the character will be kept as-is. + However, `\1`-`\9` will still trigger an error. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_EXT_ESCAPE = 1 << 10; + +/** Allow extended whitespace characters (non-standard): + - Vertical tab `\v` and form feed `\f`. + - Line separator `\u2028` and paragraph separator `\u2029`. + - Non-breaking space `\xA0`. + - Byte order mark: `\uFEFF`. + - Other Unicode characters in the Zs (Separator, space) category. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_EXT_WHITESPACE = 1 << 11; + +/** Allow strings enclosed in single quotes (non-standard), such as ``'ab'``. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_SINGLE_QUOTED_STR = 1 << 12; + +/** Allow object keys without quotes (non-standard), such as `{a:1,b:2}`. + This extends the ECMAScript IdentifierName rule by allowing any + non-whitespace character with code point above `U+007F`. */ +static const yyjson_read_flag YYJSON_READ_ALLOW_UNQUOTED_KEY = 1 << 13; + +/** Allow JSON5 format, see: [https://json5.org]. + This flag supports all JSON5 features with some additional extensions: + - Accepts more escape sequences than JSON5 (e.g. `\a`, `\e`). + - Unquoted keys are not limited to ECMAScript IdentifierName. + - Allow case-insensitive `NaN`, `Inf` and `Infinity` literals. */ +static const yyjson_read_flag YYJSON_READ_JSON5 = + (1 << 2) | /* YYJSON_READ_ALLOW_TRAILING_COMMAS */ + (1 << 3) | /* YYJSON_READ_ALLOW_COMMENTS */ + (1 << 4) | /* YYJSON_READ_ALLOW_INF_AND_NAN */ + (1 << 9) | /* YYJSON_READ_ALLOW_EXT_NUMBER */ + (1 << 10) | /* YYJSON_READ_ALLOW_EXT_ESCAPE */ + (1 << 11) | /* YYJSON_READ_ALLOW_EXT_WHITESPACE */ + (1 << 12) | /* YYJSON_READ_ALLOW_SINGLE_QUOTED_STR */ + (1 << 13); /* YYJSON_READ_ALLOW_UNQUOTED_KEY */ + + + +/** Result code for JSON reader. */ +typedef uint32_t yyjson_read_code; + +/** Success, no error. */ +static const yyjson_read_code YYJSON_READ_SUCCESS = 0; + +/** Invalid parameter, such as NULL input string or 0 input length. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_PARAMETER = 1; + +/** Memory allocation failed. */ +static const yyjson_read_code YYJSON_READ_ERROR_MEMORY_ALLOCATION = 2; + +/** Input JSON string is empty. */ +static const yyjson_read_code YYJSON_READ_ERROR_EMPTY_CONTENT = 3; + +/** Unexpected content after document, such as `[123]abc`. */ +static const yyjson_read_code YYJSON_READ_ERROR_UNEXPECTED_CONTENT = 4; + +/** Unexpected end of input, the parsed part is valid, such as `[123`. */ +static const yyjson_read_code YYJSON_READ_ERROR_UNEXPECTED_END = 5; + +/** Unexpected character inside the document, such as `[abc]`. */ +static const yyjson_read_code YYJSON_READ_ERROR_UNEXPECTED_CHARACTER = 6; + +/** Invalid JSON structure, such as `[1,]`. */ +static const yyjson_read_code YYJSON_READ_ERROR_JSON_STRUCTURE = 7; + +/** Invalid comment, deprecated, use `UNEXPECTED_END` for unclosed comment. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_COMMENT = 8; + +/** Invalid number, such as `123.e12`, `000`. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_NUMBER = 9; + +/** Invalid string, such as invalid escaped character inside a string. */ +static const yyjson_read_code YYJSON_READ_ERROR_INVALID_STRING = 10; + +/** Invalid JSON literal, such as `truu`. */ +static const yyjson_read_code YYJSON_READ_ERROR_LITERAL = 11; + +/** Failed to open a file. */ +static const yyjson_read_code YYJSON_READ_ERROR_FILE_OPEN = 12; + +/** Failed to read a file. */ +static const yyjson_read_code YYJSON_READ_ERROR_FILE_READ = 13; + +/** Incomplete input during incremental parsing; parsing state is preserved. */ +static const yyjson_read_code YYJSON_READ_ERROR_MORE = 14; + +/** Error information for JSON reader. */ +typedef struct yyjson_read_err { + /** Error code, see `yyjson_read_code` for all possible values. */ + yyjson_read_code code; + /** Error message, constant, no need to free (NULL if success). */ + const char *msg; + /** Error byte position for input data (0 if success). */ + size_t pos; +} yyjson_read_err; + + + +#if !defined(YYJSON_DISABLE_READER) || !YYJSON_DISABLE_READER + +/** + Read JSON with options. + + This function is thread-safe when: + 1. The `dat` is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param dat The JSON data (UTF-8 without BOM), null-terminator is not required. + If this parameter is NULL, the function will fail and return NULL. + The `dat` will not be modified without the flag `YYJSON_READ_INSITU`, so you + can pass a `const char *` string and case it to `char *` if you don't use + the `YYJSON_READ_INSITU` flag. + @param len The length of JSON data in bytes. + If this parameter is 0, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + */ +yyjson_api yyjson_doc *yyjson_read_opts(char *dat, + size_t len, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** + Read a JSON file. + + This function is thread-safe when: + 1. The file is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + + @warning On 32-bit operating system, files larger than 2GB may fail to read. + */ +yyjson_api yyjson_doc *yyjson_read_file(const char *path, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** + Read JSON from a file pointer. + + @param fp The file pointer. + The data will be read from the current position of the FILE to the end. + If this fp is NULL or invalid, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + + @warning On 32-bit operating system, files larger than 2GB may fail to read. + */ +yyjson_api yyjson_doc *yyjson_read_fp(FILE *fp, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** + Read a JSON string. + + This function is thread-safe. + + @param dat The JSON data (UTF-8 without BOM), null-terminator is not required. + If this parameter is NULL, the function will fail and return NULL. + @param len The length of JSON data in bytes. + If this parameter is 0, the function will fail and return NULL. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + @return A new JSON document, or NULL if an error occurs. + When it's no longer needed, it should be freed with `yyjson_doc_free()`. + */ +yyjson_api_inline yyjson_doc *yyjson_read(const char *dat, + size_t len, + yyjson_read_flag flg) { + flg &= ~YYJSON_READ_INSITU; /* const string cannot be modified */ + return yyjson_read_opts((char *)(void *)(size_t)(const void *)dat, + len, flg, NULL, NULL); +} + + + +#if !defined(YYJSON_DISABLE_INCR_READER) || !YYJSON_DISABLE_INCR_READER + +/** Opaque state for incremental JSON reader. */ +typedef struct yyjson_incr_state yyjson_incr_state; + +/** + Initialize state for incremental read. + + To read a large JSON document incrementally: + 1. Call `yyjson_incr_new()` to create the state for incremental reading. + 2. Call `yyjson_incr_read()` repeatedly. + 3. Call `yyjson_incr_free()` to free the state. + + Note: The incremental JSON reader only supports standard JSON. + Flags for non-standard features (e.g. comments, trailing commas) are ignored. + + @param buf The JSON data, null-terminator is not required. + If this parameter is NULL, the function will fail and return NULL. + @param buf_len The length of the JSON data in `buf`. + If use `YYJSON_READ_INSITU`, `buf_len` should not include the padding size. + @param flg The JSON read options. + Multiple options can be combined with `|` operator. + @param alc The memory allocator used by JSON reader. + Pass NULL to use the libc's default allocator. + @return A state for incremental reading. + It should be freed with `yyjson_incr_free()`. + NULL is returned if memory allocation fails. +*/ +yyjson_api yyjson_incr_state *yyjson_incr_new(char *buf, size_t buf_len, + yyjson_read_flag flg, + const yyjson_alc *alc); + +/** + Performs incremental read of up to `len` bytes. + + If NULL is returned and `err->code` is set to `YYJSON_READ_ERROR_MORE`, it + indicates that more data is required to continue parsing. Then, call this + function again with incremented `len`. Continue until a document is returned or + an error other than `YYJSON_READ_ERROR_MORE` is returned. + + Note: Parsing in very small increments is not efficient. An increment of + several kilobytes or megabytes is recommended. + + @param state The state for incremental reading, created using + `yyjson_incr_new()`. + @param len The number of bytes of JSON data available to parse. + If this parameter is 0, the function will fail and return NULL. + @param err A pointer to receive error information. + @return A new JSON document, or NULL if an error occurs. + When the document is no longer needed, it should be freed with + `yyjson_doc_free()`. +*/ +yyjson_api yyjson_doc *yyjson_incr_read(yyjson_incr_state *state, size_t len, + yyjson_read_err *err); + +/** Release the incremental read state and free the memory. */ +yyjson_api void yyjson_incr_free(yyjson_incr_state *state); + +#endif /* YYJSON_DISABLE_INCR_READER */ + +/** + Returns the size of maximum memory usage to read a JSON data. + + You may use this value to avoid malloc() or calloc() call inside the reader + to get better performance, or read multiple JSON with one piece of memory. + + @param len The length of JSON data in bytes. + @param flg The JSON read options. + @return The maximum memory size to read this JSON, or 0 if overflow. + + @b Example + @code + // read multiple JSON with same pre-allocated memory + + char *dat1, *dat2, *dat3; // JSON data + size_t len1, len2, len3; // JSON length + size_t max_len = MAX(len1, MAX(len2, len3)); + yyjson_doc *doc; + + // use one allocator for multiple JSON + size_t size = yyjson_read_max_memory_usage(max_len, 0); + void *buf = malloc(size); + yyjson_alc alc; + yyjson_alc_pool_init(&alc, buf, size); + + // no more alloc() or realloc() call during reading + doc = yyjson_read_opts(dat1, len1, 0, &alc, NULL); + yyjson_doc_free(doc); + doc = yyjson_read_opts(dat2, len2, 0, &alc, NULL); + yyjson_doc_free(doc); + doc = yyjson_read_opts(dat3, len3, 0, &alc, NULL); + yyjson_doc_free(doc); + + free(buf); + @endcode + @see yyjson_alc_pool_init() + */ +yyjson_api_inline size_t yyjson_read_max_memory_usage(size_t len, + yyjson_read_flag flg) { + /* + 1. The max value count is (json_size / 2 + 1), + for example: "[1,2,3,4]" size is 9, value count is 5. + 2. Some broken JSON may cost more memory during reading, but fail at end, + for example: "[[[[[[[[". + 3. yyjson use 16 bytes per value, see struct yyjson_val. + 4. yyjson use dynamic memory with a growth factor of 1.5. + + The max memory size is (json_size / 2 * 16 * 1.5 + padding). + */ + size_t mul = (size_t)12 + !(flg & YYJSON_READ_INSITU); + size_t pad = 256; + size_t max = (size_t)(~(size_t)0); + if (flg & YYJSON_READ_STOP_WHEN_DONE) len = len < 256 ? 256 : len; + if (len >= (max - pad - mul) / mul) return 0; + return len * mul + pad; +} + +/** + Read a JSON number. + + This function is thread-safe when data is not modified by other threads. + + @param dat The JSON data (UTF-8 without BOM), null-terminator is required. + If this parameter is NULL, the function will fail and return NULL. + @param val The output value where result is stored. + If this parameter is NULL, the function will fail and return NULL. + The value will hold either UINT or SINT or REAL number; + @param flg The JSON read options. + Multiple options can be combined with `|` operator. 0 means no options. + Supports `YYJSON_READ_NUMBER_AS_RAW` and `YYJSON_READ_ALLOW_INF_AND_NAN`. + @param alc The memory allocator used for long number. + It is only used when the built-in floating point reader is disabled. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return If successful, a pointer to the character after the last character + used in the conversion, NULL if an error occurs. + */ +yyjson_api const char *yyjson_read_number(const char *dat, + yyjson_val *val, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err); + +/** Same as `yyjson_read_number()`. */ +yyjson_api_inline const char *yyjson_mut_read_number(const char *dat, + yyjson_mut_val *val, + yyjson_read_flag flg, + const yyjson_alc *alc, + yyjson_read_err *err) { + return yyjson_read_number(dat, (yyjson_val *)val, flg, alc, err); +} + +#endif /* YYJSON_DISABLE_READER) */ + + + +/*============================================================================== + * MARK: - JSON Writer API + *============================================================================*/ + +/** Run-time options for JSON writer. */ +typedef uint32_t yyjson_write_flag; + +/** Default option: + - Write JSON minify. + - Report error on inf or nan number. + - Report error on invalid UTF-8 string. + - Do not escape unicode or slash. */ +static const yyjson_write_flag YYJSON_WRITE_NOFLAG = 0; + +/** Write JSON pretty with 4 space indent. */ +static const yyjson_write_flag YYJSON_WRITE_PRETTY = 1 << 0; + +/** Escape unicode as `uXXXX`, make the output ASCII only. */ +static const yyjson_write_flag YYJSON_WRITE_ESCAPE_UNICODE = 1 << 1; + +/** Escape '/' as '\/'. */ +static const yyjson_write_flag YYJSON_WRITE_ESCAPE_SLASHES = 1 << 2; + +/** Write inf and nan number as 'Infinity' and 'NaN' literal (non-standard). */ +static const yyjson_write_flag YYJSON_WRITE_ALLOW_INF_AND_NAN = 1 << 3; + +/** Write inf and nan number as null literal. + This flag will override `YYJSON_WRITE_ALLOW_INF_AND_NAN` flag. */ +static const yyjson_write_flag YYJSON_WRITE_INF_AND_NAN_AS_NULL = 1 << 4; + +/** Allow invalid unicode when encoding string values (non-standard). + Invalid characters in string value will be copied byte by byte. + If `YYJSON_WRITE_ESCAPE_UNICODE` flag is also set, invalid character will be + escaped as `U+FFFD` (replacement character). + This flag does not affect the performance of correctly encoded strings. */ +static const yyjson_write_flag YYJSON_WRITE_ALLOW_INVALID_UNICODE = 1 << 5; + +/** Write JSON pretty with 2 space indent. + This flag will override `YYJSON_WRITE_PRETTY` flag. */ +static const yyjson_write_flag YYJSON_WRITE_PRETTY_TWO_SPACES = 1 << 6; + +/** Adds a newline character `\n` at the end of the JSON. + This can be helpful for text editors or NDJSON. */ +static const yyjson_write_flag YYJSON_WRITE_NEWLINE_AT_END = 1 << 7; + + + +/** The highest 8 bits of `yyjson_write_flag` and real number value's `tag` + are reserved for controlling the output format of floating-point numbers. */ +#define YYJSON_WRITE_FP_FLAG_BITS 8 + +/** The highest 4 bits of flag are reserved for precision value. */ +#define YYJSON_WRITE_FP_PREC_BITS 4 + +/** Write floating-point number using fixed-point notation. + - This is similar to ECMAScript `Number.prototype.toFixed(prec)`, + but with trailing zeros removed. The `prec` ranges from 1 to 15. + - This will produce shorter output but may lose some precision. */ +#define YYJSON_WRITE_FP_TO_FIXED(prec) ((yyjson_write_flag)( \ + (uint32_t)((uint32_t)(prec)) << (32 - 4) )) + +/** Write floating-point numbers using single-precision (float). + - This casts `double` to `float` before serialization. + - This will produce shorter output, but may lose some precision. + - This flag is ignored if `YYJSON_WRITE_FP_TO_FIXED(prec)` is also used. */ +#define YYJSON_WRITE_FP_TO_FLOAT ((yyjson_write_flag)(1 << (32 - 5))) + + + +/** Result code for JSON writer */ +typedef uint32_t yyjson_write_code; + +/** Success, no error. */ +static const yyjson_write_code YYJSON_WRITE_SUCCESS = 0; + +/** Invalid parameter, such as NULL document. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_PARAMETER = 1; + +/** Memory allocation failure occurs. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_MEMORY_ALLOCATION = 2; + +/** Invalid value type in JSON document. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_VALUE_TYPE = 3; + +/** NaN or Infinity number occurs. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_NAN_OR_INF = 4; + +/** Failed to open a file. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_FILE_OPEN = 5; + +/** Failed to write a file. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_FILE_WRITE = 6; + +/** Invalid unicode in string. */ +static const yyjson_write_code YYJSON_WRITE_ERROR_INVALID_STRING = 7; + +/** Error information for JSON writer. */ +typedef struct yyjson_write_err { + /** Error code, see `yyjson_write_code` for all possible values. */ + yyjson_write_code code; + /** Error message, constant, no need to free (NULL if success). */ + const char *msg; +} yyjson_write_err; + + + +#if !defined(YYJSON_DISABLE_WRITER) || !YYJSON_DISABLE_WRITER + +/*============================================================================== + * MARK: - JSON Document Writer API + *============================================================================*/ + +/** + Write a document to JSON string with options. + + This function is thread-safe when: + The `alc` is thread-safe or NULL. + + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_write_opts(const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a document to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_write_file(const char *path, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to file pointer with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this fp is NULL or invalid, the function will fail and return false. + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_write_fp(FILE *fp, + const yyjson_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to JSON string. + + This function is thread-safe. + + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_write(const yyjson_doc *doc, + yyjson_write_flag flg, + size_t *len) { + return yyjson_write_opts(doc, flg, NULL, len, NULL); +} + + + +/** + Write a document to JSON string with options. + + This function is thread-safe when: + 1. The `doc` is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param doc The mutable JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_mut_write_opts(const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a document to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `doc` is not modified by other threads. + 3. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param doc The mutable JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_write_file(const char *path, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to file pointer with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this fp is NULL or invalid, the function will fail and return false. + @param doc The mutable JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_write_fp(FILE *fp, + const yyjson_mut_doc *doc, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a document to JSON string. + + This function is thread-safe when: + The `doc` is not modified by other threads. + + @param doc The JSON document. + If this doc is NULL or has no root, the function will fail and return false. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_mut_write(const yyjson_mut_doc *doc, + yyjson_write_flag flg, + size_t *len) { + return yyjson_mut_write_opts(doc, flg, NULL, len, NULL); +} + + + +/*============================================================================== + * MARK: - JSON Value Writer API + *============================================================================*/ + +/** + Write a value to JSON string with options. + + This function is thread-safe when: + The `alc` is thread-safe or NULL. + + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_val_write_opts(const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a value to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_val_write_file(const char *path, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to file pointer with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this path is NULL or invalid, the function will fail and return false. + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_val_write_fp(FILE *fp, + const yyjson_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to JSON string. + + This function is thread-safe. + + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_val_write(const yyjson_val *val, + yyjson_write_flag flg, + size_t *len) { + return yyjson_val_write_opts(val, flg, NULL, len, NULL); +} + +/** + Write a value to JSON string with options. + + This function is thread-safe when: + 1. The `val` is not modified by other threads. + 2. The `alc` is thread-safe or NULL. + + @param val The mutable JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free() or alc->free(). + */ +yyjson_api char *yyjson_mut_val_write_opts(const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + size_t *len, + yyjson_write_err *err); + +/** + Write a value to JSON file with options. + + This function is thread-safe when: + 1. The file is not accessed by other threads. + 2. The `val` is not modified by other threads. + 3. The `alc` is thread-safe or NULL. + + @param path The JSON file's path. + This should be a null-terminated string using the system's native encoding. + If this path is NULL or invalid, the function will fail and return false. + If this file is not empty, the content will be discarded. + @param val The mutable JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_val_write_file(const char *path, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to JSON file with options. + + @param fp The file pointer. + The data will be written to the current position of the file. + If this path is NULL or invalid, the function will fail and return false. + @param val The mutable JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param alc The memory allocator used by JSON writer. + Pass NULL to use the libc's default allocator. + @param err A pointer to receive error information. + Pass NULL if you don't need error information. + @return true if successful, false if an error occurs. + + @warning On 32-bit operating system, files larger than 2GB may fail to write. + */ +yyjson_api bool yyjson_mut_val_write_fp(FILE *fp, + const yyjson_mut_val *val, + yyjson_write_flag flg, + const yyjson_alc *alc, + yyjson_write_err *err); + +/** + Write a value to JSON string. + + This function is thread-safe when: + The `val` is not modified by other threads. + + @param val The JSON root value. + If this parameter is NULL, the function will fail and return NULL. + @param flg The JSON write options. + Multiple options can be combined with `|` operator. 0 means no options. + @param len A pointer to receive output length in bytes (not including the + null-terminator). Pass NULL if you don't need length information. + @return A new JSON string, or NULL if an error occurs. + This string is encoded as UTF-8 with a null-terminator. + When it's no longer needed, it should be freed with free(). + */ +yyjson_api_inline char *yyjson_mut_val_write(const yyjson_mut_val *val, + yyjson_write_flag flg, + size_t *len) { + return yyjson_mut_val_write_opts(val, flg, NULL, len, NULL); +} + +/** + Write a JSON number. + + @param val A JSON number value to be converted to a string. + If this parameter is invalid, the function will fail and return NULL. + @param buf A buffer to store the resulting null-terminated string. + If this parameter is NULL, the function will fail and return NULL. + For integer values, the buffer must be at least 21 bytes. + For floating-point values, the buffer must be at least 40 bytes. + @return On success, returns a pointer to the character after the last + written character. On failure, returns NULL. + @note + - This function is thread-safe and does not allocate memory + (when `YYJSON_DISABLE_FAST_FP_CONV` is not defined). + - This function will fail and return NULL only in the following cases: + 1) `val` or `buf` is NULL; + 2) `val` is not a number type; + 3) `val` is `inf` or `nan`, and non-standard JSON is explicitly disabled + via the `YYJSON_DISABLE_NON_STANDARD` flag. + */ +yyjson_api char *yyjson_write_number(const yyjson_val *val, char *buf); + +/** Same as `yyjson_write_number()`. */ +yyjson_api_inline char *yyjson_mut_write_number(const yyjson_mut_val *val, + char *buf) { + return yyjson_write_number((const yyjson_val *)val, buf); +} + +#endif /* YYJSON_DISABLE_WRITER */ + + + +/*============================================================================== + * MARK: - JSON Document API + *============================================================================*/ + +/** Returns the root value of this JSON document. + Returns NULL if `doc` is NULL. */ +yyjson_api_inline yyjson_val *yyjson_doc_get_root(yyjson_doc *doc); + +/** Returns read size of input JSON data. + Returns 0 if `doc` is NULL. + For example: the read size of `[1,2,3]` is 7 bytes. */ +yyjson_api_inline size_t yyjson_doc_get_read_size(yyjson_doc *doc); + +/** Returns total value count in this JSON document. + Returns 0 if `doc` is NULL. + For example: the value count of `[1,2,3]` is 4. */ +yyjson_api_inline size_t yyjson_doc_get_val_count(yyjson_doc *doc); + +/** Release the JSON document and free the memory. + After calling this function, the `doc` and all values from the `doc` are no + longer available. This function will do nothing if the `doc` is NULL. */ +yyjson_api_inline void yyjson_doc_free(yyjson_doc *doc); + + + +/*============================================================================== + * MARK: - JSON Value Type API + *============================================================================*/ + +/** Returns whether the JSON value is raw. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_raw(yyjson_val *val); + +/** Returns whether the JSON value is `null`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_null(yyjson_val *val); + +/** Returns whether the JSON value is `true`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_true(yyjson_val *val); + +/** Returns whether the JSON value is `false`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_false(yyjson_val *val); + +/** Returns whether the JSON value is bool (true/false). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_bool(yyjson_val *val); + +/** Returns whether the JSON value is unsigned integer (uint64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_uint(yyjson_val *val); + +/** Returns whether the JSON value is signed integer (int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_sint(yyjson_val *val); + +/** Returns whether the JSON value is integer (uint64_t/int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_int(yyjson_val *val); + +/** Returns whether the JSON value is real number (double). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_real(yyjson_val *val); + +/** Returns whether the JSON value is number (uint64_t/int64_t/double). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_num(yyjson_val *val); + +/** Returns whether the JSON value is string. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_str(yyjson_val *val); + +/** Returns whether the JSON value is array. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_arr(yyjson_val *val); + +/** Returns whether the JSON value is object. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_obj(yyjson_val *val); + +/** Returns whether the JSON value is container (array/object). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val); + + + +/*============================================================================== + * MARK: - JSON Value Content API + *============================================================================*/ + +/** Returns the JSON value's type. + Returns YYJSON_TYPE_NONE if `val` is NULL. */ +yyjson_api_inline yyjson_type yyjson_get_type(yyjson_val *val); + +/** Returns the JSON value's subtype. + Returns YYJSON_SUBTYPE_NONE if `val` is NULL. */ +yyjson_api_inline yyjson_subtype yyjson_get_subtype(yyjson_val *val); + +/** Returns the JSON value's tag. + Returns 0 if `val` is NULL. */ +yyjson_api_inline uint8_t yyjson_get_tag(yyjson_val *val); + +/** Returns the JSON value's type description. + The return value should be one of these strings: "raw", "null", "string", + "array", "object", "true", "false", "uint", "sint", "real", "unknown". */ +yyjson_api_inline const char *yyjson_get_type_desc(yyjson_val *val); + +/** Returns the content if the value is raw. + Returns NULL if `val` is NULL or type is not raw. */ +yyjson_api_inline const char *yyjson_get_raw(yyjson_val *val); + +/** Returns the content if the value is bool. + Returns false if `val` is NULL or type is not bool. */ +yyjson_api_inline bool yyjson_get_bool(yyjson_val *val); + +/** Returns the content and cast to uint64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline uint64_t yyjson_get_uint(yyjson_val *val); + +/** Returns the content and cast to int64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int64_t yyjson_get_sint(yyjson_val *val); + +/** Returns the content and cast to int. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int yyjson_get_int(yyjson_val *val); + +/** Returns the content if the value is real number, or 0.0 on error. + Returns 0.0 if `val` is NULL or type is not real(double). */ +yyjson_api_inline double yyjson_get_real(yyjson_val *val); + +/** Returns the content and typecast to `double` if the value is number. + Returns 0.0 if `val` is NULL or type is not number(uint/sint/real). */ +yyjson_api_inline double yyjson_get_num(yyjson_val *val); + +/** Returns the content if the value is string. + Returns NULL if `val` is NULL or type is not string. */ +yyjson_api_inline const char *yyjson_get_str(yyjson_val *val); + +/** Returns the content length (string length, array size, object size. + Returns 0 if `val` is NULL or type is not string/array/object. */ +yyjson_api_inline size_t yyjson_get_len(yyjson_val *val); + +/** Returns whether the JSON value is equals to a string. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_equals_str(yyjson_val *val, const char *str); + +/** Returns whether the JSON value is equals to a string. + The `str` should be a UTF-8 string, null-terminator is not required. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_equals_strn(yyjson_val *val, const char *str, + size_t len); + +/** Returns whether two JSON values are equal (deep compare). + Returns false if input is NULL. + @note the result may be inaccurate if object has duplicate keys. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api_inline bool yyjson_equals(yyjson_val *lhs, yyjson_val *rhs); + +/** Set the value to raw. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_raw(yyjson_val *val, + const char *raw, size_t len); + +/** Set the value to null. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_null(yyjson_val *val); + +/** Set the value to bool. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_bool(yyjson_val *val, bool num); + +/** Set the value to uint. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_uint(yyjson_val *val, uint64_t num); + +/** Set the value to sint. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_sint(yyjson_val *val, int64_t num); + +/** Set the value to int. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int num); + +/** Set the value to float. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_float(yyjson_val *val, float num); + +/** Set the value to double. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_double(yyjson_val *val, double num); + +/** Set the value to real. + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_real(yyjson_val *val, double num); + +/** Set the floating-point number's output format to fixed-point notation. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FIXED flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_fp_to_fixed(yyjson_val *val, int prec); + +/** Set the floating-point number's output format to single-precision. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FLOAT flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_fp_to_float(yyjson_val *val, bool flt); + +/** Set the value to string (null-terminated). + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_str(yyjson_val *val, const char *str); + +/** Set the value to string (with length). + Returns false if input is NULL or `val` is object or array. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_strn(yyjson_val *val, + const char *str, size_t len); + +/** Marks this string as not needing to be escaped during JSON writing. + This can be used to avoid the overhead of escaping if the string contains + only characters that do not require escaping. + Returns false if input is NULL or `val` is not string. + @see YYJSON_SUBTYPE_NOESC subtype. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_set_str_noesc(yyjson_val *val, bool noesc); + + + +/*============================================================================== + * MARK: - JSON Array API + *============================================================================*/ + +/** Returns the number of elements in this array. + Returns 0 if `arr` is NULL or type is not array. */ +yyjson_api_inline size_t yyjson_arr_size(yyjson_val *arr); + +/** Returns the element at the specified position in this array. + Returns NULL if array is NULL/empty or the index is out of bounds. + @warning This function takes a linear search time if array is not flat. + For example: `[1,{},3]` is flat, `[1,[2],3]` is not flat. */ +yyjson_api_inline yyjson_val *yyjson_arr_get(yyjson_val *arr, size_t idx); + +/** Returns the first element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. */ +yyjson_api_inline yyjson_val *yyjson_arr_get_first(yyjson_val *arr); + +/** Returns the last element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. + @warning This function takes a linear search time if array is not flat. + For example: `[1,{},3]` is flat, `[1,[2],3]` is not flat.*/ +yyjson_api_inline yyjson_val *yyjson_arr_get_last(yyjson_val *arr); + + + +/*============================================================================== + * MARK: - JSON Array Iterator API + *============================================================================*/ + +/** + A JSON array iterator. + + @b Example + @code + yyjson_val *val; + yyjson_arr_iter iter = yyjson_arr_iter_with(arr); + while ((val = yyjson_arr_iter_next(&iter))) { + your_func(val); + } + @endcode + */ +typedef struct yyjson_arr_iter { + size_t idx; /**< next value's index */ + size_t max; /**< maximum index (arr.size) */ + yyjson_val *cur; /**< next value */ +} yyjson_arr_iter; + +/** + Initialize an iterator for this array. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_arr_iter_init(yyjson_val *arr, + yyjson_arr_iter *iter); + +/** + Create an iterator with an array , same as `yyjson_arr_iter_init()`. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, an empty iterator will returned. + @return A new iterator for the array. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(yyjson_val *arr); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_arr_iter_has_next(yyjson_arr_iter *iter); + +/** + Returns the next element in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_val *yyjson_arr_iter_next(yyjson_arr_iter *iter); + +/** + Macro for iterating over an array. + It works like iterator, but with a more intuitive API. + + @b Example + @code + size_t idx, max; + yyjson_val *val; + yyjson_arr_foreach(arr, idx, max, val) { + your_func(idx, val); + } + @endcode + */ +#define yyjson_arr_foreach(arr, idx, max, val) \ + for ((idx) = 0, \ + (max) = yyjson_arr_size(arr), \ + (val) = yyjson_arr_get_first(arr); \ + (idx) < (max); \ + (idx)++, \ + (val) = unsafe_yyjson_get_next(val)) + + + +/*============================================================================== + * MARK: - JSON Object API + *============================================================================*/ + +/** Returns the number of key-value pairs in this object. + Returns 0 if `obj` is NULL or type is not object. */ +yyjson_api_inline size_t yyjson_obj_size(yyjson_val *obj); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a null-terminated UTF-8 string. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_val *yyjson_obj_get(yyjson_val *obj, const char *key); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a UTF-8 string, null-terminator is not required. + The `key_len` should be the length of the key, in bytes. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_val *yyjson_obj_getn(yyjson_val *obj, const char *key, + size_t key_len); + + + +/*============================================================================== + * MARK: - JSON Object Iterator API + *============================================================================*/ + +/** + A JSON object iterator. + + @b Example + @code + yyjson_val *key, *val; + yyjson_obj_iter iter = yyjson_obj_iter_with(obj); + while ((key = yyjson_obj_iter_next(&iter))) { + val = yyjson_obj_iter_get_val(key); + your_func(key, val); + } + @endcode + + If the ordering of the keys is known at compile-time, you can use this method + to speed up value lookups: + @code + // {"k1":1, "k2": 3, "k3": 3} + yyjson_val *key, *val; + yyjson_obj_iter iter = yyjson_obj_iter_with(obj); + yyjson_val *v1 = yyjson_obj_iter_get(&iter, "k1"); + yyjson_val *v3 = yyjson_obj_iter_get(&iter, "k3"); + @endcode + @see yyjson_obj_iter_get() and yyjson_obj_iter_getn() + */ +typedef struct yyjson_obj_iter { + size_t idx; /**< next key's index */ + size_t max; /**< maximum key index (obj.size) */ + yyjson_val *cur; /**< next key */ + yyjson_val *obj; /**< the object being iterated */ +} yyjson_obj_iter; + +/** + Initialize an iterator for this object. + + @param obj The object to be iterated over. + If this parameter is NULL or not an object, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_obj_iter_init(yyjson_val *obj, + yyjson_obj_iter *iter); + +/** + Create an iterator with an object, same as `yyjson_obj_iter_init()`. + + @param obj The object to be iterated over. + If this parameter is NULL or not an object, an empty iterator will returned. + @return A new iterator for the object. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(yyjson_val *obj); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_obj_iter_has_next(yyjson_obj_iter *iter); + +/** + Returns the next key in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_next(yyjson_obj_iter *iter); + +/** + Returns the value for key inside the iteration. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_get_val(yyjson_val *key); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_obj_get()`, but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string with null-terminator. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_get(yyjson_obj_iter *iter, + const char *key); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_obj_getn()`, but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string, null-terminator is not required. + @param key_len The the length of `key`, in bytes. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_val *yyjson_obj_iter_getn(yyjson_obj_iter *iter, + const char *key, + size_t key_len); + +/** + Macro for iterating over an object. + It works like iterator, but with a more intuitive API. + + @b Example + @code + size_t idx, max; + yyjson_val *key, *val; + yyjson_obj_foreach(obj, idx, max, key, val) { + your_func(key, val); + } + @endcode + */ +#define yyjson_obj_foreach(obj, idx, max, key, val) \ + for ((idx) = 0, \ + (max) = yyjson_obj_size(obj), \ + (key) = (obj) ? unsafe_yyjson_get_first(obj) : NULL, \ + (val) = (key) + 1; \ + (idx) < (max); \ + (idx)++, \ + (key) = unsafe_yyjson_get_next(val), \ + (val) = (key) + 1) + + + +/*============================================================================== + * MARK: - Mutable JSON Document API + *============================================================================*/ + +/** Returns the root value of this JSON document. + Returns NULL if `doc` is NULL. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_root(yyjson_mut_doc *doc); + +/** Sets the root value of this JSON document. + Pass NULL to clear root value of the document. */ +yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, + yyjson_mut_val *root); + +/** + Set the string pool size for a mutable document. + This function does not allocate memory immediately, but uses the size when + the next memory allocation is needed. + + If the caller knows the approximate bytes of strings that the document needs to + store (e.g. copy string with `yyjson_mut_strcpy` function), setting a larger + size can avoid multiple memory allocations and improve performance. + + @param doc The mutable document. + @param len The desired string pool size in bytes (total string length). + @return true if successful, false if size is 0 or overflow. + */ +yyjson_api bool yyjson_mut_doc_set_str_pool_size(yyjson_mut_doc *doc, + size_t len); + +/** + Set the value pool size for a mutable document. + This function does not allocate memory immediately, but uses the size when + the next memory allocation is needed. + + If the caller knows the approximate number of values that the document needs to + store (e.g. create new value with `yyjson_mut_xxx` functions), setting a larger + size can avoid multiple memory allocations and improve performance. + + @param doc The mutable document. + @param count The desired value pool size (number of `yyjson_mut_val`). + @return true if successful, false if size is 0 or overflow. + */ +yyjson_api bool yyjson_mut_doc_set_val_pool_size(yyjson_mut_doc *doc, + size_t count); + +/** Release the JSON document and free the memory. + After calling this function, the `doc` and all values from the `doc` are no + longer available. This function will do nothing if the `doc` is NULL. */ +yyjson_api void yyjson_mut_doc_free(yyjson_mut_doc *doc); + +/** Creates and returns a new mutable JSON document, returns NULL on error. + If allocator is NULL, the default allocator will be used. */ +yyjson_api yyjson_mut_doc *yyjson_mut_doc_new(const yyjson_alc *alc); + +/** Copies and returns a new mutable document from input, returns NULL on error. + This makes a `deep-copy` on the immutable document. + If allocator is NULL, the default allocator will be used. + @note `imut_doc` -> `mut_doc`. */ +yyjson_api yyjson_mut_doc *yyjson_doc_mut_copy(yyjson_doc *doc, + const yyjson_alc *alc); + +/** Copies and returns a new mutable document from input, returns NULL on error. + This makes a `deep-copy` on the mutable document. + If allocator is NULL, the default allocator will be used. + @note `mut_doc` -> `mut_doc`. */ +yyjson_api yyjson_mut_doc *yyjson_mut_doc_mut_copy(yyjson_mut_doc *doc, + const yyjson_alc *alc); + +/** Copies and returns a new mutable value from input, returns NULL on error. + This makes a `deep-copy` on the immutable value. + The memory was managed by mutable document. + @note `imut_val` -> `mut_val`. */ +yyjson_api yyjson_mut_val *yyjson_val_mut_copy(yyjson_mut_doc *doc, + yyjson_val *val); + +/** Copies and returns a new mutable value from input, returns NULL on error. + This makes a `deep-copy` on the mutable value. + The memory was managed by mutable document. + @note `mut_val` -> `mut_val`. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api yyjson_mut_val *yyjson_mut_val_mut_copy(yyjson_mut_doc *doc, + yyjson_mut_val *val); + +/** Copies and returns a new immutable document from input, + returns NULL on error. This makes a `deep-copy` on the mutable document. + The returned document should be freed with `yyjson_doc_free()`. + @note `mut_doc` -> `imut_doc`. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api yyjson_doc *yyjson_mut_doc_imut_copy(yyjson_mut_doc *doc, + const yyjson_alc *alc); + +/** Copies and returns a new immutable document from input, + returns NULL on error. This makes a `deep-copy` on the mutable value. + The returned document should be freed with `yyjson_doc_free()`. + @note `mut_val` -> `imut_doc`. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api yyjson_doc *yyjson_mut_val_imut_copy(yyjson_mut_val *val, + const yyjson_alc *alc); + + + +/*============================================================================== + * MARK: - Mutable JSON Value Type API + *============================================================================*/ + +/** Returns whether the JSON value is raw. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_raw(yyjson_mut_val *val); + +/** Returns whether the JSON value is `null`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_null(yyjson_mut_val *val); + +/** Returns whether the JSON value is `true`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_true(yyjson_mut_val *val); + +/** Returns whether the JSON value is `false`. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_false(yyjson_mut_val *val); + +/** Returns whether the JSON value is bool (true/false). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_bool(yyjson_mut_val *val); + +/** Returns whether the JSON value is unsigned integer (uint64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_uint(yyjson_mut_val *val); + +/** Returns whether the JSON value is signed integer (int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_sint(yyjson_mut_val *val); + +/** Returns whether the JSON value is integer (uint64_t/int64_t). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_int(yyjson_mut_val *val); + +/** Returns whether the JSON value is real number (double). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_real(yyjson_mut_val *val); + +/** Returns whether the JSON value is number (uint/sint/real). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_num(yyjson_mut_val *val); + +/** Returns whether the JSON value is string. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_str(yyjson_mut_val *val); + +/** Returns whether the JSON value is array. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_arr(yyjson_mut_val *val); + +/** Returns whether the JSON value is object. + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_obj(yyjson_mut_val *val); + +/** Returns whether the JSON value is container (array/object). + Returns false if `val` is NULL. */ +yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val); + + + +/*============================================================================== + * MARK: - Mutable JSON Value Content API + *============================================================================*/ + +/** Returns the JSON value's type. + Returns `YYJSON_TYPE_NONE` if `val` is NULL. */ +yyjson_api_inline yyjson_type yyjson_mut_get_type(yyjson_mut_val *val); + +/** Returns the JSON value's subtype. + Returns `YYJSON_SUBTYPE_NONE` if `val` is NULL. */ +yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype(yyjson_mut_val *val); + +/** Returns the JSON value's tag. + Returns 0 if `val` is NULL. */ +yyjson_api_inline uint8_t yyjson_mut_get_tag(yyjson_mut_val *val); + +/** Returns the JSON value's type description. + The return value should be one of these strings: "raw", "null", "string", + "array", "object", "true", "false", "uint", "sint", "real", "unknown". */ +yyjson_api_inline const char *yyjson_mut_get_type_desc(yyjson_mut_val *val); + +/** Returns the content if the value is raw. + Returns NULL if `val` is NULL or type is not raw. */ +yyjson_api_inline const char *yyjson_mut_get_raw(yyjson_mut_val *val); + +/** Returns the content if the value is bool. + Returns NULL if `val` is NULL or type is not bool. */ +yyjson_api_inline bool yyjson_mut_get_bool(yyjson_mut_val *val); + +/** Returns the content and cast to uint64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline uint64_t yyjson_mut_get_uint(yyjson_mut_val *val); + +/** Returns the content and cast to int64_t. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int64_t yyjson_mut_get_sint(yyjson_mut_val *val); + +/** Returns the content and cast to int. + Returns 0 if `val` is NULL or type is not integer(sint/uint). */ +yyjson_api_inline int yyjson_mut_get_int(yyjson_mut_val *val); + +/** Returns the content if the value is real number. + Returns 0.0 if `val` is NULL or type is not real(double). */ +yyjson_api_inline double yyjson_mut_get_real(yyjson_mut_val *val); + +/** Returns the content and typecast to `double` if the value is number. + Returns 0.0 if `val` is NULL or type is not number(uint/sint/real). */ +yyjson_api_inline double yyjson_mut_get_num(yyjson_mut_val *val); + +/** Returns the content if the value is string. + Returns NULL if `val` is NULL or type is not string. */ +yyjson_api_inline const char *yyjson_mut_get_str(yyjson_mut_val *val); + +/** Returns the content length (string length, array size, object size. + Returns 0 if `val` is NULL or type is not string/array/object. */ +yyjson_api_inline size_t yyjson_mut_get_len(yyjson_mut_val *val); + +/** Returns whether the JSON value is equals to a string. + The `str` should be a null-terminated UTF-8 string. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_mut_equals_str(yyjson_mut_val *val, + const char *str); + +/** Returns whether the JSON value is equals to a string. + The `str` should be a UTF-8 string, null-terminator is not required. + Returns false if input is NULL or type is not string. */ +yyjson_api_inline bool yyjson_mut_equals_strn(yyjson_mut_val *val, + const char *str, size_t len); + +/** Returns whether two JSON values are equal (deep compare). + Returns false if input is NULL. + @note the result may be inaccurate if object has duplicate keys. + @warning This function is recursive and may cause a stack overflow + if the object level is too deep. */ +yyjson_api_inline bool yyjson_mut_equals(yyjson_mut_val *lhs, + yyjson_mut_val *rhs); + +/** Set the value to raw. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_raw(yyjson_mut_val *val, + const char *raw, size_t len); + +/** Set the value to null. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_null(yyjson_mut_val *val); + +/** Set the value to bool. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_bool(yyjson_mut_val *val, bool num); + +/** Set the value to uint. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_uint(yyjson_mut_val *val, uint64_t num); + +/** Set the value to sint. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_sint(yyjson_mut_val *val, int64_t num); + +/** Set the value to int. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int num); + +/** Set the value to float. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_float(yyjson_mut_val *val, float num); + +/** Set the value to double. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_double(yyjson_mut_val *val, double num); + +/** Set the value to real. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_real(yyjson_mut_val *val, double num); + +/** Set the floating-point number's output format to fixed-point notation. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FIXED flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_mut_set_fp_to_fixed(yyjson_mut_val *val, + int prec); + +/** Set the floating-point number's output format to single-precision. + Returns false if input is NULL or `val` is not real type. + @see YYJSON_WRITE_FP_TO_FLOAT flag. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_mut_set_fp_to_float(yyjson_mut_val *val, + bool flt); + +/** Set the value to string (null-terminated). + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_str(yyjson_mut_val *val, const char *str); + +/** Set the value to string (with length). + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_strn(yyjson_mut_val *val, + const char *str, size_t len); + +/** Marks this string as not needing to be escaped during JSON writing. + This can be used to avoid the overhead of escaping if the string contains + only characters that do not require escaping. + Returns false if input is NULL or `val` is not string. + @see YYJSON_SUBTYPE_NOESC subtype. + @warning This will modify the `immutable` value, use with caution. */ +yyjson_api_inline bool yyjson_mut_set_str_noesc(yyjson_mut_val *val, + bool noesc); + +/** Set the value to array. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_arr(yyjson_mut_val *val); + +/** Set the value to array. + Returns false if input is NULL. + @warning This function should not be used on an existing object or array. */ +yyjson_api_inline bool yyjson_mut_set_obj(yyjson_mut_val *val); + + + +/*============================================================================== + * MARK: - Mutable JSON Value Creation API + *============================================================================*/ + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_raw(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawn(yyjson_mut_doc *doc, + const char *str, + size_t len); + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawcpy(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a raw value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawncpy(yyjson_mut_doc *doc, + const char *str, + size_t len); + +/** Creates and returns a null value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_null(yyjson_mut_doc *doc); + +/** Creates and returns a true value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_true(yyjson_mut_doc *doc); + +/** Creates and returns a false value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_false(yyjson_mut_doc *doc); + +/** Creates and returns a bool value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_bool(yyjson_mut_doc *doc, + bool val); + +/** Creates and returns an unsigned integer value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_uint(yyjson_mut_doc *doc, + uint64_t num); + +/** Creates and returns a signed integer value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_sint(yyjson_mut_doc *doc, + int64_t num); + +/** Creates and returns a signed integer value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_int(yyjson_mut_doc *doc, + int64_t num); + +/** Creates and returns a float number value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_float(yyjson_mut_doc *doc, + float num); + +/** Creates and returns a double number value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_double(yyjson_mut_doc *doc, + double num); + +/** Creates and returns a real number value, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_real(yyjson_mut_doc *doc, + double num); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_str(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_strn(yyjson_mut_doc *doc, + const char *str, + size_t len); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a null-terminated UTF-8 string. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_strcpy(yyjson_mut_doc *doc, + const char *str); + +/** Creates and returns a string value, returns NULL on error. + The `str` should be a UTF-8 string, null-terminator is not required. + The input string is copied and held by the document. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_strncpy(yyjson_mut_doc *doc, + const char *str, + size_t len); + + + +/*============================================================================== + * MARK: - Mutable JSON Array API + *============================================================================*/ + +/** Returns the number of elements in this array. + Returns 0 if `arr` is NULL or type is not array. */ +yyjson_api_inline size_t yyjson_mut_arr_size(yyjson_mut_val *arr); + +/** Returns the element at the specified position in this array. + Returns NULL if array is NULL/empty or the index is out of bounds. + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(yyjson_mut_val *arr, + size_t idx); + +/** Returns the first element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first(yyjson_mut_val *arr); + +/** Returns the last element of this array. + Returns NULL if `arr` is NULL/empty or type is not array. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_last(yyjson_mut_val *arr); + + + +/*============================================================================== + * MARK: - Mutable JSON Array Iterator API + *============================================================================*/ + +/** + A mutable JSON array iterator. + + @warning You should not modify the array while iterating over it, but you can + use `yyjson_mut_arr_iter_remove()` to remove current value. + + @b Example + @code + yyjson_mut_val *val; + yyjson_mut_arr_iter iter = yyjson_mut_arr_iter_with(arr); + while ((val = yyjson_mut_arr_iter_next(&iter))) { + your_func(val); + if (your_val_is_unused(val)) { + yyjson_mut_arr_iter_remove(&iter); + } + } + @endcode + */ +typedef struct yyjson_mut_arr_iter { + size_t idx; /**< next value's index */ + size_t max; /**< maximum index (arr.size) */ + yyjson_mut_val *cur; /**< current value */ + yyjson_mut_val *pre; /**< previous value */ + yyjson_mut_val *arr; /**< the array being iterated */ +} yyjson_mut_arr_iter; + +/** + Initialize an iterator for this array. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_mut_arr_iter_init(yyjson_mut_val *arr, + yyjson_mut_arr_iter *iter); + +/** + Create an iterator with an array , same as `yyjson_mut_arr_iter_init()`. + + @param arr The array to be iterated over. + If this parameter is NULL or not an array, an empty iterator will returned. + @return A new iterator for the array. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_mut_arr_iter yyjson_mut_arr_iter_with( + yyjson_mut_val *arr); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_mut_arr_iter_has_next( + yyjson_mut_arr_iter *iter); + +/** + Returns the next element in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_next( + yyjson_mut_arr_iter *iter); + +/** + Removes and returns current element in the iteration. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( + yyjson_mut_arr_iter *iter); + +/** + Macro for iterating over an array. + It works like iterator, but with a more intuitive API. + + @warning You should not modify the array while iterating over it. + + @b Example + @code + size_t idx, max; + yyjson_mut_val *val; + yyjson_mut_arr_foreach(arr, idx, max, val) { + your_func(idx, val); + } + @endcode + */ +#define yyjson_mut_arr_foreach(arr, idx, max, val) \ + for ((idx) = 0, \ + (max) = yyjson_mut_arr_size(arr), \ + (val) = yyjson_mut_arr_get_first(arr); \ + (idx) < (max); \ + (idx)++, \ + (val) = (val)->next) + + + +/*============================================================================== + * MARK: - Mutable JSON Array Creation API + *============================================================================*/ + +/** + Creates and returns an empty mutable array. + @param doc A mutable document, used for memory allocation only. + @return The new array. NULL if input is NULL or memory allocation failed. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr(yyjson_mut_doc *doc); + +/** + Creates and returns a new mutable array with the given boolean values. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of boolean values. + @param count The value count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const bool vals[3] = { true, false, true }; + yyjson_mut_val *arr = yyjson_mut_arr_with_bool(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_bool( + yyjson_mut_doc *doc, const bool *vals, size_t count); + +/** + Creates and returns a new mutable array with the given sint numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of sint numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int64_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint64(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint( + yyjson_mut_doc *doc, const int64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint64_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given real numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of real numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const double vals[3] = { 0.1, 0.2, 0.3 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_real(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_real( + yyjson_mut_doc *doc, const double *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int8 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int8 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int8_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint8(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint8( + yyjson_mut_doc *doc, const int8_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int16 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int16 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int16_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint16(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint16( + yyjson_mut_doc *doc, const int16_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int32 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int32 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int32_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint32(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint32( + yyjson_mut_doc *doc, const int32_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given int64 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of int64 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const int64_t vals[3] = { -1, 0, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_sint64(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint64( + yyjson_mut_doc *doc, const int64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint8 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint8 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint8_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint8(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint8( + yyjson_mut_doc *doc, const uint8_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint16 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint16 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint16_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint16(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint16( + yyjson_mut_doc *doc, const uint16_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint32 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint32 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint32_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint32(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint32( + yyjson_mut_doc *doc, const uint32_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given uint64 numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of uint64 numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const uint64_t vals[3] = { 0, 1, 0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_uint64(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint64( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count); + +/** + Creates and returns a new mutable array with the given float numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of float numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const float vals[3] = { -1.0f, 0.0f, 1.0f }; + yyjson_mut_val *arr = yyjson_mut_arr_with_float(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_float( + yyjson_mut_doc *doc, const float *vals, size_t count); + +/** + Creates and returns a new mutable array with the given double numbers. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of double numbers. + @param count The number count. If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const double vals[3] = { -1.0, 0.0, 1.0 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_double(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_double( + yyjson_mut_doc *doc, const double *vals, size_t count); + +/** + Creates and returns a new mutable array with the given strings, these strings + will not be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 null-terminator strings. + If this array contains NULL, the function will fail and return NULL. + @param count The number of values in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @warning The input strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. If these strings will be + modified, you should use `yyjson_mut_arr_with_strcpy()` instead. + + @b Example + @code + const char *vals[3] = { "a", "b", "c" }; + yyjson_mut_val *arr = yyjson_mut_arr_with_str(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_str( + yyjson_mut_doc *doc, const char **vals, size_t count); + +/** + Creates and returns a new mutable array with the given strings and string + lengths, these strings will not be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 strings, null-terminator is not required. + If this array contains NULL, the function will fail and return NULL. + @param lens A C array of string lengths, in bytes. + @param count The number of strings in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @warning The input strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. If these strings will be + modified, you should use `yyjson_mut_arr_with_strncpy()` instead. + + @b Example + @code + const char *vals[3] = { "a", "bb", "c" }; + const size_t lens[3] = { 1, 2, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_strn(doc, vals, lens, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strn( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count); + +/** + Creates and returns a new mutable array with the given strings, these strings + will be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 null-terminator strings. + If this array contains NULL, the function will fail and return NULL. + @param count The number of values in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const char *vals[3] = { "a", "b", "c" }; + yyjson_mut_val *arr = yyjson_mut_arr_with_strcpy(doc, vals, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strcpy( + yyjson_mut_doc *doc, const char **vals, size_t count); + +/** + Creates and returns a new mutable array with the given strings and string + lengths, these strings will be copied. + + @param doc A mutable document, used for memory allocation only. + If this parameter is NULL, the function will fail and return NULL. + @param vals A C array of UTF-8 strings, null-terminator is not required. + If this array contains NULL, the function will fail and return NULL. + @param lens A C array of string lengths, in bytes. + @param count The number of strings in `vals`. + If this value is 0, an empty array will return. + @return The new array. NULL if input is invalid or memory allocation failed. + + @b Example + @code + const char *vals[3] = { "a", "bb", "c" }; + const size_t lens[3] = { 1, 2, 1 }; + yyjson_mut_val *arr = yyjson_mut_arr_with_strn(doc, vals, lens, 3); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strncpy( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count); + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification API + *============================================================================*/ + +/** + Inserts a value into an array at a given index. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @param idx The index to which to insert the new value. + Returns false if the index is out of range. + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_arr_insert(yyjson_mut_val *arr, + yyjson_mut_val *val, size_t idx); + +/** + Inserts a value at the end of the array. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, + yyjson_mut_val *val); + +/** + Inserts a value at the head of the array. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_prepend(yyjson_mut_val *arr, + yyjson_mut_val *val); + +/** + Replaces a value at index and returns old value. + @param arr The array to which the value is to be replaced. + Returns false if it is NULL or not an array. + @param idx The index to which to replace the value. + Returns false if the index is out of range. + @param val The new value to replace. Returns false if it is NULL. + @return Old value, or NULL on error. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_replace(yyjson_mut_val *arr, + size_t idx, + yyjson_mut_val *val); + +/** + Removes and returns a value at index. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @param idx The index from which to remove the value. + Returns false if the index is out of range. + @return Old value, or NULL on error. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove(yyjson_mut_val *arr, + size_t idx); + +/** + Removes and returns the first value in this array. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @return The first value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_first( + yyjson_mut_val *arr); + +/** + Removes and returns the last value in this array. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @return The last value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_last( + yyjson_mut_val *arr); + +/** + Removes all values within a specified range in the array. + @param arr The array from which the value is to be removed. + Returns false if it is NULL or not an array. + @param idx The start index of the range (0 is the first). + @param len The number of items in the range (can be 0). + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_arr_remove_range(yyjson_mut_val *arr, + size_t idx, size_t len); + +/** + Removes all values in this array. + @param arr The array from which all of the values are to be removed. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_clear(yyjson_mut_val *arr); + +/** + Rotates values in this array for the given number of times. + For example: `[1,2,3,4,5]` rotate 2 is `[3,4,5,1,2]`. + @param arr The array to be rotated. + @param idx Index (or times) to rotate. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_arr_rotate(yyjson_mut_val *arr, + size_t idx); + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification Convenience API + *============================================================================*/ + +/** + Adds a value at the end of the array. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The value to be inserted. Returns false if it is NULL. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_val(yyjson_mut_val *arr, + yyjson_mut_val *val); + +/** + Adds a `null` value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Adds a `true` value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Adds a `false` value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Adds a bool value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param val The bool value to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + bool val); + +/** + Adds an unsigned integer value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + uint64_t num); + +/** + Adds a signed integer value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num); + +/** + Adds an integer value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num); + +/** + Adds a float value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + float num); + +/** + Adds a double value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num); + +/** + Adds a double value at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param num The number to be added. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num); + +/** + Adds a string value at the end of the array (no copy). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A null-terminated UTF-8 string. + @return Whether successful. + @warning The input string is not copied, you should keep this string unmodified + for the lifetime of this JSON document. + */ +yyjson_api_inline bool yyjson_mut_arr_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str); + +/** + Adds a string value at the end of the array (no copy). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A UTF-8 string, null-terminator is not required. + @param len The length of the string, in bytes. + @return Whether successful. + @warning The input string is not copied, you should keep this string unmodified + for the lifetime of this JSON document. + */ +yyjson_api_inline bool yyjson_mut_arr_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, + size_t len); + +/** + Adds a string value at the end of the array (copied). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A null-terminated UTF-8 string. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str); + +/** + Adds a string value at the end of the array (copied). + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @param str A UTF-8 string, null-terminator is not required. + @param len The length of the string, in bytes. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_arr_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, + size_t len); + +/** + Creates and adds a new array at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return The new array, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + +/** + Creates and adds a new object at the end of the array. + @param doc The `doc` is only used for memory allocation. + @param arr The array to which the value is to be inserted. + Returns false if it is NULL or not an array. + @return The new object, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *arr); + + + +/*============================================================================== + * MARK: - Mutable JSON Object API + *============================================================================*/ + +/** Returns the number of key-value pairs in this object. + Returns 0 if `obj` is NULL or type is not object. */ +yyjson_api_inline size_t yyjson_mut_obj_size(yyjson_mut_val *obj); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a null-terminated UTF-8 string. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(yyjson_mut_val *obj, + const char *key); + +/** Returns the value to which the specified key is mapped. + Returns NULL if this object contains no mapping for the key. + Returns NULL if `obj/key` is NULL, or type is not object. + + The `key` should be a UTF-8 string, null-terminator is not required. + The `key_len` should be the length of the key, in bytes. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(yyjson_mut_val *obj, + const char *key, + size_t key_len); + + + +/*============================================================================== + * MARK: - Mutable JSON Object Iterator API + *============================================================================*/ + +/** + A mutable JSON object iterator. + + @warning You should not modify the object while iterating over it, but you can + use `yyjson_mut_obj_iter_remove()` to remove current value. + + @b Example + @code + yyjson_mut_val *key, *val; + yyjson_mut_obj_iter iter = yyjson_mut_obj_iter_with(obj); + while ((key = yyjson_mut_obj_iter_next(&iter))) { + val = yyjson_mut_obj_iter_get_val(key); + your_func(key, val); + if (your_val_is_unused(key, val)) { + yyjson_mut_obj_iter_remove(&iter); + } + } + @endcode + + If the ordering of the keys is known at compile-time, you can use this method + to speed up value lookups: + @code + // {"k1":1, "k2": 3, "k3": 3} + yyjson_mut_val *key, *val; + yyjson_mut_obj_iter iter = yyjson_mut_obj_iter_with(obj); + yyjson_mut_val *v1 = yyjson_mut_obj_iter_get(&iter, "k1"); + yyjson_mut_val *v3 = yyjson_mut_obj_iter_get(&iter, "k3"); + @endcode + @see `yyjson_mut_obj_iter_get()` and `yyjson_mut_obj_iter_getn()` + */ +typedef struct yyjson_mut_obj_iter { + size_t idx; /**< next key's index */ + size_t max; /**< maximum key index (obj.size) */ + yyjson_mut_val *cur; /**< current key */ + yyjson_mut_val *pre; /**< previous key */ + yyjson_mut_val *obj; /**< the object being iterated */ +} yyjson_mut_obj_iter; + +/** + Initialize an iterator for this object. + + @param obj The object to be iterated over. + If this parameter is NULL or not an array, `iter` will be set to empty. + @param iter The iterator to be initialized. + If this parameter is NULL, the function will fail and return false. + @return true if the `iter` has been successfully initialized. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline bool yyjson_mut_obj_iter_init(yyjson_mut_val *obj, + yyjson_mut_obj_iter *iter); + +/** + Create an iterator with an object, same as `yyjson_obj_iter_init()`. + + @param obj The object to be iterated over. + If this parameter is NULL or not an object, an empty iterator will returned. + @return A new iterator for the object. + + @note The iterator does not need to be destroyed. + */ +yyjson_api_inline yyjson_mut_obj_iter yyjson_mut_obj_iter_with( + yyjson_mut_val *obj); + +/** + Returns whether the iteration has more elements. + If `iter` is NULL, this function will return false. + */ +yyjson_api_inline bool yyjson_mut_obj_iter_has_next( + yyjson_mut_obj_iter *iter); + +/** + Returns the next key in the iteration, or NULL on end. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_next( + yyjson_mut_obj_iter *iter); + +/** + Returns the value for key inside the iteration. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get_val( + yyjson_mut_val *key); + +/** + Removes current key-value pair in the iteration, returns the removed value. + If `iter` is NULL, this function will return NULL. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( + yyjson_mut_obj_iter *iter); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_mut_obj_get()`, but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string with null-terminator. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get( + yyjson_mut_obj_iter *iter, const char *key); + +/** + Iterates to a specified key and returns the value. + + This function does the same thing as `yyjson_mut_obj_getn()` but is much faster + if the ordering of the keys is known at compile-time and you are using the same + order to look up the values. If the key exists in this object, then the + iterator will stop at the next key, otherwise the iterator will not change and + NULL is returned. + + @param iter The object iterator, should not be NULL. + @param key The key, should be a UTF-8 string, null-terminator is not required. + @param key_len The the length of `key`, in bytes. + @return The value to which the specified key is mapped. + NULL if this object contains no mapping for the key or input is invalid. + + @warning This function takes a linear search time if the key is not nearby. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_getn( + yyjson_mut_obj_iter *iter, const char *key, size_t key_len); + +/** + Macro for iterating over an object. + It works like iterator, but with a more intuitive API. + + @warning You should not modify the object while iterating over it. + + @b Example + @code + size_t idx, max; + yyjson_mut_val *key, *val; + yyjson_mut_obj_foreach(obj, idx, max, key, val) { + your_func(key, val); + } + @endcode + */ +#define yyjson_mut_obj_foreach(obj, idx, max, key, val) \ + for ((idx) = 0, \ + (max) = yyjson_mut_obj_size(obj), \ + (key) = (max) ? ((yyjson_mut_val *)(obj)->uni.ptr)->next->next : NULL, \ + (val) = (key) ? (key)->next : NULL; \ + (idx) < (max); \ + (idx)++, \ + (key) = (val)->next, \ + (val) = (key)->next) + + + +/*============================================================================== + * MARK: - Mutable JSON Object Creation API + *============================================================================*/ + +/** Creates and returns a mutable object, returns NULL on error. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj(yyjson_mut_doc *doc); + +/** + Creates and returns a mutable object with keys and values, returns NULL on + error. The keys and values are not copied. The strings should be a + null-terminated UTF-8 string. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. + + @b Example + @code + const char *keys[2] = { "id", "name" }; + const char *vals[2] = { "01", "Harry" }; + yyjson_mut_val *obj = yyjson_mut_obj_with_str(doc, keys, vals, 2); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, + const char **keys, + const char **vals, + size_t count); + +/** + Creates and returns a mutable object with key-value pairs and pair count, + returns NULL on error. The keys and values are not copied. The strings should + be a null-terminated UTF-8 string. + + @warning The input string is not copied, you should keep this string + unmodified for the lifetime of this JSON document. + + @b Example + @code + const char *kv_pairs[4] = { "id", "01", "name", "Harry" }; + yyjson_mut_val *obj = yyjson_mut_obj_with_kv(doc, kv_pairs, 2); + @endcode + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_kv(yyjson_mut_doc *doc, + const char **kv_pairs, + size_t pair_count); + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification API + *============================================================================*/ + +/** + Adds a key-value pair at the end of the object. + This function allows duplicated key in one object. + @param obj The object to which the new key-value pair is to be added. + @param key The key, should be a string which is created by `yyjson_mut_str()`, + `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. + @param val The value to add to the object. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_add(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val); +/** + Sets a key-value pair at the end of the object. + This function may remove all key-value pairs for the given key before add. + @param obj The object to which the new key-value pair is to be added. + @param key The key, should be a string which is created by `yyjson_mut_str()`, + `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. + @param val The value to add to the object. If this value is null, the behavior + is same as `yyjson_mut_obj_remove()`. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val); + +/** + Inserts a key-value pair to the object at the given position. + This function allows duplicated key in one object. + @param obj The object to which the new key-value pair is to be added. + @param key The key, should be a string which is created by `yyjson_mut_str()`, + `yyjson_mut_strn()`, `yyjson_mut_strcpy()` or `yyjson_mut_strncpy()`. + @param val The value to add to the object. + @param idx The index to which to insert the new pair. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_insert(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val, + size_t idx); + +/** + Removes all key-value pair from the object with given key. + @param obj The object from which the key-value pair is to be removed. + @param key The key, should be a string value. + @return The first matched value, or NULL if no matched value. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove(yyjson_mut_val *obj, + yyjson_mut_val *key); + +/** + Removes all key-value pair from the object with given key. + @param obj The object from which the key-value pair is to be removed. + @param key The key, should be a UTF-8 string with null-terminator. + @return The first matched value, or NULL if no matched value. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_key( + yyjson_mut_val *obj, const char *key); + +/** + Removes all key-value pair from the object with given key. + @param obj The object from which the key-value pair is to be removed. + @param key The key, should be a UTF-8 string, null-terminator is not required. + @param key_len The length of the key. + @return The first matched value, or NULL if no matched value. + @warning This function takes a linear search time. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_keyn( + yyjson_mut_val *obj, const char *key, size_t key_len); + +/** + Removes all key-value pairs in this object. + @param obj The object from which all of the values are to be removed. + @return Whether successful. + */ +yyjson_api_inline bool yyjson_mut_obj_clear(yyjson_mut_val *obj); + +/** + Replaces value from the object with given key. + If the key is not exist, or the value is NULL, it will fail. + @param obj The object to which the value is to be replaced. + @param key The key, should be a string value. + @param val The value to replace into the object. + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_obj_replace(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val); + +/** + Rotates key-value pairs in the object for the given number of times. + For example: `{"a":1,"b":2,"c":3,"d":4}` rotate 1 is + `{"b":2,"c":3,"d":4,"a":1}`. + @param obj The object to be rotated. + @param idx Index (or times) to rotate. + @return Whether successful. + @warning This function takes a linear search time. + */ +yyjson_api_inline bool yyjson_mut_obj_rotate(yyjson_mut_val *obj, + size_t idx); + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification Convenience API + *============================================================================*/ + +/** Adds a `null` value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a `true` value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a `false` value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a bool value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, bool val); + +/** Adds an unsigned integer value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, uint64_t val); + +/** Adds a signed integer value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, int64_t val); + +/** Adds an int value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, int64_t val); + +/** Adds a float value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, float val); + +/** Adds a double value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, double val); + +/** Adds a real value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, double val); + +/** Adds a string value at the end of the object. + The `key` and `val` should be null-terminated UTF-8 strings. + This function allows duplicated key in one object. + + @warning The key/value strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, const char *val); + +/** Adds a string value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + The `val` should be a UTF-8 string, null-terminator is not required. + The `len` should be the length of the `val`, in bytes. + This function allows duplicated key in one object. + + @warning The key/value strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *val, size_t len); + +/** Adds a string value at the end of the object. + The `key` and `val` should be null-terminated UTF-8 strings. + The value string is copied. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *val); + +/** Adds a string value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + The `val` should be a UTF-8 string, null-terminator is not required. + The `len` should be the length of the `val`, in bytes. + This function allows duplicated key in one object. + + @warning The key strings are not copied, you should keep these strings + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *val, size_t len); + +/** + Creates and adds a new array to the target object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep these strings + unmodified for the lifetime of this JSON document. + @return The new array, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** + Creates and adds a new object to the target object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep these strings + unmodified for the lifetime of this JSON document. + @return The new object, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key); + +/** Adds a JSON value at the end of the object. + The `key` should be a null-terminated UTF-8 string. + This function allows duplicated key in one object. + + @warning The key string is not copied, you should keep the string + unmodified for the lifetime of this JSON document. */ +yyjson_api_inline bool yyjson_mut_obj_add_val(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + yyjson_mut_val *val); + +/** Removes all key-value pairs for the given key. + Returns the first value to which the specified key is mapped or NULL if this + object contains no mapping for the key. + The `key` should be a null-terminated UTF-8 string. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_str( + yyjson_mut_val *obj, const char *key); + +/** Removes all key-value pairs for the given key. + Returns the first value to which the specified key is mapped or NULL if this + object contains no mapping for the key. + The `key` should be a UTF-8 string, null-terminator is not required. + The `len` should be the length of the key, in bytes. + + @warning This function takes a linear search time. */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_strn( + yyjson_mut_val *obj, const char *key, size_t len); + +/** Replaces all matching keys with the new key. + Returns true if at least one key was renamed. + The `key` and `new_key` should be a null-terminated UTF-8 string. + The `new_key` is copied and held by doc. + + @warning This function takes a linear search time. + If `new_key` already exists, it will cause duplicate keys. + */ +yyjson_api_inline bool yyjson_mut_obj_rename_key(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *new_key); + +/** Replaces all matching keys with the new key. + Returns true if at least one key was renamed. + The `key` and `new_key` should be a UTF-8 string, + null-terminator is not required. The `new_key` is copied and held by doc. + + @warning This function takes a linear search time. + If `new_key` already exists, it will cause duplicate keys. + */ +yyjson_api_inline bool yyjson_mut_obj_rename_keyn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + size_t len, + const char *new_key, + size_t new_len); + + + +#if !defined(YYJSON_DISABLE_UTILS) || !YYJSON_DISABLE_UTILS + +/*============================================================================== + * MARK: - JSON Pointer API (RFC 6901) + * https://tools.ietf.org/html/rfc6901 + *============================================================================*/ + +/** JSON Pointer error code. */ +typedef uint32_t yyjson_ptr_code; + +/** No JSON pointer error. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_NONE = 0; + +/** Invalid input parameter, such as NULL input. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_PARAMETER = 1; + +/** JSON pointer syntax error, such as invalid escape, token no prefix. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_SYNTAX = 2; + +/** JSON pointer resolve failed, such as index out of range, key not found. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_RESOLVE = 3; + +/** Document's root is NULL, but it is required for the function call. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_NULL_ROOT = 4; + +/** Cannot set root as the target is not a document. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_SET_ROOT = 5; + +/** The memory allocation failed and a new value could not be created. */ +static const yyjson_ptr_code YYJSON_PTR_ERR_MEMORY_ALLOCATION = 6; + +/** Error information for JSON pointer. */ +typedef struct yyjson_ptr_err { + /** Error code, see `yyjson_ptr_code` for all possible values. */ + yyjson_ptr_code code; + /** Error message, constant, no need to free (NULL if no error). */ + const char *msg; + /** Error byte position for input JSON pointer (0 if no error). */ + size_t pos; +} yyjson_ptr_err; + +/** + A context for JSON pointer operation. + + This struct stores the context of JSON Pointer operation result. The struct + can be used with three helper functions: `ctx_append()`, `ctx_replace()`, and + `ctx_remove()`, which perform the corresponding operations on the container + without re-parsing the JSON Pointer. + + For example: + @code + // doc before: {"a":[0,1,null]} + // ptr: "/a/2" + val = yyjson_mut_doc_ptr_getx(doc, ptr, strlen(ptr), &ctx, &err); + if (yyjson_is_null(val)) { + yyjson_ptr_ctx_remove(&ctx); + } + // doc after: {"a":[0,1]} + @endcode + */ +typedef struct yyjson_ptr_ctx { + /** + The container (parent) of the target value. It can be either an array or + an object. If the target location has no value, but all its parent + containers exist, and the target location can be used to insert a new + value, then `ctn` is the parent container of the target location. + Otherwise, `ctn` is NULL. + */ + yyjson_mut_val *ctn; + /** + The previous sibling of the target value. It can be either a value in an + array or a key in an object. As the container is a `circular linked list` + of elements, `pre` is the previous node of the target value. If the + operation is `add` or `set`, then `pre` is the previous node of the new + value, not the original target value. If the target value does not exist, + `pre` is NULL. + */ + yyjson_mut_val *pre; + /** + The removed value if the operation is `set`, `replace` or `remove`. It can + be used to restore the original state of the document if needed. + */ + yyjson_mut_val *old; +} yyjson_ptr_ctx; + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(yyjson_doc *doc, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(yyjson_doc *doc, + const char *ptr, size_t len); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, + const char *ptr, size_t len, + yyjson_ptr_err *err); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_ptr_get(yyjson_val *val, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_ptr_getn(yyjson_val *val, + const char *ptr, size_t len); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t len, + yyjson_ptr_err *err); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get(yyjson_mut_doc *doc, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn(yyjson_mut_doc *doc, + const char *ptr, + size_t len); + +/** + Get value by a JSON Pointer. + @param doc The JSON document to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `doc` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(yyjson_mut_val *val, + const char *ptr); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(yyjson_mut_val *val, + const char *ptr, + size_t len); + +/** + Get value by a JSON Pointer. + @param val The JSON value to be queried. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The value referenced by the JSON pointer. + NULL if `val` or `ptr` is NULL, or the JSON pointer cannot be resolved. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Add (insert) value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_add(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val); + +/** + Add (insert) value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_addn(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val); + +/** + Add (insert) value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be added. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is added, false otherwise. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_addx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Add (insert) value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param doc Only used to create new values when needed. + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_ptr_add(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Add (insert) value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param doc Only used to create new values when needed. + @param new_val The value to be added. + @return true if JSON pointer is valid and new value is added, false otherwise. + @note The parent nodes will be created if they do not exist. + */ +yyjson_api_inline bool yyjson_mut_ptr_addn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Add (insert) value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param doc Only used to create new values when needed. + @param new_val The value to be added. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is added, false otherwise. + */ +yyjson_api_inline bool yyjson_mut_ptr_addx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Set value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The value to be set, pass NULL to remove. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_set(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val); + +/** + Set value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_setn(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val); + +/** + Set value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_doc_ptr_setx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Set value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The value to be set, pass NULL to remove. + @param doc Only used to create new values when needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_ptr_set(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Set value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @param doc Only used to create new values when needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note The parent nodes will be created if they do not exist. + If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_ptr_setn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc); + +/** + Set value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The value to be set, pass NULL to remove. + @param doc Only used to create new values when needed. + @param create_parent Whether to create parent nodes if not exist. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return true if JSON pointer is valid and new value is set, false otherwise. + @note If the target value already exists, it will be replaced by the new value. + */ +yyjson_api_inline bool yyjson_mut_ptr_setx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Replace value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replace( + yyjson_mut_doc *doc, const char *ptr, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacen( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacex( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/** + Replace value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replace( + yyjson_mut_val *val, const char *ptr, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacen( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val); + +/** + Replace value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param new_val The new value to replace the old one. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The old value that was replaced, or NULL if not found. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/** + Remove value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_remove( + yyjson_mut_doc *doc, const char *ptr); + +/** + Remove value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removen( + yyjson_mut_doc *doc, const char *ptr, size_t len); + +/** + Remove value by a JSON pointer. + @param doc The target JSON document. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removex( + yyjson_mut_doc *doc, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/** + Remove value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8 with null-terminator). + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_remove(yyjson_mut_val *val, + const char *ptr); + +/** + Remove value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removen(yyjson_mut_val *val, + const char *ptr, + size_t len); + +/** + Remove value by a JSON pointer. + @param val The target JSON value. + @param ptr The JSON pointer string (UTF-8, null-terminator is not required). + @param len The length of `ptr` in bytes. + @param ctx A pointer to store the result context, or NULL if not needed. + @param err A pointer to store the error information, or NULL if not needed. + @return The removed value, or NULL on error. + */ +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removex(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/** + Append value by JSON pointer context. + @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. + @param key New key if `ctx->ctn` is object, or NULL if `ctx->ctn` is array. + @param val New value to be added. + @return true on success or false on fail. + */ +yyjson_api_inline bool yyjson_ptr_ctx_append(yyjson_ptr_ctx *ctx, + yyjson_mut_val *key, + yyjson_mut_val *val); + +/** + Replace value by JSON pointer context. + @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. + @param val New value to be replaced. + @return true on success or false on fail. + @note If success, the old value will be returned via `ctx->old`. + */ +yyjson_api_inline bool yyjson_ptr_ctx_replace(yyjson_ptr_ctx *ctx, + yyjson_mut_val *val); + +/** + Remove value by JSON pointer context. + @param ctx The context from the `yyjson_mut_ptr_xxx()` calls. + @return true on success or false on fail. + @note If success, the old value will be returned via `ctx->old`. + */ +yyjson_api_inline bool yyjson_ptr_ctx_remove(yyjson_ptr_ctx *ctx); + + + +/*============================================================================== + * MARK: - JSON Patch API (RFC 6902) + * https://tools.ietf.org/html/rfc6902 + *============================================================================*/ + +/** Result code for JSON patch. */ +typedef uint32_t yyjson_patch_code; + +/** Success, no error. */ +static const yyjson_patch_code YYJSON_PATCH_SUCCESS = 0; + +/** Invalid parameter, such as NULL input or non-array patch. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_PARAMETER = 1; + +/** Memory allocation failure occurs. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_MEMORY_ALLOCATION = 2; + +/** JSON patch operation is not object type. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_OPERATION = 3; + +/** JSON patch operation is missing a required key. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_MISSING_KEY = 4; + +/** JSON patch operation member is invalid. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_INVALID_MEMBER = 5; + +/** JSON patch operation `test` not equal. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_EQUAL = 6; + +/** JSON patch operation failed on JSON pointer. */ +static const yyjson_patch_code YYJSON_PATCH_ERROR_POINTER = 7; + +/** Error information for JSON patch. */ +typedef struct yyjson_patch_err { + /** Error code, see `yyjson_patch_code` for all possible values. */ + yyjson_patch_code code; + /** Index of the error operation (0 if no error). */ + size_t idx; + /** Error message, constant, no need to free (NULL if no error). */ + const char *msg; + /** JSON pointer error if `code == YYJSON_PATCH_ERROR_POINTER`. */ + yyjson_ptr_err ptr; +} yyjson_patch_err; + +/** + Creates and returns a patched JSON value (RFC 6902). + The memory of the returned value is allocated by the `doc`. + The `err` is used to receive error information, pass NULL if not needed. + Returns NULL if the patch could not be applied. + */ +yyjson_api yyjson_mut_val *yyjson_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch, + yyjson_patch_err *err); + +/** + Creates and returns a patched JSON value (RFC 6902). + The memory of the returned value is allocated by the `doc`. + The `err` is used to receive error information, pass NULL if not needed. + Returns NULL if the patch could not be applied. + */ +yyjson_api yyjson_mut_val *yyjson_mut_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch, + yyjson_patch_err *err); + + + +/*============================================================================== + * MARK: - JSON Merge-Patch API (RFC 7386) + * https://tools.ietf.org/html/rfc7386 + *============================================================================*/ + +/** + Creates and returns a merge-patched JSON value (RFC 7386). + The memory of the returned value is allocated by the `doc`. + Returns NULL if the patch could not be applied. + + @warning This function is recursive and may cause a stack overflow if the + object level is too deep. + */ +yyjson_api yyjson_mut_val *yyjson_merge_patch(yyjson_mut_doc *doc, + yyjson_val *orig, + yyjson_val *patch); + +/** + Creates and returns a merge-patched JSON value (RFC 7386). + The memory of the returned value is allocated by the `doc`. + Returns NULL if the patch could not be applied. + + @warning This function is recursive and may cause a stack overflow if the + object level is too deep. + */ +yyjson_api yyjson_mut_val *yyjson_mut_merge_patch(yyjson_mut_doc *doc, + yyjson_mut_val *orig, + yyjson_mut_val *patch); + +#endif /* YYJSON_DISABLE_UTILS */ + + + +/*============================================================================== + * MARK: - JSON Structure (Implementation) + *============================================================================*/ + +/** Payload of a JSON value (8 bytes). */ +typedef union yyjson_val_uni { + uint64_t u64; + int64_t i64; + double f64; + const char *str; + void *ptr; + size_t ofs; +} yyjson_val_uni; + +/** + Immutable JSON value, 16 bytes. + */ +struct yyjson_val { + uint64_t tag; /**< type, subtype and length */ + yyjson_val_uni uni; /**< payload */ +}; + +struct yyjson_doc { + /** Root value of the document (nonnull). */ + yyjson_val *root; + /** Allocator used by document (nonnull). */ + yyjson_alc alc; + /** The total number of bytes read when parsing JSON (nonzero). */ + size_t dat_read; + /** The total number of value read when parsing JSON (nonzero). */ + size_t val_read; + /** The string pool used by JSON values (nullable). */ + char *str_pool; +}; + + + +/*============================================================================== + * MARK: - Unsafe JSON Value API (Implementation) + *============================================================================*/ + +/* + Whether the string does not need to be escaped for serialization. + This function is used to optimize the writing speed of small constant strings. + This function works only if the compiler can evaluate it at compile time. + + Clang supports it since v8.0, + earlier versions do not support constant_p(strlen) and return false. + GCC supports it since at least v4.4, + earlier versions may compile it as run-time instructions. + ICC supports it since at least v16, + earlier versions are uncertain. + + @param str The C string. + @param len The returnd value from strlen(str). + */ +yyjson_api_inline bool unsafe_yyjson_is_str_noesc(const char *str, size_t len) { +#if YYJSON_HAS_CONSTANT_P && \ + (!YYJSON_IS_REAL_GCC || yyjson_gcc_available(4, 4, 0)) + if (yyjson_constant_p(len) && len <= 32) { + /* + Same as the following loop: + + for (size_t i = 0; i < len; i++) { + char c = str[i]; + if (c < ' ' || c > '~' || c == '"' || c == '\\') return false; + } + + GCC evaluates it at compile time only if the string length is within 17 + and -O3 (which turns on the -fpeel-loops flag) is used. + So the loop is unrolled for GCC. + */ +# define yyjson_repeat32_incr(x) \ + x(0) x(1) x(2) x(3) x(4) x(5) x(6) x(7) \ + x(8) x(9) x(10) x(11) x(12) x(13) x(14) x(15) \ + x(16) x(17) x(18) x(19) x(20) x(21) x(22) x(23) \ + x(24) x(25) x(26) x(27) x(28) x(29) x(30) x(31) +# define yyjson_check_char_noesc(i) \ + if (i < len) { \ + char c = str[i]; \ + if (c < ' ' || c > '~' || c == '"' || c == '\\') return false; } + yyjson_repeat32_incr(yyjson_check_char_noesc) +# undef yyjson_repeat32_incr +# undef yyjson_check_char_noesc + return true; + } +#else + (void)str; + (void)len; +#endif + return false; +} + +yyjson_api_inline double unsafe_yyjson_u64_to_f64(uint64_t num) { +#if YYJSON_U64_TO_F64_NO_IMPL + uint64_t msb = ((uint64_t)1) << 63; + if ((num & msb) == 0) { + return (double)(int64_t)num; + } else { + return ((double)(int64_t)((num >> 1) | (num & 1))) * (double)2.0; + } +#else + return (double)num; +#endif +} + +yyjson_api_inline yyjson_type unsafe_yyjson_get_type(void *val) { + uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; + return (yyjson_type)(tag & YYJSON_TYPE_MASK); +} + +yyjson_api_inline yyjson_subtype unsafe_yyjson_get_subtype(void *val) { + uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; + return (yyjson_subtype)(tag & YYJSON_SUBTYPE_MASK); +} + +yyjson_api_inline uint8_t unsafe_yyjson_get_tag(void *val) { + uint8_t tag = (uint8_t)((yyjson_val *)val)->tag; + return (uint8_t)(tag & YYJSON_TAG_MASK); +} + +yyjson_api_inline bool unsafe_yyjson_is_raw(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_RAW; +} + +yyjson_api_inline bool unsafe_yyjson_is_null(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_NULL; +} + +yyjson_api_inline bool unsafe_yyjson_is_bool(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_BOOL; +} + +yyjson_api_inline bool unsafe_yyjson_is_num(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_NUM; +} + +yyjson_api_inline bool unsafe_yyjson_is_str(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_STR; +} + +yyjson_api_inline bool unsafe_yyjson_is_arr(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_ARR; +} + +yyjson_api_inline bool unsafe_yyjson_is_obj(void *val) { + return unsafe_yyjson_get_type(val) == YYJSON_TYPE_OBJ; +} + +yyjson_api_inline bool unsafe_yyjson_is_ctn(void *val) { + uint8_t mask = YYJSON_TYPE_ARR & YYJSON_TYPE_OBJ; + return (unsafe_yyjson_get_tag(val) & mask) == mask; +} + +yyjson_api_inline bool unsafe_yyjson_is_uint(void *val) { + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_sint(void *val) { + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_int(void *val) { + const uint8_t mask = YYJSON_TAG_MASK & (~YYJSON_SUBTYPE_SINT); + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT; + return (unsafe_yyjson_get_tag(val) & mask) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_real(void *val) { + const uint8_t patt = YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_true(void *val) { + const uint8_t patt = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_TRUE; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_is_false(void *val) { + const uint8_t patt = YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_FALSE; + return unsafe_yyjson_get_tag(val) == patt; +} + +yyjson_api_inline bool unsafe_yyjson_arr_is_flat(yyjson_val *val) { + size_t ofs = val->uni.ofs; + size_t len = (size_t)(val->tag >> YYJSON_TAG_BIT); + return len * sizeof(yyjson_val) + sizeof(yyjson_val) == ofs; +} + +yyjson_api_inline const char *unsafe_yyjson_get_raw(void *val) { + return ((yyjson_val *)val)->uni.str; +} + +yyjson_api_inline bool unsafe_yyjson_get_bool(void *val) { + uint8_t tag = unsafe_yyjson_get_tag(val); + return (bool)((tag & YYJSON_SUBTYPE_MASK) >> YYJSON_TYPE_BIT); +} + +yyjson_api_inline uint64_t unsafe_yyjson_get_uint(void *val) { + return ((yyjson_val *)val)->uni.u64; +} + +yyjson_api_inline int64_t unsafe_yyjson_get_sint(void *val) { + return ((yyjson_val *)val)->uni.i64; +} + +yyjson_api_inline int unsafe_yyjson_get_int(void *val) { + return (int)((yyjson_val *)val)->uni.i64; +} + +yyjson_api_inline double unsafe_yyjson_get_real(void *val) { + return ((yyjson_val *)val)->uni.f64; +} + +yyjson_api_inline double unsafe_yyjson_get_num(void *val) { + uint8_t tag = unsafe_yyjson_get_tag(val); + if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL)) { + return ((yyjson_val *)val)->uni.f64; + } else if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT)) { + return (double)((yyjson_val *)val)->uni.i64; + } else if (tag == (YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT)) { + return unsafe_yyjson_u64_to_f64(((yyjson_val *)val)->uni.u64); + } + return 0.0; +} + +yyjson_api_inline const char *unsafe_yyjson_get_str(void *val) { + return ((yyjson_val *)val)->uni.str; +} + +yyjson_api_inline size_t unsafe_yyjson_get_len(void *val) { + return (size_t)(((yyjson_val *)val)->tag >> YYJSON_TAG_BIT); +} + +yyjson_api_inline yyjson_val *unsafe_yyjson_get_first(yyjson_val *ctn) { + return ctn + 1; +} + +yyjson_api_inline yyjson_val *unsafe_yyjson_get_next(yyjson_val *val) { + bool is_ctn = unsafe_yyjson_is_ctn(val); + size_t ctn_ofs = val->uni.ofs; + size_t ofs = (is_ctn ? ctn_ofs : sizeof(yyjson_val)); + return (yyjson_val *)(void *)((uint8_t *)val + ofs); +} + +yyjson_api_inline bool unsafe_yyjson_equals_strn(void *val, const char *str, + size_t len) { + return unsafe_yyjson_get_len(val) == len && + memcmp(((yyjson_val *)val)->uni.str, str, len) == 0; +} + +yyjson_api_inline bool unsafe_yyjson_equals_str(void *val, const char *str) { + return unsafe_yyjson_equals_strn(val, str, strlen(str)); +} + +yyjson_api_inline void unsafe_yyjson_set_type(void *val, yyjson_type type, + yyjson_subtype subtype) { + uint8_t tag = (type | subtype); + uint64_t new_tag = ((yyjson_val *)val)->tag; + new_tag = (new_tag & (~(uint64_t)YYJSON_TAG_MASK)) | (uint64_t)tag; + ((yyjson_val *)val)->tag = new_tag; +} + +yyjson_api_inline void unsafe_yyjson_set_len(void *val, size_t len) { + uint64_t tag = ((yyjson_val *)val)->tag & YYJSON_TAG_MASK; + tag |= (uint64_t)len << YYJSON_TAG_BIT; + ((yyjson_val *)val)->tag = tag; +} + +yyjson_api_inline void unsafe_yyjson_set_tag(void *val, yyjson_type type, + yyjson_subtype subtype, + size_t len) { + uint64_t tag = (uint64_t)len << YYJSON_TAG_BIT; + tag |= (type | subtype); + ((yyjson_val *)val)->tag = tag; +} + +yyjson_api_inline void unsafe_yyjson_inc_len(void *val) { + uint64_t tag = ((yyjson_val *)val)->tag; + tag += (uint64_t)(1 << YYJSON_TAG_BIT); + ((yyjson_val *)val)->tag = tag; +} + +yyjson_api_inline void unsafe_yyjson_set_raw(void *val, const char *raw, + size_t len) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_RAW, YYJSON_SUBTYPE_NONE, len); + ((yyjson_val *)val)->uni.str = raw; +} + +yyjson_api_inline void unsafe_yyjson_set_null(void *val) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NULL, YYJSON_SUBTYPE_NONE, 0); +} + +yyjson_api_inline void unsafe_yyjson_set_bool(void *val, bool num) { + yyjson_subtype subtype = num ? YYJSON_SUBTYPE_TRUE : YYJSON_SUBTYPE_FALSE; + unsafe_yyjson_set_tag(val, YYJSON_TYPE_BOOL, subtype, 0); +} + +yyjson_api_inline void unsafe_yyjson_set_uint(void *val, uint64_t num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_UINT, 0); + ((yyjson_val *)val)->uni.u64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_sint(void *val, int64_t num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_SINT, 0); + ((yyjson_val *)val)->uni.i64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_fp_to_fixed(void *val, int prec) { + ((yyjson_val *)val)->tag &= ~((uint64_t)YYJSON_WRITE_FP_TO_FIXED(15) << 32); + ((yyjson_val *)val)->tag |= (uint64_t)YYJSON_WRITE_FP_TO_FIXED(prec) << 32; +} + +yyjson_api_inline void unsafe_yyjson_set_fp_to_float(void *val, bool flt) { + uint64_t flag = (uint64_t)YYJSON_WRITE_FP_TO_FLOAT << 32; + if (flt) ((yyjson_val *)val)->tag |= flag; + else ((yyjson_val *)val)->tag &= ~flag; +} + +yyjson_api_inline void unsafe_yyjson_set_float(void *val, float num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_REAL, 0); + ((yyjson_val *)val)->tag |= (uint64_t)YYJSON_WRITE_FP_TO_FLOAT << 32; + ((yyjson_val *)val)->uni.f64 = (double)num; +} + +yyjson_api_inline void unsafe_yyjson_set_double(void *val, double num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_REAL, 0); + ((yyjson_val *)val)->uni.f64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_real(void *val, double num) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_NUM, YYJSON_SUBTYPE_REAL, 0); + ((yyjson_val *)val)->uni.f64 = num; +} + +yyjson_api_inline void unsafe_yyjson_set_str_noesc(void *val, bool noesc) { + ((yyjson_val *)val)->tag &= ~(uint64_t)YYJSON_SUBTYPE_MASK; + if (noesc) ((yyjson_val *)val)->tag |= (uint64_t)YYJSON_SUBTYPE_NOESC; +} + +yyjson_api_inline void unsafe_yyjson_set_strn(void *val, const char *str, + size_t len) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_STR, YYJSON_SUBTYPE_NONE, len); + ((yyjson_val *)val)->uni.str = str; +} + +yyjson_api_inline void unsafe_yyjson_set_str(void *val, const char *str) { + size_t len = strlen(str); + bool noesc = unsafe_yyjson_is_str_noesc(str, len); + yyjson_subtype subtype = noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; + unsafe_yyjson_set_tag(val, YYJSON_TYPE_STR, subtype, len); + ((yyjson_val *)val)->uni.str = str; +} + +yyjson_api_inline void unsafe_yyjson_set_arr(void *val, size_t size) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_ARR, YYJSON_SUBTYPE_NONE, size); +} + +yyjson_api_inline void unsafe_yyjson_set_obj(void *val, size_t size) { + unsafe_yyjson_set_tag(val, YYJSON_TYPE_OBJ, YYJSON_SUBTYPE_NONE, size); +} + + + +/*============================================================================== + * MARK: - JSON Document API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_val *yyjson_doc_get_root(yyjson_doc *doc) { + return doc ? doc->root : NULL; +} + +yyjson_api_inline size_t yyjson_doc_get_read_size(yyjson_doc *doc) { + return doc ? doc->dat_read : 0; +} + +yyjson_api_inline size_t yyjson_doc_get_val_count(yyjson_doc *doc) { + return doc ? doc->val_read : 0; +} + +yyjson_api_inline void yyjson_doc_free(yyjson_doc *doc) { + if (doc) { + yyjson_alc alc = doc->alc; + memset(&doc->alc, 0, sizeof(alc)); + if (doc->str_pool) alc.free(alc.ctx, doc->str_pool); + alc.free(alc.ctx, doc); + } +} + + + +/*============================================================================== + * MARK: - JSON Value Type API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_is_raw(yyjson_val *val) { + return val ? unsafe_yyjson_is_raw(val) : false; +} + +yyjson_api_inline bool yyjson_is_null(yyjson_val *val) { + return val ? unsafe_yyjson_is_null(val) : false; +} + +yyjson_api_inline bool yyjson_is_true(yyjson_val *val) { + return val ? unsafe_yyjson_is_true(val) : false; +} + +yyjson_api_inline bool yyjson_is_false(yyjson_val *val) { + return val ? unsafe_yyjson_is_false(val) : false; +} + +yyjson_api_inline bool yyjson_is_bool(yyjson_val *val) { + return val ? unsafe_yyjson_is_bool(val) : false; +} + +yyjson_api_inline bool yyjson_is_uint(yyjson_val *val) { + return val ? unsafe_yyjson_is_uint(val) : false; +} + +yyjson_api_inline bool yyjson_is_sint(yyjson_val *val) { + return val ? unsafe_yyjson_is_sint(val) : false; +} + +yyjson_api_inline bool yyjson_is_int(yyjson_val *val) { + return val ? unsafe_yyjson_is_int(val) : false; +} + +yyjson_api_inline bool yyjson_is_real(yyjson_val *val) { + return val ? unsafe_yyjson_is_real(val) : false; +} + +yyjson_api_inline bool yyjson_is_num(yyjson_val *val) { + return val ? unsafe_yyjson_is_num(val) : false; +} + +yyjson_api_inline bool yyjson_is_str(yyjson_val *val) { + return val ? unsafe_yyjson_is_str(val) : false; +} + +yyjson_api_inline bool yyjson_is_arr(yyjson_val *val) { + return val ? unsafe_yyjson_is_arr(val) : false; +} + +yyjson_api_inline bool yyjson_is_obj(yyjson_val *val) { + return val ? unsafe_yyjson_is_obj(val) : false; +} + +yyjson_api_inline bool yyjson_is_ctn(yyjson_val *val) { + return val ? unsafe_yyjson_is_ctn(val) : false; +} + + + +/*============================================================================== + * MARK: - JSON Value Content API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_type yyjson_get_type(yyjson_val *val) { + return val ? unsafe_yyjson_get_type(val) : YYJSON_TYPE_NONE; +} + +yyjson_api_inline yyjson_subtype yyjson_get_subtype(yyjson_val *val) { + return val ? unsafe_yyjson_get_subtype(val) : YYJSON_SUBTYPE_NONE; +} + +yyjson_api_inline uint8_t yyjson_get_tag(yyjson_val *val) { + return val ? unsafe_yyjson_get_tag(val) : 0; +} + +yyjson_api_inline const char *yyjson_get_type_desc(yyjson_val *val) { + switch (yyjson_get_tag(val)) { + case YYJSON_TYPE_RAW | YYJSON_SUBTYPE_NONE: return "raw"; + case YYJSON_TYPE_NULL | YYJSON_SUBTYPE_NONE: return "null"; + case YYJSON_TYPE_STR | YYJSON_SUBTYPE_NONE: return "string"; + case YYJSON_TYPE_STR | YYJSON_SUBTYPE_NOESC: return "string"; + case YYJSON_TYPE_ARR | YYJSON_SUBTYPE_NONE: return "array"; + case YYJSON_TYPE_OBJ | YYJSON_SUBTYPE_NONE: return "object"; + case YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_TRUE: return "true"; + case YYJSON_TYPE_BOOL | YYJSON_SUBTYPE_FALSE: return "false"; + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_UINT: return "uint"; + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_SINT: return "sint"; + case YYJSON_TYPE_NUM | YYJSON_SUBTYPE_REAL: return "real"; + default: return "unknown"; + } +} + +yyjson_api_inline const char *yyjson_get_raw(yyjson_val *val) { + return yyjson_is_raw(val) ? unsafe_yyjson_get_raw(val) : NULL; +} + +yyjson_api_inline bool yyjson_get_bool(yyjson_val *val) { + return yyjson_is_bool(val) ? unsafe_yyjson_get_bool(val) : false; +} + +yyjson_api_inline uint64_t yyjson_get_uint(yyjson_val *val) { + return yyjson_is_int(val) ? unsafe_yyjson_get_uint(val) : 0; +} + +yyjson_api_inline int64_t yyjson_get_sint(yyjson_val *val) { + return yyjson_is_int(val) ? unsafe_yyjson_get_sint(val) : 0; +} + +yyjson_api_inline int yyjson_get_int(yyjson_val *val) { + return yyjson_is_int(val) ? unsafe_yyjson_get_int(val) : 0; +} + +yyjson_api_inline double yyjson_get_real(yyjson_val *val) { + return yyjson_is_real(val) ? unsafe_yyjson_get_real(val) : 0.0; +} + +yyjson_api_inline double yyjson_get_num(yyjson_val *val) { + return val ? unsafe_yyjson_get_num(val) : 0.0; +} + +yyjson_api_inline const char *yyjson_get_str(yyjson_val *val) { + return yyjson_is_str(val) ? unsafe_yyjson_get_str(val) : NULL; +} + +yyjson_api_inline size_t yyjson_get_len(yyjson_val *val) { + return val ? unsafe_yyjson_get_len(val) : 0; +} + +yyjson_api_inline bool yyjson_equals_str(yyjson_val *val, const char *str) { + if (yyjson_likely(val && str)) { + return unsafe_yyjson_is_str(val) && + unsafe_yyjson_equals_str(val, str); + } + return false; +} + +yyjson_api_inline bool yyjson_equals_strn(yyjson_val *val, const char *str, + size_t len) { + if (yyjson_likely(val && str)) { + return unsafe_yyjson_is_str(val) && + unsafe_yyjson_equals_strn(val, str, len); + } + return false; +} + +yyjson_api bool unsafe_yyjson_equals(yyjson_val *lhs, yyjson_val *rhs); + +yyjson_api_inline bool yyjson_equals(yyjson_val *lhs, yyjson_val *rhs) { + if (yyjson_unlikely(!lhs || !rhs)) return false; + return unsafe_yyjson_equals(lhs, rhs); +} + +yyjson_api_inline bool yyjson_set_raw(yyjson_val *val, + const char *raw, size_t len) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_raw(val, raw, len); + return true; +} + +yyjson_api_inline bool yyjson_set_null(yyjson_val *val) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_null(val); + return true; +} + +yyjson_api_inline bool yyjson_set_bool(yyjson_val *val, bool num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_bool(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_uint(yyjson_val *val, uint64_t num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_uint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_sint(yyjson_val *val, int64_t num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_sint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_int(yyjson_val *val, int num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_sint(val, (int64_t)num); + return true; +} + +yyjson_api_inline bool yyjson_set_float(yyjson_val *val, float num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_float(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_double(yyjson_val *val, double num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_double(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_real(yyjson_val *val, double num) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + unsafe_yyjson_set_real(val, num); + return true; +} + +yyjson_api_inline bool yyjson_set_fp_to_fixed(yyjson_val *val, int prec) { + if (yyjson_unlikely(!yyjson_is_real(val))) return false; + unsafe_yyjson_set_fp_to_fixed(val, prec); + return true; +} + +yyjson_api_inline bool yyjson_set_fp_to_float(yyjson_val *val, bool flt) { + if (yyjson_unlikely(!yyjson_is_real(val))) return false; + unsafe_yyjson_set_fp_to_float(val, flt); + return true; +} + +yyjson_api_inline bool yyjson_set_str(yyjson_val *val, const char *str) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + if (yyjson_unlikely(!str)) return false; + unsafe_yyjson_set_str(val, str); + return true; +} + +yyjson_api_inline bool yyjson_set_strn(yyjson_val *val, + const char *str, size_t len) { + if (yyjson_unlikely(!val || unsafe_yyjson_is_ctn(val))) return false; + if (yyjson_unlikely(!str)) return false; + unsafe_yyjson_set_strn(val, str, len); + return true; +} + +yyjson_api_inline bool yyjson_set_str_noesc(yyjson_val *val, bool noesc) { + if (yyjson_unlikely(!yyjson_is_str(val))) return false; + unsafe_yyjson_set_str_noesc(val, noesc); + return true; +} + + + +/*============================================================================== + * MARK: - JSON Array API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_arr_size(yyjson_val *arr) { + return yyjson_is_arr(arr) ? unsafe_yyjson_get_len(arr) : 0; +} + +yyjson_api_inline yyjson_val *yyjson_arr_get(yyjson_val *arr, size_t idx) { + if (yyjson_likely(yyjson_is_arr(arr))) { + if (yyjson_likely(unsafe_yyjson_get_len(arr) > idx)) { + yyjson_val *val = unsafe_yyjson_get_first(arr); + if (unsafe_yyjson_arr_is_flat(arr)) { + return val + idx; + } else { + while (idx-- > 0) val = unsafe_yyjson_get_next(val); + return val; + } + } + } + return NULL; +} + +yyjson_api_inline yyjson_val *yyjson_arr_get_first(yyjson_val *arr) { + if (yyjson_likely(yyjson_is_arr(arr))) { + if (yyjson_likely(unsafe_yyjson_get_len(arr) > 0)) { + return unsafe_yyjson_get_first(arr); + } + } + return NULL; +} + +yyjson_api_inline yyjson_val *yyjson_arr_get_last(yyjson_val *arr) { + if (yyjson_likely(yyjson_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(len > 0)) { + yyjson_val *val = unsafe_yyjson_get_first(arr); + if (unsafe_yyjson_arr_is_flat(arr)) { + return val + (len - 1); + } else { + while (len-- > 1) val = unsafe_yyjson_get_next(val); + return val; + } + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - JSON Array Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_arr_iter_init(yyjson_val *arr, + yyjson_arr_iter *iter) { + if (yyjson_likely(yyjson_is_arr(arr) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(arr); + iter->cur = unsafe_yyjson_get_first(arr); + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_arr_iter)); + return false; +} + +yyjson_api_inline yyjson_arr_iter yyjson_arr_iter_with(yyjson_val *arr) { + yyjson_arr_iter iter; + yyjson_arr_iter_init(arr, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_arr_iter_has_next(yyjson_arr_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_val *yyjson_arr_iter_next(yyjson_arr_iter *iter) { + yyjson_val *val; + if (iter && iter->idx < iter->max) { + val = iter->cur; + iter->cur = unsafe_yyjson_get_next(val); + iter->idx++; + return val; + } + return NULL; +} + + + +/*============================================================================== + * MARK: - JSON Object API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_obj_size(yyjson_val *obj) { + return yyjson_is_obj(obj) ? unsafe_yyjson_get_len(obj) : 0; +} + +yyjson_api_inline yyjson_val *yyjson_obj_get(yyjson_val *obj, + const char *key) { + return yyjson_obj_getn(obj, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_val *yyjson_obj_getn(yyjson_val *obj, + const char *_key, + size_t key_len) { + if (yyjson_likely(yyjson_is_obj(obj) && _key)) { + size_t len = unsafe_yyjson_get_len(obj); + yyjson_val *key = unsafe_yyjson_get_first(obj); + while (len-- > 0) { + if (unsafe_yyjson_equals_strn(key, _key, key_len)) return key + 1; + key = unsafe_yyjson_get_next(key + 1); + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - JSON Object Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_obj_iter_init(yyjson_val *obj, + yyjson_obj_iter *iter) { + if (yyjson_likely(yyjson_is_obj(obj) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(obj); + iter->cur = unsafe_yyjson_get_first(obj); + iter->obj = obj; + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_obj_iter)); + return false; +} + +yyjson_api_inline yyjson_obj_iter yyjson_obj_iter_with(yyjson_val *obj) { + yyjson_obj_iter iter; + yyjson_obj_iter_init(obj, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_obj_iter_has_next(yyjson_obj_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_next(yyjson_obj_iter *iter) { + if (iter && iter->idx < iter->max) { + yyjson_val *key = iter->cur; + iter->idx++; + iter->cur = unsafe_yyjson_get_next(key + 1); + return key; + } + return NULL; +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_get_val(yyjson_val *key) { + return key ? key + 1 : NULL; +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_get(yyjson_obj_iter *iter, + const char *key) { + return yyjson_obj_iter_getn(iter, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_val *yyjson_obj_iter_getn(yyjson_obj_iter *iter, + const char *key, + size_t key_len) { + if (iter && key) { + size_t idx = iter->idx; + size_t max = iter->max; + yyjson_val *cur = iter->cur; + if (yyjson_unlikely(idx == max)) { + idx = 0; + cur = unsafe_yyjson_get_first(iter->obj); + } + while (idx++ < max) { + yyjson_val *next = unsafe_yyjson_get_next(cur + 1); + if (unsafe_yyjson_equals_strn(cur, key, key_len)) { + iter->idx = idx; + iter->cur = next; + return cur + 1; + } + cur = next; + if (idx == iter->max && iter->idx < iter->max) { + idx = 0; + max = iter->idx; + cur = unsafe_yyjson_get_first(iter->obj); + } + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Structure (Implementation) + *============================================================================*/ + +/** + Mutable JSON value, 24 bytes. + The 'tag' and 'uni' field is same as immutable value. + The 'next' field links all elements inside the container to be a cycle. + */ +struct yyjson_mut_val { + uint64_t tag; /**< type, subtype and length */ + yyjson_val_uni uni; /**< payload */ + yyjson_mut_val *next; /**< the next value in circular linked list */ +}; + +/** + A memory chunk in string memory pool. + */ +typedef struct yyjson_str_chunk { + struct yyjson_str_chunk *next; /* next chunk linked list */ + size_t chunk_size; /* chunk size in bytes */ + /* char str[]; flexible array member */ +} yyjson_str_chunk; + +/** + A memory pool to hold all strings in a mutable document. + */ +typedef struct yyjson_str_pool { + char *cur; /* cursor inside current chunk */ + char *end; /* the end of current chunk */ + size_t chunk_size; /* chunk size in bytes while creating new chunk */ + size_t chunk_size_max; /* maximum chunk size in bytes */ + yyjson_str_chunk *chunks; /* a linked list of chunks, nullable */ +} yyjson_str_pool; + +/** + A memory chunk in value memory pool. + `sizeof(yyjson_val_chunk)` should not larger than `sizeof(yyjson_mut_val)`. + */ +typedef struct yyjson_val_chunk { + struct yyjson_val_chunk *next; /* next chunk linked list */ + size_t chunk_size; /* chunk size in bytes */ + /* char pad[sizeof(yyjson_mut_val) - sizeof(yyjson_val_chunk)]; padding */ + /* yyjson_mut_val vals[]; flexible array member */ +} yyjson_val_chunk; + +/** + A memory pool to hold all values in a mutable document. + */ +typedef struct yyjson_val_pool { + yyjson_mut_val *cur; /* cursor inside current chunk */ + yyjson_mut_val *end; /* the end of current chunk */ + size_t chunk_size; /* chunk size in bytes while creating new chunk */ + size_t chunk_size_max; /* maximum chunk size in bytes */ + yyjson_val_chunk *chunks; /* a linked list of chunks, nullable */ +} yyjson_val_pool; + +struct yyjson_mut_doc { + yyjson_mut_val *root; /**< root value of the JSON document, nullable */ + yyjson_alc alc; /**< a valid allocator, nonnull */ + yyjson_str_pool str_pool; /**< string memory pool */ + yyjson_val_pool val_pool; /**< value memory pool */ +}; + +/* Ensures the capacity to at least equal to the specified byte length. */ +yyjson_api bool unsafe_yyjson_str_pool_grow(yyjson_str_pool *pool, + const yyjson_alc *alc, + size_t len); + +/* Ensures the capacity to at least equal to the specified value count. */ +yyjson_api bool unsafe_yyjson_val_pool_grow(yyjson_val_pool *pool, + const yyjson_alc *alc, + size_t count); + +/* Allocate memory for string. */ +yyjson_api_inline char *unsafe_yyjson_mut_str_alc(yyjson_mut_doc *doc, + size_t len) { + char *mem; + const yyjson_alc *alc = &doc->alc; + yyjson_str_pool *pool = &doc->str_pool; + if (yyjson_unlikely((size_t)(pool->end - pool->cur) <= len)) { + if (yyjson_unlikely(!unsafe_yyjson_str_pool_grow(pool, alc, len + 1))) { + return NULL; + } + } + mem = pool->cur; + pool->cur = mem + len + 1; + return mem; +} + +yyjson_api_inline char *unsafe_yyjson_mut_strncpy(yyjson_mut_doc *doc, + const char *str, size_t len) { + char *mem = unsafe_yyjson_mut_str_alc(doc, len); + if (yyjson_unlikely(!mem)) return NULL; + memcpy((void *)mem, (const void *)str, len); + mem[len] = '\0'; + return mem; +} + +yyjson_api_inline yyjson_mut_val *unsafe_yyjson_mut_val(yyjson_mut_doc *doc, + size_t count) { + yyjson_mut_val *val; + yyjson_alc *alc = &doc->alc; + yyjson_val_pool *pool = &doc->val_pool; + if (yyjson_unlikely((size_t)(pool->end - pool->cur) < count)) { + if (yyjson_unlikely(!unsafe_yyjson_val_pool_grow(pool, alc, count))) { + return NULL; + } + } + val = pool->cur; + pool->cur += count; + return val; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Document API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_root(yyjson_mut_doc *doc) { + return doc ? doc->root : NULL; +} + +yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, + yyjson_mut_val *root) { + if (doc) doc->root = root; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Value Type API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_is_raw(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_raw(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_null(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_null(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_true(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_true(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_false(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_false(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_bool(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_bool(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_uint(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_uint(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_sint(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_sint(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_int(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_int(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_real(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_real(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_num(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_num(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_str(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_str(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_arr(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_arr(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_obj(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_obj(val) : false; +} + +yyjson_api_inline bool yyjson_mut_is_ctn(yyjson_mut_val *val) { + return val ? unsafe_yyjson_is_ctn(val) : false; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Value Content API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_type yyjson_mut_get_type(yyjson_mut_val *val) { + return yyjson_get_type((yyjson_val *)val); +} + +yyjson_api_inline yyjson_subtype yyjson_mut_get_subtype(yyjson_mut_val *val) { + return yyjson_get_subtype((yyjson_val *)val); +} + +yyjson_api_inline uint8_t yyjson_mut_get_tag(yyjson_mut_val *val) { + return yyjson_get_tag((yyjson_val *)val); +} + +yyjson_api_inline const char *yyjson_mut_get_type_desc(yyjson_mut_val *val) { + return yyjson_get_type_desc((yyjson_val *)val); +} + +yyjson_api_inline const char *yyjson_mut_get_raw(yyjson_mut_val *val) { + return yyjson_get_raw((yyjson_val *)val); +} + +yyjson_api_inline bool yyjson_mut_get_bool(yyjson_mut_val *val) { + return yyjson_get_bool((yyjson_val *)val); +} + +yyjson_api_inline uint64_t yyjson_mut_get_uint(yyjson_mut_val *val) { + return yyjson_get_uint((yyjson_val *)val); +} + +yyjson_api_inline int64_t yyjson_mut_get_sint(yyjson_mut_val *val) { + return yyjson_get_sint((yyjson_val *)val); +} + +yyjson_api_inline int yyjson_mut_get_int(yyjson_mut_val *val) { + return yyjson_get_int((yyjson_val *)val); +} + +yyjson_api_inline double yyjson_mut_get_real(yyjson_mut_val *val) { + return yyjson_get_real((yyjson_val *)val); +} + +yyjson_api_inline double yyjson_mut_get_num(yyjson_mut_val *val) { + return yyjson_get_num((yyjson_val *)val); +} + +yyjson_api_inline const char *yyjson_mut_get_str(yyjson_mut_val *val) { + return yyjson_get_str((yyjson_val *)val); +} + +yyjson_api_inline size_t yyjson_mut_get_len(yyjson_mut_val *val) { + return yyjson_get_len((yyjson_val *)val); +} + +yyjson_api_inline bool yyjson_mut_equals_str(yyjson_mut_val *val, + const char *str) { + return yyjson_equals_str((yyjson_val *)val, str); +} + +yyjson_api_inline bool yyjson_mut_equals_strn(yyjson_mut_val *val, + const char *str, size_t len) { + return yyjson_equals_strn((yyjson_val *)val, str, len); +} + +yyjson_api bool unsafe_yyjson_mut_equals(yyjson_mut_val *lhs, + yyjson_mut_val *rhs); + +yyjson_api_inline bool yyjson_mut_equals(yyjson_mut_val *lhs, + yyjson_mut_val *rhs) { + if (yyjson_unlikely(!lhs || !rhs)) return false; + return unsafe_yyjson_mut_equals(lhs, rhs); +} + +yyjson_api_inline bool yyjson_mut_set_raw(yyjson_mut_val *val, + const char *raw, size_t len) { + if (yyjson_unlikely(!val || !raw)) return false; + unsafe_yyjson_set_raw(val, raw, len); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_null(yyjson_mut_val *val) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_null(val); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_bool(yyjson_mut_val *val, bool num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_bool(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_uint(yyjson_mut_val *val, uint64_t num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_uint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_sint(yyjson_mut_val *val, int64_t num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_sint(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_int(yyjson_mut_val *val, int num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_sint(val, (int64_t)num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_float(yyjson_mut_val *val, float num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_float(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_double(yyjson_mut_val *val, double num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_double(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_real(yyjson_mut_val *val, double num) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_real(val, num); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_fp_to_fixed(yyjson_mut_val *val, + int prec) { + if (yyjson_unlikely(!yyjson_mut_is_real(val))) return false; + unsafe_yyjson_set_fp_to_fixed(val, prec); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_fp_to_float(yyjson_mut_val *val, + bool flt) { + if (yyjson_unlikely(!yyjson_mut_is_real(val))) return false; + unsafe_yyjson_set_fp_to_float(val, flt); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_str(yyjson_mut_val *val, + const char *str) { + if (yyjson_unlikely(!val || !str)) return false; + unsafe_yyjson_set_str(val, str); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_strn(yyjson_mut_val *val, + const char *str, size_t len) { + if (yyjson_unlikely(!val || !str)) return false; + unsafe_yyjson_set_strn(val, str, len); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_str_noesc(yyjson_mut_val *val, + bool noesc) { + if (yyjson_unlikely(!yyjson_mut_is_str(val))) return false; + unsafe_yyjson_set_str_noesc(val, noesc); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_arr(yyjson_mut_val *val) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_arr(val, 0); + return true; +} + +yyjson_api_inline bool yyjson_mut_set_obj(yyjson_mut_val *val) { + if (yyjson_unlikely(!val)) return false; + unsafe_yyjson_set_obj(val, 0); + return true; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Value Creation API (Implementation) + *============================================================================*/ + +#define yyjson_mut_val_one(func) \ + if (yyjson_likely(doc)) { \ + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); \ + if (yyjson_likely(val)) { \ + func \ + return val; \ + } \ + } \ + return NULL + +#define yyjson_mut_val_one_str(func) \ + if (yyjson_likely(doc && str)) { \ + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); \ + if (yyjson_likely(val)) { \ + func \ + return val; \ + } \ + } \ + return NULL + +yyjson_api_inline yyjson_mut_val *yyjson_mut_raw(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_raw(val, str, strlen(str)); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawn(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_raw(val, str, len); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawcpy(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ + size_t len = strlen(str); + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_raw(val, new_str, len); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_rawncpy(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_raw(val, new_str, len); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_null(yyjson_mut_doc *doc) { + yyjson_mut_val_one({ unsafe_yyjson_set_null(val); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_true(yyjson_mut_doc *doc) { + yyjson_mut_val_one({ unsafe_yyjson_set_bool(val, true); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_false(yyjson_mut_doc *doc) { + yyjson_mut_val_one({ unsafe_yyjson_set_bool(val, false); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_bool(yyjson_mut_doc *doc, + bool _val) { + yyjson_mut_val_one({ unsafe_yyjson_set_bool(val, _val); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_uint(yyjson_mut_doc *doc, + uint64_t num) { + yyjson_mut_val_one({ unsafe_yyjson_set_uint(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_sint(yyjson_mut_doc *doc, + int64_t num) { + yyjson_mut_val_one({ unsafe_yyjson_set_sint(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_int(yyjson_mut_doc *doc, + int64_t num) { + yyjson_mut_val_one({ unsafe_yyjson_set_sint(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_float(yyjson_mut_doc *doc, + float num) { + yyjson_mut_val_one({ unsafe_yyjson_set_float(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_double(yyjson_mut_doc *doc, + double num) { + yyjson_mut_val_one({ unsafe_yyjson_set_double(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_real(yyjson_mut_doc *doc, + double num) { + yyjson_mut_val_one({ unsafe_yyjson_set_real(val, num); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_str(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_str(val, str); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_strn(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ unsafe_yyjson_set_strn(val, str, len); }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_strcpy(yyjson_mut_doc *doc, + const char *str) { + yyjson_mut_val_one_str({ + size_t len = strlen(str); + bool noesc = unsafe_yyjson_is_str_noesc(str, len); + yyjson_subtype sub = noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_tag(val, YYJSON_TYPE_STR, sub, len); + val->uni.str = new_str; + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_strncpy(yyjson_mut_doc *doc, + const char *str, + size_t len) { + yyjson_mut_val_one_str({ + char *new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_strn(val, new_str, len); + }); +} + +#undef yyjson_mut_val_one +#undef yyjson_mut_val_one_str + + + +/*============================================================================== + * MARK: - Mutable JSON Array API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_mut_arr_size(yyjson_mut_val *arr) { + return yyjson_mut_is_arr(arr) ? unsafe_yyjson_get_len(arr) : 0; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get(yyjson_mut_val *arr, + size_t idx) { + if (yyjson_likely(idx < yyjson_mut_arr_size(arr))) { + yyjson_mut_val *val = (yyjson_mut_val *)arr->uni.ptr; + while (idx-- > 0) val = val->next; + return val->next; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_first( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_arr_size(arr) > 0)) { + return ((yyjson_mut_val *)arr->uni.ptr)->next; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_get_last( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_arr_size(arr) > 0)) { + return ((yyjson_mut_val *)arr->uni.ptr); + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Array Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_arr_iter_init(yyjson_mut_val *arr, + yyjson_mut_arr_iter *iter) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(arr); + iter->cur = iter->max ? (yyjson_mut_val *)arr->uni.ptr : NULL; + iter->pre = NULL; + iter->arr = arr; + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_mut_arr_iter)); + return false; +} + +yyjson_api_inline yyjson_mut_arr_iter yyjson_mut_arr_iter_with( + yyjson_mut_val *arr) { + yyjson_mut_arr_iter iter; + yyjson_mut_arr_iter_init(arr, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_mut_arr_iter_has_next(yyjson_mut_arr_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_next( + yyjson_mut_arr_iter *iter) { + if (iter && iter->idx < iter->max) { + yyjson_mut_val *val = iter->cur; + iter->pre = val; + iter->cur = val->next; + iter->idx++; + return iter->cur; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_iter_remove( + yyjson_mut_arr_iter *iter) { + if (yyjson_likely(iter && 0 < iter->idx && iter->idx <= iter->max)) { + yyjson_mut_val *prev = iter->pre; + yyjson_mut_val *cur = iter->cur; + yyjson_mut_val *next = cur->next; + if (yyjson_unlikely(iter->idx == iter->max)) iter->arr->uni.ptr = prev; + iter->idx--; + iter->max--; + unsafe_yyjson_set_len(iter->arr, iter->max); + prev->next = next; + iter->cur = prev; + return cur; + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Array Creation API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr(yyjson_mut_doc *doc) { + if (yyjson_likely(doc)) { + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); + if (yyjson_likely(val)) { + val->tag = YYJSON_TYPE_ARR | YYJSON_SUBTYPE_NONE; + return val; + } + } + return NULL; +} + +#define yyjson_mut_arr_with_func(func) \ + if (yyjson_likely(doc && ((0 < count && count < \ + (~(size_t)0) / sizeof(yyjson_mut_val) && vals) || count == 0))) { \ + yyjson_mut_val *arr = unsafe_yyjson_mut_val(doc, 1 + count); \ + if (yyjson_likely(arr)) { \ + arr->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_ARR; \ + if (count > 0) { \ + size_t i; \ + for (i = 0; i < count; i++) { \ + yyjson_mut_val *val = arr + i + 1; \ + func \ + val->next = val + 1; \ + } \ + arr[count].next = arr + 1; \ + arr->uni.ptr = arr + count; \ + } \ + return arr; \ + } \ + } \ + return NULL + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_bool( + yyjson_mut_doc *doc, const bool *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_bool(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint( + yyjson_mut_doc *doc, const int64_t *vals, size_t count) { + return yyjson_mut_arr_with_sint64(doc, vals, count); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count) { + return yyjson_mut_arr_with_uint64(doc, vals, count); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_real( + yyjson_mut_doc *doc, const double *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_real(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint8( + yyjson_mut_doc *doc, const int8_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint16( + yyjson_mut_doc *doc, const int16_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint32( + yyjson_mut_doc *doc, const int32_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_sint64( + yyjson_mut_doc *doc, const int64_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_sint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint8( + yyjson_mut_doc *doc, const uint8_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint16( + yyjson_mut_doc *doc, const uint16_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint32( + yyjson_mut_doc *doc, const uint32_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_uint64( + yyjson_mut_doc *doc, const uint64_t *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_uint(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_float( + yyjson_mut_doc *doc, const float *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_float(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_double( + yyjson_mut_doc *doc, const double *vals, size_t count) { + yyjson_mut_arr_with_func({ + unsafe_yyjson_set_double(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_str( + yyjson_mut_doc *doc, const char **vals, size_t count) { + yyjson_mut_arr_with_func({ + if (yyjson_unlikely(!vals[i])) return NULL; + unsafe_yyjson_set_str(val, vals[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strn( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count) { + if (yyjson_unlikely(count > 0 && !lens)) return NULL; + yyjson_mut_arr_with_func({ + if (yyjson_unlikely(!vals[i])) return NULL; + unsafe_yyjson_set_strn(val, vals[i], lens[i]); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strcpy( + yyjson_mut_doc *doc, const char **vals, size_t count) { + size_t len; + const char *str, *new_str; + yyjson_mut_arr_with_func({ + str = vals[i]; + if (yyjson_unlikely(!str)) return NULL; + len = strlen(str); + new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_strn(val, new_str, len); + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_with_strncpy( + yyjson_mut_doc *doc, const char **vals, const size_t *lens, size_t count) { + size_t len; + const char *str, *new_str; + if (yyjson_unlikely(count > 0 && !lens)) return NULL; + yyjson_mut_arr_with_func({ + str = vals[i]; + if (yyjson_unlikely(!str)) return NULL; + len = lens[i]; + new_str = unsafe_yyjson_mut_strncpy(doc, str, len); + if (yyjson_unlikely(!new_str)) return NULL; + unsafe_yyjson_set_strn(val, new_str, len); + }); +} + +#undef yyjson_mut_arr_with_func + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_arr_insert(yyjson_mut_val *arr, + yyjson_mut_val *val, size_t idx) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(idx <= len)) { + unsafe_yyjson_set_len(arr, len + 1); + if (len == 0) { + val->next = val; + arr->uni.ptr = val; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + if (idx == len) { + prev->next = val; + val->next = next; + arr->uni.ptr = val; + } else { + while (idx-- > 0) { + prev = next; + next = next->next; + } + prev->next = val; + val->next = next; + } + } + return true; + } + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + unsafe_yyjson_set_len(arr, len + 1); + if (len == 0) { + val->next = val; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + prev->next = val; + val->next = next; + } + arr->uni.ptr = val; + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_prepend(yyjson_mut_val *arr, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + unsafe_yyjson_set_len(arr, len + 1); + if (len == 0) { + val->next = val; + arr->uni.ptr = val; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + prev->next = val; + val->next = next; + } + return true; + } + return false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_replace(yyjson_mut_val *arr, + size_t idx, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && val)) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(idx < len)) { + if (yyjson_likely(len > 1)) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + while (idx-- > 0) { + prev = next; + next = next->next; + } + prev->next = val; + val->next = next->next; + if ((void *)next == arr->uni.ptr) arr->uni.ptr = val; + return next; + } else { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + val->next = val; + arr->uni.ptr = val; + return prev; + } + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove(yyjson_mut_val *arr, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(idx < len)) { + unsafe_yyjson_set_len(arr, len - 1); + if (yyjson_likely(len > 1)) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + while (idx-- > 0) { + prev = next; + next = next->next; + } + prev->next = next->next; + if ((void *)next == arr->uni.ptr) arr->uni.ptr = prev; + return next; + } else { + return ((yyjson_mut_val *)arr->uni.ptr); + } + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_first( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (len > 1) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + prev->next = next->next; + unsafe_yyjson_set_len(arr, len - 1); + return next; + } else if (len == 1) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + unsafe_yyjson_set_len(arr, 0); + return prev; + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_remove_last( + yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_likely(len > 1)) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + yyjson_mut_val *next = prev->next; + unsafe_yyjson_set_len(arr, len - 1); + while (--len > 0) prev = prev->next; + prev->next = next; + next = (yyjson_mut_val *)arr->uni.ptr; + arr->uni.ptr = prev; + return next; + } else if (len == 1) { + yyjson_mut_val *prev = ((yyjson_mut_val *)arr->uni.ptr); + unsafe_yyjson_set_len(arr, 0); + return prev; + } + } + return NULL; +} + +yyjson_api_inline bool yyjson_mut_arr_remove_range(yyjson_mut_val *arr, + size_t _idx, size_t _len) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + yyjson_mut_val *prev, *next; + bool tail_removed; + size_t len = unsafe_yyjson_get_len(arr); + if (yyjson_unlikely(_idx + _len > len)) return false; + if (yyjson_unlikely(_len == 0)) return true; + unsafe_yyjson_set_len(arr, len - _len); + if (yyjson_unlikely(len == _len)) return true; + tail_removed = (_idx + _len == len); + prev = ((yyjson_mut_val *)arr->uni.ptr); + while (_idx-- > 0) prev = prev->next; + next = prev->next; + while (_len-- > 0) next = next->next; + prev->next = next; + if (yyjson_unlikely(tail_removed)) arr->uni.ptr = prev; + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_clear(yyjson_mut_val *arr) { + if (yyjson_likely(yyjson_mut_is_arr(arr))) { + unsafe_yyjson_set_len(arr, 0); + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_rotate(yyjson_mut_val *arr, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_arr(arr) && + unsafe_yyjson_get_len(arr) > idx)) { + yyjson_mut_val *val = (yyjson_mut_val *)arr->uni.ptr; + while (idx-- > 0) val = val->next; + arr->uni.ptr = (void *)val; + return true; + } + return false; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Array Modification Convenience API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_arr_add_val(yyjson_mut_val *arr, + yyjson_mut_val *val) { + return yyjson_mut_arr_append(arr, val); +} + +yyjson_api_inline bool yyjson_mut_arr_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_null(doc); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_true(doc); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_false(doc); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + bool _val) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_bool(doc, _val); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + uint64_t num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_uint(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_sint(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + int64_t num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_sint(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + float num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_float(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_double(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + double num) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_real(doc, num); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_str(doc, str); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, size_t len) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_strn(doc, str, len); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_strcpy(doc, str); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_arr_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *arr, + const char *str, size_t len) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_strncpy(doc, str, len); + return yyjson_mut_arr_append(arr, val); + } + return false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_arr(doc); + return yyjson_mut_arr_append(arr, val) ? val : NULL; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_arr_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *arr) { + if (yyjson_likely(doc && yyjson_mut_is_arr(arr))) { + yyjson_mut_val *val = yyjson_mut_obj(doc); + return yyjson_mut_arr_append(arr, val) ? val : NULL; + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object API (Implementation) + *============================================================================*/ + +yyjson_api_inline size_t yyjson_mut_obj_size(yyjson_mut_val *obj) { + return yyjson_mut_is_obj(obj) ? unsafe_yyjson_get_len(obj) : 0; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_get(yyjson_mut_val *obj, + const char *key) { + return yyjson_mut_obj_getn(obj, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_getn(yyjson_mut_val *obj, + const char *_key, + size_t key_len) { + size_t len = yyjson_mut_obj_size(obj); + if (yyjson_likely(len && _key)) { + yyjson_mut_val *key = ((yyjson_mut_val *)obj->uni.ptr)->next->next; + while (len-- > 0) { + if (unsafe_yyjson_equals_strn(key, _key, key_len)) return key->next; + key = key->next->next; + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Iterator API (Implementation) + *============================================================================*/ + +yyjson_api_inline bool yyjson_mut_obj_iter_init(yyjson_mut_val *obj, + yyjson_mut_obj_iter *iter) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && iter)) { + iter->idx = 0; + iter->max = unsafe_yyjson_get_len(obj); + iter->cur = iter->max ? (yyjson_mut_val *)obj->uni.ptr : NULL; + iter->pre = NULL; + iter->obj = obj; + return true; + } + if (iter) memset(iter, 0, sizeof(yyjson_mut_obj_iter)); + return false; +} + +yyjson_api_inline yyjson_mut_obj_iter yyjson_mut_obj_iter_with( + yyjson_mut_val *obj) { + yyjson_mut_obj_iter iter; + yyjson_mut_obj_iter_init(obj, &iter); + return iter; +} + +yyjson_api_inline bool yyjson_mut_obj_iter_has_next(yyjson_mut_obj_iter *iter) { + return iter ? iter->idx < iter->max : false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_next( + yyjson_mut_obj_iter *iter) { + if (iter && iter->idx < iter->max) { + yyjson_mut_val *key = iter->cur; + iter->pre = key; + iter->cur = key->next->next; + iter->idx++; + return iter->cur; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get_val( + yyjson_mut_val *key) { + return key ? key->next : NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_remove( + yyjson_mut_obj_iter *iter) { + if (yyjson_likely(iter && 0 < iter->idx && iter->idx <= iter->max)) { + yyjson_mut_val *prev = iter->pre; + yyjson_mut_val *cur = iter->cur; + yyjson_mut_val *next = cur->next->next; + if (yyjson_unlikely(iter->idx == iter->max)) iter->obj->uni.ptr = prev; + iter->idx--; + iter->max--; + unsafe_yyjson_set_len(iter->obj, iter->max); + prev->next->next = next; + iter->cur = prev; + return cur->next; + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_get( + yyjson_mut_obj_iter *iter, const char *key) { + return yyjson_mut_obj_iter_getn(iter, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_iter_getn( + yyjson_mut_obj_iter *iter, const char *key, size_t key_len) { + if (iter && key) { + size_t idx = 0; + size_t max = iter->max; + yyjson_mut_val *pre, *cur = iter->cur; + while (idx++ < max) { + pre = cur; + cur = cur->next->next; + if (unsafe_yyjson_equals_strn(cur, key, key_len)) { + iter->idx += idx; + if (iter->idx > max) iter->idx -= max + 1; + iter->pre = pre; + iter->cur = cur; + return cur->next; + } + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Creation API (Implementation) + *============================================================================*/ + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj(yyjson_mut_doc *doc) { + if (yyjson_likely(doc)) { + yyjson_mut_val *val = unsafe_yyjson_mut_val(doc, 1); + if (yyjson_likely(val)) { + val->tag = YYJSON_TYPE_OBJ | YYJSON_SUBTYPE_NONE; + return val; + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_str(yyjson_mut_doc *doc, + const char **keys, + const char **vals, + size_t count) { + if (yyjson_likely(doc && ((count > 0 && keys && vals) || (count == 0)))) { + yyjson_mut_val *obj = unsafe_yyjson_mut_val(doc, 1 + count * 2); + if (yyjson_likely(obj)) { + obj->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_OBJ; + if (count > 0) { + size_t i; + for (i = 0; i < count; i++) { + yyjson_mut_val *key = obj + (i * 2 + 1); + yyjson_mut_val *val = obj + (i * 2 + 2); + uint64_t key_len = (uint64_t)strlen(keys[i]); + uint64_t val_len = (uint64_t)strlen(vals[i]); + key->tag = (key_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->tag = (val_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + key->uni.str = keys[i]; + val->uni.str = vals[i]; + key->next = val; + val->next = val + 1; + } + obj[count * 2].next = obj + 1; + obj->uni.ptr = obj + (count * 2 - 1); + } + return obj; + } + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_with_kv(yyjson_mut_doc *doc, + const char **pairs, + size_t count) { + if (yyjson_likely(doc && ((count > 0 && pairs) || (count == 0)))) { + yyjson_mut_val *obj = unsafe_yyjson_mut_val(doc, 1 + count * 2); + if (yyjson_likely(obj)) { + obj->tag = ((uint64_t)count << YYJSON_TAG_BIT) | YYJSON_TYPE_OBJ; + if (count > 0) { + size_t i; + for (i = 0; i < count; i++) { + yyjson_mut_val *key = obj + (i * 2 + 1); + yyjson_mut_val *val = obj + (i * 2 + 2); + const char *key_str = pairs[i * 2 + 0]; + const char *val_str = pairs[i * 2 + 1]; + uint64_t key_len = (uint64_t)strlen(key_str); + uint64_t val_len = (uint64_t)strlen(val_str); + key->tag = (key_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->tag = (val_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + key->uni.str = key_str; + val->uni.str = val_str; + key->next = val; + val->next = val + 1; + } + obj[count * 2].next = obj + 1; + obj->uni.ptr = obj + (count * 2 - 1); + } + return obj; + } + } + return NULL; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification API (Implementation) + *============================================================================*/ + +yyjson_api_inline void unsafe_yyjson_mut_obj_add(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val, + size_t len) { + if (yyjson_likely(len)) { + yyjson_mut_val *prev_val = ((yyjson_mut_val *)obj->uni.ptr)->next; + yyjson_mut_val *next_key = prev_val->next; + prev_val->next = key; + val->next = next_key; + } else { + val->next = key; + } + key->next = val; + obj->uni.ptr = (void *)key; + unsafe_yyjson_set_len(obj, len + 1); +} + +yyjson_api_inline yyjson_mut_val *unsafe_yyjson_mut_obj_remove( + yyjson_mut_val *obj, const char *key, size_t key_len) { + size_t obj_len = unsafe_yyjson_get_len(obj); + if (obj_len) { + yyjson_mut_val *pre_key = (yyjson_mut_val *)obj->uni.ptr; + yyjson_mut_val *cur_key = pre_key->next->next; + yyjson_mut_val *removed_item = NULL; + size_t i; + for (i = 0; i < obj_len; i++) { + if (unsafe_yyjson_equals_strn(cur_key, key, key_len)) { + if (!removed_item) removed_item = cur_key->next; + cur_key = cur_key->next->next; + pre_key->next->next = cur_key; + if (i + 1 == obj_len) obj->uni.ptr = pre_key; + i--; + obj_len--; + } else { + pre_key = cur_key; + cur_key = cur_key->next->next; + } + } + unsafe_yyjson_set_len(obj, obj_len); + return removed_item; + } else { + return NULL; + } +} + +yyjson_api_inline bool unsafe_yyjson_mut_obj_replace(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + size_t key_len = unsafe_yyjson_get_len(key); + size_t obj_len = unsafe_yyjson_get_len(obj); + if (obj_len) { + yyjson_mut_val *pre_key = (yyjson_mut_val *)obj->uni.ptr; + yyjson_mut_val *cur_key = pre_key->next->next; + size_t i; + for (i = 0; i < obj_len; i++) { + if (unsafe_yyjson_equals_strn(cur_key, key->uni.str, key_len)) { + cur_key->next->tag = val->tag; + cur_key->next->uni.u64 = val->uni.u64; + return true; + } else { + cur_key = cur_key->next->next; + } + } + } + return false; +} + +yyjson_api_inline void unsafe_yyjson_mut_obj_rotate(yyjson_mut_val *obj, + size_t idx) { + yyjson_mut_val *key = (yyjson_mut_val *)obj->uni.ptr; + while (idx-- > 0) key = key->next->next; + obj->uni.ptr = (void *)key; +} + +yyjson_api_inline bool yyjson_mut_obj_add(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + yyjson_mut_is_str(key) && val)) { + unsafe_yyjson_mut_obj_add(obj, key, val, unsafe_yyjson_get_len(obj)); + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + bool replaced = false; + size_t key_len; + yyjson_mut_obj_iter iter; + yyjson_mut_val *cur_key; + if (yyjson_unlikely(!yyjson_mut_is_obj(obj) || + !yyjson_mut_is_str(key))) return false; + key_len = unsafe_yyjson_get_len(key); + yyjson_mut_obj_iter_init(obj, &iter); + while ((cur_key = yyjson_mut_obj_iter_next(&iter)) != 0) { + if (unsafe_yyjson_equals_strn(cur_key, key->uni.str, key_len)) { + if (!replaced && val) { + replaced = true; + val->next = cur_key->next->next; + cur_key->next = val; + } else { + yyjson_mut_obj_iter_remove(&iter); + } + } + } + if (!replaced && val) unsafe_yyjson_mut_obj_add(obj, key, val, iter.max); + return true; +} + +yyjson_api_inline bool yyjson_mut_obj_insert(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + yyjson_mut_is_str(key) && val)) { + size_t len = unsafe_yyjson_get_len(obj); + if (yyjson_likely(len >= idx)) { + if (len > idx) { + void *ptr = obj->uni.ptr; + unsafe_yyjson_mut_obj_rotate(obj, idx); + unsafe_yyjson_mut_obj_add(obj, key, val, len); + obj->uni.ptr = ptr; + } else { + unsafe_yyjson_mut_obj_add(obj, key, val, len); + } + return true; + } + } + return false; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove(yyjson_mut_val *obj, + yyjson_mut_val *key) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && yyjson_mut_is_str(key))) { + return unsafe_yyjson_mut_obj_remove(obj, key->uni.str, + unsafe_yyjson_get_len(key)); + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_key( + yyjson_mut_val *obj, const char *key) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && key)) { + size_t key_len = strlen(key); + return unsafe_yyjson_mut_obj_remove(obj, key, key_len); + } + return NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_keyn( + yyjson_mut_val *obj, const char *key, size_t key_len) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && key)) { + return unsafe_yyjson_mut_obj_remove(obj, key, key_len); + } + return NULL; +} + +yyjson_api_inline bool yyjson_mut_obj_clear(yyjson_mut_val *obj) { + if (yyjson_likely(yyjson_mut_is_obj(obj))) { + unsafe_yyjson_set_len(obj, 0); + return true; + } + return false; +} + +yyjson_api_inline bool yyjson_mut_obj_replace(yyjson_mut_val *obj, + yyjson_mut_val *key, + yyjson_mut_val *val) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + yyjson_mut_is_str(key) && val)) { + return unsafe_yyjson_mut_obj_replace(obj, key, val); + } + return false; +} + +yyjson_api_inline bool yyjson_mut_obj_rotate(yyjson_mut_val *obj, + size_t idx) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && + unsafe_yyjson_get_len(obj) > idx)) { + unsafe_yyjson_mut_obj_rotate(obj, idx); + return true; + } + return false; +} + + + +/*============================================================================== + * MARK: - Mutable JSON Object Modification Convenience API (Implementation) + *============================================================================*/ + +#define yyjson_mut_obj_add_func(func) \ + if (yyjson_likely(doc && yyjson_mut_is_obj(obj) && _key)) { \ + yyjson_mut_val *key = unsafe_yyjson_mut_val(doc, 2); \ + if (yyjson_likely(key)) { \ + size_t len = unsafe_yyjson_get_len(obj); \ + yyjson_mut_val *val = key + 1; \ + size_t key_len = strlen(_key); \ + bool noesc = unsafe_yyjson_is_str_noesc(_key, key_len); \ + key->tag = YYJSON_TYPE_STR; \ + key->tag |= noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; \ + key->tag |= (uint64_t)strlen(_key) << YYJSON_TAG_BIT; \ + key->uni.str = _key; \ + func \ + unsafe_yyjson_mut_obj_add(obj, key, val, len); \ + return true; \ + } \ + } \ + return false + +yyjson_api_inline bool yyjson_mut_obj_add_null(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_null(val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_true(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_bool(val, true); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_false(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_bool(val, false); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + bool _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_bool(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_uint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + uint64_t _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_uint(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_sint(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + int64_t _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_sint(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + int64_t _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_sint(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_float(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + float _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_float(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_double(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + double _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_double(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_real(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + double _val) { + yyjson_mut_obj_add_func({ unsafe_yyjson_set_real(val, _val); }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_str(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + size_t val_len = strlen(_val); + bool val_noesc = unsafe_yyjson_is_str_noesc(_val, val_len); + val->tag = ((uint64_t)strlen(_val) << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->tag |= val_noesc ? YYJSON_SUBTYPE_NOESC : YYJSON_SUBTYPE_NONE; + val->uni.str = _val; + }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_strn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val, + size_t _len) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + val->tag = ((uint64_t)_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + val->uni.str = _val; + }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + size_t _len = strlen(_val); + val->uni.str = unsafe_yyjson_mut_strncpy(doc, _val, _len); + if (yyjson_unlikely(!val->uni.str)) return false; + val->tag = ((uint64_t)_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + }); +} + +yyjson_api_inline bool yyjson_mut_obj_add_strncpy(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + const char *_val, + size_t _len) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + val->uni.str = unsafe_yyjson_mut_strncpy(doc, _val, _len); + if (yyjson_unlikely(!val->uni.str)) return false; + val->tag = ((uint64_t)_len << YYJSON_TAG_BIT) | YYJSON_TYPE_STR; + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_arr(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_val *key = yyjson_mut_str(doc, _key); + yyjson_mut_val *val = yyjson_mut_arr(doc); + return yyjson_mut_obj_add(obj, key, val) ? val : NULL; +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_obj(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key) { + yyjson_mut_val *key = yyjson_mut_str(doc, _key); + yyjson_mut_val *val = yyjson_mut_obj(doc); + return yyjson_mut_obj_add(obj, key, val) ? val : NULL; +} + +yyjson_api_inline bool yyjson_mut_obj_add_val(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *_key, + yyjson_mut_val *_val) { + if (yyjson_unlikely(!_val)) return false; + yyjson_mut_obj_add_func({ + val = _val; + }); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_str(yyjson_mut_val *obj, + const char *key) { + return yyjson_mut_obj_remove_strn(obj, key, key ? strlen(key) : 0); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_remove_strn( + yyjson_mut_val *obj, const char *_key, size_t _len) { + if (yyjson_likely(yyjson_mut_is_obj(obj) && _key)) { + yyjson_mut_val *key; + yyjson_mut_obj_iter iter; + yyjson_mut_val *val_removed = NULL; + yyjson_mut_obj_iter_init(obj, &iter); + while ((key = yyjson_mut_obj_iter_next(&iter)) != NULL) { + if (unsafe_yyjson_equals_strn(key, _key, _len)) { + if (!val_removed) val_removed = key->next; + yyjson_mut_obj_iter_remove(&iter); + } + } + return val_removed; + } + return NULL; +} + +yyjson_api_inline bool yyjson_mut_obj_rename_key(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + const char *new_key) { + if (!key || !new_key) return false; + return yyjson_mut_obj_rename_keyn(doc, obj, key, strlen(key), + new_key, strlen(new_key)); +} + +yyjson_api_inline bool yyjson_mut_obj_rename_keyn(yyjson_mut_doc *doc, + yyjson_mut_val *obj, + const char *key, + size_t len, + const char *new_key, + size_t new_len) { + char *cpy_key = NULL; + yyjson_mut_val *old_key; + yyjson_mut_obj_iter iter; + if (!doc || !obj || !key || !new_key) return false; + yyjson_mut_obj_iter_init(obj, &iter); + while ((old_key = yyjson_mut_obj_iter_next(&iter))) { + if (unsafe_yyjson_equals_strn((void *)old_key, key, len)) { + if (!cpy_key) { + cpy_key = unsafe_yyjson_mut_strncpy(doc, new_key, new_len); + if (!cpy_key) return false; + } + yyjson_mut_set_strn(old_key, cpy_key, new_len); + } + } + return cpy_key != NULL; +} + + + +#if !defined(YYJSON_DISABLE_UTILS) || !YYJSON_DISABLE_UTILS + +/*============================================================================== + * MARK: - JSON Pointer API (Implementation) + *============================================================================*/ + +#define yyjson_ptr_set_err(_code, _msg) do { \ + if (err) { \ + err->code = YYJSON_PTR_ERR_##_code; \ + err->msg = _msg; \ + err->pos = 0; \ + } \ +} while(false) + +/* require: val != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_val *unsafe_yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t len, + yyjson_ptr_err *err); + +/* require: val != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_getx(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/* require: val/new_val/doc != NULL, *ptr == '/', len > 0 */ +yyjson_api bool unsafe_yyjson_mut_ptr_putx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, bool insert_new, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +/* require: val/err != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err); + +/* require: val/err != NULL, *ptr == '/', len > 0 */ +yyjson_api yyjson_mut_val *unsafe_yyjson_mut_ptr_removex(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err); + +yyjson_api_inline yyjson_val *yyjson_doc_ptr_get(yyjson_doc *doc, + const char *ptr) { + if (yyjson_unlikely(!ptr)) return NULL; + return yyjson_doc_ptr_getn(doc, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getn(yyjson_doc *doc, + const char *ptr, size_t len) { + return yyjson_doc_ptr_getx(doc, ptr, len, NULL); +} + +yyjson_api_inline yyjson_val *yyjson_doc_ptr_getx(yyjson_doc *doc, + const char *ptr, size_t len, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return doc->root; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_ptr_getx(doc->root, ptr, len, err); +} + +yyjson_api_inline yyjson_val *yyjson_ptr_get(yyjson_val *val, + const char *ptr) { + if (yyjson_unlikely(!ptr)) return NULL; + return yyjson_ptr_getn(val, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_val *yyjson_ptr_getn(yyjson_val *val, + const char *ptr, size_t len) { + return yyjson_ptr_getx(val, ptr, len, NULL); +} + +yyjson_api_inline yyjson_val *yyjson_ptr_getx(yyjson_val *val, + const char *ptr, size_t len, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (yyjson_unlikely(!val || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return val; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_ptr_getx(val, ptr, len, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_get(yyjson_mut_doc *doc, + const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_doc_ptr_getn(doc, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getn(yyjson_mut_doc *doc, + const char *ptr, + size_t len) { + return yyjson_mut_doc_ptr_getx(doc, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_getx(yyjson_mut_doc *doc, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return doc->root; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_getx(doc->root, ptr, len, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_get(yyjson_mut_val *val, + const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_ptr_getn(val, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getn(yyjson_mut_val *val, + const char *ptr, + size_t len) { + return yyjson_mut_ptr_getx(val, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_getx(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + return val; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_getx(val, ptr, len, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_add(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_doc_ptr_addn(doc, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_addn(yyjson_mut_doc *doc, + const char *ptr, + size_t len, + yyjson_mut_val *new_val) { + return yyjson_mut_doc_ptr_addx(doc, ptr, len, new_val, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_addx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr || !new_val)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + if (doc->root) { + yyjson_ptr_set_err(SET_ROOT, "cannot set document's root"); + return false; + } else { + doc->root = new_val; + return true; + } + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + if (yyjson_unlikely(!doc->root && !create_parent)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return false; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_mut_val *root = yyjson_mut_obj(doc); + if (yyjson_unlikely(!root)) { + yyjson_ptr_set_err(MEMORY_ALLOCATION, "failed to create value"); + return false; + } + if (unsafe_yyjson_mut_ptr_putx(root, ptr, len, new_val, doc, + create_parent, true, ctx, err)) { + doc->root = root; + return true; + } + return false; + } + return unsafe_yyjson_mut_ptr_putx(doc->root, ptr, len, new_val, doc, + create_parent, true, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_ptr_add(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_ptr_addn(val, ptr, strlen(ptr), new_val, doc); +} + +yyjson_api_inline bool yyjson_mut_ptr_addn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + return yyjson_mut_ptr_addx(val, ptr, len, new_val, doc, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_ptr_addx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr || !new_val || !doc)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return false; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + return unsafe_yyjson_mut_ptr_putx(val, ptr, len, new_val, + doc, create_parent, true, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_set(yyjson_mut_doc *doc, + const char *ptr, + yyjson_mut_val *new_val) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_doc_ptr_setn(doc, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_setn(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val) { + return yyjson_mut_doc_ptr_setx(doc, ptr, len, new_val, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_doc_ptr_setx(yyjson_mut_doc *doc, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + if (ctx) ctx->old = doc->root; + doc->root = new_val; + return true; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + if (!new_val) { + if (!doc->root) { + yyjson_ptr_set_err(RESOLVE, "JSON pointer cannot be resolved"); + return false; + } + return !!unsafe_yyjson_mut_ptr_removex(doc->root, ptr, len, ctx, err); + } + if (yyjson_unlikely(!doc->root && !create_parent)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return false; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_mut_val *root = yyjson_mut_obj(doc); + if (yyjson_unlikely(!root)) { + yyjson_ptr_set_err(MEMORY_ALLOCATION, "failed to create value"); + return false; + } + if (unsafe_yyjson_mut_ptr_putx(root, ptr, len, new_val, doc, + create_parent, false, ctx, err)) { + doc->root = root; + return true; + } + return false; + } + return unsafe_yyjson_mut_ptr_putx(doc->root, ptr, len, new_val, doc, + create_parent, false, ctx, err); +} + +yyjson_api_inline bool yyjson_mut_ptr_set(yyjson_mut_val *val, + const char *ptr, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + if (yyjson_unlikely(!ptr)) return false; + return yyjson_mut_ptr_setn(val, ptr, strlen(ptr), new_val, doc); +} + +yyjson_api_inline bool yyjson_mut_ptr_setn(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc) { + return yyjson_mut_ptr_setx(val, ptr, len, new_val, doc, true, NULL, NULL); +} + +yyjson_api_inline bool yyjson_mut_ptr_setx(yyjson_mut_val *val, + const char *ptr, size_t len, + yyjson_mut_val *new_val, + yyjson_mut_doc *doc, + bool create_parent, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr || !doc)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return false; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return false; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return false; + } + if (!new_val) { + return !!unsafe_yyjson_mut_ptr_removex(val, ptr, len, ctx, err); + } + return unsafe_yyjson_mut_ptr_putx(val, ptr, len, new_val, doc, + create_parent, false, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replace( + yyjson_mut_doc *doc, const char *ptr, yyjson_mut_val *new_val) { + if (!ptr) return NULL; + return yyjson_mut_doc_ptr_replacen(doc, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacen( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val) { + return yyjson_mut_doc_ptr_replacex(doc, ptr, len, new_val, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_replacex( + yyjson_mut_doc *doc, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr || !new_val)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_mut_val *root = doc->root; + if (yyjson_unlikely(!root)) { + yyjson_ptr_set_err(RESOLVE, "JSON pointer cannot be resolved"); + return NULL; + } + if (ctx) ctx->old = root; + doc->root = new_val; + return root; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_replacex(doc->root, ptr, len, new_val, + ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replace( + yyjson_mut_val *val, const char *ptr, yyjson_mut_val *new_val) { + if (!ptr) return NULL; + return yyjson_mut_ptr_replacen(val, ptr, strlen(ptr), new_val); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacen( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val) { + return yyjson_mut_ptr_replacex(val, ptr, len, new_val, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_replacex( + yyjson_mut_val *val, const char *ptr, size_t len, yyjson_mut_val *new_val, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr || !new_val)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return NULL; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_replacex(val, ptr, len, new_val, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_remove( + yyjson_mut_doc *doc, const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_doc_ptr_removen(doc, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removen( + yyjson_mut_doc *doc, const char *ptr, size_t len) { + return yyjson_mut_doc_ptr_removex(doc, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_ptr_removex( + yyjson_mut_doc *doc, const char *ptr, size_t len, + yyjson_ptr_ctx *ctx, yyjson_ptr_err *err) { + + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!doc || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(!doc->root)) { + yyjson_ptr_set_err(NULL_ROOT, "document's root is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_mut_val *root = doc->root; + if (ctx) ctx->old = root; + doc->root = NULL; + return root; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_removex(doc->root, ptr, len, ctx, err); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_remove(yyjson_mut_val *val, + const char *ptr) { + if (!ptr) return NULL; + return yyjson_mut_ptr_removen(val, ptr, strlen(ptr)); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removen(yyjson_mut_val *val, + const char *ptr, + size_t len) { + return yyjson_mut_ptr_removex(val, ptr, len, NULL, NULL); +} + +yyjson_api_inline yyjson_mut_val *yyjson_mut_ptr_removex(yyjson_mut_val *val, + const char *ptr, + size_t len, + yyjson_ptr_ctx *ctx, + yyjson_ptr_err *err) { + yyjson_ptr_set_err(NONE, NULL); + if (ctx) memset(ctx, 0, sizeof(*ctx)); + + if (yyjson_unlikely(!val || !ptr)) { + yyjson_ptr_set_err(PARAMETER, "input parameter is NULL"); + return NULL; + } + if (yyjson_unlikely(len == 0)) { + yyjson_ptr_set_err(SET_ROOT, "cannot set root"); + return NULL; + } + if (yyjson_unlikely(*ptr != '/')) { + yyjson_ptr_set_err(SYNTAX, "no prefix '/'"); + return NULL; + } + return unsafe_yyjson_mut_ptr_removex(val, ptr, len, ctx, err); +} + +yyjson_api_inline bool yyjson_ptr_ctx_append(yyjson_ptr_ctx *ctx, + yyjson_mut_val *key, + yyjson_mut_val *val) { + yyjson_mut_val *ctn, *pre_key, *pre_val, *cur_key, *cur_val; + if (!ctx || !ctx->ctn || !val) return false; + ctn = ctx->ctn; + + if (yyjson_mut_is_obj(ctn)) { + if (!key) return false; + key->next = val; + pre_key = ctx->pre; + if (unsafe_yyjson_get_len(ctn) == 0) { + val->next = key; + ctn->uni.ptr = key; + ctx->pre = key; + } else if (!pre_key) { + pre_key = (yyjson_mut_val *)ctn->uni.ptr; + pre_val = pre_key->next; + val->next = pre_val->next; + pre_val->next = key; + ctn->uni.ptr = key; + ctx->pre = pre_key; + } else { + cur_key = pre_key->next->next; + cur_val = cur_key->next; + val->next = cur_val->next; + cur_val->next = key; + if (ctn->uni.ptr == cur_key) ctn->uni.ptr = key; + ctx->pre = cur_key; + } + } else { + pre_val = ctx->pre; + if (unsafe_yyjson_get_len(ctn) == 0) { + val->next = val; + ctn->uni.ptr = val; + ctx->pre = val; + } else if (!pre_val) { + pre_val = (yyjson_mut_val *)ctn->uni.ptr; + val->next = pre_val->next; + pre_val->next = val; + ctn->uni.ptr = val; + ctx->pre = pre_val; + } else { + cur_val = pre_val->next; + val->next = cur_val->next; + cur_val->next = val; + if (ctn->uni.ptr == cur_val) ctn->uni.ptr = val; + ctx->pre = cur_val; + } + } + unsafe_yyjson_inc_len(ctn); + return true; +} + +yyjson_api_inline bool yyjson_ptr_ctx_replace(yyjson_ptr_ctx *ctx, + yyjson_mut_val *val) { + yyjson_mut_val *ctn, *pre_key, *cur_key, *pre_val, *cur_val; + if (!ctx || !ctx->ctn || !ctx->pre || !val) return false; + ctn = ctx->ctn; + if (yyjson_mut_is_obj(ctn)) { + pre_key = ctx->pre; + pre_val = pre_key->next; + cur_key = pre_val->next; + cur_val = cur_key->next; + /* replace current value */ + cur_key->next = val; + val->next = cur_val->next; + ctx->old = cur_val; + } else { + pre_val = ctx->pre; + cur_val = pre_val->next; + /* replace current value */ + if (pre_val != cur_val) { + val->next = cur_val->next; + pre_val->next = val; + if (ctn->uni.ptr == cur_val) ctn->uni.ptr = val; + } else { + val->next = val; + ctn->uni.ptr = val; + ctx->pre = val; + } + ctx->old = cur_val; + } + return true; +} + +yyjson_api_inline bool yyjson_ptr_ctx_remove(yyjson_ptr_ctx *ctx) { + yyjson_mut_val *ctn, *pre_key, *pre_val, *cur_key, *cur_val; + size_t len; + if (!ctx || !ctx->ctn || !ctx->pre) return false; + ctn = ctx->ctn; + if (yyjson_mut_is_obj(ctn)) { + pre_key = ctx->pre; + pre_val = pre_key->next; + cur_key = pre_val->next; + cur_val = cur_key->next; + /* remove current key-value */ + pre_val->next = cur_val->next; + if (ctn->uni.ptr == cur_key) ctn->uni.ptr = pre_key; + ctx->pre = NULL; + ctx->old = cur_val; + } else { + pre_val = ctx->pre; + cur_val = pre_val->next; + /* remove current key-value */ + pre_val->next = cur_val->next; + if (ctn->uni.ptr == cur_val) ctn->uni.ptr = pre_val; + ctx->pre = NULL; + ctx->old = cur_val; + } + len = unsafe_yyjson_get_len(ctn) - 1; + if (len == 0) ctn->uni.ptr = NULL; + unsafe_yyjson_set_len(ctn, len); + return true; +} + +#undef yyjson_ptr_set_err + + + +/*============================================================================== + * MARK: - JSON Value at Pointer API (Implementation) + *============================================================================*/ + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type bool. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_bool( + yyjson_val *root, const char *ptr, bool *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_bool(val)) { + *value = unsafe_yyjson_get_bool(val); + return true; + } else { + return false; + } +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is an integer + that fits in `uint64_t`. Returns true if successful, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_uint( + yyjson_val *root, const char *ptr, uint64_t *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && val) { + uint64_t ret = val->uni.u64; + if (unsafe_yyjson_is_uint(val) || + (unsafe_yyjson_is_sint(val) && !(ret >> 63))) { + *value = ret; + return true; + } + } + return false; +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is an integer + that fits in `int64_t`. Returns true if successful, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_sint( + yyjson_val *root, const char *ptr, int64_t *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && val) { + int64_t ret = val->uni.i64; + if (unsafe_yyjson_is_sint(val) || + (unsafe_yyjson_is_uint(val) && ret >= 0)) { + *value = ret; + return true; + } + } + return false; +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type real. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_real( + yyjson_val *root, const char *ptr, double *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_real(val)) { + *value = unsafe_yyjson_get_real(val); + return true; + } else { + return false; + } +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type sint, + uint or real. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_num( + yyjson_val *root, const char *ptr, double *value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_num(val)) { + *value = unsafe_yyjson_get_num(val); + return true; + } else { + return false; + } +} + +/** + Set provided `value` if the JSON Pointer (RFC 6901) exists and is type string. + Returns true if value at `ptr` exists and is the correct type, otherwise false. + */ +yyjson_api_inline bool yyjson_ptr_get_str( + yyjson_val *root, const char *ptr, const char **value) { + yyjson_val *val = yyjson_ptr_get(root, ptr); + if (value && yyjson_is_str(val)) { + *value = unsafe_yyjson_get_str(val); + return true; + } else { + return false; + } +} + + + +/*============================================================================== + * MARK: - Deprecated + *============================================================================*/ + +/** @deprecated renamed to `yyjson_doc_ptr_get` */ +yyjson_deprecated("renamed to yyjson_doc_ptr_get") +yyjson_api_inline yyjson_val *yyjson_doc_get_pointer(yyjson_doc *doc, + const char *ptr) { + return yyjson_doc_ptr_get(doc, ptr); +} + +/** @deprecated renamed to `yyjson_doc_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_doc_ptr_getn") +yyjson_api_inline yyjson_val *yyjson_doc_get_pointern(yyjson_doc *doc, + const char *ptr, + size_t len) { + return yyjson_doc_ptr_getn(doc, ptr, len); +} + +/** @deprecated renamed to `yyjson_mut_doc_ptr_get` */ +yyjson_deprecated("renamed to yyjson_mut_doc_ptr_get") +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_pointer( + yyjson_mut_doc *doc, const char *ptr) { + return yyjson_mut_doc_ptr_get(doc, ptr); +} + +/** @deprecated renamed to `yyjson_mut_doc_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_mut_doc_ptr_getn") +yyjson_api_inline yyjson_mut_val *yyjson_mut_doc_get_pointern( + yyjson_mut_doc *doc, const char *ptr, size_t len) { + return yyjson_mut_doc_ptr_getn(doc, ptr, len); +} + +/** @deprecated renamed to `yyjson_ptr_get` */ +yyjson_deprecated("renamed to yyjson_ptr_get") +yyjson_api_inline yyjson_val *yyjson_get_pointer(yyjson_val *val, + const char *ptr) { + return yyjson_ptr_get(val, ptr); +} + +/** @deprecated renamed to `yyjson_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_ptr_getn") +yyjson_api_inline yyjson_val *yyjson_get_pointern(yyjson_val *val, + const char *ptr, + size_t len) { + return yyjson_ptr_getn(val, ptr, len); +} + +/** @deprecated renamed to `yyjson_mut_ptr_get` */ +yyjson_deprecated("renamed to yyjson_mut_ptr_get") +yyjson_api_inline yyjson_mut_val *yyjson_mut_get_pointer(yyjson_mut_val *val, + const char *ptr) { + return yyjson_mut_ptr_get(val, ptr); +} + +/** @deprecated renamed to `yyjson_mut_ptr_getn` */ +yyjson_deprecated("renamed to yyjson_mut_ptr_getn") +yyjson_api_inline yyjson_mut_val *yyjson_mut_get_pointern(yyjson_mut_val *val, + const char *ptr, + size_t len) { + return yyjson_mut_ptr_getn(val, ptr, len); +} + +/** @deprecated renamed to `yyjson_mut_ptr_getn` */ +yyjson_deprecated("renamed to unsafe_yyjson_ptr_getn") +yyjson_api_inline yyjson_val *unsafe_yyjson_get_pointer(yyjson_val *val, + const char *ptr, + size_t len) { + yyjson_ptr_err err; + return unsafe_yyjson_ptr_getx(val, ptr, len, &err); +} + +/** @deprecated renamed to `unsafe_yyjson_mut_ptr_getx` */ +yyjson_deprecated("renamed to unsafe_yyjson_mut_ptr_getx") +yyjson_api_inline yyjson_mut_val *unsafe_yyjson_mut_get_pointer( + yyjson_mut_val *val, const char *ptr, size_t len) { + yyjson_ptr_err err; + return unsafe_yyjson_mut_ptr_getx(val, ptr, len, NULL, &err); +} + +#endif /* YYJSON_DISABLE_UTILS */ + + + +/*============================================================================== + * MARK: - Compiler Hint End + *============================================================================*/ + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +# pragma GCC diagnostic pop +# endif +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif /* warning suppress end */ + +#ifdef __cplusplus +} +#endif /* extern "C" end */ + +#endif /* YYJSON_H */ diff --git a/research/experiments/004-direct-document-path/README.md b/research/experiments/004-direct-document-path/README.md new file mode 100644 index 0000000..ae4e3b8 --- /dev/null +++ b/research/experiments/004-direct-document-path/README.md @@ -0,0 +1,50 @@ +# Direct store calls versus the Pure document path + +This is an end-to-end comparison for the existing `editor.pure` document app. +Both variants perform, for each document, a conditional create, a full read, +a conditional one-byte replacement at the end, and another full read. They +check revisions and complete bodies and compute the same digest. Both use the +same `store.c`: a write creates and synchronizes a replacement file, renames +it, then synchronizes the directory. The harness verifies equal digests and +equal final file bytes for each pair of runs. + +The direct executable calls `pure_store_splice` and `pure_store_select`; the +Pure executable runs the prepared `editor.pure` plan through `PureRuntime` and +`pure_store_host`. The direct executable mirrors only this valid-operation +workflow. It does **not** provide the runtime's admission, ownership, budget, +completion, or cancellation contract and is not a feature-equivalent runtime. +The comparison isolates the cost of choosing that contract for this app path; +it does not compare two complete products. + +Run on a Linux host with a C17 compiler, Python 3, GNU `size`, and `df`: + +```sh +python3 research/experiments/004-direct-document-path/run.py \ + --documents 256 --body-bytes 8192 --pairs 4 \ + --output research/experiments/004-direct-document-path/results/local-8k.json +python3 research/experiments/004-direct-document-path/run.py \ + --documents 32 --body-bytes 1048576 --pairs 4 \ + --output research/experiments/004-direct-document-path/results/local-1m.json +``` + +The runner uses a fresh temporary directory for each variant and alternates +direct/Pure order between pairs. Its default `--work-root` is this directory's +`results/`, so the store is on the repository filesystem. It records the +filesystem type, compiler, source hashes, executable hashes, linked sections, +operation counts, four stage times, wall time, user/system CPU time, and peak +child RSS. A small native parent collects RSS because a Python process can +otherwise contaminate the reported child high-water mark across `exec`. +Warmups are untimed, and the runner refuses to compare against a changed +`editor.pure` definition until the direct mirror is reviewed. + +The dated `ext4` JSON files are the report's evidence. The dated `tmpfs` +files are exploratory runs made before filesystem recording was added; their +store was under `/tmp` on this host. They exercised the same store calls but +cannot represent persistent-media latency. One run, even with alternating +order, cannot control host I/O scheduling, cache state, storage firmware, or +clock changes. The byte digest proves this workload's result equality, not +full behavioral equivalence in failures or concurrency. + +The [result and interpretation](../../../docs/research/054-direct-document-path.md) +distinguish linked executable size from the 5,908-byte standalone core and +avoid a speed claim unsupported by the samples. diff --git a/research/experiments/004-direct-document-path/document-path.c b/research/experiments/004-direct-document-path/document-path.c new file mode 100644 index 0000000..b4d6464 --- /dev/null +++ b/research/experiments/004-direct-document-path/document-path.c @@ -0,0 +1,207 @@ +#define _POSIX_C_SOURCE 200809L +/* Two separately linked versions of one durable document workload. The direct + * build invokes the same store primitives that the Pure host uses; the Pure + * build runs the prepared editor.pure program through its execution core. */ +#include "store.h" +#ifndef DIRECT_PATH +#include "runtime.h" +#include "store-port.h" +#include "embedded-plan.h" +#endif +#include +#include +#include +#include +#include + +#ifndef PURE_BENCH_BODY_BYTES +#define PURE_BENCH_BODY_BYTES 8192 +#endif +enum { BODY_BYTES = PURE_BENCH_BODY_BYTES }; +static uint64_t digest = UINT64_C(14695981039346656037); + +static void fail(const char *message) { + fprintf(stderr, "%s\n", message); + exit(1); +} + +static void check(int condition, const char *message) { + if (!condition) fail(message); +} + +static void hash_body(const unsigned char *body, size_t size, int64_t revision) { + for (size_t i = 0; i < size; ++i) { + digest ^= body[i]; + digest *= UINT64_C(1099511628211); + } + digest ^= (uint64_t)revision; + digest *= UINT64_C(1099511628211); +} + +static uint64_t nanoseconds(void) { + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now)) fail("clock_gettime failed"); + return (uint64_t)now.tv_sec * UINT64_C(1000000000) + (uint64_t)now.tv_nsec; +} + +static void pattern(unsigned char *body, unsigned document) { + for (size_t i = 0; i < BODY_BYTES; ++i) + body[i] = (unsigned char)('a' + (document + i) % 26); +} + +#ifdef DIRECT_PATH +static void create(const char *store, const char *key, unsigned document, + const unsigned char *expected) { + unsigned char *input = malloc(BODY_BYTES); + check(input != NULL, "direct input allocation failed"); + pattern(input, document); + check(!memcmp(input, expected, BODY_BYTES), "direct input mismatch"); + int64_t revision = 0; + PureResult status = pure_store_splice(store, key, 0, 0, 0, + (PureSpan){input, BODY_BYTES}, &revision); + check(status == PURE_OK && revision == 1, "direct create failed"); + free(input); +} + +static void edit_last(const char *store, const char *key, unsigned char replacement) { + unsigned char *input = malloc(1); + check(input != NULL, "direct edit allocation failed"); + *input = replacement; + int64_t revision = 0; + PureResult status = pure_store_splice(store, key, 1, BODY_BYTES - 1, 1, + (PureSpan){input, 1}, &revision); + check(status == PURE_OK && revision == 2, "direct edit failed"); + free(input); +} + +static void read_checked(const char *store, const char *key, + const unsigned char *expected, int64_t revision) { + PureStoredView view = {0}; + PureResult status = pure_store_select(store, key, 0, 0, true, 0, + 4u * 1024u * 1024u, &view); + check(status == PURE_OK && view.revision == revision && + view.body.size == BODY_BYTES && !memcmp(view.body.data, expected, BODY_BYTES), + "direct read mismatch"); + hash_body(view.body.data, view.body.size, revision); + pure_store_release(&view); +} +#else +static PureRuntime runtime; +static PureWork work[1]; + +static PureOutcome completed(PureTicket ticket) { + PureOutcome result; + while (!pure_runtime_result(ticket, &result)) + check(pure_runtime_step(&runtime), "runtime made no progress"); + return result; +} + +static void write_checked(const char *key, int64_t expected_revision, + size_t offset, size_t remove, size_t insert_size, + unsigned document, unsigned char replacement, + const unsigned char *expected_reply, int64_t revision) { + PureTicket ticket; + check(pure_runtime_accept(&runtime, PURE_INPUT_WRITE, key, expected_revision, + 8 + insert_size, &ticket) == PURE_OK, "runtime write admission failed"); + size_t size = 0; + unsigned char *input = pure_runtime_ingress(ticket, &size); + check(input != NULL && size == 8 + insert_size, "runtime input loan failed"); + for (unsigned i = 0; i < 4; ++i) { + input[i] = (unsigned char)(offset >> (8 * i)); + input[4 + i] = (unsigned char)(remove >> (8 * i)); + } + if (insert_size == BODY_BYTES) pattern(input + 8, document); + else input[8] = replacement; + check(pure_runtime_ready(ticket), "runtime publication failed"); + PureOutcome result = completed(ticket); + check(result.status == PURE_OK && result.revision == revision && + result.body.size == insert_size && + !memcmp(result.body.data, expected_reply, insert_size), + "runtime write result mismatch"); + check(pure_runtime_release(ticket) && !runtime.active && !runtime.retained, + "runtime write resource not released"); +} + +static void read_checked(const char *store, const char *key, + const unsigned char *expected, int64_t revision) { + (void)store; + PureTicket ticket; + check(pure_runtime_accept(&runtime, PURE_INPUT_READ, key, 0, 0, &ticket) == PURE_OK, + "runtime read admission failed"); + PureOutcome result = completed(ticket); + check(result.status == PURE_OK && result.revision == revision && + result.body.size == BODY_BYTES && !memcmp(result.body.data, expected, BODY_BYTES), + "runtime read mismatch"); + hash_body(result.body.data, result.body.size, revision); + check(pure_runtime_release(ticket) && !runtime.active && !runtime.retained, + "runtime read resource not released"); +} + +static void create(const char *store, const char *key, unsigned document, + const unsigned char *expected) { + (void)store; + write_checked(key, 0, 0, 0, BODY_BYTES, document, 0, expected, 1); +} + +static void edit_last(const char *store, const char *key, unsigned char replacement) { + (void)store; + write_checked(key, 1, BODY_BYTES - 1, 1, 1, 0, replacement, &replacement, 2); +} +#endif + +int main(int argc, char **argv) { + if (argc != 3) fail("usage: document-path STORE DOCUMENT_COUNT"); + char *end = NULL; + unsigned long parsed = strtoul(argv[2], &end, 10); + check(end && !*end && parsed > 0 && parsed <= 4096, + "DOCUMENT_COUNT must be 1..4096"); + unsigned documents = (unsigned)parsed; + check(pure_store_init(argv[1]) == PURE_OK, "store initialization failed"); + unsigned char *expected = malloc(BODY_BYTES); + check(expected != NULL, "expected document allocation failed"); +#ifndef DIRECT_PATH + check(pure_embedded_program.splice && pure_embedded_program.write.offset == 8 && + pure_embedded_program.splice_offset.value == 0 && + pure_embedded_program.splice_remove.value == 4 && + pure_embedded_program.reply_input && pure_embedded_program.read.to_end, + "prepared editor.pure no longer matches direct baseline"); + check(pure_runtime_init(&runtime, work, 1, &pure_embedded_program, + pure_store_host(argv[1]), 4u * 1024u * 1024u), + "runtime initialization failed"); +#endif + uint64_t start = nanoseconds(); + uint64_t stages[4] = {0}; + for (unsigned document = 0; document < documents; ++document) { + char key[16]; + snprintf(key, sizeof(key), "doc%04u", document); + pattern(expected, document); + uint64_t before = nanoseconds(); + create(argv[1], key, document, expected); + stages[0] += nanoseconds() - before; + before = nanoseconds(); + read_checked(argv[1], key, expected, 1); + stages[1] += nanoseconds() - before; + unsigned char replacement = (unsigned char)('A' + document % 26); + before = nanoseconds(); + edit_last(argv[1], key, replacement); + stages[2] += nanoseconds() - before; + expected[BODY_BYTES - 1] = replacement; + before = nanoseconds(); + read_checked(argv[1], key, expected, 2); + stages[3] += nanoseconds() - before; + } + uint64_t elapsed = nanoseconds() - start; + free(expected); + printf("{\"variant\":\"%s\",\"documents\":%u,\"operations\":%u," + "\"body_bytes\":%u,\"elapsed_ns\":%" PRIu64 "," + "\"stage_ns\":[%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 "]," + "\"digest\":\"%016" PRIx64 "\"}\n", +#ifdef DIRECT_PATH + "direct", +#else + "pure", +#endif + documents, documents * 4, (unsigned)BODY_BYTES, elapsed, + stages[0], stages[1], stages[2], stages[3], digest); + return 0; +} diff --git a/research/experiments/004-direct-document-path/peak-launcher.c b/research/experiments/004-direct-document-path/peak-launcher.c new file mode 100644 index 0000000..e9f6c00 --- /dev/null +++ b/research/experiments/004-direct-document-path/peak-launcher.c @@ -0,0 +1,26 @@ +#define _GNU_SOURCE +/* Fork from a small native launcher so a Python orchestrator's pre-exec RSS + * cannot become the measured worker's high-water mark. */ +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) return 2; + pid_t child = fork(); + if (child < 0) return 2; + if (!child) { + execvp(argv[1], argv + 1); + _exit(127); + } + struct rusage usage; + int status; + if (wait4(child, &status, 0, &usage) < 0 || !WIFEXITED(status) || + WEXITSTATUS(status)) return 1; + long user_ns = usage.ru_utime.tv_sec * 1000000000L + usage.ru_utime.tv_usec * 1000L; + long system_ns = usage.ru_stime.tv_sec * 1000000000L + usage.ru_stime.tv_usec * 1000L; + fprintf(stderr, "PURE_PEAK_RSS_KIB=%ld\nPURE_USER_NS=%ld\nPURE_SYSTEM_NS=%ld\n", + usage.ru_maxrss, user_ns, system_ns); + return 0; +} diff --git a/research/experiments/004-direct-document-path/results/2026-09-24-ext4-1m-linux-aarch64.json b/research/experiments/004-direct-document-path/results/2026-09-24-ext4-1m-linux-aarch64.json new file mode 100644 index 0000000..80ae293 --- /dev/null +++ b/research/experiments/004-direct-document-path/results/2026-09-24-ext4-1m-linux-aarch64.json @@ -0,0 +1,251 @@ +{ + "profile": "same-durable-document-app-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "prepared_sha256": "8e93861539a87c2a091f55afea1bf310fc3d1f1474521d8f8318bfacac3b0649", + "documents_per_variant": 32, + "operations_per_variant": 128, + "body_bytes": 1048576, + "pairs": 4, + "filesystem": { + "device": "/dev/mmcblk0p2", + "type": "ext4", + "mount": "/home/eunhhu/work/pure" + }, + "sections": { + "direct": { + "text": 10364, + "data": 856, + "bss": 16 + }, + "pure": { + "text": 18640, + "data": 864, + "bss": 376 + } + }, + "binary_sha256": { + "direct": "1389e7d95e74e93ab5d9e8c8146d0b8fc5288e7a356df231c3c018d3db9d0e05", + "pure": "9a45a3d92eed499b849560a2c949f24da7d09deb5ec9bd956ab78ae4e8273e54" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 4764012642, + "stage_ns": [ + 2307602623, + 136599863, + 2096570951, + 135696119 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4128, + "user_ns": 428385000, + "system_ns": 98253000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + }, + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 7285595814, + "stage_ns": [ + 5142956601, + 117263079, + 1831606853, + 117247619 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4128, + "user_ns": 382221000, + "system_ns": 78088000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 3766595455, + "stage_ns": [ + 1765437679, + 95183882, + 1750125337, + 95840266 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4176, + "user_ns": 290416000, + "system_ns": 85416000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + }, + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 8005833380, + "stage_ns": [ + 2106116647, + 136885177, + 5531253084, + 139444468 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4160, + "user_ns": 454253000, + "system_ns": 79693000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 10918834225, + "stage_ns": [ + 2175551809, + 106558403, + 8462573310, + 106534958 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4576, + "user_ns": 344275000, + "system_ns": 70420000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + }, + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 12394010470, + "stage_ns": [ + 7847769422, + 133123427, + 4192297149, + 133678298 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4128, + "user_ns": 443388000, + "system_ns": 78475000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 19998681944, + "stage_ns": [ + 6699663505, + 105108979, + 13020834566, + 105960162 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4144, + "user_ns": 361242000, + "system_ns": 54606000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + }, + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 9851837474, + "stage_ns": [ + 6446916975, + 136390388, + 3043885903, + 137363089 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4592, + "user_ns": 460200000, + "system_ns": 68626000, + "stored_file_bytes": 33555200, + "stored_files_sha256": "cd78e6871643d5ce1ae9f3b0c96985b36996ac23053f49c140c4224516d6e77a" + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 8928.835427, + "peak_rss_kib": 4368.0, + "user_ms": 441.319, + "system_ms": 75.0565, + "stages_ms": [ + 2241.577216, + 136.4951255, + 4287.5694935, + 136.529604 + ] + }, + "pure": { + "elapsed_ms": 9839.803142, + "peak_rss_kib": 4136.0, + "user_ms": 371.7315, + "system_ms": 78.2815, + "stages_ms": [ + 5921.310053, + 111.186029, + 3011.952001, + 111.6038905 + ] + } + }, + "median_elapsed_ratio_pure_over_direct": 1.102025367411893, + "sources_sha256": { + "research/experiments/004-direct-document-path/document-path.c": "032d8688db330c355082d150865aa7ade3e5402cc1bbe090b361fd55cb6325fc", + "research/experiments/004-direct-document-path/peak-launcher.c": "fc730205d564b60a4f9e44aa1b880df0debeb1338b86b27eabe3bde1508f3f74", + "research/experiments/004-direct-document-path/run.py": "ab9eaed05ed47afc29cb94e72759dad6534531b52b34a8ba2c5b3be6ded1cac1", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "size": "GNU size (GNU Binutils for Debian) 2.44" + } +} diff --git a/research/experiments/004-direct-document-path/results/2026-09-24-ext4-8k-linux-aarch64.json b/research/experiments/004-direct-document-path/results/2026-09-24-ext4-8k-linux-aarch64.json new file mode 100644 index 0000000..2f80581 --- /dev/null +++ b/research/experiments/004-direct-document-path/results/2026-09-24-ext4-8k-linux-aarch64.json @@ -0,0 +1,251 @@ +{ + "profile": "same-durable-document-app-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "prepared_sha256": "8e93861539a87c2a091f55afea1bf310fc3d1f1474521d8f8318bfacac3b0649", + "documents_per_variant": 256, + "operations_per_variant": 1024, + "body_bytes": 8192, + "pairs": 4, + "filesystem": { + "device": "/dev/mmcblk0p2", + "type": "ext4", + "mount": "/home/eunhhu/work/pure" + }, + "sections": { + "direct": { + "text": 10364, + "data": 856, + "bss": 16 + }, + "pure": { + "text": 18640, + "data": 864, + "bss": 376 + } + }, + "binary_sha256": { + "direct": "d265f60f9126da701369c26a6263474164171349867e11079a63b6d9bd1dbbc8", + "pure": "8e71e09bb7923de0e945f4d51c5514dd9ab07a4ff9c0982db712ab895fc54fa0" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 7241112354, + "stage_ns": [ + 3772530510, + 14877249, + 3434303263, + 14649999 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1072, + "user_ns": 21367000, + "system_ns": 89743000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + }, + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 7649396059, + "stage_ns": [ + 3825503100, + 15159869, + 3788264822, + 15324464 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1088, + "user_ns": 40394000, + "system_ns": 72710000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 7398701348, + "stage_ns": [ + 3647571585, + 15698014, + 3714469244, + 15678618 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1088, + "user_ns": 32058000, + "system_ns": 85489000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + }, + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 7807026262, + "stage_ns": [ + 4021085986, + 16197799, + 3747754749, + 16384614 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1072, + "user_ns": 23055000, + "system_ns": 96831000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 8124328908, + "stage_ns": [ + 3722508954, + 16393229, + 4364924440, + 15319853 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1072, + "user_ns": 45910000, + "system_ns": 67099000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + }, + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 8689792618, + "stage_ns": [ + 3995126011, + 17471391, + 4653440218, + 17835468 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1088, + "user_ns": 47096000, + "system_ns": 83726000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 7553568836, + "stage_ns": [ + 3612364797, + 15354345, + 3905074609, + 15629392 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1088, + "user_ns": 23794000, + "system_ns": 90418000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + }, + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 7932443534, + "stage_ns": [ + 4056315266, + 15670869, + 3839621816, + 15698993 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1072, + "user_ns": 23088000, + "system_ns": 92353000, + "stored_file_bytes": 2103296, + "stored_files_sha256": "93b837ad585f9633f60c4e3fc6c52e403238e5a4cc4ec3f6d248b3981a8f425b" + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 7869.734898, + "peak_rss_kib": 1072.0, + "user_ms": 23.0715, + "system_ms": 91.048, + "stages_ms": [ + 3896.808248, + 15.934334, + 3793.6882825, + 15.509423 + ] + }, + "pure": { + "elapsed_ms": 7601.4824475, + "peak_rss_kib": 1088.0, + "user_ms": 36.226, + "system_ms": 84.6075, + "stages_ms": [ + 3736.5373425, + 15.5261795, + 3846.6697155, + 15.654005 + ] + } + }, + "median_elapsed_ratio_pure_over_direct": 0.9659134069982239, + "sources_sha256": { + "research/experiments/004-direct-document-path/document-path.c": "032d8688db330c355082d150865aa7ade3e5402cc1bbe090b361fd55cb6325fc", + "research/experiments/004-direct-document-path/peak-launcher.c": "fc730205d564b60a4f9e44aa1b880df0debeb1338b86b27eabe3bde1508f3f74", + "research/experiments/004-direct-document-path/run.py": "ab9eaed05ed47afc29cb94e72759dad6534531b52b34a8ba2c5b3be6ded1cac1", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "size": "GNU size (GNU Binutils for Debian) 2.44" + } +} diff --git a/research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-1m-preliminary-linux-aarch64.json b/research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-1m-preliminary-linux-aarch64.json new file mode 100644 index 0000000..ab088e1 --- /dev/null +++ b/research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-1m-preliminary-linux-aarch64.json @@ -0,0 +1,238 @@ +{ + "profile": "same-durable-document-app-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "prepared_sha256": "8e93861539a87c2a091f55afea1bf310fc3d1f1474521d8f8318bfacac3b0649", + "documents_per_variant": 32, + "operations_per_variant": 128, + "body_bytes": 1048576, + "pairs": 4, + "sections": { + "direct": { + "text": 10364, + "data": 856, + "bss": 16 + }, + "pure": { + "text": 18640, + "data": 864, + "bss": 376 + } + }, + "binary_sha256": { + "direct": "1389e7d95e74e93ab5d9e8c8146d0b8fc5288e7a356df231c3c018d3db9d0e05", + "pure": "9a45a3d92eed499b849560a2c949f24da7d09deb5ec9bd956ab78ae4e8273e54" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 340081183, + "stage_ns": [ + 75261818, + 94500701, + 17506146, + 93772922 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 3872, + "user_ns": 308146000, + "system_ns": 32015000, + "stored_file_bytes": 33555200 + }, + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 341099421, + "stage_ns": [ + 79567567, + 92146853, + 17844589, + 92596187 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4336, + "user_ns": 305409000, + "system_ns": 36166000, + "stored_file_bytes": 33555200 + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 338271706, + "stage_ns": [ + 77352220, + 91696577, + 18131571, + 92186762 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 3872, + "user_ns": 310861000, + "system_ns": 27897000, + "stored_file_bytes": 33555200 + }, + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 348018886, + "stage_ns": [ + 79216590, + 98943059, + 17171958, + 93268609 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 3856, + "user_ns": 302866000, + "system_ns": 40382000, + "stored_file_bytes": 33555200 + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 337466024, + "stage_ns": [ + 74499100, + 93470034, + 17836179, + 92757726 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 3872, + "user_ns": 309997000, + "system_ns": 28181000, + "stored_file_bytes": 33555200 + }, + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 342399605, + "stage_ns": [ + 79958272, + 92458203, + 18037399, + 92971374 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 4336, + "user_ns": 311005000, + "system_ns": 31898000, + "stored_file_bytes": 33555200 + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 340585627, + "stage_ns": [ + 79565343, + 91822001, + 17952418, + 92354743 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 3872, + "user_ns": 301411000, + "system_ns": 39659000, + "stored_file_bytes": 33555200 + }, + "direct": { + "variant": "direct", + "documents": 32, + "operations": 128, + "body_bytes": 1048576, + "elapsed_ns": 336890525, + "stage_ns": [ + 74695433, + 93087459, + 17564345, + 92612242 + ], + "digest": "b74d25a65553ee41", + "peak_rss_kib": 3856, + "user_ns": 309291000, + "system_ns": 28117000, + "stored_file_bytes": 33555200 + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 338.7736035, + "peak_rss_kib": 3864.0, + "user_ms": 308.7185, + "system_ms": 30.098, + "stages_ms": [ + 74.9786255, + 93.9853675, + 17.5352455, + 93.0131675 + ] + }, + "pure": { + "elapsed_ms": 340.842524, + "peak_rss_kib": 4104.0, + "user_ms": 308.135, + "system_ms": 34.032, + "stages_ms": [ + 79.566455, + 91.984427, + 17.9949085, + 92.475465 + ] + } + }, + "median_elapsed_ratio_pure_over_direct": 1.0061070888600092, + "sources_sha256": { + "research/experiments/004-direct-document-path/document-path.c": "032d8688db330c355082d150865aa7ade3e5402cc1bbe090b361fd55cb6325fc", + "research/experiments/004-direct-document-path/peak-launcher.c": "fc730205d564b60a4f9e44aa1b880df0debeb1338b86b27eabe3bde1508f3f74", + "research/experiments/004-direct-document-path/run.py": "1919287fb5dc0f884cecf038c21e0d4326f2e826ee7a48eb65f5d480e27bacb7", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "size": "GNU size (GNU Binutils for Debian) 2.44" + } +} diff --git a/research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-8k-preliminary-linux-aarch64.json b/research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-8k-preliminary-linux-aarch64.json new file mode 100644 index 0000000..e8a9f5b --- /dev/null +++ b/research/experiments/004-direct-document-path/results/2026-09-24-tmpfs-8k-preliminary-linux-aarch64.json @@ -0,0 +1,322 @@ +{ + "profile": "same-durable-document-app-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "prepared_sha256": "8e93861539a87c2a091f55afea1bf310fc3d1f1474521d8f8318bfacac3b0649", + "documents_per_variant": 256, + "operations_per_variant": 1024, + "body_bytes": 8192, + "pairs": 6, + "sections": { + "direct": { + "text": 10364, + "data": 856, + "bss": 16 + }, + "pure": { + "text": 18640, + "data": 864, + "bss": 376 + } + }, + "binary_sha256": { + "direct": "d265f60f9126da701369c26a6263474164171349867e11079a63b6d9bd1dbbc8", + "pure": "8e71e09bb7923de0e945f4d51c5514dd9ab07a4ff9c0982db712ab895fc54fa0" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32443218, + "stage_ns": [ + 8080321, + 7513546, + 5301851, + 7787666 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 24559000, + "system_ns": 8186000, + "stored_file_bytes": 2103296 + }, + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32009201, + "stage_ns": [ + 8131949, + 7162898, + 5457529, + 7487020 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 16183000, + "system_ns": 16183000, + "stored_file_bytes": 2103296 + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 31919941, + "stage_ns": [ + 8166312, + 7144299, + 5410416, + 7352779 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1040, + "user_ns": 20228000, + "system_ns": 12137000, + "stored_file_bytes": 2103296 + }, + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32205996, + "stage_ns": [ + 7984983, + 7590856, + 5294584, + 7570908 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1040, + "user_ns": 16322000, + "system_ns": 16322000, + "stored_file_bytes": 2103296 + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32589218, + "stage_ns": [ + 8207096, + 7581051, + 5347608, + 7656447 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 16529000, + "system_ns": 16529000, + "stored_file_bytes": 2103296 + }, + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 31973552, + "stage_ns": [ + 8280075, + 7151887, + 5368944, + 7359984 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 24311000, + "system_ns": 8103000, + "stored_file_bytes": 2103296 + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32080256, + "stage_ns": [ + 8156569, + 7173028, + 5460655, + 7396332 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 28408000, + "system_ns": 4058000, + "stored_file_bytes": 2103296 + }, + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32571829, + "stage_ns": [ + 8142638, + 7715426, + 5329257, + 7625344 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1040, + "user_ns": 12350000, + "system_ns": 20584000, + "stored_file_bytes": 2103296 + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32290107, + "stage_ns": [ + 8045372, + 7543971, + 5349434, + 7600138 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 20455000, + "system_ns": 12273000, + "stored_file_bytes": 2103296 + }, + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32067756, + "stage_ns": [ + 8225774, + 7252055, + 5472276, + 7359265 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 4064000, + "system_ns": 28454000, + "stored_file_bytes": 2103296 + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "variant": "pure", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32372144, + "stage_ns": [ + 8141941, + 7171001, + 5629064, + 7673781 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 21685000, + "system_ns": 10842000, + "stored_file_bytes": 2103296 + }, + "direct": { + "variant": "direct", + "documents": 256, + "operations": 1024, + "body_bytes": 8192, + "elapsed_ns": 32314570, + "stage_ns": [ + 8063229, + 7512835, + 5380849, + 7585506 + ], + "digest": "b8a08a9615b2eb3b", + "peak_rss_kib": 1056, + "user_ns": 16375000, + "system_ns": 16375000, + "stored_file_bytes": 2103296 + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 32.378894, + "peak_rss_kib": 1056.0, + "user_ms": 16.452, + "system_ms": 16.3485, + "stages_ms": [ + 8.071775, + 7.562511, + 5.3384325, + 7.612741 + ] + }, + "pure": { + "elapsed_ms": 32.0384785, + "peak_rss_kib": 1056.0, + "user_ms": 20.9565, + "system_ms": 11.4895, + "stages_ms": [ + 8.1614405, + 7.1669495, + 5.459092, + 7.378158 + ] + } + }, + "median_elapsed_ratio_pure_over_direct": 0.9894865000638995, + "sources_sha256": { + "research/experiments/004-direct-document-path/document-path.c": "032d8688db330c355082d150865aa7ade3e5402cc1bbe090b361fd55cb6325fc", + "research/experiments/004-direct-document-path/peak-launcher.c": "fc730205d564b60a4f9e44aa1b880df0debeb1338b86b27eabe3bde1508f3f74", + "research/experiments/004-direct-document-path/run.py": "1919287fb5dc0f884cecf038c21e0d4326f2e826ee7a48eb65f5d480e27bacb7", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "size": "GNU size (GNU Binutils for Debian) 2.44" + } +} diff --git a/research/experiments/004-direct-document-path/run.py b/research/experiments/004-direct-document-path/run.py new file mode 100644 index 0000000..4987fd7 --- /dev/null +++ b/research/experiments/004-direct-document-path/run.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Compare the same durable document job through Pure and direct store calls.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import statistics +import subprocess +import tempfile + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +CONNECTIONS = ROOT / 'prototypes/connections' +EDITOR_SHA256 = '8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a' + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def run(command, timeout=180): + result = subprocess.run([str(item) for item in command], capture_output=True, + text=True, timeout=timeout) + if result.returncode: + raise AssertionError((result.args, result.returncode, + result.stdout[-2000:], result.stderr[-2000:])) + return result + + +def compile_all(directory, cc, body_bytes): + flags = [cc, '-O2', '-std=c17', '-Wall', '-Wextra', '-Wpedantic', '-Werror', + f'-DPURE_BENCH_BODY_BYTES={body_bytes}'] + compiler = directory / 'pure-compile' + run([*flags, CONNECTIONS / 'compile-program.c', CONNECTIONS / 'program.c', + '-o', compiler]) + header = directory / 'embedded-plan.h' + run([compiler, CONNECTIONS / 'examples/editor.pure', header]) + common = [HERE / 'document-path.c', CONNECTIONS / 'store.c'] + direct = directory / 'direct' + pure = directory / 'pure' + run([*flags, '-DDIRECT_PATH', '-I' + str(CONNECTIONS), *common, '-o', direct]) + run([*flags, '-I' + str(CONNECTIONS), '-I' + str(directory), *common, + CONNECTIONS / 'store-port.c', CONNECTIONS / 'runtime.c', + CONNECTIONS / 'evaluate.c', '-o', pure]) + launcher = directory / 'peak-launcher' + run([*flags, HERE / 'peak-launcher.c', '-o', launcher]) + return {'direct': direct, 'pure': pure}, launcher, header + + +def sections(binary): + fields = run(['size', binary]).stdout.splitlines()[1].split() + return {'text': int(fields[0]), 'data': int(fields[1]), 'bss': int(fields[2])} + + +def trial(binary, launcher, store, documents): + result = run([launcher, binary, store, documents]) + measured = json.loads(result.stdout) + resources = dict(line.split('=', 1) for line in result.stderr.splitlines()) + measured['peak_rss_kib'] = int(resources['PURE_PEAK_RSS_KIB']) + measured['user_ns'] = int(resources['PURE_USER_NS']) + measured['system_ns'] = int(resources['PURE_SYSTEM_NS']) + objects = sorted(store.glob('obj-*')) + assert len(objects) == documents, len(objects) + measured['stored_file_bytes'] = sum(path.stat().st_size for path in objects) + final_files = hashlib.sha256() + for path in objects: + content = path.read_bytes() + final_files.update(path.name.encode('ascii')) + final_files.update(len(content).to_bytes(8, 'little')) + final_files.update(content) + measured['stored_files_sha256'] = final_files.hexdigest() + assert measured['documents'] == documents and measured['operations'] == 4 * documents + return measured + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cc', default=os.environ.get('CC', 'cc')) + parser.add_argument('--documents', type=int, default=256) + parser.add_argument('--body-bytes', type=int, default=8192) + parser.add_argument('--pairs', type=int, default=4) + parser.add_argument('--work-root', type=Path, default=HERE / 'results', + help='filesystem for fresh stores; default is the repository filesystem') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if (not 1 <= args.documents <= 4096 or not 1 <= args.pairs <= 16 or + not 1 <= args.body_bytes <= 1024 * 1024): + parser.error('documents must be 1..4096, pairs 1..16 and body-bytes 1..1048576') + if args.output and args.output.exists(): + parser.error('evidence file already exists') + if platform.system() != 'Linux': + parser.error('this release RSS apparatus uses Linux wait4 units') + definition = CONNECTIONS / 'examples/editor.pure' + assert sha(definition) == EDITOR_SHA256, 'direct baseline needs review after editor.pure changes' + args.work_root.mkdir(parents=True, exist_ok=True) + filesystem = run(['df', '-T', args.work_root]).stdout.splitlines()[1].split() + with tempfile.TemporaryDirectory(prefix='pure-direct-document-', dir=args.work_root) as name: + root = Path(name) + binaries, launcher, prepared = compile_all(root, args.cc, args.body_bytes) + sizes = {name: sections(binary) for name, binary in binaries.items()} + binaries_sha = {name: sha(binary) for name, binary in binaries.items()} + # One small untimed run per path loads shared code and catches a broken + # comparison before the measured alternating, fresh-store trials. + warmups = {name: trial(binary, launcher, root / ('warm-' + name), 4) + for name, binary in binaries.items()} + assert warmups['direct']['digest'] == warmups['pure']['digest'] + samples = [] + for pair in range(args.pairs): + order = ('direct', 'pure') if pair % 2 == 0 else ('pure', 'direct') + entry = {'order': list(order)} + for variant in order: + entry[variant] = trial(binaries[variant], launcher, + root / f'{pair}-{variant}', args.documents) + assert entry['direct']['digest'] == entry['pure']['digest'] + assert entry['direct']['stored_file_bytes'] == entry['pure']['stored_file_bytes'] + assert entry['direct']['stored_files_sha256'] == entry['pure']['stored_files_sha256'] + samples.append(entry) + prepared_sha = sha(prepared) + medians = {variant: {'elapsed_ms': statistics.median( + sample[variant]['elapsed_ns'] for sample in samples) / 1e6, + 'peak_rss_kib': statistics.median( + sample[variant]['peak_rss_kib'] for sample in samples), + 'user_ms': statistics.median( + sample[variant]['user_ns'] for sample in samples) / 1e6, + 'system_ms': statistics.median( + sample[variant]['system_ns'] for sample in samples) / 1e6, + 'stages_ms': [statistics.median( + sample[variant]['stage_ns'][i] for sample in samples) / 1e6 + for i in range(4)]} + for variant in ('direct', 'pure')} + result = {'profile': 'same-durable-document-app-direct-versus-pure', + 'definition_sha256': sha(definition), 'prepared_sha256': prepared_sha, + 'documents_per_variant': args.documents, + 'operations_per_variant': args.documents * 4, + 'body_bytes': args.body_bytes, 'pairs': args.pairs, + 'filesystem': {'device': filesystem[0], 'type': filesystem[1], + 'mount': filesystem[-1]}, + 'sections': sizes, 'binary_sha256': binaries_sha, + 'samples': samples, 'medians': medians, + 'median_elapsed_ratio_pure_over_direct': + medians['pure']['elapsed_ms'] / medians['direct']['elapsed_ms'], + 'sources_sha256': {str(path.relative_to(ROOT)): sha(path) + for path in [HERE / 'document-path.c', HERE / 'peak-launcher.c', + HERE / 'run.py', CONNECTIONS / 'store.c', + CONNECTIONS / 'runtime.c', CONNECTIONS / 'evaluate.c', + CONNECTIONS / 'store-port.c', definition]}, + 'environment': {'platform': platform.platform(), 'machine': platform.machine(), + 'compiler': run([args.cc, '--version']).stdout.splitlines()[0], + 'size': run(['size', '--version']).stdout.splitlines()[0]}} + body = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(body) + print(body, end='') + + +if __name__ == '__main__': + main() diff --git a/research/experiments/005-direct-document-peer/README.md b/research/experiments/005-direct-document-peer/README.md new file mode 100644 index 0000000..d35ba20 --- /dev/null +++ b/research/experiments/005-direct-document-peer/README.md @@ -0,0 +1,60 @@ +# Authenticated document peer: direct versus Pure + +This experiment uses the real `editor.pure` document app over the same TLS +1.3/TCP transport and `pure-doc/1` wire format. `direct-peer.c` is a +research-only server that parses the same authored program and calls the same +store and evaluator directly. `pure-document-server` is the selected product +server build without its native client commands. The baseline is limited to +the tested synchronous document workflow: it has no Pure work tickets, +admission/lifetime contract, cross-domain forwarding, or cancellation. + +Both servers passed the existing `check_document_peer.py` suite: mutual TLS +identity, ALPN, source identity, external Python and native C clients, version +conflicts, one-byte replacement of a 1 MiB value, fragmented/invalid/truncated +frames, deadlines, and restart. The direct and Pure server builds passed the +same suite under ASan/UBSan. These checks do not establish full feature or +security equivalence under concurrent clients or failure networks. + +To run the selected product checks on Linux AArch64: + +```sh +make -C prototypes/connections check-document-peer +make -C prototypes/connections check-document-server +``` + +The first command checks the existing combined client/server executable; the +second checks the server-only code ceiling and runs the same peer scenarios +with the native C client from the combined executable. Both need permission +to bind a local TCP port. The server-only executable and budget are optional; +the default core and CLI do not link TLS. + +To reproduce the read comparison: + +```sh +python3 research/experiments/005-direct-document-peer/run.py \ + --body-bytes 8192 --reads 1024 --pairs 6 \ + --output research/experiments/005-direct-document-peer/results/local-8k.json +python3 research/experiments/005-direct-document-peer/run.py \ + --body-bytes 1048576 --reads 64 --pairs 4 \ + --output research/experiments/005-direct-document-peer/results/local-1m.json +``` + +The runner compiles both servers from source. Each trial creates and reads a +real stored document, then times repeated full reads on one authenticated, +persistent Python TLS connection. It verifies every response and the final +stored file hash. TLS handshake, certificate creation, file creation, and the +first cache-warming read are outside the timed batch. The server process RSS +is recorded before and after. Fresh stores and alternating order reduce some +order effects but do not control CPU frequency, OpenSSL or Python overhead, +network behavior beyond local loopback, or host scheduling. The JSON files +contain all individual samples, source hashes, compiler and OpenSSL versions. + +The short `before-nodelay` and `after-nodelay` artifacts repeat exactly 32 +authenticated 8 KiB reads through each server, differing only in the common +TLS transport's TCP_NODELAY option. This intervention supports a Nagle/delayed +ACK explanation for the prior ~44 ms per-request stall; no packet trace was +captured, so that exact kernel interaction remains an inference. The longer +post-change runs are for direct-versus-Pure cost, not a claim of Internet or +Kitex/QUIC performance. + +See [the result report](../../../docs/research/055-authenticated-peer-transport-cost.md). diff --git a/research/experiments/005-direct-document-peer/direct-peer-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/direct-peer-parity-linux-aarch64.json new file mode 100644 index 0000000..05c62cc --- /dev/null +++ b/research/experiments/005-direct-document-peer/direct-peer-parity-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7968, + "server_after_large_work_rss_kib": 9712 +} diff --git a/research/experiments/005-direct-document-peer/direct-peer-sanitized-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/direct-peer-sanitized-parity-linux-aarch64.json new file mode 100644 index 0000000..356128d --- /dev/null +++ b/research/experiments/005-direct-document-peer/direct-peer-sanitized-parity-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 22528, + "server_after_large_work_rss_kib": 31632 +} diff --git a/research/experiments/005-direct-document-peer/direct-peer.c b/research/experiments/005-direct-document-peer/direct-peer.c new file mode 100644 index 0000000..437672d --- /dev/null +++ b/research/experiments/005-direct-document-peer/direct-peer.c @@ -0,0 +1,207 @@ +#define _POSIX_C_SOURCE 200809L +/* Research-only server. It shares the product peer's TLS transport, wire + * format, program parser/evaluator, and store, but calls the store directly. */ +#include "program.h" +#include "store.h" +#include "../../../prototypes/common/tls-link.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { REQUEST_BYTES = 14, REPLY_BYTES = 13, HELLO_BYTES = 40, + OP_READ = 1, OP_WRITE = 2, WANT_BODY = 0x80 }; +static const unsigned char alpn[] = {10, 'p','u','r','e','-','d','o','c','/','1'}; +static volatile sig_atomic_t stopping; +static void signal_stop(int signum) { (void)signum; stopping = 1; } + +typedef struct { PureResult status; int64_t revision; PureSpan body; } DirectOutcome; + +static uint32_t get32(const unsigned char *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; +} +static uint64_t get64(const unsigned char *p) { + uint64_t value = 0; + for (unsigned i = 0; i < 8; ++i) value = (value << 8) | p[i]; + return value; +} +static void put32(unsigned char *p, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(value >> (24 - 8 * i)); +} +static void put64(unsigned char *p, uint64_t value) { + for (unsigned i = 0; i < 8; ++i) p[i] = (unsigned char)(value >> (56 - 8 * i)); +} +static bool number(const char *text, uint64_t limit, uint64_t *out) { + if (!text || !*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 bool valid_key(const char *key) { + if (!key || !*key) return false; + size_t size = 0; + for (; size <= 64 && key[size]; ++size) { + unsigned char c = (unsigned char)key[size]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '_')) return false; + } + return size <= 64; +} +static bool load_program(const char *path, PureProgram *program, unsigned char digest[32]) { + FILE *file = fopen(path, "rb"); + if (!file) 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; + char error[160]; + if (bad || !pure_program_parse(source, length, program, error, sizeof(error))) { + if (!bad) fprintf(stderr, "Program: %s\n", error); + return false; + } + return SHA256((const unsigned char *)source, length, digest) != NULL; +} +static bool write_hello(PureLink *link, const unsigned char digest[32], uint64_t deadline) { + unsigned char hello[HELLO_BYTES] = {'P','D','O','C',1,0,0,0}; + memcpy(hello + 8, digest, 32); + return pure_link_write(link, hello, sizeof(hello), deadline); +} +static bool send_result(PureLink *link, DirectOutcome outcome, bool want_body, uint64_t deadline) { + if (outcome.status > PURE_ALLOCATION || outcome.revision < 0 || + outcome.body.size > PURE_OBJECT_LIMIT || (outcome.body.size && !outcome.body.data)) return false; + unsigned char header[REPLY_BYTES]; + header[0] = (unsigned char)outcome.status; + put64(header + 1, (uint64_t)outcome.revision); + put32(header + 9, (uint32_t)outcome.body.size); + return pure_link_write(link, header, sizeof(header), deadline) && + (!want_body || pure_link_write(link, outcome.body.data, outcome.body.size, deadline)); +} +static bool serve_request(PureLink *link, const PureProgram *program, const char *store, int *end) { + uint64_t deadline = pure_link_now_ms() + PURE_LINK_DEADLINE_MS; + unsigned char header[REQUEST_BYTES]; + int got = pure_link_read(link, header, sizeof(header), deadline); + if (!got) { *end = 1; return true; } + if (got != 1) return false; + unsigned op = header[0] & ~WANT_BODY, key_size = header[1]; + bool want_body = (header[0] & WANT_BODY) != 0; + uint64_t expected = get64(header + 2); + uint32_t body_size = get32(header + 10); + if ((op != OP_READ && op != OP_WRITE) || !key_size || key_size > 64 || + expected > INT64_MAX || body_size > PURE_OBJECT_LIMIT || + (op == OP_READ && (expected || body_size))) return false; + char key[65]; + if (pure_link_read(link, key, key_size, deadline) != 1) return false; + if (memchr(key, 0, key_size)) return false; + key[key_size] = 0; + if (!valid_key(key)) { + (void)send_result(link, (DirectOutcome){.status = PURE_INVALID}, want_body, deadline); + return false; + } + DirectOutcome outcome = {0}; + if (op == OP_READ) { + PureStoredView view = {0}; + outcome.status = pure_store_select(store, key, program->read.offset, + program->read.length, program->read.to_end, program->read_required, + 4u * 1024u * 1024u, &view); + if (outcome.status == PURE_OK) { + outcome.body = view.body; + outcome.revision = view.revision; + } + bool sent = send_result(link, outcome, want_body, deadline); + pure_store_release(&view); + return sent; + } + unsigned char *input = body_size ? malloc(body_size) : NULL; + if (body_size && !input) { + (void)send_result(link, (DirectOutcome){.status = PURE_ALLOCATION}, want_body, deadline); + return false; /* Body deliberately not consumed after rejected admission. */ + } + if (body_size && pure_link_read(link, input, body_size, deadline) != 1) { + free(input); + return false; + } + PureSpan value = {0}, reply = {0}, source = {input, body_size}; + size_t offset = 0, remove = 0; + if (!pure_program_input(program, source, &value, &reply) || + (program->splice && !pure_program_splice(program, source, &offset, &remove))) + outcome.status = PURE_LIMIT; + else if (program->splice) + outcome.status = pure_store_splice(store, key, (int64_t)expected, + offset, remove, value, &outcome.revision); + else outcome.status = pure_store_put(store, key, (int64_t)expected, value, + &outcome.revision); + if (outcome.status == PURE_OK) outcome.body = reply; + bool sent = send_result(link, outcome, want_body, deadline); + free(input); + return sent; +} +static bool serve_connection(PureLink *link, const PureProgram *program, const char *store, + const unsigned char digest[32]) { + if (!write_hello(link, digest, pure_link_now_ms() + PURE_LINK_DEADLINE_MS)) return false; + int end = 0; + while (!end) if (!serve_request(link, program, store, &end)) return false; + return true; +} +static int serve(const char *bind_address, const char *port, const char *program_path, + const char *store, const char *ca, const char *cert, + const char *key, const char *client_name, unsigned max_connections) { + if (!pure_link_port_number(port, true) || !client_name || !*client_name) return 2; + PureProgram program; + unsigned char digest[32]; + if (!load_program(program_path, &program, digest) || pure_store_init(store) != PURE_OK) return 2; + SSL_CTX *ctx = pure_link_context(true, ca, cert, key, alpn); + if (!ctx) { ERR_print_errors_fp(stderr); return 2; } + int listener = pure_link_listen_at(bind_address, port); + if (listener < 0) { perror("listen"); SSL_CTX_free(ctx); return 2; } + struct sockaddr_storage address; + socklen_t address_size = sizeof(address); + if (getsockname(listener, (struct sockaddr *)&address, &address_size)) abort(); + unsigned actual_port = address.ss_family == AF_INET + ? ntohs(((struct sockaddr_in *)&address)->sin_port) + : ntohs(((struct sockaddr_in6 *)&address)->sin6_port); + printf("READY %u\n", actual_port); fflush(stdout); + signal(SIGTERM, signal_stop); signal(SIGINT, signal_stop); + unsigned accepted = 0; + while (!stopping && (!max_connections || accepted < max_connections)) { + struct pollfd p = {.fd = listener, .events = POLLIN}; + int result = poll(&p, 1, 1000); + if (result < 0) { if (errno == EINTR) continue; break; } + if (!result) continue; + int fd = accept(listener, NULL, NULL); + if (fd < 0) { if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; break; } + ++accepted; + PureLink link = {.fd = fd}; + bool healthy = pure_link_nonblocking(fd) && + pure_link_handshake(&link, ctx, true, client_name, alpn); + if (healthy) healthy = serve_connection(&link, &program, store, digest); + pure_link_close(&link, healthy); + } + close(listener); + SSL_CTX_free(ctx); + return 0; +} +int main(int argc, char **argv) { + signal(SIGPIPE, SIG_IGN); + if ((argc == 10 || argc == 11) && !strcmp(argv[1], "serve")) { + uint64_t limit = 0; + if (argc == 11 && (!number(argv[10], UINT32_MAX, &limit) || !limit)) return 2; + return serve(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], + (unsigned)limit); + } + fputs("direct-peer serve BIND_IP PORT PROGRAM STORE_DIR CA CERT KEY CLIENT_DNS [MAX_CONNECTIONS]\n", stderr); + return 2; +} diff --git a/research/experiments/005-direct-document-peer/pure-peer-after-nodelay-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/pure-peer-after-nodelay-parity-linux-aarch64.json new file mode 100644 index 0000000..45598a9 --- /dev/null +++ b/research/experiments/005-direct-document-peer/pure-peer-after-nodelay-parity-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7952, + "server_after_large_work_rss_kib": 9696 +} diff --git a/research/experiments/005-direct-document-peer/pure-peer-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/pure-peer-parity-linux-aarch64.json new file mode 100644 index 0000000..05c62cc --- /dev/null +++ b/research/experiments/005-direct-document-peer/pure-peer-parity-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7968, + "server_after_large_work_rss_kib": 9712 +} diff --git a/research/experiments/005-direct-document-peer/pure-server-after-nodelay-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/pure-server-after-nodelay-parity-linux-aarch64.json new file mode 100644 index 0000000..05c62cc --- /dev/null +++ b/research/experiments/005-direct-document-peer/pure-server-after-nodelay-parity-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7968, + "server_after_large_work_rss_kib": 9712 +} diff --git a/research/experiments/005-direct-document-peer/pure-server-after-nodelay-profile-linux-aarch64.json b/research/experiments/005-direct-document-peer/pure-server-after-nodelay-profile-linux-aarch64.json new file mode 100644 index 0000000..0f6a3ce --- /dev/null +++ b/research/experiments/005-direct-document-peer/pure-server-after-nodelay-profile-linux-aarch64.json @@ -0,0 +1,23 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-document-server", + "scope": "Reviewed server-only TLS peer ceiling on Linux AArch64/GCC 14. Excludes linked native client code and shared OpenSSL/libc implementations; process RSS is measured separately.", + "budget_sha256": "deae8827323061543193779ada90d1bca4db3a6dda80293e4ad28a9526be9819", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-document-server": { + "sections": { + "text": 43462, + "data": 1352, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libcrypto.so.3", + "libssl.so.3" + ], + "sha256": "728025cfbdbcdb6d212a2ec8297a0740967653c77ae8908d923c59ddd4ea0315" + } + } +} diff --git a/research/experiments/005-direct-document-peer/pure-server-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/pure-server-parity-linux-aarch64.json new file mode 100644 index 0000000..05c62cc --- /dev/null +++ b/research/experiments/005-direct-document-peer/pure-server-parity-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 7968, + "server_after_large_work_rss_kib": 9712 +} diff --git a/research/experiments/005-direct-document-peer/pure-server-profile-linux-aarch64.json b/research/experiments/005-direct-document-peer/pure-server-profile-linux-aarch64.json new file mode 100644 index 0000000..c74cdf9 --- /dev/null +++ b/research/experiments/005-direct-document-peer/pure-server-profile-linux-aarch64.json @@ -0,0 +1,23 @@ +{ + "passed": true, + "failures": [], + "profile": "linux-aarch64-gcc14-selected-document-server", + "scope": "Reviewed server-only TLS peer ceiling on Linux AArch64/GCC 14. Excludes linked native client code and shared OpenSSL/libc implementations; process RSS is measured separately.", + "budget_sha256": "deae8827323061543193779ada90d1bca4db3a6dda80293e4ad28a9526be9819", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "binaries": { + "pure-document-server": { + "sections": { + "text": 43462, + "data": 1352, + "bss": 16 + }, + "needed": [ + "libc.so.6", + "libcrypto.so.3", + "libssl.so.3" + ], + "sha256": "ea88a146270cea96faf976b548ceba7179c535042ec3a8656e92c3596372f9aa" + } + } +} diff --git a/research/experiments/005-direct-document-peer/pure-server-sanitized-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/pure-server-sanitized-parity-linux-aarch64.json new file mode 100644 index 0000000..161dd4f --- /dev/null +++ b/research/experiments/005-direct-document-peer/pure-server-sanitized-parity-linux-aarch64.json @@ -0,0 +1,16 @@ +{ + "profile": "selected-authenticated-document-peer", + "checks": [ + "mutual certificate identity, server hostname and ALPN separation", + "program source identity checked before C client sends work", + "persistent external TLS client: create, read, conflict and recovery", + "native peer edit and local CLI use the same authored document and storage", + "fragmented 1 MiB direct ingress, demand-selected reply and one-byte splice", + "native C peer uploads 1 MiB and receives only the requested metadata", + "incomplete, invalid and oversized frames do not publish a change", + "idle authenticated session closes after deadline", + "server restart and native C peer preserve the committed large document" + ], + "server_idle_rss_kib": 22640, + "server_after_large_work_rss_kib": 31776 +} diff --git a/research/experiments/005-direct-document-peer/results/2026-09-24-after-nodelay-8k-linux-aarch64.json b/research/experiments/005-direct-document-peer/results/2026-09-24-after-nodelay-8k-linux-aarch64.json new file mode 100644 index 0000000..f739b2b --- /dev/null +++ b/research/experiments/005-direct-document-peer/results/2026-09-24-after-nodelay-8k-linux-aarch64.json @@ -0,0 +1,109 @@ +{ + "profile": "authenticated-document-read-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "body_bytes": 8192, + "reads_per_trial": 32, + "pairs": 2, + "filesystem": { + "device": "/dev/mmcblk0p2", + "type": "ext4", + "mount": "/" + }, + "sections": { + "direct": { + "text": 37862, + "data": 1352, + "bss": 16 + }, + "pure": { + "text": 43454, + "data": 1352, + "bss": 16 + } + }, + "binary_sha256": { + "direct": "9177aebfae07d84af5b6f23fbfe28a3a89a494943b8d0c8d0d35b68593b1f704", + "pure": "be9f569ab444085c761a7f7c9aa91c2acd6581534059785964b3a94da89fff25" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 3028977, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "pure": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 2564107, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 3016236, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "direct": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 3019977, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 3.024477, + "server_rss_before_kib": 8632.0, + "server_rss_after_kib": 8632.0 + }, + "pure": { + "elapsed_ms": 2.7901715, + "server_rss_before_kib": 8640.0, + "server_rss_after_kib": 8640.0 + } + }, + "sources_sha256": { + "research/experiments/005-direct-document-peer/direct-peer.c": "b1ee6ba5ec3d09b842ac42d4ca158b339ec2d23b157d2c568180af2731737501", + "research/experiments/005-direct-document-peer/run.py": "b53539f0a915376397011713ebbc9438a9b289d3b4dd745ffab5d6039fccd958", + "prototypes/connections/document-peer.c": "0c72121a6929239f618f8a642247af13fb5b68cca2999c125e52de685e5b2e50", + "prototypes/connections/check_document_peer.py": "d9dc789148951baa6f5d47f8cdea4009fe4d2dcf67b9dfaf2e6501f8e66a0f5c", + "prototypes/common/tls-link.c": "5d47ce411d6a81ce1a29d288c93ff507539fda1f202f2f41c0ff5560564b4f5a", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "openssl": "OpenSSL 3.6.4 25 Aug 2026 (Library: OpenSSL 3.6.4 25 Aug 2026)" + } +} diff --git a/research/experiments/005-direct-document-peer/results/2026-09-24-before-nodelay-8k-linux-aarch64.json b/research/experiments/005-direct-document-peer/results/2026-09-24-before-nodelay-8k-linux-aarch64.json new file mode 100644 index 0000000..019fd1e --- /dev/null +++ b/research/experiments/005-direct-document-peer/results/2026-09-24-before-nodelay-8k-linux-aarch64.json @@ -0,0 +1,109 @@ +{ + "profile": "authenticated-document-read-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "body_bytes": 8192, + "reads_per_trial": 32, + "pairs": 2, + "filesystem": { + "device": "/dev/mmcblk0p2", + "type": "ext4", + "mount": "/" + }, + "sections": { + "direct": { + "text": 37862, + "data": 1352, + "bss": 16 + }, + "pure": { + "text": 43454, + "data": 1352, + "bss": 16 + } + }, + "binary_sha256": { + "direct": "edce33757c398fb23f6bc9e70bb31072714320324912f21599d23469e8381816", + "pure": "22560da4dd66f91f8b899569cfc771835034d7e07a68dd4da44e1605edb46c45" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 1415860581, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "pure": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 1411832180, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 1411873125, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "direct": { + "reads": 32, + "body_bytes": 8192, + "elapsed_ns": 1407871983, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 1411.866282, + "server_rss_before_kib": 8632.0, + "server_rss_after_kib": 8632.0 + }, + "pure": { + "elapsed_ms": 1411.8526525, + "server_rss_before_kib": 8632.0, + "server_rss_after_kib": 8632.0 + } + }, + "sources_sha256": { + "research/experiments/005-direct-document-peer/direct-peer.c": "b1ee6ba5ec3d09b842ac42d4ca158b339ec2d23b157d2c568180af2731737501", + "research/experiments/005-direct-document-peer/run.py": "b53539f0a915376397011713ebbc9438a9b289d3b4dd745ffab5d6039fccd958", + "prototypes/connections/document-peer.c": "0c72121a6929239f618f8a642247af13fb5b68cca2999c125e52de685e5b2e50", + "prototypes/connections/check_document_peer.py": "d9dc789148951baa6f5d47f8cdea4009fe4d2dcf67b9dfaf2e6501f8e66a0f5c", + "prototypes/common/tls-link.c": "df259073c5e1cd911495b48d4ccd43c8346d955a19a8773964abe58089fbebf9", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "openssl": "OpenSSL 3.6.4 25 Aug 2026 (Library: OpenSSL 3.6.4 25 Aug 2026)" + } +} diff --git a/research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-1m-linux-aarch64.json b/research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-1m-linux-aarch64.json new file mode 100644 index 0000000..9e65005 --- /dev/null +++ b/research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-1m-linux-aarch64.json @@ -0,0 +1,157 @@ +{ + "profile": "authenticated-document-read-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "body_bytes": 1048576, + "reads_per_trial": 64, + "pairs": 4, + "filesystem": { + "device": "/dev/mmcblk0p2", + "type": "ext4", + "mount": "/" + }, + "sections": { + "direct": { + "text": 37862, + "data": 1352, + "bss": 16 + }, + "pure": { + "text": 43454, + "data": 1352, + "bss": 16 + } + }, + "binary_sha256": { + "direct": "9177aebfae07d84af5b6f23fbfe28a3a89a494943b8d0c8d0d35b68593b1f704", + "pure": "be9f569ab444085c761a7f7c9aa91c2acd6581534059785964b3a94da89fff25" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 148966030, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + }, + "pure": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 149719696, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 149739381, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + }, + "direct": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 150962435, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 137503567, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + }, + "pure": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 155644335, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 155630742, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + }, + "direct": { + "reads": 64, + "body_bytes": 1048576, + "elapsed_ns": 155552075, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 1048600, + "stored_file_sha256": "a89fd11658d8139f5cb9723120126df35590f80d02f72fdfa46c1d7c6cf44383" + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 149.9642325, + "server_rss_before_kib": 8632.0, + "server_rss_after_kib": 8632.0 + }, + "pure": { + "elapsed_ms": 152.6850615, + "server_rss_before_kib": 8640.0, + "server_rss_after_kib": 8640.0 + } + }, + "sources_sha256": { + "research/experiments/005-direct-document-peer/direct-peer.c": "b1ee6ba5ec3d09b842ac42d4ca158b339ec2d23b157d2c568180af2731737501", + "research/experiments/005-direct-document-peer/run.py": "b53539f0a915376397011713ebbc9438a9b289d3b4dd745ffab5d6039fccd958", + "prototypes/connections/document-peer.c": "0c72121a6929239f618f8a642247af13fb5b68cca2999c125e52de685e5b2e50", + "prototypes/connections/check_document_peer.py": "d9dc789148951baa6f5d47f8cdea4009fe4d2dcf67b9dfaf2e6501f8e66a0f5c", + "prototypes/common/tls-link.c": "5d47ce411d6a81ce1a29d288c93ff507539fda1f202f2f41c0ff5560564b4f5a", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "openssl": "OpenSSL 3.6.4 25 Aug 2026 (Library: OpenSSL 3.6.4 25 Aug 2026)" + } +} diff --git a/research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-8k-linux-aarch64.json b/research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-8k-linux-aarch64.json new file mode 100644 index 0000000..153842b --- /dev/null +++ b/research/experiments/005-direct-document-peer/results/2026-09-24-nodelay-8k-linux-aarch64.json @@ -0,0 +1,205 @@ +{ + "profile": "authenticated-document-read-direct-versus-pure", + "definition_sha256": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "body_bytes": 8192, + "reads_per_trial": 1024, + "pairs": 6, + "filesystem": { + "device": "/dev/mmcblk0p2", + "type": "ext4", + "mount": "/" + }, + "sections": { + "direct": { + "text": 37862, + "data": 1352, + "bss": 16 + }, + "pure": { + "text": 43454, + "data": 1352, + "bss": 16 + } + }, + "binary_sha256": { + "direct": "9177aebfae07d84af5b6f23fbfe28a3a89a494943b8d0c8d0d35b68593b1f704", + "pure": "be9f569ab444085c761a7f7c9aa91c2acd6581534059785964b3a94da89fff25" + }, + "samples": [ + { + "order": [ + "direct", + "pure" + ], + "direct": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 76563753, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "pure": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 93622467, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 90777415, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "direct": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 92965246, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 76165419, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "pure": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 92132358, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 80493468, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "direct": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 77076696, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + }, + { + "order": [ + "direct", + "pure" + ], + "direct": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 91795043, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "pure": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 90660842, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + }, + { + "order": [ + "pure", + "direct" + ], + "pure": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 93672077, + "server_rss_before_kib": 8640, + "server_rss_after_kib": 8640, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + }, + "direct": { + "reads": 1024, + "body_bytes": 8192, + "elapsed_ns": 116319172, + "server_rss_before_kib": 8624, + "server_rss_after_kib": 8624, + "stored_file_bytes": 8216, + "stored_file_sha256": "286cfc86e3ebe6d82dd923ef777eba30f748e8b1dd58d5c4352babbf85d0d64f" + } + } + ], + "medians": { + "direct": { + "elapsed_ms": 84.4358695, + "server_rss_before_kib": 8640.0, + "server_rss_after_kib": 8640.0 + }, + "pure": { + "elapsed_ms": 91.4548865, + "server_rss_before_kib": 8640.0, + "server_rss_after_kib": 8640.0 + } + }, + "sources_sha256": { + "research/experiments/005-direct-document-peer/direct-peer.c": "b1ee6ba5ec3d09b842ac42d4ca158b339ec2d23b157d2c568180af2731737501", + "research/experiments/005-direct-document-peer/run.py": "b53539f0a915376397011713ebbc9438a9b289d3b4dd745ffab5d6039fccd958", + "prototypes/connections/document-peer.c": "0c72121a6929239f618f8a642247af13fb5b68cca2999c125e52de685e5b2e50", + "prototypes/connections/check_document_peer.py": "d9dc789148951baa6f5d47f8cdea4009fe4d2dcf67b9dfaf2e6501f8e66a0f5c", + "prototypes/common/tls-link.c": "5d47ce411d6a81ce1a29d288c93ff507539fda1f202f2f41c0ff5560564b4f5a", + "prototypes/connections/store.c": "733ddcacaffd9e9ffbadfc385eadd4c9e62736a2b97b7bc048ad14845d83070f", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/runtime.c": "1ced6734ee07f3688e5c9cc0a8af639da88a1741dc3e6f65ec109baa2d826e6f", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a" + }, + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "openssl": "OpenSSL 3.6.4 25 Aug 2026 (Library: OpenSSL 3.6.4 25 Aug 2026)" + } +} diff --git a/research/experiments/005-direct-document-peer/run.py b/research/experiments/005-direct-document-peer/run.py new file mode 100644 index 0000000..ff4654f --- /dev/null +++ b/research/experiments/005-direct-document-peer/run.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Compare authenticated document reads through direct and Pure server paths.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import statistics +import subprocess +import sys +import tempfile +import time + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +CONNECTIONS = ROOT / 'prototypes/connections' +COMMON = ROOT / 'prototypes/common' +sys.path.insert(0, str(CONNECTIONS)) +import check_document_peer as peer # noqa: E402 + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def command(*args): + result = subprocess.run([str(arg) for arg in args], capture_output=True, text=True, + timeout=60) + if result.returncode: + raise AssertionError((result.args, result.returncode, result.stdout, result.stderr)) + return result.stdout + + +def compile_servers(directory, cc): + flags = [cc, '-O2', '-g', '-std=c17', '-Wall', '-Wextra', '-Wpedantic', '-Werror', + '-I' + str(CONNECTIONS)] + common = [COMMON / 'tls-link.c', CONNECTIONS / 'store.c', + CONNECTIONS / 'program.c', CONNECTIONS / 'evaluate.c'] + direct = directory / 'direct-peer' + pure = directory / 'pure-server' + command(*flags, HERE / 'direct-peer.c', *common, '-lssl', '-lcrypto', '-o', direct) + command(*flags, '-DPURE_PEER_SERVER_ONLY', CONNECTIONS / 'document-peer.c', + *common, CONNECTIONS / 'store-port.c', CONNECTIONS / 'runtime.c', + '-lssl', '-lcrypto', '-o', pure) + return {'direct': direct, 'pure': pure} + + +def sections(binary): + row = command('size', binary).splitlines()[1].split() + return dict(zip(('text', 'data', 'bss'), map(int, row[:3]))) + + +def trial(binary, program, root, cert_root, ca, body, reads): + store = root / 'store' + server, port = peer.start(binary, program, store, cert_root, ca) + try: + with peer.connect(cert_root, ca, port) as sock: + peer.hello(sock, program) + assert peer.request(sock, 2, 'bench', 0, peer.edit(0, 0, body), + want_body=False) == (0, 1, b'', len(body)) + assert peer.request(sock, 1, 'bench') == (0, 1, body, len(body)) + rss_before = peer.rss_kib(server.pid) + start = time.perf_counter_ns() + for _ in range(reads): + assert peer.request(sock, 1, 'bench') == (0, 1, body, len(body)) + elapsed_ns = time.perf_counter_ns() - start + rss_after = peer.rss_kib(server.pid) + finally: + peer.stop(server) + stored = store / 'obj-bench' + return {'reads': reads, 'body_bytes': len(body), 'elapsed_ns': elapsed_ns, + 'server_rss_before_kib': rss_before, 'server_rss_after_kib': rss_after, + 'stored_file_bytes': stored.stat().st_size, + 'stored_file_sha256': sha(stored)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cc', default=os.environ.get('CC', 'cc')) + parser.add_argument('--body-bytes', type=int, default=8192) + parser.add_argument('--reads', type=int, default=512) + parser.add_argument('--pairs', type=int, default=4) + parser.add_argument('--work-root', type=Path, default=HERE / 'results') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if not 1 <= args.body_bytes <= 1024 * 1024 or not 1 <= args.reads <= 4096 or not 1 <= args.pairs <= 16: + parser.error('body-bytes 1..1048576, reads 1..4096, pairs 1..16') + if args.output and args.output.exists(): + parser.error('evidence file already exists') + args.work_root.mkdir(parents=True, exist_ok=True) + filesystem = command('df', '-T', args.work_root).splitlines()[1].split() + program = CONNECTIONS / 'examples/editor.pure' + body = bytes((ord('a') + i % 26 for i in range(args.body_bytes))) + with tempfile.TemporaryDirectory(prefix='pure-direct-peer-', dir=args.work_root) as name: + root = Path(name) + binaries = compile_servers(root, args.cc) + sizes = {variant: sections(binary) for variant, binary in binaries.items()} + binary_hashes = {variant: sha(binary) for variant, binary in binaries.items()} + ca = peer.certificates(root) + samples = [] + for pair in range(args.pairs): + order = ('direct', 'pure') if pair % 2 == 0 else ('pure', 'direct') + entry = {'order': list(order)} + for variant in order: + path = root / f'{pair}-{variant}' + path.mkdir() + # The certificates live at the common root. The store is unique + # to each server so conditional revision 0 always means create. + entry[variant] = trial(binaries[variant], program, path, root, ca, + body, args.reads) + assert entry['direct']['stored_file_sha256'] == entry['pure']['stored_file_sha256'] + samples.append(entry) + medians = {variant: {'elapsed_ms': statistics.median( + sample[variant]['elapsed_ns'] for sample in samples) / 1e6, + 'server_rss_before_kib': statistics.median( + sample[variant]['server_rss_before_kib'] for sample in samples), + 'server_rss_after_kib': statistics.median( + sample[variant]['server_rss_after_kib'] for sample in samples)} + for variant in ('direct', 'pure')} + sources = [HERE / 'direct-peer.c', HERE / 'run.py', + CONNECTIONS / 'document-peer.c', CONNECTIONS / 'check_document_peer.py', + COMMON / 'tls-link.c', CONNECTIONS / 'store.c', + CONNECTIONS / 'store-port.c', CONNECTIONS / 'program.c', + CONNECTIONS / 'evaluate.c', CONNECTIONS / 'runtime.c', program] + result = {'profile': 'authenticated-document-read-direct-versus-pure', + 'definition_sha256': sha(program), 'body_bytes': args.body_bytes, + 'reads_per_trial': args.reads, 'pairs': args.pairs, + 'filesystem': {'device': filesystem[0], 'type': filesystem[1], + 'mount': filesystem[-1]}, + 'sections': sizes, 'binary_sha256': binary_hashes, + 'samples': samples, 'medians': medians, + 'sources_sha256': {str(path.relative_to(ROOT)): sha(path) for path in sources}, + 'environment': {'platform': platform.platform(), 'machine': platform.machine(), + 'compiler': command(args.cc, '--version').splitlines()[0], + 'openssl': command('openssl', 'version').strip()}} + output = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output) + print(output, end='') + + +if __name__ == '__main__': + main() diff --git a/research/experiments/005-direct-document-peer/scalar-peer-after-nodelay-parity-linux-aarch64.json b/research/experiments/005-direct-document-peer/scalar-peer-after-nodelay-parity-linux-aarch64.json new file mode 100644 index 0000000..1316e2e --- /dev/null +++ b/research/experiments/005-direct-document-peer/scalar-peer-after-nodelay-parity-linux-aarch64.json @@ -0,0 +1 @@ +{"alpn": "pass", "disconnect_release": "pass", "idle_deadline": "pass", "malformed_frames": "pass", "mutual_authentication": "pass", "persist_restart": "pass", "python_client": "pass", "revision_conflict": "pass", "server_connected_rss_kib": 8640, "server_hostname": "pass", "server_idle_rss_kib": 7968} diff --git a/research/experiments/005-storage-edit-latency/2026-09-24-check-hybrid.log.gz b/research/experiments/005-storage-edit-latency/2026-09-24-check-hybrid.log.gz new file mode 100644 index 0000000..e4c0174 Binary files /dev/null and b/research/experiments/005-storage-edit-latency/2026-09-24-check-hybrid.log.gz differ diff --git a/research/experiments/005-storage-edit-latency/2026-09-24-ext4-linux-aarch64.json b/research/experiments/005-storage-edit-latency/2026-09-24-ext4-linux-aarch64.json new file mode 100644 index 0000000..f9decda --- /dev/null +++ b/research/experiments/005-storage-edit-latency/2026-09-24-ext4-linux-aarch64.json @@ -0,0 +1,463 @@ +{ + "profile": "native-cli-durable-one-mib-document-last-byte-edit", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "filesystem": "ext4 /dev/mmcblk0p2[/home/eunhhu/work/pure]", + "trials_per_variant": 18, + "final_body_sha256": "06cb5e32505f66804f6e8c79e3230105ca4769444e1389ee36f6f11fd61f2d3a", + "scope": "Each sample starts a new CLI process; create and edit each include program parsing, store initialization, file and directory synchronization. Show verifies the final revision but is outside the timed operations. The host page cache, filesystem and device remain uncontrolled.", + "variants": { + "flat": { + "binary_sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded", + "median_create_ms": 83.556, + "median_edit_ms": 78.805, + "samples": [ + { + "trial": 0, + "create_ms": 93.308, + "edit_ms": 86.265, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 1, + "create_ms": 66.85, + "edit_ms": 81.315, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 2, + "create_ms": 83.243, + "edit_ms": 26.218, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 3, + "create_ms": 25.734, + "edit_ms": 81.636, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 4, + "create_ms": 80.359, + "edit_ms": 22.923, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 5, + "create_ms": 84.855, + "edit_ms": 77.624, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 6, + "create_ms": 83.516, + "edit_ms": 24.882, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 7, + "create_ms": 100.653, + "edit_ms": 65.551, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 8, + "create_ms": 87.547, + "edit_ms": 88.306, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 9, + "create_ms": 100.781, + "edit_ms": 89.489, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 10, + "create_ms": 29.332, + "edit_ms": 79.987, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 11, + "create_ms": 80.05, + "edit_ms": 26.629, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 12, + "create_ms": 79.751, + "edit_ms": 24.165, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 13, + "create_ms": 85.309, + "edit_ms": 23.163, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 14, + "create_ms": 83.597, + "edit_ms": 82.171, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 15, + "create_ms": 97.909, + "edit_ms": 85.934, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 16, + "create_ms": 25.638, + "edit_ms": 82.514, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 17, + "create_ms": 86.699, + "edit_ms": 23.813, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + } + ] + }, + "copy": { + "binary_sha256": "52c2b828b0cba3564a37e3874f60bec793d1dd7031aff77ba850cee7683977f9", + "median_create_ms": 81.672, + "median_edit_ms": 77.908, + "samples": [ + { + "trial": 0, + "create_ms": 29.767, + "edit_ms": 78.001, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 1, + "create_ms": 89.789, + "edit_ms": 91.286, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 2, + "create_ms": 83.106, + "edit_ms": 77.868, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 3, + "create_ms": 84.346, + "edit_ms": 47.881, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 4, + "create_ms": 29.06, + "edit_ms": 78.142, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 5, + "create_ms": 29.801, + "edit_ms": 52.17, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 6, + "create_ms": 81.026, + "edit_ms": 77.948, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 7, + "create_ms": 30.027, + "edit_ms": 87.974, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 8, + "create_ms": 45.522, + "edit_ms": 87.955, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 9, + "create_ms": 80.228, + "edit_ms": 22.863, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 10, + "create_ms": 82.319, + "edit_ms": 77.564, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 11, + "create_ms": 94.951, + "edit_ms": 46.186, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 12, + "create_ms": 84.607, + "edit_ms": 80.2, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 13, + "create_ms": 29.82, + "edit_ms": 86.258, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 14, + "create_ms": 94.36, + "edit_ms": 31.893, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 15, + "create_ms": 97.289, + "edit_ms": 44.737, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 16, + "create_ms": 60.805, + "edit_ms": 80.635, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + }, + { + "trial": 17, + "create_ms": 85.887, + "edit_ms": 77.182, + "store_files": 2, + "store_file_bytes": 1048600, + "store_allocated_bytes": 1052672 + } + ] + }, + "paged": { + "binary_sha256": "e1c12a2f5e736bb86737ceff7849fe95b1a9d13136e8c5eb3f3398cb3971c6f5", + "median_create_ms": 88.948, + "median_edit_ms": 14.473, + "samples": [ + { + "trial": 0, + "create_ms": 117.897, + "edit_ms": 565.484, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 1, + "create_ms": 47.619, + "edit_ms": 13.102, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 2, + "create_ms": 89.46, + "edit_ms": 14.779, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 3, + "create_ms": 30.501, + "edit_ms": 15.016, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 4, + "create_ms": 89.223, + "edit_ms": 13.383, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 5, + "create_ms": 88.518, + "edit_ms": 12.718, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 6, + "create_ms": 88.992, + "edit_ms": 13.681, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 7, + "create_ms": 116.568, + "edit_ms": 23.607, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 8, + "create_ms": 100.547, + "edit_ms": 24.497, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 9, + "create_ms": 54.374, + "edit_ms": 13.896, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 10, + "create_ms": 84.894, + "edit_ms": 13.607, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 11, + "create_ms": 92.673, + "edit_ms": 14.166, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 12, + "create_ms": 34.961, + "edit_ms": 78.767, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 13, + "create_ms": 88.903, + "edit_ms": 13.656, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 14, + "create_ms": 57.426, + "edit_ms": 13.968, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 15, + "create_ms": 38.675, + "edit_ms": 14.937, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 16, + "create_ms": 90.664, + "edit_ms": 15.344, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + }, + { + "trial": 17, + "create_ms": 90.055, + "edit_ms": 14.926, + "store_files": 3, + "store_file_bytes": 1065520, + "store_allocated_bytes": 1069056 + } + ] + } + } +} diff --git a/research/experiments/005-storage-edit-latency/2026-09-24-sequence-ext4-linux-aarch64.json b/research/experiments/005-storage-edit-latency/2026-09-24-sequence-ext4-linux-aarch64.json new file mode 100644 index 0000000..5efbf8b --- /dev/null +++ b/research/experiments/005-storage-edit-latency/2026-09-24-sequence-ext4-linux-aarch64.json @@ -0,0 +1,862 @@ +{ + "profile": "repeated-durable-one-mib-document-last-byte-edit", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "filesystem": "ext4 /dev/mmcblk0p2[/home/eunhhu/work/pure]", + "edits_per_variant": 70, + "final_body_sha256": "3cfc240f48e8e1b8553f3e2670adf7b81cafe34f089d4372fa4c28787dc02151", + "scope": "Same actual source-loaded CLI and editor.pure, two persistent stores, alternating operation order. Timings include each edit process, parser, store init and fsync; creation and final show are not timed. Device state is uncontrolled.", + "variants": { + "flat": { + "binary_sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded", + "median_edit_ms": 83.176, + "samples": [ + { + "edit": 1, + "elapsed_ms": 24.328, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 2, + "elapsed_ms": 79.862, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 3, + "elapsed_ms": 177.059, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 4, + "elapsed_ms": 89.804, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 5, + "elapsed_ms": 92.579, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 6, + "elapsed_ms": 83.733, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 7, + "elapsed_ms": 29.379, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 8, + "elapsed_ms": 78.191, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 9, + "elapsed_ms": 57.551, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 10, + "elapsed_ms": 83.875, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 11, + "elapsed_ms": 78.209, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 12, + "elapsed_ms": 85.296, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 13, + "elapsed_ms": 25.204, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 14, + "elapsed_ms": 28.663, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 15, + "elapsed_ms": 82.153, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 16, + "elapsed_ms": 84.924, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 17, + "elapsed_ms": 82.359, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 18, + "elapsed_ms": 39.211, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 19, + "elapsed_ms": 28.648, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 20, + "elapsed_ms": 82.115, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 21, + "elapsed_ms": 83.895, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 22, + "elapsed_ms": 83.007, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 23, + "elapsed_ms": 84.496, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 24, + "elapsed_ms": 28.525, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 25, + "elapsed_ms": 86.499, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 26, + "elapsed_ms": 25.536, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 27, + "elapsed_ms": 93.217, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 28, + "elapsed_ms": 36.673, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 29, + "elapsed_ms": 84.748, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 30, + "elapsed_ms": 79.529, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 31, + "elapsed_ms": 84.024, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 32, + "elapsed_ms": 82.681, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 33, + "elapsed_ms": 28.903, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 34, + "elapsed_ms": 29.254, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 35, + "elapsed_ms": 86.486, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 36, + "elapsed_ms": 96.679, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 37, + "elapsed_ms": 41.477, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 38, + "elapsed_ms": 82.386, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 39, + "elapsed_ms": 25.063, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 40, + "elapsed_ms": 27.84, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 41, + "elapsed_ms": 86.119, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 42, + "elapsed_ms": 89.284, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 43, + "elapsed_ms": 84.316, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 44, + "elapsed_ms": 86.972, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 45, + "elapsed_ms": 88.795, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 46, + "elapsed_ms": 47.265, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 47, + "elapsed_ms": 32.736, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 48, + "elapsed_ms": 93.95, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 49, + "elapsed_ms": 29.861, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 50, + "elapsed_ms": 25.674, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 51, + "elapsed_ms": 87.319, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 52, + "elapsed_ms": 83.632, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 53, + "elapsed_ms": 83.964, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 54, + "elapsed_ms": 83.345, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 55, + "elapsed_ms": 175.728, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 56, + "elapsed_ms": 92.189, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 57, + "elapsed_ms": 98.676, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 58, + "elapsed_ms": 84.905, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 59, + "elapsed_ms": 81.966, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 60, + "elapsed_ms": 87.203, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 61, + "elapsed_ms": 25.923, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 62, + "elapsed_ms": 29.165, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 63, + "elapsed_ms": 82.076, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 64, + "elapsed_ms": 128.506, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 65, + "elapsed_ms": 91.837, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 66, + "elapsed_ms": 25.832, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 67, + "elapsed_ms": 86.189, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 68, + "elapsed_ms": 83.351, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 69, + "elapsed_ms": 85.479, + "files": 2, + "allocated_bytes": 1052672 + }, + { + "edit": 70, + "elapsed_ms": 77.587, + "files": 2, + "allocated_bytes": 1052672 + } + ] + }, + "paged": { + "binary_sha256": "e1c12a2f5e736bb86737ceff7849fe95b1a9d13136e8c5eb3f3398cb3971c6f5", + "median_edit_ms": 18.858, + "samples": [ + { + "edit": 1, + "elapsed_ms": 16.412, + "files": 3, + "allocated_bytes": 1069056 + }, + { + "edit": 2, + "elapsed_ms": 17.281, + "files": 3, + "allocated_bytes": 1085440 + }, + { + "edit": 3, + "elapsed_ms": 32.037, + "files": 3, + "allocated_bytes": 1101824 + }, + { + "edit": 4, + "elapsed_ms": 29.739, + "files": 3, + "allocated_bytes": 1118208 + }, + { + "edit": 5, + "elapsed_ms": 28.746, + "files": 3, + "allocated_bytes": 1134592 + }, + { + "edit": 6, + "elapsed_ms": 31.822, + "files": 3, + "allocated_bytes": 1150976 + }, + { + "edit": 7, + "elapsed_ms": 16.937, + "files": 3, + "allocated_bytes": 1167360 + }, + { + "edit": 8, + "elapsed_ms": 16.845, + "files": 3, + "allocated_bytes": 1183744 + }, + { + "edit": 9, + "elapsed_ms": 17.18, + "files": 3, + "allocated_bytes": 1200128 + }, + { + "edit": 10, + "elapsed_ms": 18.422, + "files": 3, + "allocated_bytes": 1216512 + }, + { + "edit": 11, + "elapsed_ms": 18.672, + "files": 3, + "allocated_bytes": 1232896 + }, + { + "edit": 12, + "elapsed_ms": 16.399, + "files": 3, + "allocated_bytes": 1249280 + }, + { + "edit": 13, + "elapsed_ms": 18.911, + "files": 3, + "allocated_bytes": 1265664 + }, + { + "edit": 14, + "elapsed_ms": 71.604, + "files": 3, + "allocated_bytes": 1282048 + }, + { + "edit": 15, + "elapsed_ms": 18.821, + "files": 3, + "allocated_bytes": 1298432 + }, + { + "edit": 16, + "elapsed_ms": 16.746, + "files": 3, + "allocated_bytes": 1314816 + }, + { + "edit": 17, + "elapsed_ms": 17.354, + "files": 3, + "allocated_bytes": 1331200 + }, + { + "edit": 18, + "elapsed_ms": 15.468, + "files": 3, + "allocated_bytes": 1347584 + }, + { + "edit": 19, + "elapsed_ms": 15.872, + "files": 3, + "allocated_bytes": 1363968 + }, + { + "edit": 20, + "elapsed_ms": 18.347, + "files": 3, + "allocated_bytes": 1380352 + }, + { + "edit": 21, + "elapsed_ms": 17.311, + "files": 3, + "allocated_bytes": 1396736 + }, + { + "edit": 22, + "elapsed_ms": 18.997, + "files": 3, + "allocated_bytes": 1413120 + }, + { + "edit": 23, + "elapsed_ms": 17.267, + "files": 3, + "allocated_bytes": 1429504 + }, + { + "edit": 24, + "elapsed_ms": 18.52, + "files": 3, + "allocated_bytes": 1445888 + }, + { + "edit": 25, + "elapsed_ms": 34.127, + "files": 3, + "allocated_bytes": 1462272 + }, + { + "edit": 26, + "elapsed_ms": 86.489, + "files": 3, + "allocated_bytes": 1478656 + }, + { + "edit": 27, + "elapsed_ms": 29.105, + "files": 3, + "allocated_bytes": 1495040 + }, + { + "edit": 28, + "elapsed_ms": 37.131, + "files": 3, + "allocated_bytes": 1511424 + }, + { + "edit": 29, + "elapsed_ms": 15.478, + "files": 3, + "allocated_bytes": 1527808 + }, + { + "edit": 30, + "elapsed_ms": 19.623, + "files": 3, + "allocated_bytes": 1544192 + }, + { + "edit": 31, + "elapsed_ms": 90.312, + "files": 3, + "allocated_bytes": 1560576 + }, + { + "edit": 32, + "elapsed_ms": 17.365, + "files": 3, + "allocated_bytes": 1576960 + }, + { + "edit": 33, + "elapsed_ms": 17.109, + "files": 3, + "allocated_bytes": 1593344 + }, + { + "edit": 34, + "elapsed_ms": 126.006, + "files": 3, + "allocated_bytes": 1609728 + }, + { + "edit": 35, + "elapsed_ms": 17.457, + "files": 3, + "allocated_bytes": 1626112 + }, + { + "edit": 36, + "elapsed_ms": 15.908, + "files": 3, + "allocated_bytes": 1642496 + }, + { + "edit": 37, + "elapsed_ms": 18.896, + "files": 3, + "allocated_bytes": 1658880 + }, + { + "edit": 38, + "elapsed_ms": 16.535, + "files": 3, + "allocated_bytes": 1675264 + }, + { + "edit": 39, + "elapsed_ms": 17.568, + "files": 3, + "allocated_bytes": 1691648 + }, + { + "edit": 40, + "elapsed_ms": 71.184, + "files": 3, + "allocated_bytes": 1708032 + }, + { + "edit": 41, + "elapsed_ms": 16.619, + "files": 3, + "allocated_bytes": 1724416 + }, + { + "edit": 42, + "elapsed_ms": 29.265, + "files": 3, + "allocated_bytes": 1740800 + }, + { + "edit": 43, + "elapsed_ms": 21.163, + "files": 3, + "allocated_bytes": 1757184 + }, + { + "edit": 44, + "elapsed_ms": 27.305, + "files": 3, + "allocated_bytes": 1773568 + }, + { + "edit": 45, + "elapsed_ms": 29.526, + "files": 3, + "allocated_bytes": 1789952 + }, + { + "edit": 46, + "elapsed_ms": 27.936, + "files": 3, + "allocated_bytes": 1806336 + }, + { + "edit": 47, + "elapsed_ms": 87.552, + "files": 3, + "allocated_bytes": 1822720 + }, + { + "edit": 48, + "elapsed_ms": 30.512, + "files": 3, + "allocated_bytes": 1839104 + }, + { + "edit": 49, + "elapsed_ms": 17.81, + "files": 3, + "allocated_bytes": 1855488 + }, + { + "edit": 50, + "elapsed_ms": 70.4, + "files": 3, + "allocated_bytes": 1871872 + }, + { + "edit": 51, + "elapsed_ms": 17.115, + "files": 3, + "allocated_bytes": 1888256 + }, + { + "edit": 52, + "elapsed_ms": 18.79, + "files": 3, + "allocated_bytes": 1904640 + }, + { + "edit": 53, + "elapsed_ms": 21.637, + "files": 3, + "allocated_bytes": 1921024 + }, + { + "edit": 54, + "elapsed_ms": 16.47, + "files": 3, + "allocated_bytes": 1937408 + }, + { + "edit": 55, + "elapsed_ms": 53.194, + "files": 3, + "allocated_bytes": 1953792 + }, + { + "edit": 56, + "elapsed_ms": 219.485, + "files": 3, + "allocated_bytes": 1970176 + }, + { + "edit": 57, + "elapsed_ms": 24.517, + "files": 3, + "allocated_bytes": 1986560 + }, + { + "edit": 58, + "elapsed_ms": 16.725, + "files": 3, + "allocated_bytes": 2002944 + }, + { + "edit": 59, + "elapsed_ms": 19.628, + "files": 3, + "allocated_bytes": 2019328 + }, + { + "edit": 60, + "elapsed_ms": 17.158, + "files": 3, + "allocated_bytes": 2035712 + }, + { + "edit": 61, + "elapsed_ms": 72.424, + "files": 3, + "allocated_bytes": 2052096 + }, + { + "edit": 62, + "elapsed_ms": 16.894, + "files": 3, + "allocated_bytes": 2068480 + }, + { + "edit": 63, + "elapsed_ms": 18.918, + "files": 3, + "allocated_bytes": 2084864 + }, + { + "edit": 64, + "elapsed_ms": 16.879, + "files": 3, + "allocated_bytes": 2101248 + }, + { + "edit": 65, + "elapsed_ms": 35.067, + "files": 3, + "allocated_bytes": 1052672 + }, + { + "edit": 66, + "elapsed_ms": 83.189, + "files": 3, + "allocated_bytes": 1069056 + }, + { + "edit": 67, + "elapsed_ms": 16.607, + "files": 3, + "allocated_bytes": 1085440 + }, + { + "edit": 68, + "elapsed_ms": 19.566, + "files": 3, + "allocated_bytes": 1101824 + }, + { + "edit": 69, + "elapsed_ms": 17.47, + "files": 3, + "allocated_bytes": 1118208 + }, + { + "edit": 70, + "elapsed_ms": 20.338, + "files": 3, + "allocated_bytes": 1134592 + } + ] + } + } +} diff --git a/research/experiments/005-storage-edit-latency/copy-memory.json b/research/experiments/005-storage-edit-latency/copy-memory.json new file mode 100644 index 0000000..969250c --- /dev/null +++ b/research/experiments/005-storage-edit-latency/copy-memory.json @@ -0,0 +1,43 @@ +{ + "passed": true, + "failures": [], + "profile": "portable-measurement-no-budget", + "variant": "source-program", + "scope": "Child-process peak RSS for native CLI document creation, read and one-byte revision edit; includes libc and the selected program form, excludes filesystem cache, kernel buffers, TLS, GUI and other apps. No cross-platform memory ceiling is asserted.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1104, + "one_mib_splice_rss_kib": 1056 + }, + "raw_samples": { + "small_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1056, + "raw_max_rss": 1056, + "raw_unit": "KiB" + }, + "one_mib_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 2080, + "raw_max_rss": 2080, + "raw_unit": "KiB" + }, + "one_mib_show_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1104, + "raw_max_rss": 1104, + "raw_unit": "KiB" + }, + "one_mib_splice_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1056, + "raw_max_rss": 1056, + "raw_unit": "KiB" + } + }, + "budget_sha256": null, + "binary_sha256": "52c2b828b0cba3564a37e3874f60bec793d1dd7031aff77ba850cee7683977f9", + "binary_bytes": 178824, + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/research/experiments/005-storage-edit-latency/flat-memory.json b/research/experiments/005-storage-edit-latency/flat-memory.json new file mode 100644 index 0000000..3f45eb1 --- /dev/null +++ b/research/experiments/005-storage-edit-latency/flat-memory.json @@ -0,0 +1,43 @@ +{ + "passed": true, + "failures": [], + "profile": "portable-measurement-no-budget", + "variant": "source-program", + "scope": "Child-process peak RSS for native CLI document creation, read and one-byte revision edit; includes libc and the selected program form, excludes filesystem cache, kernel buffers, TLS, GUI and other apps. No cross-platform memory ceiling is asserted.", + "samples": { + "small_edit_rss_kib": 1056, + "one_mib_edit_rss_kib": 2080, + "one_mib_show_rss_kib": 1056, + "one_mib_splice_rss_kib": 1104 + }, + "raw_samples": { + "small_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1056, + "raw_max_rss": 1056, + "raw_unit": "KiB" + }, + "one_mib_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 2080, + "raw_max_rss": 2080, + "raw_unit": "KiB" + }, + "one_mib_show_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1056, + "raw_max_rss": 1056, + "raw_unit": "KiB" + }, + "one_mib_splice_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1104, + "raw_max_rss": 1104, + "raw_unit": "KiB" + } + }, + "budget_sha256": null, + "binary_sha256": "23ebc0ce1df9c49216511bee658fd5b9e373509e296bd2c2f8e76e6d3081cded", + "binary_bytes": 178336, + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/research/experiments/005-storage-edit-latency/measure.py b/research/experiments/005-storage-edit-latency/measure.py new file mode 100644 index 0000000..b390c92 --- /dev/null +++ b/research/experiments/005-storage-edit-latency/measure.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Time the same durable one-MiB document creation and one-byte revision edit.""" +import argparse +import hashlib +import json +from pathlib import Path +import platform +import statistics +import subprocess +import tempfile +import time + +HERE = Path(__file__).resolve().parents[3] +PROGRAM = HERE / 'prototypes/connections/examples/editor.pure' +INITIAL = b'A' * 1_048_576 +EXPECTED = INITIAL[:-1] + b'Z' + + +def invoke(command): + started = time.monotonic_ns() + result = subprocess.run([str(arg) for arg in command], capture_output=True, timeout=30) + elapsed_ms = (time.monotonic_ns() - started) / 1_000_000 + if result.returncode: + raise AssertionError((command, result.returncode, result.stderr[-1000:])) + return result, elapsed_ms + + +def one(binary, root, initial, replacement): + base = [binary, '--program', PROGRAM, '--store', root] + created, create_ms = invoke([*base, 'edit', 'document', 0, 0, 0, initial]) + assert json.loads(created.stdout)['revision'] == '1' + edited, edit_ms = invoke([*base, 'edit', 'document', 1, 1_048_575, 1, replacement]) + assert json.loads(edited.stdout)['revision'] == '2' + shown, _ = invoke([*base, 'show', 'document']) + assert shown.stdout == EXPECTED + assert json.loads(shown.stderr)['revision'] == '2' + files = [path for path in root.iterdir() if path.is_file()] + return {'create_ms': round(create_ms, 3), 'edit_ms': round(edit_ms, 3), + 'store_files': len(files), + 'store_file_bytes': sum(path.stat().st_size for path in files), + 'store_allocated_bytes': sum(path.stat().st_blocks * 512 for path in files)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--flat', type=Path, required=True) + parser.add_argument('--copy', type=Path, required=True) + parser.add_argument('--paged', type=Path, required=True) + parser.add_argument('--scratch-parent', type=Path, required=True) + parser.add_argument('--trials', type=int, default=18) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + if args.trials < 3 or args.trials % 3 or args.output.exists(): + parser.error('trials must be a positive multiple of three and output must be new') + variants = {'flat': args.flat.resolve(), 'copy': args.copy.resolve(), + 'paged': args.paged.resolve()} + for binary in variants.values(): + if not binary.is_file(): + parser.error(f'missing binary: {binary}') + args.scratch_parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='pure-storage-timing-', + dir=args.scratch_parent.resolve()) as name: + root = Path(name) + (root / 'initial').write_bytes(INITIAL) + (root / 'replacement').write_bytes(b'Z') + filesystem = subprocess.run(['findmnt', '-T', str(root), '-no', 'FSTYPE,SOURCE'], + capture_output=True, text=True, check=True).stdout.strip() + samples = {name: [] for name in variants} + names = list(variants) + for trial in range(args.trials): + for name in names[trial % 3:] + names[:trial % 3]: + sample = one(variants[name], root / f'{name}-{trial}', + root / 'initial', root / 'replacement') + samples[name].append({'trial': trial, **sample}) + result = {'profile': 'native-cli-durable-one-mib-document-last-byte-edit', + 'platform': platform.platform(), 'filesystem': filesystem, + 'trials_per_variant': args.trials, + 'final_body_sha256': hashlib.sha256(EXPECTED).hexdigest(), + 'scope': ('Each sample starts a new CLI process; create and edit each ' + 'include program parsing, store initialization, file and ' + 'directory synchronization. Show verifies the final revision ' + 'but is outside the timed operations. The host page cache, ' + 'filesystem and device remain uncontrolled.'), + 'variants': {name: {'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(), + 'median_create_ms': round(statistics.median( + row['create_ms'] for row in samples[name]), 3), + 'median_edit_ms': round(statistics.median( + row['edit_ms'] for row in samples[name]), 3), + 'samples': samples[name]} + for name, binary in variants.items()}} + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({name: {key: value for key, value in entry.items() + if key.startswith('median_')} + for name, entry in result['variants'].items()}, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/research/experiments/005-storage-edit-latency/paged-memory.json b/research/experiments/005-storage-edit-latency/paged-memory.json new file mode 100644 index 0000000..a9bef53 --- /dev/null +++ b/research/experiments/005-storage-edit-latency/paged-memory.json @@ -0,0 +1,43 @@ +{ + "passed": true, + "failures": [], + "profile": "portable-measurement-no-budget", + "variant": "source-program", + "scope": "Child-process peak RSS for native CLI document creation, read and one-byte revision edit; includes libc and the selected program form, excludes filesystem cache, kernel buffers, TLS, GUI and other apps. No cross-platform memory ceiling is asserted.", + "samples": { + "small_edit_rss_kib": 1088, + "one_mib_edit_rss_kib": 2112, + "one_mib_show_rss_kib": 1088, + "one_mib_splice_rss_kib": 1056 + }, + "raw_samples": { + "small_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1088, + "raw_max_rss": 1088, + "raw_unit": "KiB" + }, + "one_mib_edit_rss_kib": { + "exit_code": 0, + "max_rss_kib": 2112, + "raw_max_rss": 2112, + "raw_unit": "KiB" + }, + "one_mib_show_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1088, + "raw_max_rss": 1088, + "raw_unit": "KiB" + }, + "one_mib_splice_rss_kib": { + "exit_code": 0, + "max_rss_kib": 1056, + "raw_max_rss": 1056, + "raw_unit": "KiB" + } + }, + "budget_sha256": null, + "binary_sha256": "e1c12a2f5e736bb86737ceff7849fe95b1a9d13136e8c5eb3f3398cb3971c6f5", + "binary_bytes": 200704, + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41" +} diff --git a/research/experiments/005-storage-edit-latency/sequence.py b/research/experiments/005-storage-edit-latency/sequence.py new file mode 100644 index 0000000..57ff835 --- /dev/null +++ b/research/experiments/005-storage-edit-latency/sequence.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Measure repeated durable edits and page-segment rebuilds on one real document.""" +import argparse +import hashlib +import json +from pathlib import Path +import platform +import statistics +import subprocess +import tempfile + +from measure import INITIAL, PROGRAM, invoke + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--flat', type=Path, required=True) + parser.add_argument('--paged', type=Path, required=True) + parser.add_argument('--scratch-parent', type=Path, required=True) + parser.add_argument('--edits', type=int, default=70) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + if args.edits < 66 or args.output.exists(): + parser.error('at least 66 edits are needed to observe the current rebuild boundary; output must be new') + binaries = {'flat': args.flat.resolve(), 'paged': args.paged.resolve()} + args.scratch_parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='pure-storage-sequence-', + dir=args.scratch_parent.resolve()) as name: + root = Path(name) + filesystem = subprocess.run(['findmnt', '-T', str(root), '-no', 'FSTYPE,SOURCE'], + capture_output=True, text=True, check=True).stdout.strip() + (root / 'initial').write_bytes(INITIAL) + patches = {'Y': root / 'Y', 'Z': root / 'Z'} + for label, file in patches.items(): + file.write_bytes(label.encode()) + bases = {variant: [binary, '--program', PROGRAM, '--store', root / variant] + for variant, binary in binaries.items()} + for base in bases.values(): + result, _ = invoke([*base, 'edit', 'document', 0, 0, 0, root / 'initial']) + assert json.loads(result.stdout)['revision'] == '1' + samples = {variant: [] for variant in binaries} + for edit in range(1, args.edits + 1): + patch = 'Z' if edit % 2 else 'Y' + order = ('flat', 'paged') if edit % 2 else ('paged', 'flat') + for variant in order: + base = bases[variant] + result, elapsed = invoke([*base, 'edit', 'document', edit, + len(INITIAL) - 1, 1, patches[patch]]) + assert json.loads(result.stdout)['revision'] == str(edit + 1) + files = [file for file in (root / variant).iterdir() if file.is_file()] + samples[variant].append({'edit': edit, 'elapsed_ms': round(elapsed, 3), + 'files': len(files), + 'allocated_bytes': sum(file.stat().st_blocks * 512 + for file in files)}) + expected = INITIAL[:-1] + patches[patch].read_bytes() + for base in bases.values(): + shown, _ = invoke([*base, 'show', 'document']) + assert shown.stdout == expected + assert json.loads(shown.stderr)['revision'] == str(args.edits + 1) + result = {'profile': 'repeated-durable-one-mib-document-last-byte-edit', + 'platform': platform.platform(), 'filesystem': filesystem, + 'edits_per_variant': args.edits, + 'final_body_sha256': hashlib.sha256(expected).hexdigest(), + 'scope': ('Same actual source-loaded CLI and editor.pure, two persistent ' + 'stores, alternating operation order. Timings include each ' + 'edit process, parser, store init and fsync; creation and ' + 'final show are not timed. Device state is uncontrolled.'), + 'variants': {variant: { + 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(), + 'median_edit_ms': round(statistics.median(row['elapsed_ms'] + for row in samples[variant]), 3), + 'samples': samples[variant]} + for variant, binary in binaries.items()}} + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({variant: {'median_edit_ms': row['median_edit_ms'], + 'max_edit_ms': max(sample['elapsed_ms'] + for sample in row['samples']), + 'max_allocated_bytes': max(sample['allocated_bytes'] + for sample in row['samples'])} + for variant, row in result['variants'].items()}, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/research/experiments/006-native-save-latency/2026-09-24-ext4-linux-aarch64.json b/research/experiments/006-native-save-latency/2026-09-24-ext4-linux-aarch64.json new file mode 100644 index 0000000..0f2d215 --- /dev/null +++ b/research/experiments/006-native-save-latency/2026-09-24-ext4-linux-aarch64.json @@ -0,0 +1,221 @@ +{ + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "filesystem": "/dev/mmcblk0p2[/home/eunhhu/work/pure] ext4", + "workload": "one-byte final-character replacement, Ctrl+S to complete visible frame; includes full draft comparison, runtime, durable store and redraw; initial creation and final independent CLI read excluded", + "rounds": 3, + "edits_per_session": 8, + "binaries": { + "flat": { + "cli_sha256": "27d9ad1c83265cef24f5e756537a68692852145bfc7c1e31255e18c08fac6295", + "tui_sha256": "e8b224e3ce5f5d83d597418d4027ec36dd44bfe84d17f6e9224135dc457316c1" + }, + "paged": { + "cli_sha256": "e829bd6bad3b56bc319cdab03aba7f45c44afab53876e462ae57c35b82823e9a", + "tui_sha256": "09303ec33d07c12b07834b07fb5eeb836005c23fcb3a6b6cc2683631a9d4b13c" + } + }, + "runs": { + "flat": { + "256": [ + { + "samples_ns": [ + 5958081, + 7778966, + 6548524, + 19995479, + 62301450, + 9400518, + 5842210, + 8589502 + ], + "median_ns": 8184234, + "final_revision": 9, + "body_sha256": "e075f2f51cad23d0537186cfcd50f911ea954f9c2e32a437f45327f1b7899bbb" + }, + { + "samples_ns": [ + 9415093, + 16809059, + 8447428, + 8676279, + 6705838, + 9542795, + 10029758, + 6036581 + ], + "median_ns": 9045686, + "final_revision": 9, + "body_sha256": "e075f2f51cad23d0537186cfcd50f911ea954f9c2e32a437f45327f1b7899bbb" + }, + { + "samples_ns": [ + 7148893, + 10145461, + 7778114, + 6517728, + 62411765, + 50067253, + 7390504, + 8495113 + ], + "median_ns": 8136613, + "final_revision": 9, + "body_sha256": "e075f2f51cad23d0537186cfcd50f911ea954f9c2e32a437f45327f1b7899bbb" + } + ], + "1048576": [ + { + "samples_ns": [ + 145172249, + 96882586, + 40365568, + 349602121, + 39360237, + 89495933, + 183181933, + 87081031 + ], + "median_ns": 93189259, + "final_revision": 9, + "body_sha256": "4e29ad18ab9f42d7c233500771a39d7c852b200baf328fd00fbbe3fecea1eb56" + }, + { + "samples_ns": [ + 29222924, + 88900009, + 92498076, + 104674699, + 38498924, + 95164515, + 104447385, + 98216731 + ], + "median_ns": 93831295, + "final_revision": 9, + "body_sha256": "4e29ad18ab9f42d7c233500771a39d7c852b200baf328fd00fbbe3fecea1eb56" + }, + { + "samples_ns": [ + 150963793, + 89303082, + 89762414, + 128623245, + 37319148, + 93848499, + 101872780, + 90491636 + ], + "median_ns": 92170067, + "final_revision": 9, + "body_sha256": "4e29ad18ab9f42d7c233500771a39d7c852b200baf328fd00fbbe3fecea1eb56" + } + ] + }, + "paged": { + "256": [ + { + "samples_ns": [ + 8496650, + 19415609, + 6801246, + 9666703, + 10048962, + 6038507, + 8610260, + 7698300 + ], + "median_ns": 8553455, + "final_revision": 9, + "body_sha256": "e075f2f51cad23d0537186cfcd50f911ea954f9c2e32a437f45327f1b7899bbb" + }, + { + "samples_ns": [ + 9062278, + 62254117, + 6872783, + 9583444, + 50943122, + 6743320, + 9396814, + 9949258 + ], + "median_ns": 9490129, + "final_revision": 9, + "body_sha256": "e075f2f51cad23d0537186cfcd50f911ea954f9c2e32a437f45327f1b7899bbb" + }, + { + "samples_ns": [ + 9526426, + 8758853, + 6706653, + 97037325, + 5886063, + 20592941, + 9999906, + 6661357 + ], + "median_ns": 9142639, + "final_revision": 9, + "body_sha256": "e075f2f51cad23d0537186cfcd50f911ea954f9c2e32a437f45327f1b7899bbb" + } + ], + "1048576": [ + { + "samples_ns": [ + 20936773, + 33867655, + 20611237, + 21735235, + 20170997, + 34975172, + 20189182, + 22094974 + ], + "median_ns": 21336004, + "final_revision": 9, + "body_sha256": "4e29ad18ab9f42d7c233500771a39d7c852b200baf328fd00fbbe3fecea1eb56" + }, + { + "samples_ns": [ + 97702566, + 25086172, + 20087812, + 22047179, + 53326394, + 22037808, + 20212422, + 21726179 + ], + "median_ns": 22042493, + "final_revision": 9, + "body_sha256": "4e29ad18ab9f42d7c233500771a39d7c852b200baf328fd00fbbe3fecea1eb56" + }, + { + "samples_ns": [ + 19868442, + 22179308, + 21243699, + 30931198, + 21662716, + 68731271, + 23544638, + 26258818 + ], + "median_ns": 22861973, + "final_revision": 9, + "body_sha256": "4e29ad18ab9f42d7c233500771a39d7c852b200baf328fd00fbbe3fecea1eb56" + } + ] + } + }, + "median_ns": { + "flat": { + "256": 8542307, + "1048576": 93173287 + }, + "paged": { + "256": 9229546, + "1048576": 22042493 + } + } +} diff --git a/research/experiments/006-native-save-latency/measure.py b/research/experiments/006-native-save-latency/measure.py new file mode 100644 index 0000000..f1e2a00 --- /dev/null +++ b/research/experiments/006-native-save-latency/measure.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Time durable one-byte saves in the actual native terminal document app.""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import platform +import pty +import select +import statistics +import struct +import subprocess +import tempfile +import termios +import time + + +def run(*args): + return subprocess.run([str(arg) for arg in args], check=True, + capture_output=True, timeout=30) + + +def frame(master, process): + output = bytearray() + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + if select.select([master], [], [], 0.5)[0]: + output.extend(os.read(master, 65536)) + if b'\x1b[?25h' in output: + return bytes(output) + if process.poll() is not None: + break + raise AssertionError((process.poll(), output[-1000:])) + + +def one_session(cli, tui, root, size, iterations): + root.mkdir(parents=True) + store = root / 'store' + body = b'A' * size + insert = root / 'initial.bin' + insert.write_bytes(body) + assert json.loads(run(cli, '--store', store, 'edit', 'document', + 0, 0, 0, insert).stdout)['revision'] == '1' + master, slave = pty.openpty() + fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0)) + original = termios.tcgetattr(slave) + process = subprocess.Popen([str(tui), '--store', str(store), 'document'], + stdin=slave, stdout=slave, stderr=slave, + env={**os.environ, 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8'}) + samples = [] + try: + assert b'Pure | document' in frame(master, process) + for index in range(iterations): + replacement = b'B' if index % 2 == 0 else b'A' + os.write(master, b'\x7f') + frame(master, process) + os.write(master, replacement) + frame(master, process) + start = time.perf_counter_ns() + os.write(master, b'\x13') + shown = frame(master, process) + elapsed = time.perf_counter_ns() - start + expected = f'Saved 1-byte edit at byte {size - 1}'.encode() + revision = f'revision {index + 2}'.encode() + assert expected in shown and revision in shown, shown[-1500:] + samples.append(elapsed) + os.write(master, b'\x11') + process.wait(timeout=8) + assert process.returncode == 0 + assert termios.tcgetattr(slave) == original + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + os.close(master) + os.close(slave) + final = run(cli, '--store', store, 'show', 'document') + expected_body = body[:-1] + (b'B' if iterations % 2 else b'A') + assert final.stdout == expected_body + assert json.loads(final.stderr)['revision'] == str(iterations + 1) + return {'samples_ns': samples, 'median_ns': int(statistics.median(samples)), + 'final_revision': iterations + 1, + 'body_sha256': hashlib.sha256(final.stdout).hexdigest()} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--flat-cli', type=Path, required=True) + parser.add_argument('--flat-tui', type=Path, required=True) + parser.add_argument('--paged-cli', type=Path, required=True) + parser.add_argument('--paged-tui', type=Path, required=True) + parser.add_argument('--scratch-parent', type=Path, required=True) + parser.add_argument('--rounds', type=int, default=3) + parser.add_argument('--edits', type=int, default=8) + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.rounds < 1 or args.edits < 1 or (args.output and args.output.exists()): + parser.error('rounds and edits must be positive; output must not exist') + binaries = {name: (cli.resolve(), tui.resolve()) for name, cli, tui in + (('flat', args.flat_cli, args.flat_tui), + ('paged', args.paged_cli, args.paged_tui))} + parent = args.scratch_parent.resolve() + parent.mkdir(parents=True, exist_ok=True) + filesystem = run('findmnt', '-T', parent, '-no', 'SOURCE,FSTYPE').stdout.decode().strip() + raw = {name: {'256': [], '1048576': []} for name in binaries} + with tempfile.TemporaryDirectory(prefix='pure-native-save-', dir=parent) as directory: + root = Path(directory) + for size in (256, 1_048_576): + for round_number in range(args.rounds): + order = ('flat', 'paged') if round_number % 2 == 0 else ('paged', 'flat') + for name in order: + cli, tui = binaries[name] + raw[name][str(size)].append(one_session( + cli, tui, root / f'{size}-{round_number}-{name}', size, args.edits)) + result = {'platform': platform.platform(), 'filesystem': filesystem, + 'workload': 'one-byte final-character replacement, Ctrl+S to complete visible frame; ' + 'includes full draft comparison, runtime, durable store and redraw; ' + 'initial creation and final independent CLI read excluded', + 'rounds': args.rounds, 'edits_per_session': args.edits, + 'binaries': {name: {'cli_sha256': hashlib.sha256(cli.read_bytes()).hexdigest(), + 'tui_sha256': hashlib.sha256(tui.read_bytes()).hexdigest()} + for name, (cli, tui) in binaries.items()}, + 'runs': raw, + 'median_ns': {name: {size: int(statistics.median( + sample for session in sessions for sample in session['samples_ns'])) + for size, sessions in sizes.items()} for name, sizes in raw.items()}} + encoded = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(encoded) + print(json.dumps({'platform': result['platform'], 'filesystem': filesystem, + 'median_ns': result['median_ns'], 'rounds': args.rounds, + 'edits_per_session': args.edits})) + + +if __name__ == '__main__': + main() -- cgit v1.2.3