Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ jobs:
wasm_gc_effect_arg_overflow_regression \
wasm_gc_games_differential_mir \
wasm_gc_handler_carrier \
wasm_gc_map_versions_spec \
wasm_gc_optimize_trunc_sat \
wasm_gc_packed_sequence \
wasm_gc_perslot_int_unboxing_differential \
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The generated loop is now written from the program's source alone, and the manif

### Fixed

- **wasm-gc: `Map.set` no longer copies the map, and `Map.remove` no longer changes the map it was given.** `set` copied every bucket unless the compiler could prove the map had no other holder, which it cannot for a map held in a record field such as an answer module's state. A 100 000-entry state map served 2000 `Map.set` requests in 11 s under `aver run --wasm-gc` and 1.9 s on Node 26. `remove` wrote into the map it was given, so a caller that still held that map saw the key gone. Both now write into the map's arrays in place and return a new version. The version they were given stays valid, because a record of what the write replaced is kept with it. The same run now takes 0.4 s under `aver run --wasm-gc` and 0.3 s on Node. Reading an older version again costs one step for each write made since.
- **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.
- **`check` no longer asks a verify block of a function no verify case can call.** A parameter of a capability resource type (`Tcp.Connection`, `Work.Job`, a job kind's handle), or of a tuple, record or sum of the module that always carries one, has no value a case can write, so such a pure branching helper failed `error[missing-verify]` with no way to satisfy it. It is now exempt, the way effectful functions are. A parameter with an empty value (`List`, `Option`, a sum with a resource-free variant) still needs its verify block.
- **A `main` that answers `Err` exits non-zero on wasm-gc and wasip2**, with the error on stderr on wasm-gc, as it already did on the VM and in generated Rust. Both wasm targets used to exit zero.
Expand Down
4 changes: 4 additions & 0 deletions src/codegen/wasm_gc/body/builtins_wasip2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,9 @@ fn emit_poll_wasip2(
WasmGcError::Validation(format!("{operation} on wasip2: helper fn idx missing"))
})?;
emit_mir_expr(func, &args[0], slots, ctx)?;
// The poll helper walks the map's buckets directly: hand it the
// current version.
super::from_mir::emit_map_arg_current(func, &args[0], ctx)?;
emit_mir_expr(func, &args[1], slots, ctx)?;
func.instruction(&Instruction::Call(helper));
Ok(())
Expand Down Expand Up @@ -1750,6 +1753,7 @@ fn emit_http_simple_method_wasip2(
func.instruction(&Instruction::ArrayNewDefault(map_slots.values_array));
func.instruction(&Instruction::I32Const(INITIAL_CAP));
func.instruction(&Instruction::ArrayNewDefault(map_slots.hashes_array));
crate::codegen::wasm_gc::maps::emit_no_diff(func, map_slots);
func.instruction(&Instruction::StructNew(map_slots.map));
func.instruction(&Instruction::Call(fn_idx));
Ok(())
Expand Down
37 changes: 27 additions & 10 deletions src/codegen/wasm_gc/body/from_mir/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1966,11 +1966,34 @@ pub(crate) fn emit_mir_result_from_option(
Ok(MirBuiltinEmit::Produced(true))
}

/// With a `Map` argument of `arg`'s type on the stack, make it the current
/// version of its map (`maps/versions.rs`). A host import reads the map's
/// buckets directly and cannot do it itself. Any other type is left alone.
pub(crate) fn emit_map_arg_current(
func: &mut Function,
arg: &Spanned<MirExpr>,
ctx: &EmitCtx<'_>,
) -> Result<(), WasmGcError> {
let aver = aver_type_str_of(arg);
let canonical: String = aver.chars().filter(|c| !c.is_whitespace()).collect();
if !canonical.starts_with("Map<") {
return Ok(());
}
let helpers = ctx
.fn_map
.map_helpers_lookup(&canonical)
.ok_or(WasmGcError::Validation(format!(
"a `{aver}` argument crosses to the host but no Map helpers are registered for it"
)))?;
func.instruction(&Instruction::Call(helpers.reroot));
Ok(())
}

/// Mirror of `emit_map_kv_call`: the `Map.*` methods dispatch to the
/// per-`Map<K,V>` helpers (`fn_map.map_helpers_lookup`). `has` reuses
/// the `get_pair` helper and drops the value; `set` picks `set_in_place`
/// vs the clone-on-write `set` by `mir_arg_uniquely_owned` (the MIR
/// analogue of the oracle's `arg_uniquely_owned`). The canonical comes
/// the `get_pair` helper and drops the value. `set` and `remove` need no
/// ownership fact: they write in place and leave the map they were given
/// valid as an older version (`maps/versions.rs`). The canonical comes
/// from the map arg's stamped type; every arg recurses `emit_mir_expr`.
pub(crate) fn emit_mir_map_builtin(
func: &mut Function,
Expand Down Expand Up @@ -2062,13 +2085,7 @@ pub(crate) fn emit_mir_map_builtin(
"Map.{method}: map argument has type `{map_aver}` but no helpers are registered"
)))?;
match method {
"set" => {
if mir_arg_uniquely_owned(&args[0], ctx) {
helpers.set_in_place
} else {
helpers.set
}
}
"set" => helpers.set,
"get" => helpers.get,
"len" => helpers.len,
"keys" => helpers.keys,
Expand Down
5 changes: 5 additions & 0 deletions src/codegen/wasm_gc/body/from_mir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub(super) use super::slots::count_value_params;
pub(super) use super::{CallerFnCollector, EmitCtx, FnMap, SlotTable, Wasip2Lowering};

mod builtins;
pub(super) use builtins::emit_map_arg_current;
mod collections;
mod constructors;
mod control;
Expand Down Expand Up @@ -922,6 +923,10 @@ pub(crate) fn emit_mir_expr(
if emit_mir_expr(func, arg, slots, ctx)?.is_none() {
return Ok(None);
}
// The host reads a map's buckets directly
// (`Tcp.poll`, `Wait.poll`), so hand it the
// current version.
builtins::emit_map_arg_current(func, arg, ctx)?;
if ctx.registry.bignum && int_args.contains(&i) {
// CHECKED (not saturating): an out-of-i64 Int
// crossing the host effect boundary must
Expand Down
Loading
Loading