Skip to content

Fix four process layer v2 bugs found porting follow - #1424

Merged
jasisz merged 2 commits into
mainfrom
fix/process-v2-bugs
Sep 24, 2026
Merged

jasisz merged 2 commits into
mainfrom
fix/process-v2-bugs

Conversation

@jasisz

@jasisz jasisz commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Four compiler bugs found while moving btc-listener's follow onto process layer v2 (#1421), plus the doubled answer-shape warning.

B1: aver check . reported unknown-ident 'Lib' for a process calling a capability under a subdirectory

aver check main.av passed, and so did aver run. A directory check walks lib/ticks.av as the entry of its own program. There it is named by its module Ticks line, so it added the pair (Lib.Clock, Ticks) to the answers the batch shares. main.av was then lowered against a Ticks that nothing can import, the loop's generated imports failed, and the process lost the Lib namespace.

Fix: prime_batch_answers skips a pair whose answer module is the walk's own entry unless that name resolves from the module root to the same file. The programs that import the module by its real name (Lib.Ticks) still add it.

Test: run_all_spec::a_directory_check_names_an_answer_module_under_a_subdirectory_the_way_its_importer_does (fixture run_process_subdir_capability) checks main.av and . and runs the program.

B2: wasm-gc failed with carrier eq inner type 'App.Owner.State' has no eq dispatch

Every tuple gets an eager List<Tuple<..>>, in case List.zip builds one, so every answer function's Tuple<State, Result<R, Run.Wake>> has one. The seed walker in module.rs registered an eq helper for every list and vector element with no resolvability check. When the state has no equality, the tuple's eq body could not be emitted. In btc the state reaches a Kv.Handle resource.

Fix: an element is seeded only when its list/vector helpers compare elements at all (list_eq_kind is Some, the same test that gives the helpers their contains/eq/hash slots). If nothing compares the element, no eq carrier is registered. A real comparison still registers its own helper at the comparison site, and still fails there.

Test: wasm_work_spec::an_answer_state_without_equality_compiles_on_wasm_gc (fixture run_answer_state_without_eq, where the state holds a List, a Map and an Option of a capability resource). Before the fix it failed with the carrier-eq error; now it compiles.

B3: the answer module's state was copied on every change (Rust backend)

The loop called Owner.heard(run.owner, …) while run still held the state, and every __Run param down the serve chain was borrowed. The Map inside was never uniquely owned, so each Map.set did Rc::make_mut and copied the whole Map.

Fix, in two parts:

  • Generated loop. __Run holds each answer state as Option<State>. A new __take<Module>(run) -> Tuple<__Run, Option<State>> hands the state out, and the serve arm matches on it and passes the taken state to the answer function. __serve<P><Kind> writes back Option.Some(__next). Keyed seating reads the keys through __keysOf<P>(run). __workHostStep binds the slot ids before calling __serveEach, so the run is passed at its last use.
  • Rust backend. A record param is taken by value when the function consumes it: it is the base of a record update at its last use, or it is passed at its last use to a callee position that is by value. This is a least fixpoint over the program. The result is computed once after the Rust MIR rewrites (CodegenContext::rust_owned_record_params), and signatures and call sites both read it. With this, __serveIf, __serve, __serve<P>, __take<Module>, __turn and __workHostStep take run by value, so the run moves from __serveEach to the answer function.

Measurement, aver compile --target rust, release build: the owner holds a Map<Int, Int> of 100,000 entries and one process makes 2,000 requests, each doing Map.set.

before after
Rust (release) 0.76 s 0.01 s

The output is the same (total 4000). The ~0.37 ms per request before is one clone of the 100k-entry map.

The VM and wasm-gc do not have this pattern fixed:

  • VM (debug aver): 15.0 s before and 15.6 s after for the same program, and 3.1 s of that is building the map. The VM does an in-place Map.set only on a slot own_param proves unique. Map.set(state.counts, …) updates a field of a param, so it copies whatever the loop does.
  • wasm-gc: 508 s before (debug aver run --wasm-gc). The Map there copies on every set, and the loop shape does not change that.

Tests:

  • rust_work_spec::an_answer_modules_state_reaches_its_answer_function_uniquely_owned (fixture run_owned_answer_state) checks the by-value chain in the generated crate (__serveIf/__serve/__serveTicker/__takeOwner take mut run: __Run, and the answer function gets __taken). It also builds the crate and compares its output with the VM.
  • run_all_spec dump expectations now include __takeLedger and __keysOfScorer.
  • run_schedule_cases reads the ledger through servedBy, because the state field is now an Option.

B4: aver effects --write said "already minimal" while check reported a missing Console.print in a process

