● Phase A is complete. Here's what landed. #27
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "wasm-tier-2-phase-A"
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?
Phase A — runtime: I/O-free core + productized reactor ABI ✅
A1 — I/O-free core (crates/runtime/src/core.rs, new)
map their own surface — CLI exit codes 2/3, reactor {"error":…}).
single-sourced (finding #4; spike hard-coded true).
entry.rs's output_json/output_error and the old reactor.rs JSON are gone, both now call the
core. 4 new unit tests pin the byte shape.
A2 — Machine::reset_per_query() (machine.rs)
spike's by-name reset()), leaves caller-set limits alone (finding #3).
A4 — write/1 capture (machine.rs + builtins/miscops.rs)
write/1/writeln/1/nl/0 route through write_out — CLI streams to stdout unchanged; the reactor
captures (D4). Captured bytes ride back in an "output" JSON field (sorted between exhausted
and solutions); CLI passes None so native bytes stay byte-identical to v1.
A3 — productized reactor ABI (reactor.rs)
solution/step/metacall-depth limits (finding #5); 0 sentinels documented. Layout-exact
alloc/free kept with the finding-#1 warning. set_machine swaps in Capture mode and drops the
spike's 1e9 step hack. D3 single-in-flight contract documented in the module header; findings
#7/#8 noted in place. No JSON duplication — delegates to the core.
Verification
integration / golden-IR suites all pass — native output is byte-identical and codegen is
untouched.
One judgment call worth flagging: captured write/1 output is surfaced on the success path
only ("output" field); the error object stays {"error":…}. Lossless for the normal case, and
it keeps the error shape byte-identical to native. Say the word if you'd rather carry
captured output on the error path too.
Review — #27 Phase A: I/O-free core + productized reactor ABI
This is a clean productization of the spike. The shape is right: a single
coremodule owns parse+solve and the v1 JSON wire shape, and all three transports (CLI/entry.rs, WASI, reactor) now route through it instead of each carrying its ownformat!of the JSON. The duplicatedsolutions_json/error_json/resetinreactor.rsare gone and the spike'sTHROWAWAYheader is replaced with a real concurrency contract. The two pre-merge items the #23 review flagged as becoming load-bearing once an embedding landed — theDrop-basedMetacallDepthGuardandPLG_METACALL_DEPTH— are both present, and the reactor now threads per-requestdepth_limit. Good follow-through.A few specific things I checked and liked:
exhaustedsingle-sourced and faithful. The CLI used to computeargs.limit.is_none_or(|l| count < l)inline; it now setsm.solution_limit = args.limit(entry.rs:178) and callscore::exhausted(m), which is the same rule overm.solution_limit. The reactor previously hard-codedtrue(finding #4) and now computes it the same way. Verified the wiring — equivalent for the CLI, fixed for the reactor.write_outas the single output seam.write/1/writeln/1/nl/0all funnel throughm.write_out, so the Stdout-vs-Capture decision is made in exactly one place.writelnbuildings.push('\n')then onewrite_outis byte-equivalent to the oldprintln!. CLI passesNoneforoutputso native JSON stays byte-identical to v1 — confirmed by the unchanged differential/golden suites.Layoutalloc + finding-#1 warning kept. TheNEVER Vec::with_capacitycomment is the right thing to preserve loudly.Real concern
1. Reactor per-request
step_limit/depth_limitlatch — the documented "0= keep the module default" is violated under reuse.plg_rt_run_queryonly overwrites when the arg is non-zero:and
reset_per_query()deliberately does not touchstep_limit/metacall_depth_limit(correct for the CLI, which sets them once). But the reactor reuses onestaticMachine across every request. So:step_limit = 5000→m.step_limit = 5000step_limit = 0("use default") → theifis skipped,reset_per_queryleaves it alone →m.step_limitstays 5000, not the module default (10_000).A host passing
0gets request N-1's value, not the default — a cross-request state leak, in exactly the sustained-reuse scenarioreset_per_query/A2 exists to prevent. It only affects the limit fields because they're the onesreset_per_queryintentionally excludes.solution_limitis fine (always assigned,0 => None). This didn't bite the spike because it pinnedstep_limitto 1e9 once; making the limits per-request is what exposes it.Fix: capture the module defaults at
plg_rt_set_machinetime (the values codegen/plg_initbaked in) and on each request assign unconditionally —m.step_limit = if step_limit != 0 { step_limit } else { default_step }— or, minimally, document on the ABI that0means "inherit the previous request's value" rather than "module default." The current doc comment and the actual behavior disagree, and there's no test covering two sequential requests with differing limits, so this would pass CI silently.Small observations
2. Reactor always emits
"output", even empty.set_machineinstallsOutputSink::Capture(String::new()), so after a query with nowrite/1,captured_output()returnsSome("")(notNone), andwrite_solutions_jsonemits,"output":"". That's consistent and arguably nicer for a host (.outputis always present) — just confirm it's the intended D4 contract, since it'sSome("")vsNonethat decides field presence, and an empty string is "present."3. Infallibility comment is accurate. The
let _ = core::write_*calls in the reactor are over aVecsink and the "writes never fail" note is correct; no concern, just noting I checked the swallowedio::Results are genuinely infallible there (unlike the stdout path inentry.rs, where a broken pipe is silently dropped — pre-existing v1 behavior, fine to leave).Net: #1 is worth closing before this lands on
main, since the reactor's whole reason for existing is Machine reuse and that's precisely where the limit contract breaks. Everything else is solid.