From 77fec75ca1a7a1a6562de0cf1587e4449fb1844e Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Sun, 9 Aug 2026 17:20:00 -0400 Subject: [PATCH] =?UTF-8?q?architecture:=20specialized-JS=20executor=20is?= =?UTF-8?q?=20emission-only=20=E2=80=94=20unsafe-eval=20is=20out,=20as=20i?= =?UTF-8?q?nvariant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator decision (2026-08-10), rescoping #8: no code path may require CSP beyond wasm-unsafe-eval. The v2 host-boundary executor becomes a descriptor->JS-module generator with an AOT-shaped contract (explicit linking context, no closure capture of live runtime state), delivered by deploy-time emission or a server-host first-load cache import — never eval/new Function. Browsers running dynamically-loaded components stay on the interpreter. Kills the CSP feature-detection/fallback machinery and a third executor mode in the test matrix; the differential oracle survives via imported emitted modules (the production delivery mechanism itself). Deploy-time unbundling moves to the caching track (#7) — packaging, independent of executor choice. All still gated on #17 measuring a gap. §3 CSP bullet, §8 v2 + future-options bullets, §11 diff-testing note, §12 risk row; issues #8 (rewritten) and #7 (scope comment) updated alongside. --- docs/architecture.md | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index d74a497..9c31f5e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -115,9 +115,12 @@ Notes: signatures are not available from `WebAssembly.Module.imports()`. The architecture below sidesteps this (the translator emits all type information), but no design may assume type reflection exists. -- CSP: compiling from bytes requires `wasm-unsafe-eval`. The baseline design - requires nothing beyond that. Full `unsafe-eval` is only needed for the - optional generated-JS fast path (§8). +- CSP: compiling from bytes requires `wasm-unsafe-eval`. The runtime + requires nothing beyond that — **a design invariant, not a default** + (decided 2026-08-10, [#8](https://github.com/lann/deltic/issues/8)): no + code path may require full `unsafe-eval`. The specialized-JS executor is + emission-only (§8) — a deploy-time AOT step or a server-side first-load + cache import — never runtime `eval`/`new Function`. - The runtime core is platform-neutral by contract (§4.3) and this is pinned by `runtime/tests/platform_purity_test.ts` — no `node:*` builtins, no Deno APIs (M3 removed the one violation, an `AsyncLocalStorage` ambient). @@ -381,16 +384,28 @@ Requirement: not critical now; must become fast **without rearchitecting**. - v1: a generic interpreter walks descriptors. CSP-clean, everywhere. This is what ships today; it has been fast enough for every consumer gate so far. - - v2 (when needed, [#8](https://github.com/lann/deltic/issues/8)): generate - specialized JS from the same descriptors. Deno: always available. - Browsers: gated on CSP `unsafe-eval`, interpreter as fallback. Two - executors over one IR double as a differential-testing oracle. + - v2 (when a gap is measured — gated on + [#17](https://github.com/lann/deltic/issues/17), not the calendar; + [#8](https://github.com/lann/deltic/issues/8)): a generator from the + same descriptors to specialized JS **modules** — emission-only, never + `eval`/`new Function` (§3's CSP invariant). One mechanism, two + invocation times: a deploy-time AOT step, or — on server hosts + (Deno/Node, no CSP) — first-load emission into a cache directory and + `import()`; pre-warming and freezing that cache *is* the AOT step. + Browsers running dynamically-loaded components stay on the interpreter. + The generated-module contract is AOT-shaped from day one (explicit + linking context; no closure capture of live runtime state) so both + invocation times share one artifact. Two executors over one IR double + as a differential-testing oracle — exercised by importing emitted + modules, i.e. the production delivery mechanism itself. - Disciplines adopted from the start because they're hard to retrofit: slab handle tables; no per-call closure/object allocation on hot paths; view reuse with grow-aware invalidation; `encodeInto` for strings. - Future options, noted not planned: JS string builtins (now widely shipped) - for string-heavy host boundaries; deploy-time unbundling for engine code - caching (§10). + for string-heavy host boundaries. Deploy-time unbundling (real URLs per + module → engine code-cache hits) is a packaging concern independent of the + executor choice and moved to the caching track (§10, + [#7](https://github.com/lann/deltic/issues/7)). ## 9. Bindings generation @@ -493,8 +508,9 @@ probe run weekly as findings-only canaries (`.github/workflows/canary.yml`) with a capability-probe preamble that surfaces wasm-proposal landings (multi-memory, GC, EH, memory64, …) before the corpus exercises them. -Also planned: differential testing of interpreter vs generated-JS executors -(§8, [#8](https://github.com/lann/deltic/issues/8)); differential fuzzing +Also planned: differential testing of the interpreter vs emitted +specialized-JS modules (§8, [#8](https://github.com/lann/deltic/issues/8)); +differential fuzzing against native wasmtime with `wasm-smith`-generated components ([#9](https://github.com/lann/deltic/issues/9)). @@ -515,7 +531,7 @@ are kept as gates. | JSC/SpiderMonkey engine gaps | medium | Deno-first dev; file upstream. M3 evidence: SpiderMonkey JSPI clean over the full corpus (pref-flipped); **JSC's real gap was missing multi-memory** (capped the WebKit lane — the CABI routinely needs >1 memory per module), not JSPI. Resolved in WebKit trunk: default-on from the webkit-2342 playwright roll, lane at effective parity there ([#11](https://github.com/lann/deltic/issues/11)) | | Testing wasmtime-with-wasmtime blind spots | medium | official suite + definitions.py ports as independent checks; consumer suites as real-workload gates | | Testing-toolchain format skew: testgen assembles with `wast` 255 while the shim validates with wasmparser 0.252 (wasmtime-47 pin) — the 0.253–0.255 window re-arited 🧵 thread opcodes (byte-level desync) | low, bounded | known 5-entry xfail set; exits on the wasmtime bump ([#1](https://github.com/lann/deltic/issues/1)); testgen cannot downgrade (suite text syntax needs `wast` ≥255) | -| CSP variance in embedders | low | baseline needs only `wasm-unsafe-eval`; JS codegen is optional | +| CSP variance in embedders | low | baseline needs only `wasm-unsafe-eval` — an invariant, no path may require full `unsafe-eval` (§3); specialized JS is emission-only, deploy-time or server-side cache import ([#8](https://github.com/lann/deltic/issues/8)) | | Consumer coupling churn: 7+ downstream repos tracking pre-1.0 plan/contract formats | medium | versioned releases ([#16](https://github.com/lann/deltic/issues/16)); strict formatVersion equality already fails loud; consumer matrices as release gate (the wasmtime↔embedder relationship); both sides practice exact pinning | | Consumer scope creep pulling WASI implementations into the core | medium | shim package is a separate deliverable with consumer-driven scope; §2 non-goal stands; the L3 runner belongs in polymorph-test ([#14](https://github.com/lann/deltic/issues/14)) | | Host-boundary perf vs jco's generated JS (v1 interpreter) | low-medium | translation throughput measured (multi-MB components in tens of ms); cutover benches tracked with [#8](https://github.com/lann/deltic/issues/8); iroh's polling-workaround removal dominates first-consumer numbers regardless |