The rewriter left every yielding function alone. It now adds what the body performs in place, and never removes anything, because what a process reaches through its stops belongs to the lowering. load_surface now also lowers each program against that program's own answer modules (program.marked()) instead of the manifest alone, the same way check does.

Test: effects_command_spec::write_adds_what_a_process_performs_in_place. It strips Console.print from the fixture, checks that check fails, that the report says missing: Console.print, and that --write restores both lists. Then check passes.

G3: answer-shape warnings printed twice

The warning was raised once by the answer module (Owner) and once by the entry that imports it (App.Owner). An answer-shape warning is now reported only when the checked module is the answer module itself. Errors are unchanged.

Test: answer_shape_spec::an_answer_function_with_effects_is_allowed_and_said_so asserts the warning appears once.

Not fixed here

With these fixes, aver compile main.av --module-root . --target wasm-gc on the btc feat/follow-processes worktree now stops at a different error: capability ABI lacks List<Infra.BlockJobs.Bytes> slot. The job kind Infra.BlockJobs names Bytes through its dependencies' types, and the ABI qualifies it as a type of the capability. That is a separate bug in the job-kind boundary (#1401 area) and needs its own issue.

Checks run locally

  • cargo fmt --all -- --check
  • cargo clippy -p aver-lang --lib --bin aver --benches -- -D warnings, and the same with --features wasm,wasip2
  • cargo test -p aver-lang --lib --features wasm,wasip2: 1702 passed
  • --features wasm,wasip2: run_all_spec 32, effects_command_spec 20, answer_shape_spec 18, typechecker_spec 349, yield_spec 38, yield_verify_spec 16, wasm_work_spec 26, wasip2_coordinator_spec 7, guide_example_spec 10, rust_work_spec 23, own_param_graduation 13, own_param_soundness 27, rust_codegen_regression 4. All pass.

Lean and certification were not run locally. The generated loop changed shape and the wasm-gc seeding registers fewer eq helpers when an element has no equality, so the Proof and Certification workflows are the ones to watch.

jasisz and others added 2 commits September 25, 2026 00:06
- Directory check: an answer module under a subdirectory, walked as the
  entry of its own program, lent the batch its bare module name. Every
  program lowered against that name lost its loop imports, and a process
  calling the capability failed with unknown-ident. Such a pair is now
  left to the programs that import the module by its real name.
- wasm-gc: the seed walker demanded an eq helper for every list and
  vector element type, including the eager List<Tuple<State, Result>> a
  tuple of every answer function gets. A state with no equality (one
  holding a provider resource) then failed with "carrier eq inner type
  has no eq dispatch". An element is now seeded only when its list
  helpers compare elements at all; a real comparison still demands and
  fails on its own.
- Generated loop: the answer module's state is held as an Option in the
  run and handed out (__take<Module>) for the length of one answer, and
  the Rust backend takes a record param by value when the function
  consumes it (updates it at its last use, or passes it at its last use
  to a callee that takes it by value). The run now moves down the serve
  chain, so the answer function holds the only reference to the state
  and Map/Vector updates in it are in place instead of a full copy per
  request.
- aver effects --write now adds the in-place effects a process is
  missing, and lowers each program against its own answer modules, as
  check does.
- An answer-shape warning is reported by the answer module only, not
  again by every module that imports it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jasisz
jasisz merged commit de26320 into main Sep 24, 2026
28 checks passed
jasisz added a commit that referenced this pull request Sep 25, 2026
* Release a consumed tuple or Option box on the VM

The generated loop hands an answer module's state out of the run in an
Option, inside a tuple, and takes the answer back in another tuple. On
the VM the Option box and the tuples kept no holder count, so after a
match had taken them apart they still counted as holders of the state,
and every answer copied the module's Maps. #1424 fixed this on the Rust
side only.

Tuples and the boxes of Option.Some, Result.Ok and Result.Err now count
their off-stack holders the way maps, vectors and records do: every
entry, global and constant that stores one registers itself. A match
whose subject nothing reads afterwards (a temporary, or a local at its
last use) now releases what it takes apart: POP_CONSUMED after a tuple
arm empties the tuple, and MATCH_UNWRAP with the consume bit empties the
box, each only when nothing off the stack and no stack cell holds the
tuple or box, and only when a Map or Vector inside makes it worth the
walk. `?` does the same for an Ok box through PROPAGATE_ERR_CONSUMED.

aver run --profile now also prints how many map entries the writes that
were not in place copied.

Release build without LTO, run_owned_answer_state scaled to 2000
requests against a 100k-entry Map: 1.84 s -> 0.13 s. The fixture as
checked in (200 requests, 1k entries) is 0.06 s either way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Close the release helper the merge left open

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant