From a6a187ddab9a96cbdcf879e9c293f87e511b21aa Mon Sep 17 00:00:00 2001 From: jasisz Date: Fri, 25 Sep 2026 03:51:43 +0200 Subject: [PATCH 1/3] Carry waits keyed by a program's own type beside the generated loop 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 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) --- CHANGELOG.md | 1 + docs/services.md | 2 +- src/capability/work.rs | 2 +- src/ir/pipeline.rs | 37 ++++ src/yield_lowering/carried_waits.rs | 181 ++++++++++++++++++ src/yield_lowering/mod.rs | 51 +++++ tests/fixtures/run_wait_own_key/aver.toml | 6 + tests/fixtures/run_wait_own_key/clock.av | 10 + tests/fixtures/run_wait_own_key/collecting.av | 58 ++++++ tests/fixtures/run_wait_own_key/main.av | 33 ++++ tests/fixtures/run_wait_own_key/scoring.av | 11 ++ tests/fixtures/run_wait_own_key/ticks.av | 21 ++ tests/fixtures/run_wait_own_key/validation.av | 16 ++ tests/run_all_spec.rs | 80 ++++++++ tests/rust_work_spec.rs | 25 +++ tests/wasm_work_spec.rs | 18 ++ 16 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 src/yield_lowering/carried_waits.rs create mode 100644 tests/fixtures/run_wait_own_key/aver.toml create mode 100644 tests/fixtures/run_wait_own_key/clock.av create mode 100644 tests/fixtures/run_wait_own_key/collecting.av create mode 100644 tests/fixtures/run_wait_own_key/main.av create mode 100644 tests/fixtures/run_wait_own_key/scoring.av create mode 100644 tests/fixtures/run_wait_own_key/ticks.av create mode 100644 tests/fixtures/run_wait_own_key/validation.av diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e21def1d..688b2847f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ The generated loop is now written from the program's source alone, and the manif ### Fixed +- **A program with the generated loop can key its own waits by a type of its own.** The loop keys its wait by `Int`, and a hand-written `Wait.poll` keyed by a sum beside it used to be refused by the Rust door ("this program keys one wait set by 'Int' and another by 'Watch'") and to fail wasm-gc validation. In a program that answers a capability of its own, each such wait, in the entry or in a dependency, now goes through helpers generated for its key type that carry it through an `Int`-keyed wait. It answers the same keys in the same order. Its recording holds the `Int`-keyed wait. The wasm-gc wait ABI is unchanged, and `--target wasip2` runs such waits too. A program that answers no capability still keys all its waits one way. - **A wait over sockets and jobs keeps watching its sockets after a job outside its set settles.** The wake from that job used to end the socket poll, and the wait then slept out the rest of its timeout on the job engine alone, missing sockets that became ready meanwhile and never reporting them. The VM, generated Rust and the wasm-gc native host now share one wait loop that polls the whole set again. - **A handle whose slot the engine has forgotten answers `work: unknown job` on the VM**, as it already did elsewhere, instead of claiming another job kind started it. Which kind began a job is now kept in the job's own slot, so nothing a job kind keeps grows with the number of jobs it starts. - **The VM runs a function whose bytecode is larger than 32 KiB.** Jump offsets were sixteen bits and a longer forward jump wrapped into a backward one, which crashed `aver verify` on large generated trace laws. diff --git a/docs/services.md b/docs/services.md index 613127510..f81bfc1c4 100644 --- a/docs/services.md +++ b/docs/services.md @@ -488,7 +488,7 @@ fn interests(eye: Eye, job: Work.Job) -> Map The key type of a wait is the key type of the map it was handed, and the type checker settles it. A wait set written at the call is keyed by what it holds. One built by another function and passed in is keyed by what that function returns. Nothing needs an annotation. -One program uses one wait key type. A turn has one wait, the key is how the program says what it is waiting for, and the keys leave every backend through one set of helpers built from that type. A program that keys one wait one way and another wait another way is refused. The fix is to name both kinds as constructors of one type. A wait whose key type nothing settles is refused for the same reason, and the fix is to bind the map to a name with its type written down. `aver check` reads one file at a time, so a program whose modules disagree about the key is refused at the compile door, which reads the entry module and the modules it depends on together. `--target wasip2` keys a wait by `Int`. It carries the wait through canonical ABI imports instead of the helpers an external host walks, and a wait keyed by another type is refused for that target. +One program uses one wait key type. A turn has one wait, the key is how the program says what it is waiting for, and the keys leave every backend through one set of helpers built from that type. The generated loop is the exception. It keys its own wait by `Int`, so in a program that answers a capability of its own, every other wait keyed by another type is carried through an `Int`-keyed wait. That covers the entry and its dependencies alike. The compiler generates one set of helpers per key type, in the `__` namespace of the module that waits, for example `__waitPollByWatch`. They number the keys in the order the map puts them in, wait on the numbered set, and answer the keys the ready numbers stand for, in the same order. The program gets the answer it would have got, and every backend, `--target wasip2` included, sees `Int` keys. A recording holds the `Int`-keyed wait. Outside such a program, a program that keys one wait one way and another wait another way is refused. The fix is to name both kinds as constructors of one type. A wait whose key type nothing settles is refused for the same reason, and the fix is to bind the map to a name with its type written down. `aver check` reads one file at a time, so a program whose modules disagree about the key is refused at the compile door, which reads the entry module and the modules it depends on together. `--target wasip2` keys a wait by `Int`. It carries the wait through canonical ABI imports instead of the helpers an external host walks, and a wait keyed by another type is refused for that target. A wait set written empty at the call is a turn with nothing to watch. It holds nothing, so it names no key of its own and is read as keyed by `Int`, which is what such a call has always meant. In a program that keys its waits by a type of its own, that reads as a second key type and the program is refused by name. Write the type on that one set: diff --git a/src/capability/work.rs b/src/capability/work.rs index 9342e3d38..11f57ee04 100644 --- a/src/capability/work.rs +++ b/src/capability/work.rs @@ -1652,7 +1652,7 @@ fn wait_set_key(annotation: &str) -> Option { /// /// The generic contract's own `Map` is the signature rather /// than a choice a program made, so a type variable is not an answer. -fn wait_set_key_of_type(ty: &Type) -> Option { +pub(crate) fn wait_set_key_of_type(ty: &Type) -> Option { fn walk(ty: &Type, out: &mut Option) { match ty { Type::Map(key, value) if is_wait_item(value) => *out = Some((**key).clone()), diff --git a/src/ir/pipeline.rs b/src/ir/pipeline.rs index 883f7909b..4b440ff98 100644 --- a/src/ir/pipeline.rs +++ b/src/ir/pipeline.rs @@ -1028,6 +1028,43 @@ pub fn front(items: &mut Vec, cfg: FrontConfig<'_, '_>) -> FrontResult ..phase_one }, } + } else if !marked.is_empty() && crate::yield_lowering::calls_wait_poll(items) { + // A module of a program that answers a capability of its own, with no + // process of its own: its waits keyed by another type than `Int` are + // carried through an `Int`-keyed one, the same way the entry's are, so + // they can meet the generated loop's wait in one program. The keys are + // read off a checked copy. + let written: Vec = items + .iter() + .map(|item| match item { + TopLevel::FnDef(fd) => TopLevel::FnDef(crate::ast::FnDef { + body: std::sync::Arc::new(fd.body.as_ref().clone()), + ..fd.clone() + }), + other => other.clone(), + }) + .collect(); + let phase_one = typecheck(&written, mode); + if phase_one.errors.is_empty() { + match crate::yield_lowering::carry_waits(items, &written, &phase_one.type_spellings) { + Ok(Some(source)) => { + if std::env::var_os("AVER_YIELD_DUMP").is_some() { + eprintln!("{source}"); + } + if run_tco { + tco(items); + } + typecheck_gate(items, mode, &items[..user_program_len]) + } + Ok(None) => typecheck_gate(items, mode, &items[..user_program_len]), + Err(errors) => TypeCheckResult { + errors, + ..phase_one + }, + } + } else { + typecheck_gate(items, mode, &items[..user_program_len]) + } } else { typecheck_gate(items, mode, &items[..user_program_len]) }; diff --git a/src/yield_lowering/carried_waits.rs b/src/yield_lowering/carried_waits.rs new file mode 100644 index 000000000..f52f1e8d4 --- /dev/null +++ b/src/yield_lowering/carried_waits.rs @@ -0,0 +1,181 @@ +//! Waits the program keys by a type of its own, carried through an `Int`-keyed +//! wait (jasisz/aver#1329, process layer v2). +//! +//! The generated loop keys its one wait by `Int`: it numbers every item every +//! parked request waits on. A program that also waits by hand, keyed by a type +//! of its own (`Map`), would then key waits two ways, and +//! every backend names the wait's boundary from one key type. So in a program +//! that answers a capability of its own, each such wait is rewritten to call a +//! helper generated for its key type, in the reserved `__` namespace of the +//! module that makes it. The helper numbers the keys in the order the map puts +//! them in, waits on the numbered set, and hands back the keys the numbers +//! stand for, in the same order. The program sees the same answer it would +//! have seen; the backends see one key type. +//! +//! No generics: one set of helpers per key type, spelled for that type. + +use std::collections::BTreeMap; + +use crate::ast::{Expr, FnDef, Spanned, Stmt, TopLevel, Type}; +use crate::codegen::expr_walk::for_each_child_mut; + +const WAIT_POLL: &str = "Wait.poll"; + +/// What was carried: the helpers generated, as source, and their items. +pub(crate) struct Carried { + pub source: String, + pub items: Vec, +} + +fn walk(expr: &mut Spanned, visit: &mut impl FnMut(&mut Spanned)) { + visit(expr); + for_each_child_mut(expr, &mut |child| walk(child, visit)); +} + +fn walk_fn(fd: &mut FnDef, visit: &mut impl FnMut(&mut Spanned)) { + let body = std::sync::Arc::make_mut(&mut fd.body); + for stmt in body.stmts_mut() { + match stmt { + Stmt::Binding(_, _, value) | Stmt::Expr(value) => walk(value, visit), + } + } +} + +fn is_wait_poll(callee: &Expr) -> bool { + fn dotted(expr: &Expr) -> Option { + match expr { + Expr::Ident(name) => Some(name.clone()), + Expr::Attr(obj, field) => Some(format!("{}.{field}", dotted(&obj.node)?)), + _ => None, + } + } + dotted(callee).as_deref() == Some(WAIT_POLL) +} + +/// Whether a module calls `Wait.poll` anywhere, read off the source alone. +pub(crate) fn calls_wait_poll(items: &[TopLevel]) -> bool { + items.iter().any(|item| { + let TopLevel::FnDef(fd) = item else { + return false; + }; + fd.body.stmts().iter().any(|stmt| { + let value = match stmt { + Stmt::Binding(_, _, value) | Stmt::Expr(value) => value, + }; + crate::codegen::expr_walk::any( + value, + &mut |e| matches!(&e.node, Expr::FnCall(callee, _) if is_wait_poll(&callee.node)), + ) + }) + }) +} + +/// The key of every `Wait.poll` call in `fd`, in walk order, read off the +/// types the checker stamped on each wait set. +fn keys_of(fd: &FnDef) -> Vec> { + let mut copy = fd.clone(); + let mut keys = Vec::new(); + walk_fn(&mut copy, &mut |expr| { + if let Expr::FnCall(callee, args) = &expr.node + && is_wait_poll(&callee.node) + { + keys.push( + args.first() + .and_then(|set| set.ty()) + .and_then(crate::capability::work::wait_set_key_of_type), + ); + } + }); + keys +} + +/// `Infra.Watch` → `InfraWatch`: the part of a helper's name that says which +/// key type it carries. +fn suffix(spelled: &str) -> String { + spelled + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .collect() +} + +/// Rewrite every wait of `items` keyed by something other than `Int` to go +/// through a generated helper for its key. `stamped` is the same module after +/// a type check, in the same order, so every wait set carries its type. The +/// functions `skip` names were replaced by generated code and are left alone. +pub(crate) fn carry( + items: &mut Vec, + stamped: &[TopLevel], + spellings: &super::TypeSpellings, + skip: &dyn Fn(&str) -> bool, +) -> Result, String> { + let mut by_fn: BTreeMap>> = BTreeMap::new(); + for item in stamped { + if let TopLevel::FnDef(fd) = item + && !skip(&fd.name) + { + let keys = keys_of(fd); + if keys + .iter() + .any(|key| key.as_ref().is_some_and(|key| *key != Type::Int)) + { + by_fn.insert(fd.name.clone(), keys); + } + } + } + if by_fn.is_empty() { + return Ok(None); + } + // One helper set per key type, named after the key as the module spells it. + let mut helpers: BTreeMap = BTreeMap::new(); + for item in items.iter_mut() { + let TopLevel::FnDef(fd) = item else { continue }; + let Some(keys) = by_fn.get(&fd.name) else { + continue; + }; + let mut at = 0usize; + walk_fn(fd, &mut |expr| { + let Expr::FnCall(callee, _) = &mut expr.node else { + return; + }; + if !is_wait_poll(&callee.node) { + return; + } + let key = keys.get(at).cloned().flatten(); + at += 1; + let Some(key) = key.filter(|key| *key != Type::Int) else { + return; + }; + let spelled = super::spell_type(&key, spellings); + let name = format!("__waitPollBy{}", suffix(&spelled)); + helpers.entry(spelled).or_insert_with(|| name.clone()); + callee.node = Expr::Ident(name); + }); + } + let mut source = String::new(); + for (key, name) in &helpers { + source.push_str(&helper_source(key, name)); + } + let tokens = crate::lexer::Lexer::new(&source) + .tokenize() + .map_err(|error| error.to_string())?; + let parsed = crate::parser::Parser::new_compiler_generated(tokens) + .parse() + .map_err(|error| error.to_string())?; + items.extend(parsed.iter().cloned()); + Ok(Some(Carried { + source, + items: parsed, + })) +} + +/// The helpers that carry one key type through an `Int`-keyed wait. +fn helper_source(key: &str, name: &str) -> String { + let tail = &name["__waitPollBy".len()..]; + format!( + "\nfn {name}(items: Map<{key}, Wait.Item>, timeoutMs: Int) -> Result, String>\n ? \"Waits on a set keyed by {key} through a wait keyed by the position of each key, and answers the keys that were ready, in the order the set puts them in.\"\n ! [Wait.poll]\n keys = Map.keys(items)\n ready = Wait.poll(__waitNumbered{tail}(items, keys, 0, {{}}), timeoutMs)?\n Result.Ok(__waitKeysAt{tail}(Vector.fromList(keys), ready, []))\n\ + \nfn __waitNumbered{tail}(items: Map<{key}, Wait.Item>, keys: List<{key}>, next: Int, acc: Map) -> Map\n ? \"The same items, keyed by the position of their key in the set's own order.\"\n match keys\n [] -> acc\n [key, ..rest] -> __waitNumbered{tail}(items, rest, next + 1, __waitNumberedAt{tail}(items, key, next, acc))\n\ + \nfn __waitNumberedAt{tail}(items: Map<{key}, Wait.Item>, key: {key}, next: Int, acc: Map) -> Map\n ? \"One item under its position.\"\n match Map.get(items, key)\n Option.None -> acc\n Option.Some(item) -> Map.set(acc, next, item)\n\ + \nfn __waitKeysAt{tail}(keys: Vector<{key}>, ready: List, acc: List<{key}>) -> List<{key}>\n ? \"The keys the ready positions stand for, in the order they were reported.\"\n match ready\n [] -> acc\n [at, ..rest] -> __waitKeysAt{tail}(keys, rest, __waitKeyAt{tail}(keys, at, acc))\n\ + \nfn __waitKeyAt{tail}(keys: Vector<{key}>, at: Int, acc: List<{key}>) -> List<{key}>\n ? \"One ready position, as its key.\"\n match Vector.get(keys, at)\n Option.None -> acc\n Option.Some(key) -> List.concat(acc, [key])\n" + ) +} diff --git a/src/yield_lowering/mod.rs b/src/yield_lowering/mod.rs index 926238d25..676cd7752 100644 --- a/src/yield_lowering/mod.rs +++ b/src/yield_lowering/mod.rs @@ -56,6 +56,7 @@ pub(crate) fn spell_type(ty: &crate::ast::Type, spellings: &TypeSpellings) -> St } mod build; +mod carried_waits; mod coordinator; mod lower; mod trace; @@ -210,6 +211,31 @@ impl YieldLoweringReport { pub const YIELD_EFFECT: &str = "yield"; /// Whether a function body calls `Run.all()`, which runs the generated loop. +/// Whether a module calls `Wait.poll` anywhere, read off its source. +pub fn calls_wait_poll(items: &[TopLevel]) -> bool { + carried_waits::calls_wait_poll(items) +} + +/// Carry the waits of a module with no process through an `Int`-keyed wait; +/// see `carried_waits`. `stamped` is the module after a type check. Answers +/// the generated source when anything was carried. +pub fn carry_waits( + items: &mut Vec, + stamped: &[TopLevel], + spellings: &TypeSpellings, +) -> Result, Vec> { + carried_waits::carry(items, stamped, spellings, &|_| false) + .map(|carried| carried.map(|carried| carried.source)) + .map_err(|parse| { + vec![error_at( + 1, + format!( + "internal error carrying this module's waits through an Int-keyed wait: {parse}; please report this program" + ), + )] + }) +} + pub fn calls_run_all(fd: &FnDef) -> bool { coordinator::calls_run_all(fd) } @@ -585,6 +611,31 @@ pub fn lower( } } + // The generated loop keys its wait by `Int`. A program that answers a + // capability of its own carries every other wait it writes through an + // `Int`-keyed one, so the loop's wait and the program's own can meet in + // one program. + if !marked.is_empty() { + let carried = carried_waits::carry(items, stamped, type_spellings, &|name| { + yield_fns.contains(name) + }) + .map_err(|parse| { + vec![error_at( + 1, + format!( + "internal error carrying this module's waits through an Int-keyed wait: {parse}; please report this program" + ), + )] + })?; + if let Some(carried) = carried { + report.generated.extend(carried.items); + match report.loop_source.as_mut() { + Some(source) => source.push_str(&carried.source), + None => report.loop_source = Some(carried.source), + } + } + } + // Materialize the default export surface before replacing source process // names with reserved protocol names (which the underscore rule hides). let exports = crate::visibility::collect_module_exports(stamped); diff --git a/tests/fixtures/run_wait_own_key/aver.toml b/tests/fixtures/run_wait_own_key/aver.toml new file mode 100644 index 000000000..d8c878e9e --- /dev/null +++ b/tests/fixtures/run_wait_own_key/aver.toml @@ -0,0 +1,6 @@ +[providers] +schema = 1 + +[[providers.bindings]] +capability = "Validation" +work = "Scoring.score" diff --git a/tests/fixtures/run_wait_own_key/clock.av b/tests/fixtures/run_wait_own_key/clock.av new file mode 100644 index 000000000..649326d70 --- /dev/null +++ b/tests/fixtures/run_wait_own_key/clock.av @@ -0,0 +1,10 @@ +module Clock + kind = capability + semantics = effectful + intent = "The tick the processes count." + exposes [tick] + +operation tick() -> Int + ? "The number of the next tick." + oracle = generative + replay = recorded diff --git a/tests/fixtures/run_wait_own_key/collecting.av b/tests/fixtures/run_wait_own_key/collecting.av new file mode 100644 index 000000000..3f4b2552c --- /dev/null +++ b/tests/fixtures/run_wait_own_key/collecting.av @@ -0,0 +1,58 @@ +module Collecting + intent = "A wait keyed by a sum of this module's own, in a dependency of a program whose generated loop keys its wait by Int." + depends [Validation] + exposes [Watch, collect] + effects [Console.print, Validation.take, Wait.poll] + +type Watch + Read(Int) + Write(Int) + +fn collect(pending: Map, turns: Int) -> Result + ? "Waits once per call until every job has landed." + ! [Console.print, Validation.take, Wait.poll] + match Map.len(pending) == 0 + true -> Result.Ok(Unit) + false -> match turns < 1 + true -> Result.Err("the jobs did not finish within the turns allowed") + false -> collected(pending, Wait.poll(pending, 200)?, turns) + +fn collected(pending: Map, ready: List, turns: Int) -> Result + ? "Takes every job the wait reported, then waits again for the rest." + ! [Console.print, Validation.take, Wait.poll] + remaining = harvest(pending, ready)? + collect(remaining, turns - 1) + +fn harvest(pending: Map, ready: List) -> Result, String> + ? "Takes one reported key at a time; a job that has not finished stays pending." + ! [Console.print, Validation.take] + match ready + [] -> Result.Ok(pending) + [key, ..rest] -> match Map.get(pending, key) + Option.None -> harvest(pending, rest) + Option.Some(item) -> harvest(took(pending, key, item)?, rest) + +fn took(pending: Map, key: Watch, item: Wait.Item) -> Result, String> + ? "One key: its job's score printed and the key dropped, or the key kept while the job runs." + ! [Console.print, Validation.take] + match item + Wait.Item.Socket(_) -> Result.Ok(Map.remove(pending, key)) + Wait.Item.Job(job) -> match Validation.take(job)? + Option.None -> Result.Ok(pending) + Option.Some(points) -> printed(Map.remove(pending, key), key, points) + +fn printed(pending: Map, key: Watch, points: Int) -> Result, String> + ? "Says which key landed and what its job scored." + ! [Console.print] + Console.print("{describe(key)} scored {points}") + Result.Ok(pending) + +fn describe(key: Watch) -> String + ? "A key as a line of output." + match key + Watch.Read(n) -> "read {n}" + Watch.Write(n) -> "write {n}" + +verify describe + describe(Watch.Read(1)) => "read 1" + describe(Watch.Write(2)) => "write 2" diff --git a/tests/fixtures/run_wait_own_key/main.av b/tests/fixtures/run_wait_own_key/main.av new file mode 100644 index 000000000..6ef49bfce --- /dev/null +++ b/tests/fixtures/run_wait_own_key/main.av @@ -0,0 +1,33 @@ +module Main + intent = "A program whose generated loop keys its wait by Int, and whose own waits, run with `manual`, are keyed by a sum: once here in the entry and again in a dependency." + depends [Clock, Ticks, Validation, Scoring, Collecting] + effects [Args.get, Clock.tick, Console.print, Validation.begin, Validation.take, Wait.poll, yield] + +fn main() -> Result + ? "`manual` waits on two jobs keyed by Collecting.Watch; anything else runs the loop." + ! [Args.get, Console.print, Validation.begin, Validation.take, Wait.poll] + match List.contains(Args.get(), "manual") + true -> manual() + false -> Run.all() + +fn manual() -> Result + ? "Begins two jobs, waits once here on a set keyed by Collecting.Watch that holds nothing, and collects the jobs in Collecting." + ! [Console.print, Validation.begin, Validation.take, Wait.poll] + first = Validation.begin("alpha")? + second = Validation.begin("beta-two")? + idle: Map = {} + nothing = Wait.poll(idle, 0)? + Console.print("an empty wait reported {List.len(nothing)} keys") + Collecting.collect({Collecting.Watch.Read(1) => Wait.Item.Job(first), Collecting.Watch.Write(2) => Wait.Item.Job(second)}, 50) + +fn ticker() -> Unit + ? "Three ticks." + ! [Clock.tick, Console.print, yield] + ticking(0) + +fn ticking(seen: Int) -> Unit + ? "One tick at a time." + ! [Clock.tick, Console.print, yield] + match Clock.tick() >= 3 + true -> Console.print("ticked {seen + 1} times") + false -> ticking(seen + 1) diff --git a/tests/fixtures/run_wait_own_key/scoring.av b/tests/fixtures/run_wait_own_key/scoring.av new file mode 100644 index 000000000..45c533e00 --- /dev/null +++ b/tests/fixtures/run_wait_own_key/scoring.av @@ -0,0 +1,11 @@ +module Scoring + intent = "The pure function one scoring job runs." + exposes [score] + effects [] + +fn score(task: String) -> Int + ? "Scores one task by its length." + String.len(task) + +verify score + score("alpha") => 5 diff --git a/tests/fixtures/run_wait_own_key/ticks.av b/tests/fixtures/run_wait_own_key/ticks.av new file mode 100644 index 000000000..9cbe4d62b --- /dev/null +++ b/tests/fixtures/run_wait_own_key/ticks.av @@ -0,0 +1,21 @@ +module Ticks + intent = "Answers Clock at once: every ask is the next tick, whoever asks." + depends [Clock] + exposes [State, fresh, tick] + effects [] + answers [Clock] + +record State + count: Int + +fn fresh() -> State + ? "No tick given yet." + State(count = 0) + +fn tick(state: State) -> Tuple> + ? "The next tick, answered in the turn it was asked." + next = state.count + 1 + (State(count = next), Result.Ok(next)) + +verify tick + tick(fresh()) => (State(count = 1), Result.Ok(1)) diff --git a/tests/fixtures/run_wait_own_key/validation.av b/tests/fixtures/run_wait_own_key/validation.av new file mode 100644 index 000000000..43166f14a --- /dev/null +++ b/tests/fixtures/run_wait_own_key/validation.av @@ -0,0 +1,16 @@ +module Validation + kind = capability + semantics = effectful + depends [Work] + intent = "One job kind: scoring a text off the turn." + exposes [begin, take] + +operation begin(task: String) -> Result + ? "Starts one scoring off the turn and returns its job handle at once." + oracle = generativeOutput + replay = recorded + +operation take(job: Work.Job) -> Result, String> + ? "Collects one finished scoring: None while it runs, Some(score) once it finished." + oracle = generativeOutput + replay = recorded diff --git a/tests/run_all_spec.rs b/tests/run_all_spec.rs index 576eb2329..50539d127 100644 --- a/tests/run_all_spec.rs +++ b/tests/run_all_spec.rs @@ -1130,3 +1130,83 @@ fn a_directory_check_names_an_answer_module_under_a_subdirectory_the_way_its_imp assert!(out.status.success(), "{}", format_output(&out)); assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "ticked"); } + +/// A program whose generated loop keys its wait by `Int` can key its own +/// waits by a sum, in the entry and in a dependency. Each such wait is +/// carried through an `Int`-keyed wait by helpers generated for its key type, +/// and answers the same keys it would have answered, in the same order. +#[test] +fn waits_keyed_by_a_sum_run_beside_the_generated_loop() { + let looped = aver_within("run_wait_own_key", &["run"], 60); + assert!(looped.status.success(), "{}", format_output(&looped)); + assert_eq!( + String::from_utf8_lossy(&looped.stdout).trim(), + "ticked 3 times" + ); + + let manual = aver_within("run_wait_own_key", &["run", "--", "manual"], 60); + assert!(manual.status.success(), "{}", format_output(&manual)); + let text = String::from_utf8_lossy(&manual.stdout); + let mut lines: Vec<&str> = text.lines().collect(); + lines.sort_unstable(); + assert_eq!( + lines, + [ + "an empty wait reported 0 keys", + "read 1 scored 5", + "write 2 scored 8", + ], + "{}", + format_output(&manual) + ); + + let dir = fixture("run_wait_own_key"); + let dump = Command::new(aver_bin()) + .current_dir(&dir) + .env("AVER_YIELD_DUMP", "1") + .arg("check") + .arg("main.av") + .arg("--module-root") + .arg(&dir) + .output() + .expect("aver runs"); + assert!(dump.status.success(), "{}", format_output(&dump)); + let dumped = combined(&dump); + for helper in [ + "fn __waitPollByCollectingWatch(items: Map, timeoutMs: Int) -> Result, String>", + "fn __waitKeysAtCollectingWatch(", + ] { + assert!(dumped.contains(helper), "missing `{helper}`:\n{dumped}"); + } +} + +/// A recording of the hand-written waits replays: the recording holds the +/// `Int`-keyed wait every backend performs, and the replay performs it again. +#[test] +fn a_recording_of_carried_waits_replays() { + let dir = scratch("own-key-replay"); + let mut recorded = Command::new(aver_bin()); + recorded + .current_dir(fixture("run_wait_own_key")) + .arg("run") + .arg("main.av") + .arg("--module-root") + .arg(fixture("run_wait_own_key")) + .arg("--record") + .arg(&dir) + .args(["--", "manual"]); + let out = recorded.output().expect("aver runs"); + assert!(out.status.success(), "{}", format_output(&out)); + let recording = only_recording(&dir); + let mut command = Command::new(aver_bin()); + command.current_dir(fixture("run_wait_own_key")); + command.arg("replay").arg(&recording).arg("--check-args"); + let replayed = command.output().expect("aver replays"); + assert!(replayed.status.success(), "{}", format_output(&replayed)); + assert!( + combined(&replayed).contains("Output: MATCH"), + "{}", + format_output(&replayed) + ); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/tests/rust_work_spec.rs b/tests/rust_work_spec.rs index 6b35ad1e0..262999642 100644 --- a/tests/rust_work_spec.rs +++ b/tests/rust_work_spec.rs @@ -1022,3 +1022,28 @@ mod map_replay_regression { #[path = "rust_work_spec/native_transfer.rs"] mod native_transfer; + +/// A program whose generated loop keys its wait by `Int` and whose own waits +/// are keyed by a sum, in the entry and in a dependency, compiles to Rust and +/// does the same work as the VM on both of its paths: the loop, and the two +/// jobs it collects by hand. The jobs land in whatever order they finish, so +/// the lines are compared as a multiset. +#[test] +fn waits_keyed_by_a_sum_beside_the_generated_loop_match_the_vm() { + let name = "run_wait_own_key"; + let ws = temp_dir(name); + let project = ws.join("project"); + fs::create_dir_all(&project).expect("create project dir"); + let result = (|| -> Result<(), String> { + compile_rust(name, &project, name, &[])?; + let bin = cargo_build(&project, name)?; + for args in [&[][..], &["manual"][..]] { + let vm = run_vm_with(name, args)?; + let rust = run_binary_with(&bin, args)?; + same_lines(name, &vm, &rust)?; + } + Ok(()) + })(); + let _ = fs::remove_dir_all(&ws); + result.unwrap_or_else(|error| panic!("{error}")); +} diff --git a/tests/wasm_work_spec.rs b/tests/wasm_work_spec.rs index 0d7bf714c..055b2aec9 100644 --- a/tests/wasm_work_spec.rs +++ b/tests/wasm_work_spec.rs @@ -619,3 +619,21 @@ fn one_recording(dir: &Path) -> Result { other => Err(format!("expected exactly one recording, found {other}")), } } + +// ── Waits keyed by a type of the program's own beside the loop ────────── + +/// The generated loop keys its wait by `Int`, and the program's own waits, +/// in the entry and in a dependency, are keyed by a sum. Both wasm targets +/// run both paths and do the same work as the VM. The jobs land in whatever +/// order they finish, so the lines are compared as a multiset. +#[test] +fn waits_keyed_by_a_sum_beside_the_generated_loop_match_the_vm_on_both_wasm_targets() { + let name = "run_wait_own_key"; + for target in [&["--wasm-gc"][..], &["--wasip2"][..]] { + for args in [&[][..], &["manual"][..]] { + let vm = run(name, &[], args).unwrap_or_else(|error| panic!("{error}")); + let wasm = run(name, target, args).unwrap_or_else(|error| panic!("{error}")); + same_lines(name, &vm, &wasm).unwrap_or_else(|error| panic!("{error}")); + } + } +} From d8fd01b014c866276d4d0e5a7b753611da0e3883 Mon Sep 17 00:00:00 2001 From: jasisz Date: Fri, 25 Sep 2026 04:46:00 +0200 Subject: [PATCH 2/3] tests: run the wasip2 half only when the wasip2 feature is built Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/wasm_work_spec.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/wasm_work_spec.rs b/tests/wasm_work_spec.rs index 055b2aec9..bb37521da 100644 --- a/tests/wasm_work_spec.rs +++ b/tests/wasm_work_spec.rs @@ -63,6 +63,16 @@ fn run(name: &str, target: &[&str], program_args: &[&str]) -> Result Vec<&'static [&'static str]> { + let mut targets: Vec<&'static [&'static str]> = vec![&["--wasm-gc"]]; + if cfg!(feature = "wasip2") { + targets.push(&["--wasip2"]); + } + targets +} + fn target_name(target: &[&str]) -> &'static str { match target.first() { Some(&"--wasm-gc") => "wasm-gc", @@ -629,7 +639,7 @@ fn one_recording(dir: &Path) -> Result { #[test] fn waits_keyed_by_a_sum_beside_the_generated_loop_match_the_vm_on_both_wasm_targets() { let name = "run_wait_own_key"; - for target in [&["--wasm-gc"][..], &["--wasip2"][..]] { + for target in wasm_targets() { for args in [&[][..], &["manual"][..]] { let vm = run(name, &[], args).unwrap_or_else(|error| panic!("{error}")); let wasm = run(name, target, args).unwrap_or_else(|error| panic!("{error}")); From 84a7a198e3ea6011f12a00da215f2605862f2eca Mon Sep 17 00:00:00 2001 From: jasisz Date: Fri, 25 Sep 2026 06:25:58 +0200 Subject: [PATCH 3/3] tests: carry waits in a dependency that also matches nested patterns 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) --- .../run_wait_own_key_nested/aver.toml | 6 ++ .../fixtures/run_wait_own_key_nested/clock.av | 10 +++ .../run_wait_own_key_nested/collecting.av | 63 ++++++++++++++ .../fixtures/run_wait_own_key_nested/main.av | 44 ++++++++++ .../run_wait_own_key_nested/scoring.av | 11 +++ .../fixtures/run_wait_own_key_nested/ticks.av | 21 +++++ .../run_wait_own_key_nested/validation.av | 16 ++++ tests/run_all_spec.rs | 87 ++++++++++--------- tests/rust_work_spec.rs | 37 ++++---- tests/wasm_work_spec.rs | 17 ++-- 10 files changed, 249 insertions(+), 63 deletions(-) create mode 100644 tests/fixtures/run_wait_own_key_nested/aver.toml create mode 100644 tests/fixtures/run_wait_own_key_nested/clock.av create mode 100644 tests/fixtures/run_wait_own_key_nested/collecting.av create mode 100644 tests/fixtures/run_wait_own_key_nested/main.av create mode 100644 tests/fixtures/run_wait_own_key_nested/scoring.av create mode 100644 tests/fixtures/run_wait_own_key_nested/ticks.av create mode 100644 tests/fixtures/run_wait_own_key_nested/validation.av diff --git a/tests/fixtures/run_wait_own_key_nested/aver.toml b/tests/fixtures/run_wait_own_key_nested/aver.toml new file mode 100644 index 000000000..d8c878e9e --- /dev/null +++ b/tests/fixtures/run_wait_own_key_nested/aver.toml @@ -0,0 +1,6 @@ +[providers] +schema = 1 + +[[providers.bindings]] +capability = "Validation" +work = "Scoring.score" diff --git a/tests/fixtures/run_wait_own_key_nested/clock.av b/tests/fixtures/run_wait_own_key_nested/clock.av new file mode 100644 index 000000000..649326d70 --- /dev/null +++ b/tests/fixtures/run_wait_own_key_nested/clock.av @@ -0,0 +1,10 @@ +module Clock + kind = capability + semantics = effectful + intent = "The tick the processes count." + exposes [tick] + +operation tick() -> Int + ? "The number of the next tick." + oracle = generative + replay = recorded diff --git a/tests/fixtures/run_wait_own_key_nested/collecting.av b/tests/fixtures/run_wait_own_key_nested/collecting.av new file mode 100644 index 000000000..16ffca8f7 --- /dev/null +++ b/tests/fixtures/run_wait_own_key_nested/collecting.av @@ -0,0 +1,63 @@ +module Collecting + intent = "A wait keyed by a sum of this module's own, matched with nested patterns, in a dependency of a program whose generated loop keys its wait by Int." + depends [Validation] + exposes [Watch, collect] + effects [Console.print, Validation.take, Wait.poll] + +type Watch + Read(Int) + Write(Int) + +fn collect(pending: Map, turns: Int) -> Result + ? "Waits once per call until every job has landed; a wait that reports no key waits again." + ! [Console.print, Validation.take, Wait.poll] + match Map.len(pending) == 0 + true -> Result.Ok(Unit) + false -> match turns < 1 + true -> Result.Err("the jobs did not finish within the turns allowed") + false -> match Wait.poll(pending, 200) + Result.Ok([]) -> collect(pending, turns - 1) + Result.Ok(ready) -> collected(pending, ready, turns) + Result.Err(reason) -> Result.Err(reason) + +fn collected(pending: Map, ready: List, turns: Int) -> Result + ? "Takes every job the wait reported, then waits again for the rest." + ! [Console.print, Validation.take, Wait.poll] + remaining = harvest(pending, ready)? + collect(remaining, turns - 1) + +fn harvest(pending: Map, ready: List) -> Result, String> + ? "Takes one reported key at a time; a job that has not finished stays pending." + ! [Console.print, Validation.take] + match ready + [] -> Result.Ok(pending) + [key, ..rest] -> match Map.get(pending, key) + Option.None -> harvest(pending, rest) + Option.Some(item) -> harvest(took(pending, key, item)?, rest) + +fn took(pending: Map, key: Watch, item: Wait.Item) -> Result, String> + ? "One key: its job's score printed and the key dropped, or the key kept while the job runs." + ! [Console.print, Validation.take] + match item + Wait.Item.Socket(_) -> Result.Ok(Map.remove(pending, key)) + Wait.Item.Job(job) -> match Validation.take(job)? + Option.None -> Result.Ok(pending) + Option.Some(points) -> printed(Map.remove(pending, key), key, points) + +fn printed(pending: Map, key: Watch, points: Int) -> Result, String> + ? "Says which key landed and what its job scored." + ! [Console.print] + Console.print("{describe(key)} scored {points}") + Result.Ok(pending) + +fn describe(key: Watch) -> String + ? "A key as a line of output." + match key + Watch.Read(0) -> "read nothing" + Watch.Read(n) -> "read {n}" + Watch.Write(n) -> "write {n}" + +verify describe + describe(Watch.Read(0)) => "read nothing" + describe(Watch.Read(1)) => "read 1" + describe(Watch.Write(2)) => "write 2" diff --git a/tests/fixtures/run_wait_own_key_nested/main.av b/tests/fixtures/run_wait_own_key_nested/main.av new file mode 100644 index 000000000..3f56513e4 --- /dev/null +++ b/tests/fixtures/run_wait_own_key_nested/main.av @@ -0,0 +1,44 @@ +module Main + intent = "A program whose generated loop keys its wait by Int, and whose own waits, run with `manual`, are keyed by a sum and matched with nested patterns: once here in the entry and again in a dependency." + depends [Clock, Ticks, Validation, Scoring, Collecting] + effects [Args.get, Clock.tick, Console.print, Validation.begin, Validation.take, Wait.poll, yield] + +fn main() -> Result + ? "`manual` waits on two jobs keyed by Collecting.Watch; anything else runs the loop." + ! [Args.get, Console.print, Validation.begin, Validation.take, Wait.poll] + match List.contains(Args.get(), "manual") + true -> manual() + false -> Run.all() + +fn manual() -> Result + ? "Begins two jobs, waits once here on a set keyed by Collecting.Watch that holds nothing, and collects the jobs in Collecting." + ! [Console.print, Validation.begin, Validation.take, Wait.poll] + first = Validation.begin("alpha")? + second = Validation.begin("beta-two")? + idle: Map = {} + Console.print(emptiness(Wait.poll(idle, 0))) + Collecting.collect({Collecting.Watch.Read(1) => Wait.Item.Job(first), Collecting.Watch.Write(2) => Wait.Item.Job(second)}, 50) + +fn ticker() -> Unit + ? "Three ticks." + ! [Clock.tick, Console.print, yield] + ticking(0) + +fn ticking(seen: Int) -> Unit + ? "One tick at a time." + ! [Clock.tick, Console.print, yield] + match Clock.tick() >= 3 + true -> Console.print("ticked {seen + 1} times") + false -> ticking(seen + 1) + +fn emptiness(waited: Result, String>) -> String + ? "What a wait on an empty set reported." + match waited + Result.Ok([]) -> "an empty wait reported 0 keys" + Result.Ok(keys) -> "an empty wait reported {List.len(keys)} keys" + Result.Err(reason) -> "an empty wait failed: {reason}" + +verify emptiness + emptiness(Result.Ok([])) => "an empty wait reported 0 keys" + emptiness(Result.Ok([Collecting.Watch.Read(1)])) => "an empty wait reported 1 keys" + emptiness(Result.Err("closed")) => "an empty wait failed: closed" diff --git a/tests/fixtures/run_wait_own_key_nested/scoring.av b/tests/fixtures/run_wait_own_key_nested/scoring.av new file mode 100644 index 000000000..45c533e00 --- /dev/null +++ b/tests/fixtures/run_wait_own_key_nested/scoring.av @@ -0,0 +1,11 @@ +module Scoring + intent = "The pure function one scoring job runs." + exposes [score] + effects [] + +fn score(task: String) -> Int + ? "Scores one task by its length." + String.len(task) + +verify score + score("alpha") => 5 diff --git a/tests/fixtures/run_wait_own_key_nested/ticks.av b/tests/fixtures/run_wait_own_key_nested/ticks.av new file mode 100644 index 000000000..9cbe4d62b --- /dev/null +++ b/tests/fixtures/run_wait_own_key_nested/ticks.av @@ -0,0 +1,21 @@ +module Ticks + intent = "Answers Clock at once: every ask is the next tick, whoever asks." + depends [Clock] + exposes [State, fresh, tick] + effects [] + answers [Clock] + +record State + count: Int + +fn fresh() -> State + ? "No tick given yet." + State(count = 0) + +fn tick(state: State) -> Tuple> + ? "The next tick, answered in the turn it was asked." + next = state.count + 1 + (State(count = next), Result.Ok(next)) + +verify tick + tick(fresh()) => (State(count = 1), Result.Ok(1)) diff --git a/tests/fixtures/run_wait_own_key_nested/validation.av b/tests/fixtures/run_wait_own_key_nested/validation.av new file mode 100644 index 000000000..43166f14a --- /dev/null +++ b/tests/fixtures/run_wait_own_key_nested/validation.av @@ -0,0 +1,16 @@ +module Validation + kind = capability + semantics = effectful + depends [Work] + intent = "One job kind: scoring a text off the turn." + exposes [begin, take] + +operation begin(task: String) -> Result + ? "Starts one scoring off the turn and returns its job handle at once." + oracle = generativeOutput + replay = recorded + +operation take(job: Work.Job) -> Result, String> + ? "Collects one finished scoring: None while it runs, Some(score) once it finished." + oracle = generativeOutput + replay = recorded diff --git a/tests/run_all_spec.rs b/tests/run_all_spec.rs index af1f1adfa..7467372f5 100644 --- a/tests/run_all_spec.rs +++ b/tests/run_all_spec.rs @@ -1134,49 +1134,58 @@ fn a_directory_check_names_an_answer_module_under_a_subdirectory_the_way_its_imp /// A program whose generated loop keys its wait by `Int` can key its own /// waits by a sum, in the entry and in a dependency. Each such wait is /// carried through an `Int`-keyed wait by helpers generated for its key type, -/// and answers the same keys it would have answered, in the same order. +/// and answers the same keys it would have answered, in the same order. The +/// second fixture also matches the waits' answers with nested patterns, in +/// the entry and in the dependency function that waits: the dependency's +/// patterns are compiled first and its waits carried after. #[test] fn waits_keyed_by_a_sum_run_beside_the_generated_loop() { - let looped = aver_within("run_wait_own_key", &["run"], 60); - assert!(looped.status.success(), "{}", format_output(&looped)); - assert_eq!( - String::from_utf8_lossy(&looped.stdout).trim(), - "ticked 3 times" - ); + for name in ["run_wait_own_key", "run_wait_own_key_nested"] { + let looped = aver_within(name, &["run"], 60); + assert!(looped.status.success(), "{}", format_output(&looped)); + assert_eq!( + String::from_utf8_lossy(&looped.stdout).trim(), + "ticked 3 times" + ); - let manual = aver_within("run_wait_own_key", &["run", "--", "manual"], 60); - assert!(manual.status.success(), "{}", format_output(&manual)); - let text = String::from_utf8_lossy(&manual.stdout); - let mut lines: Vec<&str> = text.lines().collect(); - lines.sort_unstable(); - assert_eq!( - lines, - [ - "an empty wait reported 0 keys", - "read 1 scored 5", - "write 2 scored 8", - ], - "{}", - format_output(&manual) - ); + let manual = aver_within(name, &["run", "--", "manual"], 60); + assert!(manual.status.success(), "{}", format_output(&manual)); + let text = String::from_utf8_lossy(&manual.stdout); + let mut lines: Vec<&str> = text.lines().collect(); + lines.sort_unstable(); + assert_eq!( + lines, + [ + "an empty wait reported 0 keys", + "read 1 scored 5", + "write 2 scored 8", + ], + "{name}: {}", + format_output(&manual) + ); - let dir = fixture("run_wait_own_key"); - let dump = Command::new(aver_bin()) - .current_dir(&dir) - .env("AVER_YIELD_DUMP", "1") - .arg("check") - .arg("main.av") - .arg("--module-root") - .arg(&dir) - .output() - .expect("aver runs"); - assert!(dump.status.success(), "{}", format_output(&dump)); - let dumped = combined(&dump); - for helper in [ - "fn __waitPollByCollectingWatch(items: Map, timeoutMs: Int) -> Result, String>", - "fn __waitKeysAtCollectingWatch(", - ] { - assert!(dumped.contains(helper), "missing `{helper}`:\n{dumped}"); + let dir = fixture(name); + let dump = Command::new(aver_bin()) + .current_dir(&dir) + .env("AVER_YIELD_DUMP", "1") + .arg("check") + .arg("main.av") + .arg("--module-root") + .arg(&dir) + .output() + .expect("aver runs"); + assert!(dump.status.success(), "{}", format_output(&dump)); + let dumped = combined(&dump); + for helper in [ + "fn __waitPollByCollectingWatch(items: Map, timeoutMs: Int) -> Result, String>", + "fn __waitKeysAtCollectingWatch(", + "fn __waitPollByWatch(items: Map, timeoutMs: Int) -> Result, String>", + ] { + assert!( + dumped.contains(helper), + "{name}: missing `{helper}`:\n{dumped}" + ); + } } } diff --git a/tests/rust_work_spec.rs b/tests/rust_work_spec.rs index 7d2a33d1e..3ccdb4801 100644 --- a/tests/rust_work_spec.rs +++ b/tests/rust_work_spec.rs @@ -1075,25 +1075,28 @@ mod native_transfer; /// are keyed by a sum, in the entry and in a dependency, compiles to Rust and /// does the same work as the VM on both of its paths: the loop, and the two /// jobs it collects by hand. The jobs land in whatever order they finish, so -/// the lines are compared as a multiset. +/// the lines are compared as a multiset. The second fixture also matches the +/// waits' answers with nested patterns, in the entry and in a dependency +/// function that waits. #[test] fn waits_keyed_by_a_sum_beside_the_generated_loop_match_the_vm() { - let name = "run_wait_own_key"; - let ws = temp_dir(name); - let project = ws.join("project"); - fs::create_dir_all(&project).expect("create project dir"); - let result = (|| -> Result<(), String> { - compile_rust(name, &project, name, &[])?; - let bin = cargo_build(&project, name)?; - for args in [&[][..], &["manual"][..]] { - let vm = run_vm_with(name, args)?; - let rust = run_binary_with(&bin, args)?; - same_lines(name, &vm, &rust)?; - } - Ok(()) - })(); - let _ = fs::remove_dir_all(&ws); - result.unwrap_or_else(|error| panic!("{error}")); + for name in ["run_wait_own_key", "run_wait_own_key_nested"] { + let ws = temp_dir(name); + let project = ws.join("project"); + fs::create_dir_all(&project).expect("create project dir"); + let result = (|| -> Result<(), String> { + compile_rust(name, &project, name, &[])?; + let bin = cargo_build(&project, name)?; + for args in [&[][..], &["manual"][..]] { + let vm = run_vm_with(name, args)?; + let rust = run_binary_with(&bin, args)?; + same_lines(name, &vm, &rust)?; + } + Ok(()) + })(); + let _ = fs::remove_dir_all(&ws); + result.unwrap_or_else(|error| panic!("{error}")); + } } // ── Run.fail ──────────────────────────────────────────────────────────── diff --git a/tests/wasm_work_spec.rs b/tests/wasm_work_spec.rs index d1fdd79b6..4ea398359 100644 --- a/tests/wasm_work_spec.rs +++ b/tests/wasm_work_spec.rs @@ -635,15 +635,18 @@ fn one_recording(dir: &Path) -> Result { /// The generated loop keys its wait by `Int`, and the program's own waits, /// in the entry and in a dependency, are keyed by a sum. Both wasm targets /// run both paths and do the same work as the VM. The jobs land in whatever -/// order they finish, so the lines are compared as a multiset. +/// order they finish, so the lines are compared as a multiset. The second +/// fixture also matches the waits' answers with nested patterns, in the entry +/// and in a dependency function that waits. #[test] fn waits_keyed_by_a_sum_beside_the_generated_loop_match_the_vm_on_both_wasm_targets() { - let name = "run_wait_own_key"; - for target in wasm_targets() { - for args in [&[][..], &["manual"][..]] { - let vm = run(name, &[], args).unwrap_or_else(|error| panic!("{error}")); - let wasm = run(name, target, args).unwrap_or_else(|error| panic!("{error}")); - same_lines(name, &vm, &wasm).unwrap_or_else(|error| panic!("{error}")); + for name in ["run_wait_own_key", "run_wait_own_key_nested"] { + for target in wasm_targets() { + for args in [&[][..], &["manual"][..]] { + let vm = run(name, &[], args).unwrap_or_else(|error| panic!("{error}")); + let wasm = run(name, target, args).unwrap_or_else(|error| panic!("{error}")); + same_lines(name, &vm, &wasm).unwrap_or_else(|error| panic!("{error}")); + } } } }