⏺ Tier 2 gate spike: PASSED — proven on workerd (real Workers #26
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "wasm-t2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
runtime, V8 isolate, HTTP)
All four unknowns retired:
compiles and runs; the I/O-free run_query path never
touches the stdout/argv/exit stubs.
count(1000000) (a 1,000,000-deep call/1 recursion) returns
{"count":1} inside a V8 isolate. return_call confirmed
present (43) in the unknown-unknown object; V8 honors it →
constant stack. This is Checkpoint 0 on the actual Workers
engine, not just wasmtime.
Machine) + plg_rt_alloc/plg_rt_run_query/plg_rt_free over
linear memory, driven by a ~30-line JS Worker. Query in →
JSON out, byte-identical to native (dep(app, X) and the
existence_error path both match).
Workers budget (compresses well under the limit).
Two findings worth recording
bug in my buffer ABI, not anything architectural — freeing
not a real gap.
the same program runs unchanged on Tier 1 (wasi/wasmtime)
and Tier 2 (unknown-unknown/V8) — only the entry (no main,
an exported plg_init + buffer ABI) and the link (--no-entry
--export, no libc) differ.
State / honest notes
pub mod reactor;). It's the throwaway buffer-ABI; it builds
clean, is dead-stripped from native binaries, and the full
runtime suite (123 tests) + clippy are green with it
present. It's the natural seed for productization (the real
I/O-free-core extraction would replace its duplicated JSON
formatting) — or trivially revertable if you'd rather
start clean. Everything else (the IR awk transform, the
workerd config/worker.js) is throwaway in /tmp/t2.
feature-gated reactor archive, no Worker template/wrangler,
no docs, no write/1 capture. Those wait until you say
continue.
Productization, when you want it, is now "engineering, no
existential unknowns": a Target::WasmReactor codegen arm
emitting plg_init instead of main, a second feature-gated
archive, the reactor link path in link.rs, the
I/O-free-core extraction (shared with INVOCATION's resident
mode), a Worker template, and docs.
runtime, V8 isolate, HTTP) All four unknowns retired: 1. Runtime functions on wasm32-unknown-unknown (no WASI) — compiles and runs; the I/O-free run_query path never touches the stdout/argv/exit stubs. 2. musttail → return_call lowers and runs on V8 at depth — count(1000000) (a 1,000,000-deep call/1 recursion) returns {"count":1} inside a V8 isolate. return_call confirmed present (43) in the unknown-unknown object; V8 honors it → constant stack. This is Checkpoint 0 on the actual Workers engine, not just wasmtime. 3. Buffer ABI round-trips — plg_init (hands over the Machine) + plg_rt_alloc/plg_rt_run_query/plg_rt_free over linear memory, driven by a ~30-line JS Worker. Query in → JSON out, byte-identical to native (dep(app, X) and the existence_error path both match). 4. Module size: 1.65 MB raw — comfortably within the Workers budget (compresses well under the limit). Two findings worth recording - The hard part was a Vec::with_capacity over-allocation bug in my buffer ABI, not anything architectural — freeing not a real gap. - WASI-vs-isolate is a real shape difference, as scoped: the same program runs unchanged on Tier 1 (wasi/wasmtime) and Tier 2 (unknown-unknown/V8) — only the entry (no main, an exported plg_init + buffer ABI) and the link (--no-entry --export, no libc) differ. State / honest notes - One in-tree artifact: crates/runtime/src/reactor.rs (+ pub mod reactor;). It's the throwaway buffer-ABI; it builds clean, is dead-stripped from native binaries, and the full runtime suite (123 tests) + clippy are green with it present. It's the natural seed for productization (the real I/O-free-core extraction would replace its duplicated JSON formatting) — or trivially revertable if you'd rather start clean. Everything else (the IR awk transform, the workerd config/worker.js) is throwaway in /tmp/t2. - Not productized, as agreed: no codegen target arm, no feature-gated reactor archive, no Worker template/wrangler, no docs, no write/1 capture. Those wait until you say continue. Productization, when you want it, is now "engineering, no existential unknowns": a Target::WasmReactor codegen arm emitting plg_init instead of main, a second feature-gated archive, the reactor link path in link.rs, the I/O-free-core extraction (shared with INVOCATION's resident mode), a Worker template, and docs.Review — Tier 2 gate spike (workerd / V8 / wasm32-unknown-unknown)
Right scope, right discipline. The gate is "does this approach actually work end-to-end on the real target?" and the answer (constant-stack to a million-deep on V8, buffer ABI round-trips byte-identical to native, module fits the budget) is the only answer that lets productization proceed without speculative architecture work. The PR description's "engineering, no existential unknowns" line is the correct framing — that's what a spike is for.
What the gate actually proved, restated for the reader who's looking at the disposition later:
run_querypath never touches stdout/argv/exit stubs, so the no-stdlib environment isn't a blocker. The staticMACHINEslot + reactor entry points show how: hand the host an ABI it can drive over linear memory and never touchlibc.musttaillowers toreturn_callon V8 at depth. This is the load-bearing finding for the whole Tier 2 plan. wasmtime (Tier 1) honoring tail-calls didn't guarantee V8 would; you'd have had to retreat to CPS-style trampolining in the runtime if it hadn't. It does, so the IR investment from PRs #20/#24 carries forward to Workers without rework.alloc / run_query / freeover a packed(len << 32) | ptrreturn is a known-good pattern; that it round-trips byte-identical to native including theexistence_errorpath is the gate's strongest evidence — it means the JSON format and the error rendering path don't need parallel implementations.The spike itself is small, contained, and the engineering inside it is honest. Below: things worth flagging as gate findings (for the productization disposition to react to) and one footgun to record before the file gets repurposed.
Findings worth recording for productization
1. The
Vec::with_capacity→Layout-exact allocator switch is a real ABI rule. Documented inraw_alloc's comment. Worth lifting that observation to the productization plan: any host-visible buffer that the host frees by(ptr, len)must bealloc::alloc(Layout::from_size_align(len, 1)), notVec::with_capacity(len). The next contributor implementing the productized reactor will reach forVecreflexively. The current comment ("Vec::with_capacity may over-allocate") tells what but not why — adding "the host frees by requested-length, so actual-capacity > requested-length corrupts the allocator" would prevent the recurrence.2.
MACHINEas astatic AtomicPtris single-tenant, single-in-flight per isolate. V8 isolates aren't multithreaded, but a Worker can have multiple in-flight async tasks sharing one isolate. For the gate (one query, JS awaits the result) this is fine. For productization either (a) document "one in-flight query per isolate" as the contract, or (b) take per-request state out ofMACHINEand pass it through the buffer ABI. The contract route is much simpler and matches how most Workers use isolates; worth recording the choice now while the rationale is fresh.3.
reset()is the inverse ofMachine::new. It clears each per-query field by name. This works for the spike but it knows the Machine's field set — a future field added without reset coverage gives the next query state-leak symptoms that won't show up in single-query smoke tests. AMachine::reset_per_query()method living next to the field declarations would make "did I forget to clear this?" a local question. Worth noting now because productization will reuse this exact reset, and the leak class is the kind of bug that only surfaces under sustained traffic.4. The hard-coded
"exhausted":trueis correct for the spike (no--limit, sosolvealways runs to completion). Productization needs to take limit from the request and compute exhausted exactly likeentry.rs:225:args.limit.is_none_or(|l| count < l). Worth pinning this in a productization note — it's the kind of detail that gets cargo-culted from the spike and then doesn't get revisited.5. The 1 GB step limit override is documented as spike-only ("productization passes it alongside the query"). Productization needs both per-request step and metacall-depth limits over the buffer ABI; mirror the
PLG_MAX_STEPS/PLG_METACALL_DEPTHprecedent. The metacall depth one matters because PR #25's doc note acknowledged a wasm engine's ~1 MB default stack is smaller than native's ~8 MB.6. JSON formatting duplication is acknowledged. The PR description names this: "productization extracts a single I/O-free core shared by the WASI shell and this one." Right call to not extract during the spike — gate-vs-design separation. When extraction happens, the shared core wants three knobs: (a) limit-aware exhausted computation, (b) error JSON shape, (c) success JSON shape.
entry.rs:113–138andreactor.rs:104–122are very nearly the same code with different output sinks; anio::Writeparameter would be enough.Honest concerns inside the spike
7.
plg_rt_run_queryreturns(len << 32) | ptrassuming wasm32. Correct on the actual target; would break on wasm64. Worth one line in the function doc — not a code change for the spike, but a record for the productization plan since wasm64 is an emerging target and the packed-u64 ABI would need rethinking there.8.
MACHINEis never freed. Long-running isolates accumulate oneMachineper cold start, which is what you want — but a productization plan that supports updating the embedded program inside a live isolate would need a teardown entry point. Spike-acceptable; flag for productization.9. The
unsafe(no_mangle) pub extern "C"signature pattern is correctly used throughout. Each entry has a# Safetydoc exceptplg_rt_alloc, which is genuinely safe (returns a pointer; the host's subsequent writes are its unsafety, not ours). The consistency is good — readers can trust that "no Safety doc = safe".10.
raw_allocreturnsNonNull::dangling().as_ptr()forlen == 0. Correct behavior (alloc with size 0 is UB in Rust's allocator API), and the host'splg_rt_freecorrectly no-ops onlen == 0to match. The two halves agree, but they're load-bearing-by-convention rather than load-bearing-by-API. One line onplg_rt_free("len == 0must match alen == 0allocation; the dangling sentinel fromraw_alloc(0)is implicit") would close the convention.Disposition recommendations
reactor.rsin-tree. It's small (133 lines), clippy-clean, dead-stripped from native, and forms the natural seed for productization. Reverting and re-deriving from history would lose the "Vec::with_capacity" lesson that's now baked into the comments. The PR description's "trivially revertable" is honest, but the file pays its own freight as a reference.docs/design/WASM_TIER_2.mdif one doesn't exist, or appended toWASM.mdif it does. Findings 1–6 above are exactly the kind of "I forgot why we did it this way" knowledge that disappears between the spike and the productization PR.What's good
MACHINEasRelaxedatomic is correct for single-threaded wasm and explicitly justified in the comment. The next reader doesn't have to wonder about memory ordering.raw_allocexact-Layoutallocation is the right primitive given the host frees by(ptr, len). The bug it replaced (Vec over-allocation) is exactly the kind of finding that would have ambushed productization if not caught in a spike.reset()keeps program state and clears per-query state. Atoms/registry/srcmap/limits survive. That's the right split — productization just needs to make the limits parameterizable.#[cfg(feature = "wasm_reactor")]or whatever it ends up being./tmp/t2— exactly right for spike-only scaffolding. Nothing throwaway leaks into the repo.Suggested order
Nothing to fix in the spike — it serves its purpose as-is. The items above are productization-plan inputs, not PR change requests. The one judgment call worth making before merge: write the productization brief now (findings 1–6 in a
WASM_TIER_2.mdsection) while the lessons are fresh, or trust that the spike's comments + this review carry the knowledge forward.