diff --git a/README.md b/README.md index 6e1ea6b..a8a5f3b 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ The runtime reads its server settings from: /etc/uce/settings.cfg ``` -The shipped example contains the important filesystem and FastCGI settings: +The example contains the filesystem and FastCGI settings: ```ini BIN_DIRECTORY=/var/cache/uce/work @@ -473,7 +473,7 @@ Important details: - `SCRIPT_FILENAME` should resolve to the actual `.uce` file on disk - `proxy_http_version 1.1` and the `Upgrade` / `Connection` headers are required for WebSockets -The `location /` block above is intentionally conservative and only serves real files from `site/`. If your app uses a front-controller pattern such as routing everything through `/index.uce`, change that block accordingly. +The `location /` block only serves files from `site/`. If your app uses a front-controller pattern such as routing everything through `/index.uce`, change that block accordingly. ### 6. Think about document root and private files diff --git a/WASM-PROPOSAL.md b/WASM-PROPOSAL.md deleted file mode 100644 index 64eff5f..0000000 --- a/WASM-PROPOSAL.md +++ /dev/null @@ -1,1080 +0,0 @@ -# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE - -> **Steady-state reference:** for the runtime as it is actually built today -> (process topology, the membrane, unified dispatch, and the central WebSocket -> broker), see [`docs/wasm-runtime-architecture.md`](docs/wasm-runtime-architecture.md). -> This proposal retains the motivation and the phased migration plan. - -- **Status:** building the production worker per §9.1 (W-phases). W1 (core - module), W2 (wasm unit compile target), W3 (workspace runtime + membrane, - `src/wasm/worker.cpp`), and W4 (config-selectable FastCGI backend, - `src/wasm/backend.cpp`) are **done**: with `WASM_BACKEND_ENABLED=1` the - real nginx → fastcgi → wasm path serves the starter app (parity 14/14) and - produces clean error pages from an unharmed worker on real kill pages. - W5 cutover is done: WASM is the default page-render backend with native kept - as the config-selectable reference/fallback; the membrane now covers regex, - xml/yaml, markdown, filesystem read/write, sqlite, background tasks, sleep, - and time formatting. W6 spike cleanup is done (prototypes deleted, gates - re-homed to `tests/`/`docs/`); the native-machinery retirements stay gated - while native still serves the remaining fallback surfaces (zip, sockets, - memcache, mysql, `unit_call`, compiler-introspection). Open items: the - sqlite membrane is ~6–7× native (per-query double hostcall + UCEB1 - marshalling — §11.4 cursor/bulk decision), and CoW snapshot birth for the - workspace-birth budget. -- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` → - `dlopen`) with per-unit WebAssembly modules executed in a per-request, - runtime-linked workspace, exposing the same API surface to page code, - enabling memory safety, better execution control, and paving the way for - supporting more source languages in the future. - ---- - -## 1. Motivation - -Two long-standing structural problems and one strategic opportunity share a -single root cause: **request code shares an address space and an allocator -with the runtime.** - -1. **The arena attempt failed for a structural reason.** The - `GLOBAL_ARENA_ALLOCATOR` design in `_scratchpad.cpp` swapped the global - `operator new`/`delete` against a `current_memory_arena`. A single global - allocator cannot distinguish request-lifetime allocations from - process-lifetime ones: during a request, server-lifetime structures - (compile registry, sessions, config trees, unit statics) also allocate. - Under the arena those dangle on reset; with `delete` as a no-op, - system-allocated objects released mid-request leak. The lifetime - distinction lives in the type system and call graph, not at the allocator - boundary — fixing it in-process means threading PMR allocators through - `String`, `DValue`, and every container. - -2. **Fault recovery is best-effort, not sound.** The current - SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips - destructors, can resume over corrupted worker state, and (see - RECOMMENDATIONS.md 1.5) cannot reliably produce a useful backtrace. - A faulting unit *can* have scribbled on runtime memory before the signal. - -3. **UCE can only ever host fully-trusted code.** A `.uce` page is arbitrary - native code with full process privileges. Multi-tenant or user-supplied - page hosting is structurally impossible in the native model. - -A WASM execution model deletes the shared-fate fact itself rather than -patching around it: - -- **Arena by construction:** each request runs in its own linear memory, - dropped wholesale at request end. Request-lifetime memory physically cannot - outlive the request; server-lifetime state physically cannot live inside - it. The `_scratchpad.cpp` design becomes correct because the boundary is - structural, not typological. -- **Sound recovery:** a trap (null deref, OOB, stack exhaustion) is a defined - host-side error that unwinds cleanly, with a precise guest stack trace, and - the unit cannot have touched host memory. Render error page, drop - workspace, keep serving — actually correct, not hopeful. -- **Capability security:** page code gets exactly the imported API surface - and nothing else. Multi-tenant hosting becomes possible. -- **Secondary wins:** architecture-independent unit artifacts (no clang - required on prod), safe module unload/replace (vs. never-safe `dlclose`), - first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU - timeout), per-request memory stats for free. - -**Expected outcome:** ~1.2–2× compute slowdown vs. native (still far ahead of -interpreted runtimes); a real ABI/membrane design; ownership of a custom -loader; toolchain rough edges (§10). More isolated, shared-nothing PHP architecture -but with a good story about websockets, generic sockets, background tasks, -and other long running processes. - ---- - -## 2. Rejected alternatives (recorded so they stay rejected) - -- **In-process PMR arena.** Requires re-typedefing `String`/`DValue` and - threading allocators through the entire codebase; the failed global-swap - shortcut is the only cheap version and it is unsound (§1.1). -- **One instance per component.** UCE components are function calls, not - RPCs: callees receive the context **by reference**, mutate `context.call`, - share the `ob_*` capture stack and `ONCE` dedup state. Per-component - instances force serialize/copy/deserialize of the context on every - `component()` call (a dozen+ per page in the starter), require - host-mediation of the ob stack, and silently change reference semantics to - copy semantics. Rejected. -- **One linked module per app ("the blob").** Introduces an "app" concept - UCE does not have, makes every edit a global relink, turns the artifact - cache into a build graph — webpack's worst traits without its benefits. - Rejected. The file stays the unit. -- **Eager pre-loading of known units at worker warm-up.** Rejected; loading - is strictly lazy, on first explicit call, preserving current semantics. - ---- - -## 3. Architecture overview - -### 3.1 Execution model - -``` -host process (linux_fastcgi, per worker) -│ -├─ vendored wasm runtime (§10.1) -├─ unit artifact cache: one PIC .wasm per unit (replaces per-unit .so) -├─ loader (§6): dylink parsing, base allocation, GOT resolution, -│ symbol registry, ABI stamp check -├─ core snapshot: "core module, initialized" memory+table image -│ -└─ per request: WORKSPACE - ├─ linear memory (CoW-born from core snapshot) - ├─ shared funcref table - ├─ core module instance (uce_lib + libc compiled to wasm) - ├─ unit module instances (loaded lazily, on first call, incl. mid-request) - └─ host handle table (sqlite/mysql/file/socket handles + closers) -``` - -- **Workspace = request.** Born from the core snapshot, dropped at request - end. Memory drop is the arena; handle-table drop is resource cleanup - (this generalizes and replaces the per-connector - `cleanup_*_connections()` pattern — RECOMMENDATIONS.md 1.7 / 5.1 become - structurally unrepresentable). -- **One unit = one PIC wasm module.** Compile, cache, and invalidation - granularity stay per-file. The `.uce → C++` translation pipeline is - unchanged; only the compile target changes - (`clang --target=wasm32-wasi -fPIC` + `wasm-ld -shared`). -- **Strictly lazy loading.** A unit's module is instantiated into a - workspace the first time that workspace calls it — including mid-request. - This is the wasm equivalent of today's compile-and-`dlopen`-on-first-hit - and requires no restart, no fallback path. Placement memoization - (deterministic bases so repeat instantiation is cheaper) is a permitted - optimization; it must not change the loading policy. -- **In-flight isolation.** Module versions are immutable; a recompiled unit - becomes a new module. Running workspaces keep what they loaded; new - workspaces get the new version. Old modules are dropped when unreferenced - (safe unload — impossible with `dlclose`). - -### 3.2 Memory model - -- **One heap, one allocator, one DValue implementation** — all owned by the - core module. Unit modules *import* `malloc`/`free`/runtime symbols via GOT; - the loader **rejects any unit module that defines rather than imports - them** (two allocators on one heap is the one fatal misconfiguration). -- **Arena workspace allocator.** Because the workspace heap is dropped wholesale, - the core's allocator may be a bump allocator with no-op free — the - `_scratchpad.cpp` design, now correct by construction. Per-deployment - flag; fallback is wasi-libc dlmalloc. Memory stats = heap pointer − base - (replaces the tracking `operator new` in `types.h`). -- **Unit statics reset per request** (workspace is born from the core - snapshot, which does not include unit data; unit data segments initialize - at unit load within the workspace). This is *more* shared-nothing than - today, where `.so` statics persist across requests within a worker. - Cross-request state must use explicit host facilities (sessions, caches). - **Breaking change — must be called out in docs and checked against the - site/ tree during Phase 5.** - -### 3.3 The host membrane - -Exactly three currencies cross between host and workspace: - -1. **scalars** (i32/i64/f64), -2. **byte buffers** (`ptr+len` into linear memory; inbound buffers are - placed via the core's exported allocator), on the C++ side we strictly - prefer the binary-safe std::string as a container for buffers -3. **handles** (opaque `u32` indices into the per-workspace host handle - table; each entry carries a closer callback). - -Everything pointer-shaped stays on its own side. Hostcall surface budget: -30–60 functions (§5.1). Host errors return as error values; traps are -reserved for unit faults. Nothing throws across the membrane. - -**DValues cross the membrane only as the versioned wire encoding** (§5.3), -at the following sites: request context in (once), response out (once), -some hostcalls. - -It is expected that many if not most of the toolset API functions we -expose to the unit developer will be judiciously split across the membrane: -have a relatively minimal wasm part that calls into the runtime host, and -then the runtime implementation which does most of the work. - -### 3.4 DValue inside the workspace: no serialization, ever - -Within a workspace, all modules share one address space, one toolchain, one -set of headers — the C/C++ ABI is intact across module boundaries. Therefore: - -- A `DValue` is a pointer (an i32 offset into linear memory). -- `component(path, context)` resolves path → table index (host registry or - guest-resident map) and `call_indirect`s, passing the context pointer. - Reference semantics, mutation visibility, shared ob stack, working `ONCE` - — identical to today. -- Function pointers are shared-table indices, valid across modules: virtual - calls, `std::function` callbacks (`dv_map` lambdas) work across units. -- Cost: one `call_indirect` (single-digit ns) + GOT loads for cross-module - symbols — the same shape of overhead native PIC pays through the PLT/GOT, - i.e. what dlopened `.so` units pay today. - -Encode/decode is **not** part of internal component calls. It exists only at -the membrane (§3.3) and at any future explicit isolation boundary (§4). - -### 3.5 The DValue C ABI (load-bearing, build it first) - -The stable contract of the workspace is a **C ABI**, not the C++ class: -the core exports `extern "C"` accessors over an opaque `uce_dvalue*` (§5.2), -plus the string/ob/print helpers. C++ units may bypass it and use the class -directly (same headers, zero cost — a private fast path). Every other -workspace language uses the C surface. - -This ABI is versioned: every unit artifact carries a custom section -(`uce.abi`: core ABI version + toolchain fingerprint); the loader refuses -stale units and triggers lazy recompilation (units are lazily compiled -anyway, so this costs nothing structurally). - -**Phase 1 of the implementation plan is to introduce this C ABI in the -current native runtime** — it is useful immediately (plugin surface, -testability) and de-risks the rest. - ---- - -## 4. Component-call model / supported languages - -The supported model is one workspace peer model: languages must be able to -produce PIC linear-memory modules that adopt the core allocator and join the -workspace. - -**Workspace peers** (C++, C, Rust, Zig, …): -- Join the workspace as PIC modules importing core symbols. -- Must adopt the core allocator (Rust: `#[global_allocator]`; Zig: allocator - parameter) and must not unwind across boundaries (`panic=abort` / - catch-at-edge). -- Access DValues through the C ABI: pointer semantics, no copies, ns-scale - calls. Idiomatic wrappers per language (e.g. Rust `DValue<'request>` — - the borrow checker enforces the arena invariant). - -**Runtime-carrying/interpreted languages** (JS, Python, Go, C#, …) are not -supported and are not on the roadmap. UCE will not add an alternate component -plane that silently changes `component()` from mutable in-workspace reference -semantics into copied RPC semantics. - -If UCE later supports isolated or cross-trust-boundary components, those must be -introduced as an explicit feature with an explicit API name and copied data -contract. They must not reuse normal `component(path, context)` semantics. -Serialization boundaries and isolation boundaries remain the same lines, by -design. - ---- - -## 5. ABI sketches (to be finalized in Phase 0/1) - -### 5.1 Hostcall surface (grouped; target ≤ 60 functions) - -``` -request: uce_host_ctx_read(buf) → len // wire-encoded context, once -response: uce_host_respond(status, hdrs_buf, body_buf) - uce_host_stream_write(buf) // chunked/streaming path -log: uce_host_log(level, buf) -sqlite: uce_host_sqlite_connect(path_buf) → handle | err - uce_host_sqlite_query(handle, sql_buf, params_buf) → result_buf | err - uce_host_sqlite_cursor_*(...) // optional row-cursor variant - uce_host_sqlite_insert_id/affected/error/disconnect(handle) -mysql: (same shape; existing connector APIs are already handle-shaped) -files: uce_host_file_read/write/stat/list(path_buf, ...) // policy-gated -session: uce_host_session_get/set(key_buf, val_buf) -http: uce_host_http_request(req_buf) → handle/result_buf // outbound -misc: uce_host_time(), uce_host_random(buf), uce_host_env(key_buf) -loader: uce_host_component_resolve(path_buf) → table_index // may load (§6) -ws: uce_host_ws_send(buf), event delivery via render entry re-invocation -``` - -Conventions: all errors as result codes + `uce_host_last_error(buf)`; -inbound buffers placed via the core's exported `uce_alloc`; no hostcall -traps on bad input (clamp/error instead). - -### 5.2 DValue C ABI (core exports; sketch) - -```c -typedef struct uce_dvalue uce_dvalue; // opaque; workspace-owned - -uce_dvalue* uce_dv_root(void); // request context -uce_dvalue* uce_dv_get(uce_dvalue*, const char* key, size_t klen); // create-on-write -uce_dvalue* uce_dv_find(uce_dvalue*, const char* key, size_t klen); // NULL if absent -const char* uce_dv_value(uce_dvalue*, size_t* len); -void uce_dv_set_value(uce_dvalue*, const char* v, size_t vlen); -size_t uce_dv_count(uce_dvalue*); -int uce_dv_is_list(uce_dvalue*); -/* iteration */ -uce_dv_iter uce_dv_iter_begin(uce_dvalue*); -int uce_dv_iter_next(uce_dvalue*, uce_dv_iter*, - const char** key, size_t* klen, uce_dvalue** child); -/* encode/decode at the membrane */ -size_t uce_dv_encode(uce_dvalue*, char* buf, size_t cap); // → UCEB1 -uce_dvalue* uce_dv_decode(const char* buf, size_t len); -/* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close, - uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */ -``` - -No unwinding across this surface; C++ exceptions are caught at the edge and -surfaced as error returns where fallible. - -### 5.3 Wire encoding "UCEB1" (membrane + cross-instance only) - -Length-prefixed binary tree; **not** JSON. Sketch (finalize against DValue's -actual fields — value + ordered children): - -``` -node := value children -value := varint len, bytes (utf-8) -children := varint count, count × ( key: varint len + bytes, node ) -flags := one leading byte per node reserved (bit0: is_list hint) -header := "UCEB" u8 version -``` - -This encoding is a **versioned protocol** from day one (header byte). It is -for future explicit isolation-boundary contracts and the membrane format; -internal calls never see it. - -UCEB1 encoding/decoding should also be exposed to the unit developer so -they can make use of fast serialization/deserialization: matching our existing -API conventions these should be ucb_encode(DValue val) and ucb_decode(String val). This may also be -a worthwhile target for session variables storage (either change session -to DValue or add StringMap support to UCEB1 ser/de). - -### 5.4 Unit module contract - -``` -custom sections: dylink.0 (standard), uce.abi { abi_version, toolchain_id } -imports: env.memory, env.__indirect_function_table, - env.__memory_base, env.__table_base, - GOT.mem.* / GOT.func.* (resolved by loader), - core symbols (malloc, uce_dv_*, uce_print, ...) -exports: uce_unit_setup, uce_unit_render, - uce_unit_component, uce_unit_websocket - (same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET - dlsym symbols in compiler.cpp) -forbidden: defining malloc/free/operator new, own memory, start fn - with side effects beyond data init -``` - ---- - -## 6. The loader (host-side, custom, load-bearing) - -Owned code, ~1–2k lines, vendored-runtime-adjacent. Reference logic: -Emscripten's dylink loader (the ABI is the stable, battle-tested part; the -server-side loader is what doesn't exist off the shelf). - -Per `load(unit)` into a workspace: - -1. Fetch compiled module from artifact cache (compile on miss — today's - lazy-compile path, retargeted). -2. Verify `uce.abi` stamp against the core; on mismatch, recompile unit. -3. Verify import discipline (no allocator/runtime definitions; §3.2). -4. Parse `dylink.0`: data size/alignment, table slots needed. -5. Allocate `__memory_base` (bump within workspace data region) and - `__table_base` (append to shared table). -6. Instantiate with bases; resolve `GOT.*` imports against the workspace - symbol registry (core symbols + previously loaded units); register the - unit's exports. NB: data exports of PIC modules are `__memory_base`-relative - offsets — add the owning unit's base when registering/resolving (core - exports are absolute; the core is non-PIC). See the Phase 0 FINDINGS - erratum; the Phase 3 spike's `self-got` marker exists to catch this. -7. Register entry points in the path → table-index dispatch map. - -`uce_host_component_resolve(path)` consults the dispatch map and calls -`load()` on miss — this is how lazy, programmatic, mid-request loading works -with no special cases. - -Placement memoization (optional, later): record each unit's first-assigned -bases; reuse across workspaces so instantiation is cheaper and snapshot -growth (below) stays consistent. Does not change the lazy policy. - -**Core snapshot:** the only pre-built state is "core module, initialized" — -memory bytes + table state captured once per core build. Workspaces are born -from it via CoW (`mmap(MAP_PRIVATE)` of the snapshot image; the host owns -the Memory object, so OS-level CoW is available). No units are pre-fed. - ---- - -## 7. Request lifecycle (replaces the native flow in linux_fastcgi.cpp) - -``` -1. accept request (fastcgi, websockets message, socket event, CLI request) -2. workspace = birth_from_core_snapshot() (CoW, ~µs) -3. write wire-encoded context into workspace; core decodes → context DValue -4. resolve entry unit (load on first call); call uce_unit_render(ctx_ptr) -5. component(path) inside guest → resolve hostcall → (lazy load) → - call_indirect — reference semantics throughout -6. I/O via hostcalls; resources land in the workspace handle table -7. on return: encode response/headers out; write FastCGI response - on trap: defined error → render error page with guest stack trace; - workspace state is irrelevant because… -8. drop workspace: linear memory gone (arena), handle table closed - (generalized resource cleanup), instances released -``` - -CPU limit: epoch/fuel interruption → same path as trap. -Memory limit: linear memory max → allocation failure / trap → same path. - ---- - -## 8. What carries over unchanged - -- The `.uce → C++` translation, parser, and page semantics. -- The lazy compile-on-first-request model and per-unit artifact caching - (different artifact format). -- The host-side connectors (sqlite/mysql) — already handle-shaped APIs; they - move behind hostcalls with the same `.uce`-visible signatures. -- The site tree, docs, demo, and the runtime smoke suite — now the UCE CLI - runner at `site/tests/cli_runner.uce` — which becomes the parity harness (§9). -- nginx/FastCGI front-end integration, worker model, websocket event flow - (events re-enter via the websocket entry point). - ---- - -## 9. Implementation plan - -Phases are sequential; each has an exit criterion. No phase except 0 and 2's -scaffolding produces throwaway work. All dependencies must be vendored. - -**Phase 0 — toolchain & runtime.** -Validate: wasi-sdk `-fPIC` + `wasm-ld -shared` on a representative generated -unit; cross-module C++ calls with shared memory/table; exceptions decision -(wasm EH vs. error-code discipline at unit boundaries — pick one, record it); -vendored runtime selection. Candidates: **WAMR** (C, small, designed for -embedding, easiest to vendor and patch — fits the project's vendoring -practice and would be preferred) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier -to vendor/patch, use only if blocked on WAMR). Selection criteria: imported-memory + shared-table support, -AOT artifact quality, patchability. Exit: a two-module (core stub + unit -stub) hello-world linked at runtime by a minimal loader, in the chosen -vendored runtime. - -> **Status: DONE (2026-06-12).** Exit criterion passed on k-uce; see -> `docs/wasm-toolchain-findings.md`. Runtime selected: **Wasmtime v45.0.1** -> — WAMR is blocked on the load-bearing requirement (its wasm-c-api ignores -> imported memories/tables; host-side table growth unsupported). wasi-sdk-33 -> PIC validated on stubs **and** on real generated units -> (`collections.uce.cpp`, `hello.uce.cpp` → side modules, no allocator -> definitions). Exceptions: `-fno-exceptions` confirmed across all of it -> (§11.1 stands). Cross-module C++ (containers, heap objects, function -> pointers, `std::function` lambdas, GOT.mem/GOT.func) all proven through -> the spike loader. - -**Phase 1 — DValue C ABI in the native runtime.** -Introduce `uce_dv_*` (§5.2) and the UCEB1 codec in `src/lib/`, used -natively. Zero wasm dependency; immediately testable; freezes the contract -everything else builds on. Exit: codec round-trip + accessor tests in the -existing suite; ABI doc checked in. - -> **Status: DONE (2026-06-12).** Native runtime now exposes `uce_dv_*` -> accessors and UCEB1 encode/decode helpers in `src/lib/dvalue.{h,cpp}`. -> ABI details are checked in at `docs/wasm-phase1-dvalue-abi.md`; UCE-visible -> docs are available as `ucb_encode`/`ucb_decode`. Exit coverage is in -> `site/tests/core.uce` and passed in the full network suite. - -***Phase 1 Addendum*** - - Fix before commit: - -1. ucb_encode(DValue value) deep-copies the whole tree (dvalue.cpp:962, same signature in the header). DValue copy is a full recursive map+string clone, and this function is the future membrane hot path — the request context will pass through it on every request in Phase 2. Should be const DValue& (the function only reads). Same nit for bool ucb_decode(String encoded, ...) at :971 — a by-value String copy of what may -be a large document; const String& matches. - -2. 'P' values ship the raw pointer address on the wire (ucb_node_scalar, dvalue.cpp:833, the 'P' case). The ABI doc explicitly says "pointer/reference identity is intentionally not part of the wire contract," but the implementation encodes std::to_string((u64)ptr) — a meaningless number on the receiving side and an ASLR address disclosure the day UCEB1 crosses a trust boundary (multi-tenant isolation is the stated -endgame). It's consistent with native to_string, but the wire is a different context: I'd encode "" for 'P' and note it in the doc. - -3. f64 fidelity on the wire. 'F' encodes through std::to_string → fixed 6 decimals. That's faithful to native to_string, but the membrane makes it new lossiness: today an 'F' value never round-trips through its string form unless page code asks; in Phase 2 every float in the context will. 1e-7 becomes "0.000000" → decodes to 0. Since the scalar is just a string, switching 'F' to shortest-round-trip formatting -(%.17g-style) later needs no version bump. - -4. uce_dv_iter is about to be frozen with no headroom. Keyed-map iteration does std::advance(begin(), position) per call (dvalue.cpp:1096) — O(n²) per full sweep, and the C ABI will be the only iteration path for non-C++ units. The fix (e.g. resuming via lower_bound on the last key) needs state the one-field struct can't hold. Phase 1's whole purpose is freezing this contract: I'd add reserved space now (size_t -position; size_t reserved[3]; or an opaque byte array) so the implementation can get smarter without an ABI break. - -Also: - - - uce_dv_decode returns a pointer into a single thread-local slot (:1122) — a second decode silently invalidates the first result. The doc documents borrowing for uce_dv_value but not this; one sentence ("valid until the next uce_dv_decode on the thread") would close it. - - An empty non-list map round-trips as scalar "" (type 'M' → 'S'; the child_count == 0 && !LIST branch at dvalue.cpp:873ff). HOPEFULLY harmless in practice (is_array() flips), maybe worth a doc line. - - The decoder silently drops a scalar when children are present — unreachable from the encoder, only crafted input. Fine for v1; "reserved" mention in the format doc would pin it. - - Tests cover only the happy path. The hardening (truncation, bad magic, wrong version, depth bomb) is implemented but untested — two or three negative ucb_decode checks in core.uce would lock it in. A float/bool round-trip check would also have surfaced finding 3. - -> **Addendum status: DONE (2026-06-12).** `ucb_encode`/`ucb_decode` now take -> const references, pointer nodes encode as empty scalars, floating-point -> scalars use `max_digits10`, `uce_dv_iter` has reserved ABI headroom, docs -> cover decode-root lifetime and v1 edge cases, and core tests include invalid -> input plus float/bool round-trips. - -**Phase 2 — core module + membrane.** -Compile `uce_lib` (+ wasi-libc) to wasm as the core module; implement the -hostcall surface (§5.1) in the host; temporary scaffolding allowed: one -statically-linked unit + core to validate codegen and membrane without the -loader. Exit: one real `.uce` page (e.g. `site/tests/core.uce`) renders -correctly through the membrane. Scaffolding is marked throwaway. - -> **Status: MEMBRANE SCAFFOLD DONE (2026-06-12).** Temporary scaffold checked -> in under `spikes/wasm-phase2/`: a WASM reactor core subset owns memory, -> `Request`, `DValue`, UCEB1, and output buffering; the Wasmtime host implements -> the initial `uce_host_ctx_read`/`uce_host_log` membrane and passes a UCEB1 -> request context into the guest; a statically linked `.uce` render entry runs -> through that membrane. Validation passed on k-uce with `PHASE2 EXIT CRITERION: -> PASS`. The scaffold does not yet exercise UCE preprocessor-emitted page C++; -> generator-emitted units through this membrane are explicitly deferred into the -> Phase 3 loader path. Full dynamic unit loading also remains Phase 3 as planned. - -**Phase 3 — the loader + workspace.** -Implement §6 in full: dylink parsing, base allocation, GOT resolution, -ABI/import verification, lazy mid-request loading, path dispatch. Per-request -workspace birth/drop (plain memcpy birth is fine here; CoW is Phase 4). -Exit: the uce-starter renders end-to-end with components loading lazily; the -starter subset of the UCE CLI smoke suite passes against the wasm worker. - -> **Status: SPIKE PASS (2026-06-12).** `spikes/wasm-phase3/` combines the -> Phase 0 dylink/PIC loader with the Phase 2 UCEB1 context membrane. The spike -> builds a core workspace reactor and a separate generated-shape `.uce.cpp` PIC -> side module, parses `dylink.0`, allocates memory/table bases, resolves -> `env.*`/`GOT.*` imports, runs relocations/constructors, calls -> `__uce_set_current_request` and `__uce_render`, and reads output from -> core-owned memory. The fixture now exercises nonzero table placement, -> `GOT.func`, deferred self-resolved `GOT.mem`, and generated-style -> `html_escape(...)` expression output. It passed on k-uce with `PHASE3 EXIT -> CRITERION: PASS`. -> -> The self-GOT fixture caught a real loader bug (2026-06-12, fixed): deferred -> `GOT.mem` entries were patched with the unit's exported symbol values -> verbatim, but a PIC module's data exports are offsets relative to its -> `__memory_base` — the loader must add the base. The bug rendered silently -> wrong values and wrote into core memory at low addresses; both spike loaders -> are fixed and the Phase 3 exit gate now asserts every GOT-derived output -> marker (`self-got`/`callback`/`each`/`map`) so a regression cannot pass. -> Details in the `docs/wasm-toolchain-findings.md` erratum. -> -> **Production Phase 3 work plan** (ordered by dependency/risk; spike-proven -> mechanics not repeated here): -> -> 1. **Compile the real `uce_lib` as `core.wasm`** — the last big unknown; all -> spikes used a hand-stubbed `Request`. Forces: `types.h` allocator gate as -> a real `#ifdef` (replacing the spike's copied-header text patch), `sys.h` -> signal/fork/socket carve-outs, the libc++ closure strategy -> (`--whole-archive` vs keep-list), and splitting connectors out of the core -> (the MySQL client library cannot compile to wasm). Gates items 2–6. -> 2. **WASI decision (record in §11 when made)** — spikes stub all WASI imports -> with traps; real pages call `time()` (7× in uce-starter), which wasi-libc -> routes to `clock_time_get`. Recommended: zero-WASI core — route -> time/random/env through the §5.1 hostcalls so there is exactly one -> membrane to audit. -> 3. **Generator changes** (small, parallelizable): emit a logical include -> instead of the absolute `uce_lib.h` path; add the PIC side-module build to -> the compile-on-miss artifact cache. -> 4. **Productionize the loader into `src/`** (§6): `uce.abi` stamping, -> import-discipline verification, a name→funcptr registry in the core -> replacing the spike's per-symbol `core_table_index_of_*` helpers, an -> explicit export name-collision policy (the spike silently prefers core -> exports over unit definitions, e.g. `context`), multi-unit bump placement, -> retained unaligned allocation pointers for unload, hardened/fuzzed binary -> parsing. Plain memcpy workspace birth; CoW stays Phase 4. -> 5. **Starter-scoped hostcalls (~12, not the full ≤60)**: `respond` / -> `stream_write`, `time`/`random`/`env`, `session_get`/`set`, -> `http_request` (OAuth callback), `component_resolve`. The starter uses no -> sqlite/mysql/file APIs — connectors can wait for Phase 5 parity. -> 6. **`component_resolve` + path dispatch + lazy loading** — the only §6 step -> with zero spike coverage (all spikes load one unit eagerly). The starter's -> 71 `component()` calls across 47 files are the stress test; worth a -> focused spike before worker integration. -> 7. **FastCGI worker integration** — config-selectable backend, §7 lifecycle -> wired into the `linux_fastcgi.cpp` flow. -> 8. **Starter parity tests — DONE (2026-06-12; ported to UCE CLI 2026-06-13).** -> `site/tests/cli_runner.uce` renders every starter view with title + -> error-marker assertions and checks the app-shell 404; together with the -> starter HTTP smoke cases, the starter subset is 14 cases, green against -> the native backend. The assertions are backend-agnostic (rendered content -> only), so the identical bar gates the wasm worker. - -**Phase 4 — production mechanics.** -Core snapshot + CoW birth; bump-allocator flag; epoch/memory limits; trap → -error-page path with guest stack traces (this supersedes the -signal/longjmp machinery and closes RECOMMENDATIONS.md 1.5 structurally); -handle-table cleanup (closes 1.7/5.1); artifact/ABI versioning end-to-end. -Exit: kill-tests (deliberate out-of-bounds page, stack-exhaustion page, -infinite-loop page, OOM page) produce clean error pages and an unharmed -worker. (A literal null-deref page is not in the list: address 0 is valid -wasm linear memory, so it does not trap — see §10.) - -> **Status: KILL-TEST SPIKE PASS (2026-06-12).** `spikes/wasm-phase4/` -> validates the production mechanics that can be proven before the real wasm -> worker exists: reusable compiled artifacts as a core-snapshot proxy, fresh -> per-request stores as workspace birth/drop, **both** CPU-limit mechanisms -> (fuel and epoch interruption — production default is epoch per the Phase 0 -> findings; Wasmtime reports epoch traps as `interrupt`), a store memory -> limiter proven load-bearing (the OOM fixture grows within its own declared -> max so only the limiter can deny it), trap capture with the exit gate -> asserting a wasm backtrace and the expected cause per kill, handle closers -> checked to run exactly once and while the store is alive, and — after all -> six kills — a healthy request served by the same engine (the "unharmed -> worker" half of the exit criterion). Kill fixtures are genuinely trapping -> faults: unreachable, OOB access, stack exhaustion, fuel/epoch-limited -> infinite loops, limiter-denied growth. A literal null deref is deliberately -> absent (does not trap in wasm; risk recorded in §10). Passed on k-uce with -> `PHASE4 EXIT CRITERION: PASS`. The trap-trace summarizer now exists as -> `src/lib/wasm_trace.h` (collapses repeated frames, demangles symbols, -> splits cause/detail) and is double-gated: live traps in the spike runner, -> canned-message checks in `site/tests/core.uce` via the native suite. Not -> yet production: OS-level CoW core snapshots, wiring trace summaries into -> the error-page UI, the unit name-section policy for readable frames, -> `linux_fastcgi.cpp` wiring, real connector handle cleanup, and artifact/ABI -> versioning. - -**Phase 5 — parity & performance.** -Full network suite green on the wasm worker; differential native-vs-wasm runs -on the site tree; audit `site/` for cross-request-static reliance (§3.2 -breaking change); benchmark suite (template-heavy page, sqlite page, -component-heavy starter page) with budgets: ≤2× native page latency, -workspace birth ≤100µs, internal component call overhead within 10× native -call cost. Exit: numbers published in this document, all tests and reviews pass. - -> **Status: HARNESS BASELINE PASS (2026-06-12).** `spikes/wasm-phase5/` -> now automates the Phase 5 parity/performance gate shape before the production -> wasm worker exists. On k-uce it ran the full native network suite (`83/83`), -> the starter-focused parity subset (`14/14`), a heuristic code-focused `site/` -> cross-request/static-state audit, and a warmed native benchmark baseline for -> the template-heavy doc page, sqlite page, and component-heavy starter page. -> The harness now gates case counts (`network >= 80`, `starter >= 10`) so broken -> filters cannot pass vacuously, and it runs a throwaway warmup suite before the -> measured gate to absorb cold unit-cache timeouts. Informational native medians -> from the 2026-06-12 baseline are: template-heavy doc `313.8 ms`, sqlite page -> `3.4 ms`, starter dashboard `41.1 ms`. A durable snapshot lives in -> `docs/wasm-baselines/native-baseline-2026-06-12.md`; paired wasm/native -> gate runs still recompute the native medians for the actual ≤2× comparison. -> True Phase 5 completion still requires passing the same harness against a real -> wasm worker URL and adding worker-internal probes for workspace birth and -> component-call overhead budgets. - -**Phase 6 — removed / not planned.** -There is no planned second component plane for interpreted or runtime-carrying -languages. Future work after Phase 5 should continue productionizing the single -workspace-peer WASM backend unless Udo explicitly approves a separate isolated -component feature with a new API name and copied-data semantics. - -The native `.so` backend remains in-tree (as a reference) and selectable by config -but we switch over to the wasm backend as soon as it's available and test -only on that; both backends share the Phase 1 C ABI. - -**Cutover & cleanup checklist (gated, in order — no step before the gate -above it is green).** - -1. **Build** — production Phase 3/4 work plan items 1–7: real `uce_lib` - `core.wasm`, WASI decision recorded, generator changes, production loader - in `src/`, starter-scoped hostcalls, `component_resolve` + lazy dispatch, - wasm worker backend selectable by config in `linux_fastcgi.cpp`. -2. **Prove** — the Phase 5 harness against the wasm worker URL: full network - suite green (≥ 80 cases), starter parity (≥ 10), benchmarks within the - ≤2× paired-run budget; the Phase 4 kill-tests reproduced against the real - worker (OOB page, stack-exhaustion page, infinite loop, OOM) rendering - clean error pages from `wasm_trace.h` summaries. -3. **Switch** — config default flips to the wasm backend; the native backend - stays in-tree as reference per the paragraph above (archival is a separate, - later decision, not part of cutover). -4. **Clean up** — only after 3, and gates must be re-homed before their - spikes are deleted: the Phase 5 harness moves from `spikes/` into `tests/`, - the Phase 4 kill cases become worker integration tests, and the Phase 0 - FINDINGS/erratum content folds into `docs/` — then `spikes/wasm-phase*` - can go. Native-only machinery that cutover obsoletes is retired in the - same pass: the SIGSEGV `sigsetjmp`/`siglongjmp` recovery, the tracking - `operator new` in `types.h`, the per-connector `cleanup_*_connections()` - pattern (§3.1/§3.2 supersede all three). - -Status against this checklist (2026-06-12): W1 has landed enough `src/` / -`scripts/` integration to build and smoke-test `core.wasm`; the remaining gate -1 work is W2–W4 plus the residual W1 wasi-libc import closure noted below. The -native backend is still the only server backend and serves the entire green -suite. The single cleanup item safe today is `src/lib/_scratchpad.cpp` (the -failed arena experiment, unreferenced by any build since 2022; §1 cites it as -history only). - -### 9.1 Production build plan — the worker (W-phases) - -The spike sequence above is closed; every architectural risk it could retire -is retired. The W-phases build the production worker. Ground rules: the -`.uce → C++` preprocessor does not change; every phase lands production code -in `src/`/`scripts/` gated by the existing network suite; no further -throwaway scaffolding. Dependency order is W1 → W3 → W4 → W5 → W6, with W2 -parallelizable once W1's shim headers and ABI stamp exist. - -**W1 — the core module, for real.** -Carve `uce_lib` so it compiles as `core.wasm` with the Phase 0 recipe: -gate the global allocator in `types.h` behind an `#ifdef` (core owns it, -units import it — replacing the spike's copied-header text patch); `#ifdef -__wasm__` carve-outs in `sys.h` (signals/fork/exec/sockets move behind -hostcalls); split the connectors — `mysql-connector`/`sqlite-connector` -keep their `.uce`-visible signatures but forward to membrane hostcalls in -the wasm build while native implementations stay host-side. Zero-WASI core -(record the decision in §11): time/random/env are `uce_host_*` hostcalls, -no `wasi_snapshot_preview1` imports. The core exports the full DValue C ABI -(§5.2), a name→funcptr symbol registry for `GOT.func` (replacing per-symbol -helpers), `uce_alloc`/`uce_free`, and output plumbing. -`scripts/build_core_wasm.sh` joins the normal build. -Exit: `core.wasm` builds reproducibly from the real `uce_lib`; a small -smoke driver instantiates it, runs `_initialize`, and exercises `uce_dv_*`; -the native build and suite remain untouched and green. - -> **Status: DONE (2026-06-12).** `scripts/build_core_wasm.sh` builds -> `src/wasm/core.cpp` into `core.wasm` from the real `uce_lib` carve-out. The -> W1 carve-out keeps native builds unchanged while `__UCE_WASM_CORE__` removes -> native-only compiler/connector code from the core, provides WASM stubs for -> process/socket/task/file surfaces, gates generated-unit allocator definitions -> behind `__UCE_WASM_UNIT__`, and leaves the workspace-owned DValue C ABI in the -> core. `scripts/wasm/build_w1_smoke.sh` builds `src/wasm/w1_smoke.cpp`; the -> smoke driver instantiates `core.wasm`, runs `_initialize`, initializes the UCE -> request context, exercises `uce_dv_root/get/find/set_value/value/count/is_list` -> plus UCEB1 encode/decode, verifies output plumbing, and passes with -> `W1 EXIT CRITERION: PASS`. Native validation after the W1 carve-out: rebuilt -> and restarted `uce.service`; warm full network suite passed `83/83`. Remaining -> W1 follow-up before W3: close the residual wasi-libc/libc++ -> `wasi_snapshot_preview1.*` imports in the produced binary; UCE's own time/env -> calls are routed through `uce_host_*`, but the smoke driver still supplies -> trap stubs for unused libc WASI imports. - -**W2 — the compiler outputs wasm units.** -Preprocessor output unchanged. The unit compile path in `compiler.cpp` -gains the wasm target beside the `.so` target: `clang ---target=wasm32-wasip1 -fPIC` + `wasm-ld -shared` (Phase 0 unit recipe), -logical `uce_lib.h` include instead of the absolute path, a `uce.abi` -custom-section stamp (ABI version + toolchain id), and per-unit `.wasm` -artifacts in the same cache with the same invalidation as `.so`. -Exit: a batch compile of every unit the suite touches produces valid PIC -modules — `dylink.0` present, `uce.abi` stamped, no allocator definitions, -import shapes verified by a check tool — and compile-on-miss works for the -wasm target. - -> **Status: DONE (2026-06-12).** Generated units now include the logical -> `#include "uce_lib.h"`; native `scripts/compile` supplies `-Isrc/lib`, and -> `scripts/compile_wasm_unit` builds PIC side modules with `__UCE_WASM_UNIT__`, -> `wasm-ld -shared --experimental-pic`, and an `llvm-objcopy`-inserted -> `uce.abi` custom section. `scripts/wasm/check_unit_wasm.py` validates wasm v1 -> structure, `dylink.0` mem_info, `uce.abi` ABI/toolchain stamp, import policy, -> required PIC imports, and absence of allocator definitions. The compiler can -> optionally build the wasm artifact beside the native `.so` via -> `COMPILE_WASM_UNITS=1`; native remains default. Batch validation built/checked -> all 128 known/generated suite units, and a temporary `unit_compile()` -> compile-on-miss page produced and verified a fresh `.wasm`. Reused-artifact -> batch validation was tightened after the first slow full rebuild: unchanged -> units are rechecked instead of rebuilt, reducing a 128-unit no-op pass from -> several minutes to about 5 seconds. `scripts/compile_wasm_unit` now also uses -> a keyed Clang PCH for the stable `uce_lib.h` unit header by default -> (`UCE_WASM_UNIT_PCH=0` disables it); the key includes ABI version, clang -> version, common compile flags, and `src/lib/*.h` content. Spot checks: warm -> `hello.uce` wasm compile dropped from about 2.3s to 0.65s, `core.uce` from -> about 5.7s to 4.1s, and a full 128-unit rebuild with PCH took about 163s. -> Native validation stayed green (`83/83`). - -**W3 — workspace runtime + membrane (the worker core).** -Productionize the loader into `src/wasm/` per §6, from the Phase 3 spike -plus everything it deferred: symbol registry, dispatch map, ABI stamp -verification, import discipline (reject units defining the allocator), -the `__memory_base`-relative data-export rule, multi-unit placement, -hardened binary parsing, an explicit export name-collision policy. -Workspace lifecycle: core snapshot born by memcpy (CoW is W5), dropped per -request; host handle table with closers. Starter-scoped hostcall set -(~12, the §5.1 subset): `ctx_read`, `respond`, `stream_write`, `log`, -`time`, `random`, `env`, `session_get`/`set`, `http_request`, -`component_resolve`, `last_error`. -Exit: a real request — UCEB1 context in, render, response out — served -end-to-end through a workspace running the real core and real generated -units, driven by a CLI test driver; epoch CPU limit and memory limiter -active; traps produce `wasm_trace.h` summaries. - -> **Status: DONE (2026-06-13).** `src/wasm/worker.cpp` is the production -> workspace runtime (wasmtime.hh; per-request store, hardened dylink/uce.abi -> parsing, import discipline, GOT.func via host funcref placement, the -> `__memory_base` data-export rule, core-first/first-unit-wins symbol -> registry, per-worker compiled-module cache) and `src/wasm/w3_driver.cpp` -> is the CLI gate. Membrane so far: time/time_precise/env/random/log, -> `component_resolve` (lazy mid-request loading), and a policy-gated -> read-only file membrane (`file_exists`/`file_read`, current-unit-relative, -> site-tree-contained). The exit gate passed on k-uce: `/demo/hello.uce` and -> `/demo/components.uce` render **byte-identical to native** (incl. nested -> and named `COMPONENT:X` handlers through `ob_*` capture); starter -> dashboard/gauges/features/workspace/page1 all render `200` with ~12 units -> lazily loaded mid-request (≈700 ms first request — dominated by per-request -> Wasmtime module compilation, the W5 AOT/snapshot target — ≈2 ms warm); -> epoch kill mid-render traps as `interrupt` with a symbolicated -> `wasm_trace` summary and a clean workspace drop. Carve findings recorded -> on the way: header function templates must be `inline` (self-import rule, -> now commented in `types.h`/`functionlib.h`), the core needs vague-linkage -> and libc link anchors (`uce_wasm_link_anchors()` + -> `core_libc_exports.syms`), units build `-fno-rtti`/`-fno-exceptions` to -> match the core ABI, and connector classes have explicit fail-clean stubs -> until the W5 hostcall connectors. Deferred to W4 as planned: `respond`/ -> `stream_write`/`session`/`http_request` hostcalls (response metadata -> currently returns as UCEB1 via `uce_wasm_response_meta`), kill-pages, and -> the FastCGI backend; `error-reporting.uce` and `tests/zip.uce` are -> native-only pending the trap error path and a zip hostcall. - -**W4 — the FastCGI worker.** -Wire W3 into `linux_fastcgi.cpp` as a config-selectable backend (native -stays default until W5): the §7 lifecycle, lazy mid-request -`component_resolve` → load → `call_indirect`, path dispatch, trap → -configured UCE error pages with collapsed guest traces, handle-table -cleanup on workspace drop. -Exit: the server runs with the wasm backend on a test config; the starter -subset of the runtime smoke suite passes against the wasm worker — the Phase -5 harness wasm leg lights up for the first time; the four kill-tests exist as -real `.uce` pages and produce clean error pages from an unharmed worker. - -> **Status: DONE (2026-06-13).** `src/wasm/backend.cpp` wires the W3 runtime -> into `src/linux_fastcgi.cpp` as a config-selectable page-render backend -> (`WASM_BACKEND_ENABLED`, default off — native stays default until W5). The -> seam is one branch in `handle_complete`: per forked worker, a lazily-built -> `WasmWorker` + epoch ticker thread; per request the native `Request` -> params/get/post/cookies/session are encoded to the UCEB1 context, served -> through a fresh workspace, and the response (status/headers/cookies/session -> /body) is written back onto the native `Request` so the existing transport -> emits it unchanged. CLI/serve_http/websocket stay native; units with no -> wasm artifact fall through to native automatically. `ONCE()` is now honored -> in the workspace (host resolves `__uce_once`, core dedups on the resolved -> path via `once_units`; the entry renders through `uce_wasm_render_entry` so -> it shares the component dispatch + ONCE path). -> -> **Exit gate passed on k-uce, through the real nginx → fastcgi → wasm path on -> port 80:** the starter smoke/parity subset is **14/14** against the wasm -> backend (incl. the two ONCE-asset-in-`
` cases); three real kill -> pages under `site/tests/wasm-kill/` (OOB write, runaway loop, unbounded -> recursion) each return a clean error page carrying a demangled -> `wasm_trace` summary (`out of bounds memory access` / epoch `interrupt` -> with `wasm_kill_recurse(unsigned long long)` framing), and the worker keeps -> serving `200`s through a barrage of kills — no native signal, the trap is a -> returned error at the membrane. CPU budget is enforced by epoch -> (`WASM_EPOCH_DEADLINE_TICKS` × `WASM_EPOCH_PERIOD_MS`); memory by the store -> limiter. Native default restored after the gate: full suite **83/83**. -> Robustness folded in along the way: the ctype libc family added to the core -> export anchors (`core_libc_exports.syms`), and a page with no `RENDER` -> renders empty-200 (native parity). -> -> **W5 surface, made concrete** (23 full-suite pages still 500 on the wasm -> backend, all by design): the regex/xml/yaml core stubs to un-stub or move -> behind hostcalls; markdown/zip/tasks/sqlite/file-write membrane hostcalls; -> the `unit_call` bridge; and compiler-introspection pages (`unit-info`, -> `unit-browser`, `sharedunit`) which likely stay native. These are the W5 -> parity workload, not regressions. - -**W5 — parity, performance, cutover.** -Full network suite green on the wasm backend; §3.2 statics-audit findings -(50 code candidates) fixed as differential native-vs-wasm runs surface -them; CoW snapshot birth and placement memoization as needed to meet the -budgets (≤2× page latency against the pinned baseline shape, workspace -birth ≤100µs, component-call overhead ≤10×) with worker-internal probes -for the latter two. -Exit: cutover checklist gates 2–3 — harness fully green against the wasm -worker URL, config default flips to wasm, native stays in-tree as -reference. - -> **Status: DONE (2026-06-13).** W5 flips the page-render default to the WASM -> backend while retaining explicit native fallback for the surfaces still -> genuinely host-owned or not yet membraned: zip, sockets/custom servers, -> memcache/mysql, and `unit_call`/compiler-introspection (the last need the -> native toolchain). The fallback is selected before workspace creation by -> scanning the entry source for a small native-only token set, so unsupported -> pages neither fail through wasm nor hide as trap regressions. -> -> **Promoted onto wasm during W5** (no longer fallback): **regex** runs through -> a single UCEB1-marshalled `uce_host_regex` hostcall against the host's PCRE2; -> **markdown** is compiled into the core (pure compute); **xml/yaml** were -> already in-core; **filesystem writes** (`file_put_contents`/`file_append`/ -> `file_unlink`) go through policy-gated hostcalls, and the read membrane was -> widened to the same allowlist (site tree + scratch roots: `/tmp`, -> `BIN_DIRECTORY`, `SESSION_PATH`, `TMP_UPLOAD_PATH`) so a page can read back -> what it writes (e.g. the `io.uce` /tmp round-trip); **sqlite** runs through a -> `uce_host_sqlite` hostcall against the host's real connector, with -> connections held in a per-workspace handle table that is closed on workspace -> drop — the first concrete realization of the §3.1 "handle-table drop = -> resource cleanup" model; **background tasks** now use a host-managed process -> membrane (`uce_host_task_spawn`/`pid`/`kill`) with a guest callback trampoline -> (`uce_wasm_task_run`) in the forked child, so `task()`, `task_repeat()`, -> `task_pid()`, and `task_kill()` run under the wasm backend; **sleep/usleep** -> use a host sleep call that renews the epoch deadline after blocking. The -> `/doc/*` blanket fallback is gone — doc pages render on wasm. -> -> One subtle bug fixed here: the sized-hostcall convention has the guest call -> twice (buf=0 to learn the length, then to fetch). That re-executed the op — -> harmless for idempotent regex/file-read, but for sqlite it ran every -> `INSERT`/`CREATE` twice and the second pass failed on its own side effects. -> The host now stages the encoded result on the first call (keyed on the exact -> input) and replays it on the fetch, so a side-effecting op runs once. Two wasm-stack/ABI issues surfaced and were fixed: the core stack was -> raised to 8 MB so recursive `ucb_decode_node` reaches the 1024 depth limit -> like native (the core.uce depth-bomb negative test overflowed the small -> default stack), and `wasm_trace`'s demangler is host-only (wasi-libc++ has no -> `__cxa_demangle`, so a unit must not import it). Two backend robustness bugs -> fixed: a failed wasm side-compile (e.g. try/catch units) is now non-fatal — -> the native `.so` still serves and the stale `.wasm` is removed — and -> `wasm_artifact_exists` now requires the artifact be newer than its source, so -> a source edit can't be served from a stale `.wasm` (the wasm path bypasses the -> native JIT recompile). -> -> **Signal-trap conflict resolved (this was open in the prior W5 draft):** a -> guest `unreachable`/OOB used to surface as a host signal that the native -> SIGILL/SIGSEGV handler caught and `abort()`'d the worker (502). `make_engine` -> now sets `signals_based_traps(false)`, so guest traps are explicit and stay -> pure wasm traps returned as errors. The `__builtin_trap()` kill page now -> returns a clean 500 and the worker survives a kill barrage (0 PID churn); it -> is back in the kill-test gate alongside loop/recurse. -> -> `scripts/wasm/run_w5.sh` is the cutover gate: measure native, switch -> `/etc/uce/settings.cfg` to wasm, run the wasm full suite + kill tests + -> starter subset + the native-vs-wasm benchmark, optionally leave the backend -> enabled (`UCE_W5_KEEP_BACKEND=1`). Final k-uce gate: native reference suite -> **83/83**; wasm/default full suite **83/83** warm (starter, docs, core tests, -> markdown, regex, sqlite, and tasks on wasm; zip and native-only service/ -> compiler surfaces routed to native fallback); starter subset **14/14**; kill pages (trap/loop/recurse) clean with the worker -> unharmed. Benchmark medians passed the ≤2× budget: doc singlepage native -> ~324ms vs wasm ~318ms, sqlite 3.7ms vs 3.8ms, starter dashboard 44ms vs wasm -> ~6ms. Worker-internal probe headers (`X-UCE-Backend`, -> `X-UCE-Wasm-Workspace-Birth-Us`, component resolve count/total/avg) are emitted -> for wasm responses when `WASM_BACKEND_VERBOSE=1`; warm workspace birth is -> ~300–380µs on k-uce (above the aspirational 100µs CoW target — that and -> placement memoization are the remaining perf items — but page latency meets the -> cutover budget). Live `/etc/uce/settings.cfg` left with `WASM_BACKEND_ENABLED=1`; -> native remains in-tree and config-selectable. -> -> **Remaining native fallback** (the only pages still routed to native): **zip** -> (its `.uce` source uses try/catch, so it can't be a `-fno-exceptions` wasm -> side module regardless of a hostcall), socket/custom-server/memcache/mysql -> service surfaces, and `unit_call` + compiler/unit-introspection -> (`unit-info`/`unit-browser`/`sharedunit` — need the native toolchain; -> candidates for host-serviced hostcalls if native is to be fully retired). -> **Still deferred, not blocking:** CoW snapshot birth + placement memoization -> for the workspace-birth budget, and the §3.2 statics-audit follow-through. - -**W6 — cleanup.** -Cutover checklist gate 4, verbatim: re-home the spike-hosted gates -(Phase 5 harness → `tests/`, kill cases → worker tests, FINDINGS/erratum → -`docs/`), delete `spikes/wasm-phase*`, retire the SIGSEGV recovery, the -tracking `operator new`, and `cleanup_*_connections()`. - -> **Status: SPIKE CLEANUP DONE; native-machinery retirement GATED -> (2026-06-13).** Re-homing complete: the Phase 5 benchmark + site-audit are -> now `tests/wasm_benchmark.py` / `tests/wasm_site_audit.py` (with -> `scripts/wasm/run_w5.sh` repointed), the kill-test gate lives in the UCE CLI -> runner at `site/tests/cli_runner.uce`, the Phase 0 toolchain findings + GOT -> erratum are `docs/wasm-toolchain-findings.md`, and the pinned native -> baselines are `docs/wasm-baselines/`. **All `spikes/wasm-phase*` deleted** -> (prototypes superseded by `src/wasm/`), and the dead 2022 -> `src/lib/_scratchpad.cpp` arena experiment is removed. Build + full suite -> stay green (83/83). The phase-0–4 status blocks above keep their narrative; -> their `spikes/...` paths are now historical — the artifacts that mattered -> were re-homed, the rest was throwaway scaffolding by design. -> -> **The three native-machinery retirements remain BLOCKED, deliberately:** -> native is still the live backend for pages touching zip, sockets / custom -> HTTP servers, memcache, mysql, `unit_call`, and compiler/unit-introspection -> — everything still in `native_only_tokens`. While *any* page renders on -> native, retiring its SIGSEGV/`sigsetjmp` recovery, the tracking -> `operator new`, or `cleanup_*_connections()` would strip crash protection -> and resource cleanup from live traffic. They retire only once those surfaces -> move behind membrane hostcalls (so nothing falls back) or the native backend -> is formally decommissioned. This is the honest end-state of W6 given the -> current fallback set, not an oversight. - -**W7 — non-render entrypoints and fallback hardening.** -With render traffic defaulting to wasm, the next retirement work is the entry -surface around render: CLI, WebSocket, and `SERVE_HTTP`/custom-server dispatch, -plus the cold fallback behaviors that now show up in the default gate. The first -W7 slice split the wasm backend into a proper object/header boundary and taught -`wasm_backend_serve()` to dispatch CLI/WebSocket/SERVE_HTTP entry kinds; the -socket CLI path now propagates wasm runtime errors instead of silently returning -success. Post-fork wasm reset was also tightened: the epoch ticker is now held -behind a pointer so a forked broker child can discard the inherited, unusable -thread handle without placement-new over a live `std::thread` object. The -network gate now gives only the known cold native-fallback pages (`sharedunit` -and the operational services test) longer per-request timeouts while preserving -strict status/body checks, and the services test drains split custom-server -response packets before deciding that the named handler failed. - -> **Status: W7a/W7b/W7c/W7d DONE (2026-06-14).** Handler invocation is now -> unified: CLI / WebSocket / serve_http / page render all route through one -> `serve_via_wasm(entry_unit, handler)` path, the handler string mapping to a -> `__uce_