Skip to content

Carry waits keyed by a program's own type beside the generated loop - #1431

Merged
jasisz merged 5 commits into
mainfrom
fix/wait-keys-beside-loop
Sep 25, 2026
Merged

jasisz merged 5 commits into
mainfrom
fix/wait-keys-beside-loop

Conversation

@jasisz

@jasisz jasisz commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Bug

A program with the generated loop could not also key its own Wait.poll by a sum type. The loop keys its wait by Int, and every backend names the wait's boundary from one key type per program. Repro: the probe with type Watch plus a hand-written Wait.poll(idle: Map<Watch, Wait.Item>, 10) beside Run.all().

  • aver compile --target rust refused it: "this program keys one wait set by 'Int' and another by 'Watch'…".
  • aver run --wasm-gc failed module validation (type mismatch: expected (ref null $type), found (ref null $type)). The registry picked Watch as the one key, and the loop's wait passed Map<Int, Wait.Item>.
  • The VM already ran it.

The limit lives in capability::work::wait_key_type / wait_key_conflict, read by the wasm-gc type registry (wait_set_key, the aver.wait_poll import signature, the __rt_wait_* and __rt_result_wait_keys_ok/_err factories) and by the Rust door (the Wait.poll instantiation and the key codec).

Fix

The waits are generated per key type rather than making the boundary generic. In a program that answers a capability of its own, every wait keyed by a type other than Int is rewritten to call helpers generated for that key type in the waiting module's __ namespace:

fn __waitPollByWatch(items: Map<Watch, Wait.Item>, timeoutMs: Int) -> Result<List<Watch>, String>
    keys = Map.keys(items)
    ready = Wait.poll(__waitNumberedWatch(items, keys, 0, {}), timeoutMs)?
    Result.Ok(__waitKeysAtWatch(Vector.fromList(keys), ready, []))

The keys are numbered in the order the map puts them in, the wait runs on the Int-keyed set, and the ready numbers map back to their keys in the same order. The answer is the same one the program would have got.

  • Entry module: carried inside the yield lowering, after the loop is generated, from the stamped copy the lowering already has.
  • Modules with no process (dependencies): carried in pipeline::front from a checked copy, the same two-phase shape the lowering uses. This runs only when the program answers a capability and the module calls Wait.poll.

ABI

There is no ABI change. Every backend sees Int keys only, so aver.wait_poll, __rt_wait_set_order, __rt_wait_set_key_at, __rt_wait_keys_cons/_nil and __rt_result_wait_keys_ok/_err keep their names and their Int instantiation. The vendored JS adapters (tools/wasm-work/host.mjs, Robin's copy) need nothing. A recording of such a wait holds the Int-keyed wait. --target wasip2, which lowers only Int-keyed waits, now runs these programs too.

A program that answers no capability keeps the old rule and its own key type: the socket-only JS test still sees Map<Watch, Wait.Item>.

Tests

The new fixture tests/fixtures/run_wait_own_key has a loop, one wait in the entry keyed by Collecting.Watch, and a job collection keyed by Collecting.Watch in a dependency.

  • run_all_spec: both paths run on the VM, the helpers are generated for both modules, and a recording of the hand-written waits replays to MATCH.
  • rust_work_spec: VM/Rust parity on both paths.
  • wasm_work_spec: VM parity on wasm-gc and wasip2, both paths.

jasisz and others added 5 commits September 25, 2026 03:51
The generated loop keys its one wait by Int. A program that also waited
by hand, keyed by a sum of its own, then keyed waits two ways: the Rust
door refused it, and wasm-gc failed validation, because every backend
names the wait's boundary from one key type.

In a program that answers a capability of its own, every wait keyed by
another type than Int, in the entry or in a dependency, is now rewritten
to call helpers generated for its key type in the module's __ namespace
(__waitPollBy<Key> and four small recursive helpers). They number the
keys in the order the map puts them in, wait on the Int-keyed set, and
answer the keys the ready numbers stand for, in the same order. The
entry is carried inside the yield lowering, after the loop is generated;
a module with no process is carried in the front door from a checked
copy, the same two-phase shape the lowering uses.

No backend changes: every backend sees Int keys, so the wasm-gc wait ABI
(aver.wait_poll, __rt_result_wait_keys_ok/_err and the rest) keeps its
names and shape, and --target wasip2, which only lowers Int-keyed waits,
runs these programs too. A program that answers no capability keeps the
old rule and its own key type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in nested literal and list patterns (#1434), Run.fail (#1430),
#1428, #1429 and #1432.

A module with no process of its own used to go down one of two exclusive
branches of the front pipeline: compile its nested patterns, or carry its
waits keyed by a type other than Int. A dependency can need both, so the
branch now does both in order: check the module as written, so errors
name the patterns the user wrote; compile the nested patterns and check
again; carry the waits with the key types read off that check of the
lowered module; and check what the carrying wrote once more.

The entry already did both: the yield lowering carries its waits over the
module as written, and the nested patterns are compiled after it. Nested
patterns inside a yield function are still refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A second copy of the wait fixture whose dependency matches the answer of
its own Wait.poll with nested patterns in the same function that waits,
and whose entry matches its empty wait's answer with nested patterns too.
The run, the dump of the generated helpers, generated Rust and both wasm
targets all do the same work as the VM.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jasisz
jasisz merged commit 7621de5 into main Sep 25, 2026
28 checks passed
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