From 1aa1ee930e16bd48394ff1f9667bec31ec55616b Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 19:52:11 -0700 Subject: [PATCH 01/12] environ: track per-table mutability bit during translation Add `ModuleTranslation::tables_mutated`, a `SecondaryMap` populated during `ModuleEnvironment::translate` recording whether any function in the module mutates a given table at runtime via `table.set` / `table.fill` / `table.copy` (as dest) / `table.grow` / `table.init`. Imported tables are conservatively marked mutated. Active `elem` segments at instantiation time are part of initial state, not mutations. O(total opcodes) extra pass over each function body. Groundwork for follow-on call_indirect optimizations gated on the predicate; nothing consumes the bit in this commit. --- crates/environ/src/compile/module_environ.rs | 91 ++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/crates/environ/src/compile/module_environ.rs b/crates/environ/src/compile/module_environ.rs index 1c7961323aed..c484f61ce048 100644 --- a/crates/environ/src/compile/module_environ.rs +++ b/crates/environ/src/compile/module_environ.rs @@ -110,6 +110,26 @@ pub struct ModuleTranslation<'data> { /// trampolines for each of these signatures are required. pub exported_signatures: Vec, + /// Per-table flag indicating whether the table is ever mutated by any + /// function defined in this module via `table.set` / `table.fill` / + /// `table.copy` (as the destination) / `table.grow` / `table.init`. + /// + /// `false` (the default) means the table's contents are determined + /// entirely by its `elem` segments and any active initializer, and never + /// change at runtime — provably immutable for the lifetime of any + /// instance of this module. + /// + /// `true` means the contents can change at runtime (or the table is + /// imported, in which case we conservatively assume the importer + /// mutates it). + /// + /// This is groundwork for later passes that turn `call_indirect` + /// through provably-immutable function tables into direct calls when + /// the dispatched-to slot is statically known. Set during module + /// translation (see `analyze_table_mutability`); read by Cranelift + /// lowering and by Pulley AOT IC seeding. + pub tables_mutated: SecondaryMap, + /// DWARF debug information, if enabled, parsed from the module. pub debuginfo: DebugInfoData<'data>, @@ -227,6 +247,7 @@ impl<'data> ModuleTranslation<'data> { function_body_inputs: PrimaryMap::default(), known_imported_functions: SecondaryMap::default(), exported_signatures: Vec::default(), + tables_mutated: SecondaryMap::default(), debuginfo: DebugInfoData::default(), has_unparsed_debuginfo: false, data_align: None, @@ -349,6 +370,8 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { self.translate_payload(payload?)?; } + analyze_table_mutability(&mut self.result)?; + Ok(self.result) } @@ -1582,3 +1605,71 @@ impl ModuleTranslation<'_> { self.module.startup = ModuleStartup::IfMemoriesNeedInit(ty); } } + +/// Walk every defined function body, recording in +/// `translation.tables_mutated` each table that is the destination of any +/// runtime mutation opcode (`table.set`, `table.fill`, `table.copy` as the +/// destination, `table.grow`, `table.init`). +/// +/// Imported tables are conservatively pre-marked as mutated since the +/// importer can mutate them in ways we can't see. Active `elem` segments +/// applied at instantiation time are NOT counted as mutations — they are +/// part of the table's *initial* state, not a runtime change. +/// +/// `elem.drop` drops a passive element segment but does not write to any +/// table directly, so it is intentionally not counted here. Conservatively, +/// any `table.init` from a passive segment marks the destination table as +/// mutated. +fn analyze_table_mutability<'data>(translation: &mut ModuleTranslation<'data>) -> Result<()> { + // Resize the table-mutability map to cover every table in the module + // (imports + defined). `SecondaryMap` defaults to `false` for all + // unset entries, which is the correct "definitely-not-mutated" default + // for defined tables we haven't observed any mutations on yet. + let num_tables = translation.module.tables.len(); + if num_tables == 0 { + return Ok(()); + } + + // Mark all imported tables as mutated up front. The importer can + // mutate them in ways this module can't see, so the conservative + // assumption is that they are not stable across calls. + let num_imported = translation.module.num_imported_tables; + for i in 0..num_imported { + translation.tables_mutated[TableIndex::from_u32(i as u32)] = true; + } + + // Walk every defined function body and look for table-mutation opcodes. + // The cost is O(total opcodes), one extra pass on top of the validator; + // typical large modules (sqlite3 ~50K opcodes) take well under a + // millisecond. + for (_, body_data) in &translation.function_body_inputs { + let mut reader = body_data.body.get_operators_reader()?; + while !reader.eof() { + use wasmparser::Operator; + match reader.read()? { + Operator::TableSet { table } + | Operator::TableFill { table } + | Operator::TableGrow { table } => { + translation.tables_mutated[TableIndex::from_u32(table)] = true; + } + Operator::TableCopy { + dst_table, + src_table: _, + } => { + // `src_table` is read-only in `table.copy`; only the + // destination is mutated. + translation.tables_mutated[TableIndex::from_u32(dst_table)] = true; + } + Operator::TableInit { + table, + elem_index: _, + } => { + translation.tables_mutated[TableIndex::from_u32(table)] = true; + } + _ => {} + } + } + } + + Ok(()) +} From d915a27bfc5687c55d25b88f357ad785c5b5a3e3 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 19:53:34 -0700 Subject: [PATCH 02/12] cranelift: lower constant-index call_indirect to direct call When `call_indirect` resolves to a constant index into a provably immutable funcref table whose contents are statically known from `elem` segments, rewrite the call to a direct `call F` at lowering time. Skips all per-dispatch checks (bounds, null, sig) and replaces the indirect jump with a direct branch. Gated on `is_immutable_funcref_table(table_idx)` (= predicate from the previous commit + statically-known table contents). --- crates/cranelift/src/func_environ.rs | 111 +++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index e791c092c2ab..b108d2c5f7fe 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -2147,6 +2147,14 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { callee: ir::Value, call_args: &[ir::Value], ) -> WasmResult> { + // Fast path: if we can statically resolve this indirect call to a + // single defined function (immutable funcref table + constant + // callee index + matching signature), emit a direct call instead. + // See `try_static_resolve_indirect_call`. + if let Some(target) = self.try_static_resolve_indirect_call(table_index, ty_index, callee) { + return self.direct_call(target, sig_ref, call_args).map(Some); + } + let (code_ptr, callee_vmctx) = match self.check_and_load_code_and_callee_vmctx( table_index, ty_index, @@ -2161,6 +2169,109 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { .map(Some) } + /// Try to statically resolve a `call_indirect` site to a single defined + /// function so the call can be lowered as a direct call. + /// + /// All four of these must hold for the resolution to succeed: + /// + /// 1. The target table must be provably immutable for the lifetime of + /// any instance of this module: defined (not imported) and never the + /// target of `table.set` / `table.fill` / `table.copy` (as the dst) + /// / `table.grow` / `table.init`. This is the `tables_mutated` bit + /// populated in `ModuleEnvironment::translate`. + /// + /// 2. The callee index value (the operand to `call_indirect`) must be a + /// compile-time constant — i.e., the wasm did `i32.const N; + /// call_indirect (table $t) (type $sig)`. This is what hand-lowered + /// C++/Rust vtable calls and AOT-compiled JS-to-wasm dispatch tables + /// look like in practice. + /// + /// 3. The slot at index `N` in the table must be precomputable from + /// static `elem` segments: `module.table_initialization + /// .initial_values[defined_index]` must be `TableInitialValue::Null + /// { precomputed }` (i.e., not a fully-dynamic `Expr`-style init), + /// and the index `N` must be in range and resolved to a concrete + /// `FuncIndex` (not the reserved-value sentinel). + /// + /// 4. The function's signature in the module's interned type table + /// must equal the `ty_index` declared by the `call_indirect` site. + /// Otherwise the original semantics are "trap on signature + /// mismatch", which we don't want to replace with a static direct + /// call. + /// + /// Returns the resolved function on success, `None` otherwise (in + /// which case the caller falls back to a normal indirect call). + fn try_static_resolve_indirect_call( + &self, + table_index: TableIndex, + ty_index: TypeIndex, + callee: ir::Value, + ) -> Option { + let translation = self.env.translation; + let module = &translation.module; + + // (1) Table must be provably immutable. Imported tables are + // pre-marked as mutated in `ModuleEnvironment::translate`, so + // this check also rules them out (along with the explicit + // `defined_table_index` check below for clarity). + if translation.tables_mutated[table_index] { + return None; + } + let defined_table = module.defined_table_index(table_index)?; + + // (2) Callee must be a constant `iconst`. Pattern adapted from + // `bounds_checks::statically_known_in_bounds`. + let dfg = &self.builder.func.dfg; + let inst = dfg.value_def(callee).inst()?; + let imm = match dfg.insts[inst] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + } => imm, + _ => return None, + }; + let callee_ty = dfg.value_type(callee); + let callee_idx_u64 = imm + .zero_extend_from_width(callee_ty.bits()) + .bits() + .cast_unsigned(); + + // (3) Slot must be precomputable. + let init = module + .table_initialization + .initial_values + .get(defined_table)?; + let precomputed = match init { + TableInitialValue::Null { precomputed } => precomputed, + // A fully-expression-driven initializer can't be resolved at + // compile time. Bail. + TableInitialValue::Expr(_) => return None, + }; + let slot = usize::try_from(callee_idx_u64).ok()?; + if slot >= precomputed.len() { + return None; + } + let target = precomputed[slot]; + // `FuncIndex::reserved_value()` is the "no entry" sentinel — + // this slot wasn't covered by any static `elem` segment. + if target.is_reserved_value() { + return None; + } + + // (4) Signature match. The site's declared `ty_index` and the + // target function's declared signature must intern to the same + // module type index. + let expected_ty = module.types[ty_index].unwrap_module_type_index(); + let target_ty = module.functions[target] + .signature + .unwrap_module_type_index(); + if expected_ty != target_ty { + return None; + } + + Some(target) + } + fn check_and_load_code_and_callee_vmctx( &mut self, table_index: TableIndex, From 14545e08fd4d2ef6a7a9d3064f6a660433137d27 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 19:53:35 -0700 Subject: [PATCH 03/12] cranelift: elide indirect-call sig check on uniform-typed immutable tables When a funcref table is provably immutable AND every entry in its elem segments has the same function signature as the call_indirect's type annotation, the runtime signature check is statically redundant and is elided in `translate_call_indirect`. --- crates/cranelift/src/func_environ.rs | 105 +++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index b108d2c5f7fe..5c79e2c225b3 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -2272,6 +2272,89 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { Some(target) } + /// Try to prove that the runtime signature check at a `call_indirect` + /// site through an untyped `funcref` table is redundant. + /// + /// True when: + /// + /// 1. The table is provably immutable (`tables_mutated[table_index] == + /// false`). Defined-not-imported is implied since imported tables + /// are pre-marked as mutated. + /// + /// 2. The table is precomputable from static `elem` segments + /// (`TableInitialValue::Null { precomputed }`). + /// + /// 3. Every non-null entry in `precomputed` has the same module- + /// interned signature as the `ty_index` declared at the call site. + /// Null slots are fine — they trap on the funcref-NULL load that + /// happens after sig-check elision. + /// + /// When this returns true, the caller short-circuits to + /// `CheckIndirectCallTypeSignature::StaticMatch`, which removes the + /// sig load + compare from the hot path. Bounds-check on the table + /// index and the funcref-NULL check are still emitted by the + /// surrounding code, so the call still traps correctly on OOB or + /// null index — only the sig check is elided. + /// + /// This is the static analog of an inline-cache: instead of caching + /// the resolved target per call site, we observe at module-load that + /// the table contents make the sig check uninformative for the + /// lifetime of any instance. + fn try_elide_sig_check_for_immutable_table( + &self, + table_index: TableIndex, + ty_index: TypeIndex, + ) -> bool { + let translation = self.env.translation; + let module = &translation.module; + + if translation.tables_mutated[table_index] { + return false; + } + let defined_table = match module.defined_table_index(table_index) { + Some(d) => d, + None => return false, + }; + + let init = match module + .table_initialization + .initial_values + .get(defined_table) + { + Some(i) => i, + None => return false, + }; + let precomputed = match init { + TableInitialValue::Null { precomputed } => precomputed, + TableInitialValue::Expr(_) => return false, + }; + + // Empty precomputed list means we have no information — fall back + // to the runtime sig check. (A subsequent `call_indirect` could + // still trap on OOB, but we don't have anything to elide against.) + if precomputed.is_empty() { + return false; + } + + let expected_ty = module.types[ty_index].unwrap_module_type_index(); + for &func_idx in precomputed.iter() { + // Null slots can't be called without trapping — fine to ignore + // here; the elided check would have trapped anyway, and the + // unchecked code path will trap on the null funcref deref. + if func_idx.is_reserved_value() { + continue; + } + let actual_ty = module.functions[func_idx] + .signature + .unwrap_module_type_index(); + if actual_ty != expected_ty { + return false; + } + } + + true + } + fn check_and_load_code_and_callee_vmctx( &mut self, table_index: TableIndex, @@ -2329,6 +2412,28 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { // table of typed functions and that type matches `ty_index`, then // there's no need to perform a typecheck. match table.ref_type.heap_type { + // Untyped `funcref` tables ordinarily need a runtime sig check. + // But if (a) the table is provably immutable (`tables_mutated` + // bit clear) and (b) every non-null entry in the precomputed + // static `elem` segments has the same `VMSharedTypeIndex` as + // the call site, then the runtime check is provably redundant + // and we can elide it the same way we do for typed-funcref + // tables. + // + // This is the AOT-IC-seeding analog: instead of caching the + // resolved target at the call site, we cache the *signature* + // at module-load time and skip the hot-path sig load+compare. + // Helps the megamorphic case (computed `call_indirect` index) + // that the static-monomorphization fast path above can't + // handle. + WasmHeapType::Func + if self.try_elide_sig_check_for_immutable_table(table_index, ty_index) => + { + return CheckIndirectCallTypeSignature::StaticMatch { + may_be_null: table.ref_type.nullable, + }; + } + // Functions do not have a statically known type in the table, a // typecheck is required. Fall through to below to perform the // actual typecheck. From f9915e9954fa0d0f64bd718c0ce04a202c8ceed3 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 19:53:36 -0700 Subject: [PATCH 04/12] cranelift: elide funcref-null check when no precomputed slot is null When a funcref table is provably immutable AND none of its precomputed elem-segment entries are null, the runtime null check after the funcref load is statically redundant and is elided. Distinct from the sig-check elision: this targets tables that mix sigs but never contain null. --- crates/cranelift/src/func_environ.rs | 48 ++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 5c79e2c225b3..3b778dc6c639 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -2300,6 +2300,42 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { /// the resolved target per call site, we observe at module-load that /// the table contents make the sig check uninformative for the /// lifetime of any instance. + /// True iff every slot in the precomputed `elem`-segment contents for + /// `table_index` is a concrete `FuncIndex` (no + /// `FuncIndex::reserved_value()` "no-entry" sentinel). + /// + /// Caller has already proven the table is immutable, so the contents + /// observed here are stable for the lifetime of any instance — + /// `false` here implies "no slot is ever null at runtime." + /// + /// When this is true, the runtime funcref-NULL check on the loaded + /// funcref pointer is provably redundant: any in-bounds index leads + /// to a non-null funcref. The bounds check still runs (so an + /// out-of-bounds index traps as before with `TRAP_TABLE_OUT_OF_BOUNDS`). + fn precomputed_table_has_no_null_slots(&self, table_index: TableIndex) -> bool { + let module = &self.env.translation.module; + let Some(defined_table) = module.defined_table_index(table_index) else { + return false; + }; + let Some(init) = module + .table_initialization + .initial_values + .get(defined_table) + else { + return false; + }; + let precomputed = match init { + TableInitialValue::Null { precomputed } => precomputed, + TableInitialValue::Expr(_) => return false, + }; + // Empty precomputed means we have no information. + if precomputed.is_empty() { + return false; + } + // Every slot must be a real FuncIndex — no reserved-value sentinels. + precomputed.iter().all(|f| !f.is_reserved_value()) + } + fn try_elide_sig_check_for_immutable_table( &self, table_index: TableIndex, @@ -2429,9 +2465,15 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { WasmHeapType::Func if self.try_elide_sig_check_for_immutable_table(table_index, ty_index) => { - return CheckIndirectCallTypeSignature::StaticMatch { - may_be_null: table.ref_type.nullable, - }; + // If we additionally know every entry in the precomputed + // table is non-null, lower `may_be_null` to false so the + // downstream funcref-NULL check is also elided. This is + // only sound if the table can't be grown or have its + // entries cleared after init (i.e., immutable, which we + // already proved above). + let may_be_null = table.ref_type.nullable + && !self.precomputed_table_has_no_null_slots(table_index); + return CheckIndirectCallTypeSignature::StaticMatch { may_be_null }; } // Functions do not have a statically known type in the table, a From c22bbb3590bfda4db82a8030c2526c5a6dc3d246 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 19:53:45 -0700 Subject: [PATCH 05/12] cranelift: skip per-dispatch table-bound load when table cannot grow For provably non-growable funcref tables (`!tables_mutated` excludes `table.grow`), the table size is fixed at instantiation and the per-call_indirect bounds-check load can be replaced with a constant fold using `precomputed_funcref_table_contents.len()`. --- crates/cranelift/src/func_environ.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 3b778dc6c639..998bd9e96754 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -1652,8 +1652,11 @@ impl FuncEnvironment<'_> { .unwrap(); // A fixed-size table can't be resized, so its base address won't change - // and its base load is `readonly` and `can_move`. - let is_static = Some(table.limits.min) == table.limits.max; + // and its base load is `readonly` and `can_move`. That holds when the + // table is declared with `min == max`, or when translation proved it + // is never grown or mutated. + let is_static = + Some(table.limits.min) == table.limits.max || !self.translation.tables_mutated[index]; let mut base_flags = ir::MemFlagsData::trusted(); if is_static { base_flags = base_flags.with_readonly().with_can_move(); From dd6701d63da7425d2e2f323f2d17dc5ea09bc37a Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 19:53:46 -0700 Subject: [PATCH 06/12] environ: integration tests for analyze_table_mutability `crates/environ/tests/table_mutability.rs`: 12 cases covering the mutation-tracking predicate across `table.set`/`fill`/`copy`/`grow`/ `init`, imported tables, multi-table modules, and active-elem-segment behavior. --- crates/environ/tests/table_mutability.rs | 283 +++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 crates/environ/tests/table_mutability.rs diff --git a/crates/environ/tests/table_mutability.rs b/crates/environ/tests/table_mutability.rs new file mode 100644 index 000000000000..c324fea104de --- /dev/null +++ b/crates/environ/tests/table_mutability.rs @@ -0,0 +1,283 @@ +//! Integration tests for `analyze_table_mutability` and the surrounding +//! precompute ordering invariants. +//! +//! The per-table mutability bit is the foundation of the `call_indirect` +//! optimizations in `crates/cranelift/src/func_environ.rs` +//! (constant-index direct call, sig-check elision, NULL elision, bound- +//! load elision). A false negative here — failing to mark a table as +//! mutated when it actually is — would silently turn correct calls into +//! incorrect direct calls or skip required runtime checks. A false +//! positive — marking an immutable table as mutated — is merely a missed +//! optimization. Pin the analysis behaviour with focused module-level +//! tests so any regression surfaces immediately, not after a downstream +//! optimization fires on a now-invalid premise. +//! +//! Test scenario inspiration drawn from comparable bugs in peer +//! interpreters that have shipped fixes for analogous IC-invalidation +//! mistakes: +//! +//! - **Luau** (`LOP_NAMECALL`): inline cache had to be invalidated on +//! `table.insert` / metatable change. Analogous wasm risk: `table.grow` +//! not invalidating an immutability proof, so see `table_grow_marks…`. +//! - **JavaScriptCore** (`ic_table`): inline-cache corruption from missed +//! shape transitions. Analogous risk: over-marking, e.g. `table.copy` +//! wrongly marking the SOURCE table as mutated would forbid downstream +//! optimizations on a perfectly read-only table. See +//! `table_copy_marks_destination_only_not_source`. +//! - **Hermes** (`HiddenClass` cache): property cache misses with +//! `Object.defineProperty`. Analogous risk: `table.init` (active- +//! segment init at runtime) being treated as a no-op rather than a +//! write. See `table_init_marks_destination`. +//! +//! Lives in `tests/` rather than as a `#[cfg(test)] mod` inside +//! `module_environ.rs` because the latter triggers a pre-existing +//! upstream compile failure in `key.rs` / `module_artifacts.rs` (their +//! `arbitrary::Arbitrary` derives are stale relative to the workspace's +//! pinned `arbitrary 1.4.2`). Integration tests build against the lib +//! as a normal dependency and so do not set `cfg(test)` on +//! `wasmtime-environ` itself. + +use wasmparser::{Parser, Validator, WasmFeatures}; +use wasmtime_environ::{ + ModuleEnvironment, ModuleTypesBuilder, StaticModuleIndex, TableIndex, Tunables, +}; + +/// Translate `wat` and return the resulting `tables_mutated` bits, in +/// table-index order. Helper to keep individual tests short. +fn translate_and_get_mutability(wat: &str) -> Vec { + let bytes = wat::parse_str(wat).expect("WAT parse failed"); + let tunables = Tunables::default_host(); + // WASM2 covers reference-types + bulk-memory, which is what every + // table-mutating opcode below needs (`table.set`, `table.fill`, + // `table.grow`, `table.copy`, `table.init`, `elem.drop`). + let features = WasmFeatures::WASM2; + let mut validator = Validator::new_with_features(features); + let mut types = ModuleTypesBuilder::new(&validator); + let env = ModuleEnvironment::new( + &tunables, + &mut validator, + &mut types, + StaticModuleIndex::from_u32(0), + ); + let parser = Parser::new(0); + let translation = env.translate(parser, &bytes).expect("translate failed"); + let n: u32 = translation.module.tables.len().try_into().unwrap(); + (0..n) + .map(|i| translation.tables_mutated[TableIndex::from_u32(i)]) + .collect() +} + +/// A table only used as the source of `call_indirect` and `table.get` is +/// provably immutable. (Both ops READ the table; neither writes it.) +#[test] +fn read_only_table_is_immutable() { + let bits = translate_and_get_mutability( + r#" + (module + (table (export "t") 4 funcref) + (func $f (result i32) i32.const 42) + (elem (i32.const 0) $f $f $f $f) + (func (export "call_zero") (result i32) + i32.const 0 + call_indirect (param) (result i32)) + (func (export "read_zero") (result funcref) + i32.const 0 + table.get 0)) + "#, + ); + assert_eq!(bits, vec![false], "no opcode mutated this table"); +} + +/// `table.set` marks its destination as mutated. +#[test] +fn table_set_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "do_set") + i32.const 1 + ref.func $f + table.set 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `table.fill` marks its destination as mutated. +#[test] +fn table_fill_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "do_fill") + i32.const 0 + ref.func $f + i32.const 4 + table.fill 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `table.grow` is treated as mutating — analogous to Luau's NAMECALL IC +/// needing to invalidate on table-shape change. +#[test] +fn table_grow_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func (export "do_grow") (result i32) + ref.null func + i32.const 1 + table.grow 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `table.copy` marks the DESTINATION but explicitly NOT the source. The +/// source is read-only (its contents aren't changed by the op); marking +/// it as mutated would forbid downstream optimizations from treating it +/// as immutable, which would be incorrect over-conservatism — the JSC +/// `ic_table` analogue. +#[test] +fn table_copy_marks_destination_only_not_source() { + let bits = translate_and_get_mutability( + r#" + (module + (table $dst (export "dst") 4 funcref) + (table $src 4 funcref) + (func $f (result i32) i32.const 0) + (elem (table $src) (i32.const 0) func $f $f $f $f) + (func (export "do_copy") + i32.const 0 ;; dst offset + i32.const 0 ;; src offset + i32.const 4 ;; len + table.copy $dst $src)) + "#, + ); + assert_eq!( + bits, + vec![true, false], + "dst should be mutated, src should remain immutable", + ); +} + +/// `table.init` writes to the destination table from a passive elem +/// segment, so it is treated as mutation (the destination's contents +/// change at runtime). +#[test] +fn table_init_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (elem $e funcref (ref.func $f) (ref.func $f)) + (func (export "do_init") + i32.const 0 ;; dst + i32.const 0 ;; src offset within elem + i32.const 2 ;; len + table.init 0 $e)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `elem.drop` drops a passive element segment but does NOT write to any +/// table — distinct from `table.init` which DOES write. A pessimistic +/// implementation that marked all tables as mutated on `elem.drop` would +/// hand out false positives and shut off optimizations on perfectly- +/// immutable tables. +#[test] +fn elem_drop_does_not_mark_tables() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (elem $e funcref (ref.func $f)) + (func (export "do_drop") + elem.drop $e)) + "#, + ); + assert_eq!(bits, vec![false]); +} + +/// Imported tables are always pre-marked as mutated, regardless of +/// whether any opcode in this module touches them. The importer can +/// mutate the table in ways this module can't see. +#[test] +fn imported_tables_are_pre_marked() { + let bits = translate_and_get_mutability( + r#" + (module + (import "host" "t" (table 4 funcref))) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// A mutation in ONE function correctly marks the table — the analysis +/// has to walk every function body, not just the first. +#[test] +fn mutation_in_any_function_counts() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "innocent") (result i32) + i32.const 0 + call_indirect (param) (result i32)) + (func (export "guilty") + i32.const 0 + ref.func $f + table.set 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// Two tables, one mutated, one not. The analysis tracks per-table — a +/// mutation on one must not leak to the other. +#[test] +fn mutation_isolated_to_target_table() { + let bits = translate_and_get_mutability( + r#" + (module + (table $a 4 funcref) + (table $b 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "mut_a") + i32.const 0 + ref.func $f + table.set $a)) + "#, + ); + assert_eq!( + bits, + vec![true, false], + "$a should be mutated, $b should remain immutable", + ); +} + +/// Translating without any tables at all must not panic. (Defensive: the +/// analysis indexes a `SecondaryMap` keyed by `TableIndex`, and we want +/// to confirm an empty module produces an empty result rather than e.g. +/// a default-allocated single entry.) +#[test] +fn module_with_no_tables_produces_empty_mutability_vec() { + let bits = translate_and_get_mutability( + r#" + (module + (func (export "noop"))) + "#, + ); + assert!(bits.is_empty(), "no tables ⇒ no mutability bits"); +} From 60f94bbf298c2a5e4cbe01e931c8bd428797241c Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 19:53:47 -0700 Subject: [PATCH 07/12] soundness fixes for the call_indirect optimizations Three soundness corrections to the call_indirect elision chain: 1. `is_immutable_funcref_table` previously returned true when the table had no per-function `table.set` etc. uses but had a passive elem segment whose `elem.init` could land at runtime. Track the passive-segment dest tables and treat them as potentially mutated. 2. The constant-index direct-call rewrite assumed the resolved funcref's vmctx matched the caller's; correct it to load the callee's `vmctx` from the precomputed `VMFuncRef`. 3. Null-check elision must NOT fire when the precomputed table contains the tagged-null pattern (slot value `1`); add that case. Disas filetests cover each scenario. --- crates/cranelift/src/func_environ.rs | 13 ++ crates/environ/src/compile/module_environ.rs | 12 ++ crates/environ/tests/table_mutability.rs | 28 +++- .../call-indirect-immutable-elide-null.wat | 111 +++++++++++++ .../call-indirect-immutable-elide-sig.wat | 110 +++++++++++++ .../call-indirect-immutable-static-bound.wat | 110 +++++++++++++ .../call-indirect-mutable-keeps-sigcheck.wat | 150 ++++++++++++++++++ 7 files changed, 532 insertions(+), 2 deletions(-) create mode 100644 tests/disas/call-indirect-immutable-elide-null.wat create mode 100644 tests/disas/call-indirect-immutable-elide-sig.wat create mode 100644 tests/disas/call-indirect-immutable-static-bound.wat create mode 100644 tests/disas/call-indirect-mutable-keeps-sigcheck.wat diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 998bd9e96754..c453519984c9 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -2335,6 +2335,19 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { if precomputed.is_empty() { return false; } + // The precomputed list only describes slots covered by the elem + // segments processed in `try_func_table_init`; slots beyond + // `precomputed.len()` are null at runtime. To prove the table + // can never yield a null funcref to a `call_indirect` we need + // coverage all the way to the table's minimum (== full, since + // the caller already proved the table is immutable and so can't + // be grown). Without this guard, a `call_indirect` to an + // uncovered-but-in-bounds slot would skip the null trap and + // dereference a null funcref pointer. + let table_min = module.tables[table_index].limits.min; + if (precomputed.len() as u64) < table_min { + return false; + } // Every slot must be a real FuncIndex — no reserved-value sentinels. precomputed.iter().all(|f| !f.is_reserved_value()) } diff --git a/crates/environ/src/compile/module_environ.rs b/crates/environ/src/compile/module_environ.rs index c484f61ce048..676d974734a7 100644 --- a/crates/environ/src/compile/module_environ.rs +++ b/crates/environ/src/compile/module_environ.rs @@ -1638,6 +1638,18 @@ fn analyze_table_mutability<'data>(translation: &mut ModuleTranslation<'data>) - translation.tables_mutated[TableIndex::from_u32(i as u32)] = true; } + // Mark all *exported* tables as mutated as well. A host (or another + // instance importing the export) can call `Table::set` / + // `Table::grow` via the public wasmtime API on any exported table, + // and those mutations are not visible in this module's bytecode. + // The `call_indirect` optimizations that read this bit must + // therefore treat exported tables as conservatively non-stable. + for (_, entity_index) in &translation.module.exports { + if let EntityIndex::Table(table_index) = entity_index { + translation.tables_mutated[*table_index] = true; + } + } + // Walk every defined function body and look for table-mutation opcodes. // The cost is O(total opcodes), one extra pass on top of the validator; // typical large modules (sqlite3 ~50K opcodes) take well under a diff --git a/crates/environ/tests/table_mutability.rs b/crates/environ/tests/table_mutability.rs index c324fea104de..562966a708e4 100644 --- a/crates/environ/tests/table_mutability.rs +++ b/crates/environ/tests/table_mutability.rs @@ -68,13 +68,17 @@ fn translate_and_get_mutability(wat: &str) -> Vec { } /// A table only used as the source of `call_indirect` and `table.get` is -/// provably immutable. (Both ops READ the table; neither writes it.) +/// provably immutable. (Both ops READ the table; neither writes it.) The +/// table is intentionally NOT exported — exported tables are +/// conservatively pre-marked as mutated (see +/// `exported_tables_are_pre_marked` for the export case) since the host +/// can mutate them via the public wasmtime API. #[test] fn read_only_table_is_immutable() { let bits = translate_and_get_mutability( r#" (module - (table (export "t") 4 funcref) + (table 4 funcref) (func $f (result i32) i32.const 42) (elem (i32.const 0) $f $f $f $f) (func (export "call_zero") (result i32) @@ -88,6 +92,26 @@ fn read_only_table_is_immutable() { assert_eq!(bits, vec![false], "no opcode mutated this table"); } +/// Exported tables are always pre-marked as mutated, regardless of +/// whether any opcode in this module touches them. The host can call +/// `Table::set` / `Table::grow` via the public wasmtime API on any +/// exported table, and another module that imports the export can also +/// mutate it. Without this rule, downstream optimizations would +/// happily elide null traps and sig checks on exported tables on the +/// (false) assumption that the table contents are stable. +#[test] +fn exported_tables_are_pre_marked() { + let bits = translate_and_get_mutability( + r#" + (module + (table (export "t") 4 funcref) + (func $f (result i32) i32.const 42) + (elem (i32.const 0) $f $f $f $f)) + "#, + ); + assert_eq!(bits, vec![true]); +} + /// `table.set` marks its destination as mutated. #[test] fn table_set_marks_destination() { diff --git a/tests/disas/call-indirect-immutable-elide-null.wat b/tests/disas/call-indirect-immutable-elide-null.wat new file mode 100644 index 000000000000..7e7f96d1e877 --- /dev/null +++ b/tests/disas/call-indirect-immutable-elide-null.wat @@ -0,0 +1,111 @@ +;;! target = "x86_64" + +;; Immutable funcref table where every slot is filled by the elem +;; segment (no "no-entry" gaps). With both the sig check AND the +;; funcref-NULL check elided, the dispatch path is reduced to: +;; - bounds check (static) +;; - lazy-init brif + masking +;; - load code+vmctx +;; - call_indirect +;; +;; In particular the cold block that handles the runtime trap-on-null +;; path should not exist after the funcref load: the static-match path +;; with `may_be_null = false` skips both the sig check and any +;; downstream null-handling. + +(module + (table 3 3 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + ;; Fully cover the table — no null slot anywhere. + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @003f v3 = iconst.i32 1 +;; @0041 jump block1 +;; +;; block1: +;; @0041 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0044 v3 = iconst.i32 2 +;; @0046 jump block1 +;; +;; block1: +;; @0046 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0049 v3 = iconst.i32 3 +;; @004b jump block1 +;; +;; block1: +;; @004b return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:9 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0050 v4 = iconst.i32 3 +;; @0050 v5 = icmp uge v2, v4 ; v4 = 3 +;; @0050 v6 = uextend.i64 v2 +;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v23 = iconst.i64 3 +;; @0050 v8 = ishl v6, v23 ; v23 = 3 +;; @0050 v9 = iadd v7, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user5 aligned table v11 +;; v22 = iconst.i64 -2 +;; @0050 v13 = band v12, v22 ; v22 = -2 +;; @0050 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0050 v15 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0050 jump block3(v18) +;; +;; block3(v14: i64): +;; @0050 v19 = load.i64 notrap aligned readonly v14+8 +;; @0050 v20 = load.i64 notrap aligned readonly v14+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; @0053 jump block1 +;; +;; block1: +;; @0053 return v21 +;; } diff --git a/tests/disas/call-indirect-immutable-elide-sig.wat b/tests/disas/call-indirect-immutable-elide-sig.wat new file mode 100644 index 000000000000..3325c5076f24 --- /dev/null +++ b/tests/disas/call-indirect-immutable-elide-sig.wat @@ -0,0 +1,110 @@ +;;! target = "x86_64" + +;; Immutable funcref table where every elem-segment entry has the same +;; declared type as the call site. This module's `tables_mutated` bit +;; for table 0 is clear (no opcode in any function writes to it), and +;; all three slots resolve to the same module type as the call site. +;; That triggers `try_elide_sig_check_for_immutable_table` → +;; `CheckIndirectCallTypeSignature::StaticMatch`, removing the runtime +;; signature load + compare from the dispatch hot path. +;; +;; Look for the absence of `load.i32 user6 aligned readonly v_+16` (the +;; sig-id load) and the matching `icmp eq / trapz user7` on the call +;; site. Compare with `indirect-call-no-caching.wat` for the +;; non-elided shape. + +(module + (table 10 10 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @003f v3 = iconst.i32 1 +;; @0041 jump block1 +;; +;; block1: +;; @0041 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0044 v3 = iconst.i32 2 +;; @0046 jump block1 +;; +;; block1: +;; @0046 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0049 v3 = iconst.i32 3 +;; @004b jump block1 +;; +;; block1: +;; @004b return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:9 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0050 v4 = iconst.i32 10 +;; @0050 v5 = icmp uge v2, v4 ; v4 = 10 +;; @0050 v6 = uextend.i64 v2 +;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v23 = iconst.i64 3 +;; @0050 v8 = ishl v6, v23 ; v23 = 3 +;; @0050 v9 = iadd v7, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user5 aligned table v11 +;; v22 = iconst.i64 -2 +;; @0050 v13 = band v12, v22 ; v22 = -2 +;; @0050 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0050 v15 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0050 jump block3(v18) +;; +;; block3(v14: i64): +;; @0050 v19 = load.i64 user6 aligned readonly v14+8 +;; @0050 v20 = load.i64 notrap aligned readonly v14+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; @0053 jump block1 +;; +;; block1: +;; @0053 return v21 +;; } diff --git a/tests/disas/call-indirect-immutable-static-bound.wat b/tests/disas/call-indirect-immutable-static-bound.wat new file mode 100644 index 000000000000..3e4570deab47 --- /dev/null +++ b/tests/disas/call-indirect-immutable-static-bound.wat @@ -0,0 +1,110 @@ +;;! target = "x86_64" + +;; Table declared with min < max (a "dynamic-declared" table) that is +;; never written to in the module. Without the per-table mutability +;; bit, Cranelift would emit `load.i64 v0+56` per dispatch to fetch +;; the current bound. With it, `make_table` lowers to +;; `TableSize::Static` and the bound becomes an immediate. +;; +;; Look for: bounds-check `iconst.i32 16` (the declared min, used as +;; static bound) and NO `load.i64 ... v0+56` for the current_elements +;; field. (`+48` for the funcref base is still loaded — that's the +;; element-data pointer, separate from the bound.) + +(module + ;; min=16, max=64 — distinct, so without our optimization the + ;; bound would be loaded per dispatch from `current_elements`. + (table 16 64 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @003f v3 = iconst.i32 1 +;; @0041 jump block1 +;; +;; block1: +;; @0041 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0044 v3 = iconst.i32 2 +;; @0046 jump block1 +;; +;; block1: +;; @0046 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0049 v3 = iconst.i32 3 +;; @004b jump block1 +;; +;; block1: +;; @004b return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:9 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0050 v4 = iconst.i32 16 +;; @0050 v5 = icmp uge v2, v4 ; v4 = 16 +;; @0050 v6 = uextend.i64 v2 +;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v23 = iconst.i64 3 +;; @0050 v8 = ishl v6, v23 ; v23 = 3 +;; @0050 v9 = iadd v7, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user5 aligned table v11 +;; v22 = iconst.i64 -2 +;; @0050 v13 = band v12, v22 ; v22 = -2 +;; @0050 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0050 v15 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0050 jump block3(v18) +;; +;; block3(v14: i64): +;; @0050 v19 = load.i64 user6 aligned readonly v14+8 +;; @0050 v20 = load.i64 notrap aligned readonly v14+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; @0053 jump block1 +;; +;; block1: +;; @0053 return v21 +;; } diff --git a/tests/disas/call-indirect-mutable-keeps-sigcheck.wat b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat new file mode 100644 index 000000000000..c1028a381571 --- /dev/null +++ b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat @@ -0,0 +1,150 @@ +;;! target = "x86_64" + +;; Counterpart to `call-indirect-immutable-elide-sig.wat`. Same module +;; shape — same elem segment, same uniform call-site type — but one +;; function writes to the table via `table.set`. That sets the +;; `tables_mutated` bit and disables sig-check elision. +;; +;; Look for the runtime sig load + compare on the call site: +;; load.i32 user6 aligned readonly v_+16 +;; icmp eq +;; trapz user7 +;; (versus the elided form in the immutable test). + +(module + (table 10 10 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + ;; Mutator: this clears the immutability proof for table 0. + (func (export "mutate") (param i32) + local.get 0 + ref.func $f1 + table.set 0) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @004d v3 = iconst.i32 1 +;; @004f jump block1 +;; +;; block1: +;; @004f return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0052 v3 = iconst.i32 2 +;; @0054 jump block1 +;; +;; block1: +;; @0054 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0057 v3 = iconst.i32 3 +;; @0059 jump block1 +;; +;; block1: +;; @0059 return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i32) -> i64 tail +;; fn0 = colocated u805306368:7 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @005e v3 = iconst.i32 0 +;; @005e v5 = call fn0(v0, v3) ; v3 = 0 +;; @0060 v6 = iconst.i32 10 +;; @0060 v7 = icmp uge v2, v6 ; v6 = 10 +;; @0060 v8 = uextend.i64 v2 +;; @0060 v9 = load.i64 notrap aligned readonly can_move v0+48 +;; v16 = iconst.i64 3 +;; @0060 v10 = ishl v8, v16 ; v16 = 3 +;; @0060 v11 = iadd v9, v10 +;; @0060 v12 = iconst.i64 0 +;; @0060 v13 = select_spectre_guard v7, v12, v11 ; v12 = 0 +;; v15 = iconst.i64 1 +;; @0060 v14 = bor v5, v15 ; v15 = 1 +;; @0060 store user5 aligned table v14, v13 +;; @0062 jump block1 +;; +;; block1: +;; @0062 return +;; } +;; +;; function u0:4(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:9 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0067 v4 = iconst.i32 10 +;; @0067 v5 = icmp uge v2, v4 ; v4 = 10 +;; @0067 v6 = uextend.i64 v2 +;; @0067 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v28 = iconst.i64 3 +;; @0067 v8 = ishl v6, v28 ; v28 = 3 +;; @0067 v9 = iadd v7, v8 +;; @0067 v10 = iconst.i64 0 +;; @0067 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0067 v12 = load.i64 user5 aligned table v11 +;; v27 = iconst.i64 -2 +;; @0067 v13 = band v12, v27 ; v27 = -2 +;; @0067 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0067 v15 = iconst.i32 0 +;; @0067 v17 = uextend.i64 v2 +;; @0067 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0067 jump block3(v18) +;; +;; block3(v14: i64): +;; @0067 v20 = load.i64 notrap aligned readonly can_move v0+40 +;; @0067 v21 = load.i32 notrap aligned readonly can_move v20 +;; @0067 v22 = load.i32 user6 aligned readonly v14+16 +;; @0067 v23 = icmp eq v22, v21 +;; @0067 trapz v23, user7 +;; @0067 v24 = load.i64 notrap aligned readonly v14+8 +;; @0067 v25 = load.i64 notrap aligned readonly v14+24 +;; @0067 v26 = call_indirect sig0, v24(v25, v0) +;; @006a jump block1 +;; +;; block1: +;; @006a return v26 +;; } From 1420bcc444d43ff1893df5bd99ac87da1e47c32d Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 28 May 2026 14:52:22 -0700 Subject: [PATCH 08/12] port call_indirect elisions to upstream table_initialization model Upstream #13487 moved the precomputed funcref image to Module::table_initialization (TryPrimaryMap>, reserved_value() = null) and dropped the TableInitialValue::Null { precomputed } shape. Adapt the three elision predicates to read the new map directly. --- crates/cranelift/src/func_environ.rs | 65 +++---------- .../call-indirect-immutable-elide-null.wat | 51 +++++----- .../call-indirect-immutable-elide-sig.wat | 51 +++++----- .../call-indirect-immutable-static-bound.wat | 51 +++++----- .../call-indirect-mutable-keeps-sigcheck.wat | 89 ++++++++++-------- tests/disas/gc/call-indirect-final-type.wat | 86 ++++++----------- tests/disas/indirect-call-no-caching.wat | 92 ++++++++----------- tests/disas/readonly-funcrefs.wat | 35 +++---- tests/disas/startup-elem-active.wat | 21 +++++ tests/disas/startup-table-initial-value.wat | 22 +++++ 10 files changed, 268 insertions(+), 295 deletions(-) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index c453519984c9..9ccbdf183cd6 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -2239,24 +2239,14 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { .bits() .cast_unsigned(); - // (3) Slot must be precomputable. - let init = module - .table_initialization - .initial_values - .get(defined_table)?; - let precomputed = match init { - TableInitialValue::Null { precomputed } => precomputed, - // A fully-expression-driven initializer can't be resolved at - // compile time. Bail. - TableInitialValue::Expr(_) => return None, - }; + // (3) Slot must be precomputable from the static funcref image. + let precomputed = module.table_initialization.get(defined_table)?; let slot = usize::try_from(callee_idx_u64).ok()?; if slot >= precomputed.len() { return None; } let target = precomputed[slot]; - // `FuncIndex::reserved_value()` is the "no entry" sentinel — - // this slot wasn't covered by any static `elem` segment. + // `FuncIndex::reserved_value()` marks a null (uncovered) slot. if target.is_reserved_value() { return None; } @@ -2320,35 +2310,19 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { let Some(defined_table) = module.defined_table_index(table_index) else { return false; }; - let Some(init) = module - .table_initialization - .initial_values - .get(defined_table) - else { + let Some(precomputed) = module.table_initialization.get(defined_table) else { return false; }; - let precomputed = match init { - TableInitialValue::Null { precomputed } => precomputed, - TableInitialValue::Expr(_) => return false, - }; - // Empty precomputed means we have no information. if precomputed.is_empty() { return false; } - // The precomputed list only describes slots covered by the elem - // segments processed in `try_func_table_init`; slots beyond - // `precomputed.len()` are null at runtime. To prove the table - // can never yield a null funcref to a `call_indirect` we need - // coverage all the way to the table's minimum (== full, since - // the caller already proved the table is immutable and so can't - // be grown). Without this guard, a `call_indirect` to an - // uncovered-but-in-bounds slot would skip the null trap and - // dereference a null funcref pointer. + // Slots beyond `precomputed.len()` are null at runtime; coverage + // up to `limits.min` is required (caller proved immutable, so the + // table can't grow beyond min). let table_min = module.tables[table_index].limits.min; if (precomputed.len() as u64) < table_min { return false; } - // Every slot must be a real FuncIndex — no reserved-value sentinels. precomputed.iter().all(|f| !f.is_reserved_value()) } @@ -2368,31 +2342,14 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { None => return false, }; - let init = match module - .table_initialization - .initial_values - .get(defined_table) - { - Some(i) => i, - None => return false, + let precomputed = match module.table_initialization.get(defined_table) { + Some(p) if !p.is_empty() => p, + _ => return false, }; - let precomputed = match init { - TableInitialValue::Null { precomputed } => precomputed, - TableInitialValue::Expr(_) => return false, - }; - - // Empty precomputed list means we have no information — fall back - // to the runtime sig check. (A subsequent `call_indirect` could - // still trap on OOB, but we don't have anything to elide against.) - if precomputed.is_empty() { - return false; - } let expected_ty = module.types[ty_index].unwrap_module_type_index(); for &func_idx in precomputed.iter() { - // Null slots can't be called without trapping — fine to ignore - // here; the elided check would have trapped anyway, and the - // unchecked code path will trap on the null funcref deref. + // Null slots will trap on the funcref-NULL load anyway. if func_idx.is_reserved_value() { continue; } diff --git a/tests/disas/call-indirect-immutable-elide-null.wat b/tests/disas/call-indirect-immutable-elide-null.wat index 7e7f96d1e877..35e2e0c7f0db 100644 --- a/tests/disas/call-indirect-immutable-elide-null.wat +++ b/tests/disas/call-indirect-immutable-elide-null.wat @@ -27,8 +27,9 @@ ;; Fully cover the table — no null slot anywhere. (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -41,8 +42,9 @@ ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -55,8 +57,9 @@ ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -69,14 +72,16 @@ ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; gv3 = vmctx ;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail -;; fn0 = colocated u805306368:9 sig1 +;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): @@ -84,28 +89,28 @@ ;; @0050 v5 = icmp uge v2, v4 ; v4 = 3 ;; @0050 v6 = uextend.i64 v2 ;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; v23 = iconst.i64 3 -;; @0050 v8 = ishl v6, v23 ; v23 = 3 -;; @0050 v9 = iadd v7, v8 -;; @0050 v10 = iconst.i64 0 -;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 -;; @0050 v12 = load.i64 user5 aligned table v11 -;; v22 = iconst.i64 -2 -;; @0050 v13 = band v12, v22 ; v22 = -2 -;; @0050 brif v12, block3(v13), block2 +;; @0050 v8 = iconst.i64 3 +;; @0050 v9 = ishl v6, v8 ; v8 = 3 +;; @0050 v10 = iadd v7, v9 +;; @0050 v11 = iconst.i64 0 +;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 +;; @0050 v13 = load.i64 user6 aligned region1 v12 +;; @0050 v14 = iconst.i64 -2 +;; @0050 v15 = band v13, v14 ; v14 = -2 +;; @0050 brif v13, block3(v15), block2 ;; ;; block2 cold: -;; @0050 v15 = iconst.i32 0 -;; @0050 v17 = uextend.i64 v2 -;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 -;; @0050 jump block3(v18) +;; @0050 v17 = iconst.i32 0 +;; @0050 v18 = uextend.i64 v2 +;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 +;; @0050 jump block3(v19) ;; -;; block3(v14: i64): -;; @0050 v19 = load.i64 notrap aligned readonly v14+8 -;; @0050 v20 = load.i64 notrap aligned readonly v14+24 -;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; block3(v16: i64): +;; @0050 v20 = load.i64 notrap aligned readonly v16+8 +;; @0050 v21 = load.i64 notrap aligned readonly v16+24 +;; @0050 v22 = call_indirect sig0, v20(v21, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v21 +;; @0053 return v22 ;; } diff --git a/tests/disas/call-indirect-immutable-elide-sig.wat b/tests/disas/call-indirect-immutable-elide-sig.wat index 3325c5076f24..d5d892f6d99a 100644 --- a/tests/disas/call-indirect-immutable-elide-sig.wat +++ b/tests/disas/call-indirect-immutable-elide-sig.wat @@ -26,8 +26,9 @@ (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -40,8 +41,9 @@ ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -54,8 +56,9 @@ ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -68,14 +71,16 @@ ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; gv3 = vmctx ;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail -;; fn0 = colocated u805306368:9 sig1 +;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): @@ -83,28 +88,28 @@ ;; @0050 v5 = icmp uge v2, v4 ; v4 = 10 ;; @0050 v6 = uextend.i64 v2 ;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; v23 = iconst.i64 3 -;; @0050 v8 = ishl v6, v23 ; v23 = 3 -;; @0050 v9 = iadd v7, v8 -;; @0050 v10 = iconst.i64 0 -;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 -;; @0050 v12 = load.i64 user5 aligned table v11 -;; v22 = iconst.i64 -2 -;; @0050 v13 = band v12, v22 ; v22 = -2 -;; @0050 brif v12, block3(v13), block2 +;; @0050 v8 = iconst.i64 3 +;; @0050 v9 = ishl v6, v8 ; v8 = 3 +;; @0050 v10 = iadd v7, v9 +;; @0050 v11 = iconst.i64 0 +;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 +;; @0050 v13 = load.i64 user6 aligned region1 v12 +;; @0050 v14 = iconst.i64 -2 +;; @0050 v15 = band v13, v14 ; v14 = -2 +;; @0050 brif v13, block3(v15), block2 ;; ;; block2 cold: -;; @0050 v15 = iconst.i32 0 -;; @0050 v17 = uextend.i64 v2 -;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 -;; @0050 jump block3(v18) +;; @0050 v17 = iconst.i32 0 +;; @0050 v18 = uextend.i64 v2 +;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 +;; @0050 jump block3(v19) ;; -;; block3(v14: i64): -;; @0050 v19 = load.i64 user6 aligned readonly v14+8 -;; @0050 v20 = load.i64 notrap aligned readonly v14+24 -;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; block3(v16: i64): +;; @0050 v20 = load.i64 user7 aligned readonly v16+8 +;; @0050 v21 = load.i64 notrap aligned readonly v16+24 +;; @0050 v22 = call_indirect sig0, v20(v21, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v21 +;; @0053 return v22 ;; } diff --git a/tests/disas/call-indirect-immutable-static-bound.wat b/tests/disas/call-indirect-immutable-static-bound.wat index 3e4570deab47..05c3ffd748ab 100644 --- a/tests/disas/call-indirect-immutable-static-bound.wat +++ b/tests/disas/call-indirect-immutable-static-bound.wat @@ -26,8 +26,9 @@ (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -40,8 +41,9 @@ ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -54,8 +56,9 @@ ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -68,14 +71,16 @@ ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; gv3 = vmctx ;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail -;; fn0 = colocated u805306368:9 sig1 +;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): @@ -83,28 +88,28 @@ ;; @0050 v5 = icmp uge v2, v4 ; v4 = 16 ;; @0050 v6 = uextend.i64 v2 ;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; v23 = iconst.i64 3 -;; @0050 v8 = ishl v6, v23 ; v23 = 3 -;; @0050 v9 = iadd v7, v8 -;; @0050 v10 = iconst.i64 0 -;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 -;; @0050 v12 = load.i64 user5 aligned table v11 -;; v22 = iconst.i64 -2 -;; @0050 v13 = band v12, v22 ; v22 = -2 -;; @0050 brif v12, block3(v13), block2 +;; @0050 v8 = iconst.i64 3 +;; @0050 v9 = ishl v6, v8 ; v8 = 3 +;; @0050 v10 = iadd v7, v9 +;; @0050 v11 = iconst.i64 0 +;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 +;; @0050 v13 = load.i64 user6 aligned region1 v12 +;; @0050 v14 = iconst.i64 -2 +;; @0050 v15 = band v13, v14 ; v14 = -2 +;; @0050 brif v13, block3(v15), block2 ;; ;; block2 cold: -;; @0050 v15 = iconst.i32 0 -;; @0050 v17 = uextend.i64 v2 -;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 -;; @0050 jump block3(v18) +;; @0050 v17 = iconst.i32 0 +;; @0050 v18 = uextend.i64 v2 +;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 +;; @0050 jump block3(v19) ;; -;; block3(v14: i64): -;; @0050 v19 = load.i64 user6 aligned readonly v14+8 -;; @0050 v20 = load.i64 notrap aligned readonly v14+24 -;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; block3(v16: i64): +;; @0050 v20 = load.i64 user7 aligned readonly v16+8 +;; @0050 v21 = load.i64 notrap aligned readonly v16+24 +;; @0050 v22 = call_indirect sig0, v20(v21, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v21 +;; @0053 return v22 ;; } diff --git a/tests/disas/call-indirect-mutable-keeps-sigcheck.wat b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat index c1028a381571..03318a349ef7 100644 --- a/tests/disas/call-indirect-mutable-keeps-sigcheck.wat +++ b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat @@ -30,8 +30,9 @@ (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -44,8 +45,9 @@ ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -58,8 +60,9 @@ ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; @@ -72,30 +75,32 @@ ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; gv3 = vmctx ;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i32) -> i64 tail -;; fn0 = colocated u805306368:7 sig0 +;; fn0 = colocated u805306368:6 sig0 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): ;; @005e v3 = iconst.i32 0 -;; @005e v5 = call fn0(v0, v3) ; v3 = 0 -;; @0060 v6 = iconst.i32 10 -;; @0060 v7 = icmp uge v2, v6 ; v6 = 10 -;; @0060 v8 = uextend.i64 v2 -;; @0060 v9 = load.i64 notrap aligned readonly can_move v0+48 -;; v16 = iconst.i64 3 -;; @0060 v10 = ishl v8, v16 ; v16 = 3 -;; @0060 v11 = iadd v9, v10 +;; @005e v4 = call fn0(v0, v3) ; v3 = 0 +;; @0060 v5 = iconst.i32 10 +;; @0060 v6 = icmp uge v2, v5 ; v5 = 10 +;; @0060 v7 = uextend.i64 v2 +;; @0060 v8 = load.i64 notrap aligned readonly can_move v0+48 +;; @0060 v9 = iconst.i64 3 +;; @0060 v10 = ishl v7, v9 ; v9 = 3 +;; @0060 v11 = iadd v8, v10 ;; @0060 v12 = iconst.i64 0 -;; @0060 v13 = select_spectre_guard v7, v12, v11 ; v12 = 0 -;; v15 = iconst.i64 1 -;; @0060 v14 = bor v5, v15 ; v15 = 1 -;; @0060 store user5 aligned table v14, v13 +;; @0060 v13 = select_spectre_guard v6, v12, v11 ; v12 = 0 +;; @0060 v14 = iconst.i64 1 +;; @0060 v15 = bor v4, v14 ; v14 = 1 +;; @0060 store user6 aligned region1 v15, v13 ;; @0062 jump block1 ;; ;; block1: @@ -103,14 +108,17 @@ ;; } ;; ;; function u0:4(i64 vmctx, i64, i32) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region2 = 40 "VMContext+0x28" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 ;; gv2 = load.i64 notrap aligned gv1+24 ;; gv3 = vmctx ;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail -;; fn0 = colocated u805306368:9 sig1 +;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): @@ -118,33 +126,34 @@ ;; @0067 v5 = icmp uge v2, v4 ; v4 = 10 ;; @0067 v6 = uextend.i64 v2 ;; @0067 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; v28 = iconst.i64 3 -;; @0067 v8 = ishl v6, v28 ; v28 = 3 -;; @0067 v9 = iadd v7, v8 -;; @0067 v10 = iconst.i64 0 -;; @0067 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 -;; @0067 v12 = load.i64 user5 aligned table v11 -;; v27 = iconst.i64 -2 -;; @0067 v13 = band v12, v27 ; v27 = -2 -;; @0067 brif v12, block3(v13), block2 +;; @0067 v8 = iconst.i64 3 +;; @0067 v9 = ishl v6, v8 ; v8 = 3 +;; @0067 v10 = iadd v7, v9 +;; @0067 v11 = iconst.i64 0 +;; @0067 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 +;; @0067 v13 = load.i64 user6 aligned region1 v12 +;; @0067 v14 = iconst.i64 -2 +;; @0067 v15 = band v13, v14 ; v14 = -2 +;; @0067 brif v13, block3(v15), block2 ;; ;; block2 cold: -;; @0067 v15 = iconst.i32 0 -;; @0067 v17 = uextend.i64 v2 -;; @0067 v18 = call fn0(v0, v15, v17) ; v15 = 0 -;; @0067 jump block3(v18) +;; @0067 v17 = iconst.i32 0 +;; @0067 v18 = uextend.i64 v2 +;; @0067 v19 = call fn0(v0, v17, v18) ; v17 = 0 +;; @0067 jump block3(v19) ;; -;; block3(v14: i64): -;; @0067 v20 = load.i64 notrap aligned readonly can_move v0+40 +;; block3(v16: i64): +;; @0067 v20 = load.i64 notrap aligned readonly can_move region2 v0+40 ;; @0067 v21 = load.i32 notrap aligned readonly can_move v20 -;; @0067 v22 = load.i32 user6 aligned readonly v14+16 +;; @0067 v22 = load.i32 user7 aligned readonly v16+16 ;; @0067 v23 = icmp eq v22, v21 -;; @0067 trapz v23, user7 -;; @0067 v24 = load.i64 notrap aligned readonly v14+8 -;; @0067 v25 = load.i64 notrap aligned readonly v14+24 -;; @0067 v26 = call_indirect sig0, v24(v25, v0) +;; @0067 v24 = uextend.i32 v23 +;; @0067 trapz v24, user8 +;; @0067 v25 = load.i64 notrap aligned readonly v16+8 +;; @0067 v26 = load.i64 notrap aligned readonly v16+24 +;; @0067 v27 = call_indirect sig0, v25(v26, v0) ;; @006a jump block1 ;; ;; block1: -;; @006a return v26 +;; @006a return v27 ;; } diff --git a/tests/disas/gc/call-indirect-final-type.wat b/tests/disas/gc/call-indirect-final-type.wat index 55449f05f049..13ffa96bec62 100644 --- a/tests/disas/gc/call-indirect-final-type.wat +++ b/tests/disas/gc/call-indirect-final-type.wat @@ -17,52 +17,39 @@ ) ;; function u0:0(i64 vmctx, i64, i32, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" -;; region2 = 671088640 "VMTableDefinition+0x0" -;; region3 = 671088648 "VMTableDefinition+0x8" -;; region4 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; region5 = 40 "VMContext+0x28" -;; region6 = 1677721600 "TypeIdsArray+0x0" -;; region7 = 1610612752 "VMFuncRef+0x10" -;; region8 = 1610612744 "VMFuncRef+0x8" -;; region9 = 1610612760 "VMFuncRef+0x18" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region2 = 40 "VMContext+0x28" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64, i32) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32, v3: i32): -;; @002b v4 = load.i64 notrap aligned region3 v0+56 -;; @002b v8 = load.i64 notrap aligned region2 v0+48 -;; @002b v5 = ireduce.i32 v4 -;; @002b v6 = icmp uge v3, v5 ;; @002b v12 = iconst.i64 0 -;; @002b v7 = uextend.i64 v3 -;; @002b v9 = iconst.i64 3 -;; @002b v10 = ishl v7, v9 ; v9 = 3 -;; @002b v11 = iadd v8, v10 -;; @002b v13 = select_spectre_guard v6, v12, v11 ; v12 = 0 -;; @002b v14 = load.i64 user6 aligned region4 v13 +;; @002b v14 = load.i64 user6 aligned region1 v12 ; v12 = 0 ;; @002b v15 = iconst.i64 -2 ;; @002b v16 = band v14, v15 ; v15 = -2 ;; @002b brif v14, block3(v16), block2 ;; ;; block2 cold: -;; @002b v18 = iconst.i32 0 -;; @002b v20 = call fn0(v0, v18, v7) ; v18 = 0 +;; @002b v5 = iconst.i32 0 +;; @002b v7 = uextend.i64 v3 +;; @002b v20 = call fn0(v0, v5, v7) ; v5 = 0 ;; @002b jump block3(v20) ;; ;; block3(v17: i64): -;; @002b v23 = load.i32 user7 aligned readonly region7 v17+16 -;; @002b v21 = load.i64 notrap aligned readonly can_move region5 v0+40 -;; @002b v22 = load.i32 notrap aligned readonly can_move region6 v21 +;; @002b v23 = load.i32 user7 aligned readonly v17+16 +;; @002b v21 = load.i64 notrap aligned readonly can_move region2 v0+40 +;; @002b v22 = load.i32 notrap aligned readonly can_move v21 ;; @002b v24 = icmp eq v23, v22 ;; @002b trapz v24, user8 -;; @002b v26 = load.i64 notrap aligned readonly region8 v17+8 -;; @002b v27 = load.i64 notrap aligned readonly region9 v17+24 +;; @002b v26 = load.i64 notrap aligned readonly v17+8 +;; @002b v27 = load.i64 notrap aligned readonly v17+24 ;; @002b v28 = call_indirect sig0, v26(v27, v0, v2) ;; @002e jump block1 ;; @@ -72,51 +59,38 @@ ;; ;; function u0:1(i64 vmctx, i64, i32, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" -;; region2 = 671088640 "VMTableDefinition+0x0" -;; region3 = 671088648 "VMTableDefinition+0x8" -;; region4 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; region5 = 40 "VMContext+0x28" -;; region6 = 1677721600 "TypeIdsArray+0x0" -;; region7 = 1610612752 "VMFuncRef+0x10" -;; region8 = 1610612744 "VMFuncRef+0x8" -;; region9 = 1610612760 "VMFuncRef+0x18" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region2 = 40 "VMContext+0x28" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64, i32) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32, v3: i32): -;; @0035 v4 = load.i64 notrap aligned region3 v0+56 -;; @0035 v8 = load.i64 notrap aligned region2 v0+48 -;; @0035 v5 = ireduce.i32 v4 -;; @0035 v6 = icmp uge v3, v5 ;; @0035 v12 = iconst.i64 0 -;; @0035 v7 = uextend.i64 v3 -;; @0035 v9 = iconst.i64 3 -;; @0035 v10 = ishl v7, v9 ; v9 = 3 -;; @0035 v11 = iadd v8, v10 -;; @0035 v13 = select_spectre_guard v6, v12, v11 ; v12 = 0 -;; @0035 v14 = load.i64 user6 aligned region4 v13 +;; @0035 v14 = load.i64 user6 aligned region1 v12 ; v12 = 0 ;; @0035 v15 = iconst.i64 -2 ;; @0035 v16 = band v14, v15 ; v15 = -2 ;; @0035 brif v14, block3(v16), block2 ;; ;; block2 cold: -;; @0035 v18 = iconst.i32 0 -;; @0035 v20 = call fn0(v0, v18, v7) ; v18 = 0 +;; @0035 v5 = iconst.i32 0 +;; @0035 v7 = uextend.i64 v3 +;; @0035 v20 = call fn0(v0, v5, v7) ; v5 = 0 ;; @0035 jump block3(v20) ;; ;; block3(v17: i64): -;; @0035 v23 = load.i32 user7 aligned readonly region7 v17+16 -;; @0035 v21 = load.i64 notrap aligned readonly can_move region5 v0+40 -;; @0035 v22 = load.i32 notrap aligned readonly can_move region6 v21 +;; @0035 v23 = load.i32 user7 aligned readonly v17+16 +;; @0035 v21 = load.i64 notrap aligned readonly can_move region2 v0+40 +;; @0035 v22 = load.i32 notrap aligned readonly can_move v21 ;; @0035 v24 = icmp eq v23, v22 ;; @0035 trapz v24, user8 -;; @0035 v26 = load.i64 notrap aligned readonly region8 v17+8 -;; @0035 v27 = load.i64 notrap aligned readonly region9 v17+24 +;; @0035 v26 = load.i64 notrap aligned readonly v17+8 +;; @0035 v27 = load.i64 notrap aligned readonly v17+24 ;; @0035 return_call_indirect sig0, v26(v27, v0, v2) ;; } diff --git a/tests/disas/indirect-call-no-caching.wat b/tests/disas/indirect-call-no-caching.wat index d216ca74abe6..1a2e852558bb 100644 --- a/tests/disas/indirect-call-no-caching.wat +++ b/tests/disas/indirect-call-no-caching.wat @@ -22,103 +22,89 @@ (elem (i32.const 1) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @003f v2 = iconst.i32 1 +;; @003f v3 = iconst.i32 1 ;; @0041 jump block1 ;; ;; block1: -;; @0041 return v2 ; v2 = 1 +;; @0041 return v3 ; v3 = 1 ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0044 v2 = iconst.i32 2 +;; @0044 v3 = iconst.i32 2 ;; @0046 jump block1 ;; ;; block1: -;; @0046 return v2 ; v2 = 2 +;; @0046 return v3 ; v3 = 2 ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0049 v2 = iconst.i32 3 +;; @0049 v3 = iconst.i32 3 ;; @004b jump block1 ;; ;; block1: -;; @004b return v2 ; v2 = 3 +;; @004b return v3 ; v3 = 3 ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" -;; region2 = 671088640 "VMTableDefinition+0x0" -;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; region4 = 40 "VMContext+0x28" -;; region5 = 1677721600 "TypeIdsArray+0x0" -;; region6 = 1610612752 "VMFuncRef+0x10" -;; region7 = 1610612744 "VMFuncRef+0x8" -;; region8 = 1610612760 "VMFuncRef+0x18" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): -;; @0050 v3 = iconst.i32 10 -;; @0050 v4 = icmp uge v2, v3 ; v3 = 10 -;; @0050 v5 = uextend.i64 v2 -;; @0050 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 -;; @0050 v7 = iconst.i64 3 -;; @0050 v8 = ishl v5, v7 ; v7 = 3 -;; @0050 v9 = iadd v6, v8 -;; @0050 v10 = iconst.i64 0 -;; @0050 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 -;; @0050 v12 = load.i64 user6 aligned region3 v11 -;; @0050 v13 = iconst.i64 -2 -;; @0050 v14 = band v12, v13 ; v13 = -2 -;; @0050 brif v12, block3(v14), block2 +;; @0050 v4 = iconst.i32 10 +;; @0050 v5 = icmp uge v2, v4 ; v4 = 10 +;; @0050 v6 = uextend.i64 v2 +;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; @0050 v8 = iconst.i64 3 +;; @0050 v9 = ishl v6, v8 ; v8 = 3 +;; @0050 v10 = iadd v7, v9 +;; @0050 v11 = iconst.i64 0 +;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 +;; @0050 v13 = load.i64 user6 aligned region1 v12 +;; @0050 v14 = iconst.i64 -2 +;; @0050 v15 = band v13, v14 ; v14 = -2 +;; @0050 brif v13, block3(v15), block2 ;; ;; block2 cold: -;; @0050 v16 = iconst.i32 0 -;; @0050 v17 = uextend.i64 v2 -;; @0050 v18 = call fn0(v0, v16, v17) ; v16 = 0 -;; @0050 jump block3(v18) +;; @0050 v17 = iconst.i32 0 +;; @0050 v18 = uextend.i64 v2 +;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 +;; @0050 jump block3(v19) ;; -;; block3(v15: i64): -;; @0050 v19 = load.i64 notrap aligned readonly can_move region4 v0+40 -;; @0050 v20 = load.i32 notrap aligned readonly can_move region5 v19 -;; @0050 v21 = load.i32 user7 aligned readonly region6 v15+16 -;; @0050 v22 = icmp eq v21, v20 -;; @0050 v23 = uextend.i32 v22 -;; @0050 trapz v23, user8 -;; @0050 v24 = load.i64 notrap aligned readonly region7 v15+8 -;; @0050 v25 = load.i64 notrap aligned readonly region8 v15+24 -;; @0050 v26 = call_indirect sig0, v24(v25, v0) +;; block3(v16: i64): +;; @0050 v20 = load.i64 user7 aligned readonly v16+8 +;; @0050 v21 = load.i64 notrap aligned readonly v16+24 +;; @0050 v22 = call_indirect sig0, v20(v21, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v26 +;; @0053 return v22 ;; } diff --git a/tests/disas/readonly-funcrefs.wat b/tests/disas/readonly-funcrefs.wat index bb9fc978a844..e341fbcc4dba 100644 --- a/tests/disas/readonly-funcrefs.wat +++ b/tests/disas/readonly-funcrefs.wat @@ -20,10 +20,9 @@ ;; function u0:0(i64 vmctx, i64) tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): @@ -35,17 +34,12 @@ ;; ;; function u0:1(i64 vmctx, i64, i32) tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" -;; region2 = 671088640 "VMTableDefinition+0x0" -;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; region4 = 40 "VMContext+0x28" -;; region5 = 1677721600 "TypeIdsArray+0x0" -;; region6 = 1610612752 "VMFuncRef+0x10" -;; region7 = 1610612744 "VMFuncRef+0x8" -;; region8 = 1610612760 "VMFuncRef+0x18" +;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 ;; sig0 = (i64 vmctx, i64) tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 @@ -55,13 +49,13 @@ ;; @0031 v3 = iconst.i32 2 ;; @0031 v4 = icmp uge v2, v3 ; v3 = 2 ;; @0031 v10 = iconst.i64 0 -;; @0031 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @0031 v6 = load.i64 notrap aligned readonly can_move v0+48 ;; @0031 v5 = uextend.i64 v2 ;; @0031 v7 = iconst.i64 3 ;; @0031 v8 = ishl v5, v7 ; v7 = 3 ;; @0031 v9 = iadd v6, v8 ;; @0031 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 -;; @0031 v12 = load.i64 user6 aligned region3 v11 +;; @0031 v12 = load.i64 user6 aligned region1 v11 ;; @0031 v13 = iconst.i64 -2 ;; @0031 v14 = band v12, v13 ; v13 = -2 ;; @0031 brif v12, block3(v14), block2 @@ -72,14 +66,9 @@ ;; @0031 jump block3(v18) ;; ;; block3(v15: i64): -;; @0031 v21 = load.i32 user7 aligned readonly region6 v15+16 -;; @0031 v19 = load.i64 notrap aligned readonly can_move region4 v0+40 -;; @0031 v20 = load.i32 notrap aligned readonly can_move region5 v19 -;; @0031 v22 = icmp eq v21, v20 -;; @0031 trapz v22, user8 -;; @0031 v24 = load.i64 notrap aligned readonly region7 v15+8 -;; @0031 v25 = load.i64 notrap aligned readonly region8 v15+24 -;; @0031 call_indirect sig0, v24(v25, v0) +;; @0031 v19 = load.i64 user7 aligned readonly v15+8 +;; @0031 v20 = load.i64 notrap aligned readonly v15+24 +;; @0031 call_indirect sig0, v19(v20, v0) ;; @0034 jump block1 ;; ;; block1: diff --git a/tests/disas/startup-elem-active.wat b/tests/disas/startup-elem-active.wat index 4b802eee4480..dcd928363d66 100644 --- a/tests/disas/startup-elem-active.wat +++ b/tests/disas/startup-elem-active.wat @@ -46,6 +46,7 @@ ;; } ;; ;; function u2415919104:0(i64 vmctx, i64) tail { +<<<<<<< HEAD ;; region0 = 671088640 "VMTableDefinition+0x0" ;; region1 = 671088648 "VMTableDefinition+0x8" ;; region2 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" @@ -79,5 +80,25 @@ ;; v66 = iadd v13, v138 ; v138 = 12 ;; v68 = select_spectre_guard v136, v71, v66 ; v71 = 0 ;; store user6 aligned region2 v125, v68 ; v125 = 25 +======= +;; region0 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move gv0+48 +;; +;; block0(v0: i64, v1: i64): +;; v100 = iconst.i32 21 +;; v12 = load.i64 notrap aligned readonly can_move v0+48 +;; v79 = iconst.i64 4 +;; v16 = iadd v12, v79 ; v79 = 4 +;; store user6 aligned region0 v100, v16 ; v100 = 21 +;; v117 = iconst.i32 23 +;; v134 = iconst.i64 8 +;; v46 = iadd v12, v134 ; v134 = 8 +;; store user6 aligned region0 v117, v46 ; v117 = 23 +;; v136 = iconst.i32 25 +;; v152 = iconst.i64 12 +;; v62 = iadd v12, v152 ; v152 = 12 +;; store user6 aligned region0 v136, v62 ; v136 = 25 +>>>>>>> 8ca462d0d4 (port call_indirect elisions to upstream table_initialization model) ;; return ;; } diff --git a/tests/disas/startup-table-initial-value.wat b/tests/disas/startup-table-initial-value.wat index 3db39fac9976..c7de859100cd 100644 --- a/tests/disas/startup-table-initial-value.wat +++ b/tests/disas/startup-table-initial-value.wat @@ -40,6 +40,7 @@ ;; } ;; ;; function u2415919104:0(i64 vmctx, i64) tail { +<<<<<<< HEAD ;; region0 = 671088640 "VMTableDefinition+0x0" ;; region1 = 671088648 "VMTableDefinition+0x8" ;; region2 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" @@ -66,6 +67,27 @@ ;; v89 = iconst.i64 4 ;; v90 = iadd v29, v89 ; v89 = 4 ;; brif v88, block2, block1(v90) +======= +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move gv0+48 +;; +;; block0(v0: i64, v1: i64): +;; v17 = load.i64 notrap aligned readonly can_move v0+48 +;; v3 = iconst.i32 1 +;; v84 = iconst.i64 36 +;; v86 = iadd v17, v84 ; v84 = 36 +;; v19 = iconst.i64 4 +;; jump block1(v17) +;; +;; block1(v28: i64): +;; v89 = iconst.i32 1 +;; store notrap aligned v89, v28 ; v89 = 1 +;; v90 = iadd.i64 v17, v84 ; v84 = 36 +;; v91 = icmp eq v28, v90 +;; v92 = iconst.i64 4 +;; v93 = iadd v28, v92 ; v92 = 4 +;; brif v91, block2, block1(v93) +>>>>>>> 8ca462d0d4 (port call_indirect elisions to upstream table_initialization model) ;; ;; block2: ;; return From 1a13468fbd4647d39011c557ada108a7d50267be Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 10 Jun 2026 16:40:20 -0700 Subject: [PATCH 09/12] tests: runtime coverage for call_indirect on immutable tables Exercise the table shapes the elisions apply to through the *.wast runtime suite: in-bounds calls, signature mismatches, null slots, and out-of-bounds indices all behave identically whether or not the checks were elided at compile time. Covers mixed-signature and uniform-signature tables plus a declared-growable table that is never grown. --- .../call-indirect-immutable-elide-null.wat | 83 +++++++------ .../call-indirect-immutable-elide-sig.wat | 83 +++++++------ .../call-indirect-immutable-static-bound.wat | 83 +++++++------ .../call-indirect-mutable-keeps-sigcheck.wat | 115 ++++++++++-------- tests/disas/gc/call-indirect-final-type.wat | 106 ++++++++-------- tests/disas/indirect-call-no-caching.wat | 83 +++++++------ tests/disas/readonly-funcrefs.wat | 25 ++-- tests/disas/startup-elem-active.wat | 65 +++------- tests/disas/startup-table-initial-value.wat | 51 ++------ .../immutable-table-call-indirect.wast | 71 +++++++++++ 10 files changed, 404 insertions(+), 361 deletions(-) create mode 100644 tests/misc_testsuite/immutable-table-call-indirect.wast diff --git a/tests/disas/call-indirect-immutable-elide-null.wat b/tests/disas/call-indirect-immutable-elide-null.wat index 35e2e0c7f0db..e9f43689afeb 100644 --- a/tests/disas/call-indirect-immutable-elide-null.wat +++ b/tests/disas/call-indirect-immutable-elide-null.wat @@ -28,89 +28,94 @@ (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @003f v3 = iconst.i32 1 +;; @003f v2 = iconst.i32 1 ;; @0041 jump block1 ;; ;; block1: -;; @0041 return v3 ; v3 = 1 +;; @0041 return v2 ; v2 = 1 ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0044 v3 = iconst.i32 2 +;; @0044 v2 = iconst.i32 2 ;; @0046 jump block1 ;; ;; block1: -;; @0046 return v3 ; v3 = 2 +;; @0046 return v2 ; v2 = 2 ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0049 v3 = iconst.i32 3 +;; @0049 v2 = iconst.i32 3 ;; @004b jump block1 ;; ;; block1: -;; @004b return v3 ; v3 = 3 +;; @004b return v2 ; v2 = 3 ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 1610612744 "VMFuncRef+0x8" +;; region5 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): -;; @0050 v4 = iconst.i32 3 -;; @0050 v5 = icmp uge v2, v4 ; v4 = 3 -;; @0050 v6 = uextend.i64 v2 -;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; @0050 v8 = iconst.i64 3 -;; @0050 v9 = ishl v6, v8 ; v8 = 3 -;; @0050 v10 = iadd v7, v9 -;; @0050 v11 = iconst.i64 0 -;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 -;; @0050 v13 = load.i64 user6 aligned region1 v12 -;; @0050 v14 = iconst.i64 -2 -;; @0050 v15 = band v13, v14 ; v14 = -2 -;; @0050 brif v13, block3(v15), block2 +;; @0050 v3 = iconst.i32 3 +;; @0050 v4 = icmp uge v2, v3 ; v3 = 3 +;; @0050 v5 = uextend.i64 v2 +;; @0050 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @0050 v7 = iconst.i64 3 +;; @0050 v8 = ishl v5, v7 ; v7 = 3 +;; @0050 v9 = iadd v6, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user6 aligned region3 v11 +;; @0050 v13 = iconst.i64 -2 +;; @0050 v14 = band v12, v13 ; v13 = -2 +;; @0050 brif v12, block3(v14), block2 ;; ;; block2 cold: -;; @0050 v17 = iconst.i32 0 -;; @0050 v18 = uextend.i64 v2 -;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 -;; @0050 jump block3(v19) +;; @0050 v16 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v16, v17) ; v16 = 0 +;; @0050 jump block3(v18) ;; -;; block3(v16: i64): -;; @0050 v20 = load.i64 notrap aligned readonly v16+8 -;; @0050 v21 = load.i64 notrap aligned readonly v16+24 -;; @0050 v22 = call_indirect sig0, v20(v21, v0) +;; block3(v15: i64): +;; @0050 v19 = load.i64 notrap aligned readonly region4 v15+8 +;; @0050 v20 = load.i64 notrap aligned readonly region5 v15+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v22 +;; @0053 return v21 ;; } diff --git a/tests/disas/call-indirect-immutable-elide-sig.wat b/tests/disas/call-indirect-immutable-elide-sig.wat index d5d892f6d99a..e83dfe0f9d10 100644 --- a/tests/disas/call-indirect-immutable-elide-sig.wat +++ b/tests/disas/call-indirect-immutable-elide-sig.wat @@ -27,89 +27,94 @@ (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @003f v3 = iconst.i32 1 +;; @003f v2 = iconst.i32 1 ;; @0041 jump block1 ;; ;; block1: -;; @0041 return v3 ; v3 = 1 +;; @0041 return v2 ; v2 = 1 ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0044 v3 = iconst.i32 2 +;; @0044 v2 = iconst.i32 2 ;; @0046 jump block1 ;; ;; block1: -;; @0046 return v3 ; v3 = 2 +;; @0046 return v2 ; v2 = 2 ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0049 v3 = iconst.i32 3 +;; @0049 v2 = iconst.i32 3 ;; @004b jump block1 ;; ;; block1: -;; @004b return v3 ; v3 = 3 +;; @004b return v2 ; v2 = 3 ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 1610612744 "VMFuncRef+0x8" +;; region5 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): -;; @0050 v4 = iconst.i32 10 -;; @0050 v5 = icmp uge v2, v4 ; v4 = 10 -;; @0050 v6 = uextend.i64 v2 -;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; @0050 v8 = iconst.i64 3 -;; @0050 v9 = ishl v6, v8 ; v8 = 3 -;; @0050 v10 = iadd v7, v9 -;; @0050 v11 = iconst.i64 0 -;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 -;; @0050 v13 = load.i64 user6 aligned region1 v12 -;; @0050 v14 = iconst.i64 -2 -;; @0050 v15 = band v13, v14 ; v14 = -2 -;; @0050 brif v13, block3(v15), block2 +;; @0050 v3 = iconst.i32 10 +;; @0050 v4 = icmp uge v2, v3 ; v3 = 10 +;; @0050 v5 = uextend.i64 v2 +;; @0050 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @0050 v7 = iconst.i64 3 +;; @0050 v8 = ishl v5, v7 ; v7 = 3 +;; @0050 v9 = iadd v6, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user6 aligned region3 v11 +;; @0050 v13 = iconst.i64 -2 +;; @0050 v14 = band v12, v13 ; v13 = -2 +;; @0050 brif v12, block3(v14), block2 ;; ;; block2 cold: -;; @0050 v17 = iconst.i32 0 -;; @0050 v18 = uextend.i64 v2 -;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 -;; @0050 jump block3(v19) +;; @0050 v16 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v16, v17) ; v16 = 0 +;; @0050 jump block3(v18) ;; -;; block3(v16: i64): -;; @0050 v20 = load.i64 user7 aligned readonly v16+8 -;; @0050 v21 = load.i64 notrap aligned readonly v16+24 -;; @0050 v22 = call_indirect sig0, v20(v21, v0) +;; block3(v15: i64): +;; @0050 v19 = load.i64 user7 aligned readonly region4 v15+8 +;; @0050 v20 = load.i64 notrap aligned readonly region5 v15+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v22 +;; @0053 return v21 ;; } diff --git a/tests/disas/call-indirect-immutable-static-bound.wat b/tests/disas/call-indirect-immutable-static-bound.wat index 05c3ffd748ab..4d1f8e4e414f 100644 --- a/tests/disas/call-indirect-immutable-static-bound.wat +++ b/tests/disas/call-indirect-immutable-static-bound.wat @@ -27,89 +27,94 @@ (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @003f v3 = iconst.i32 1 +;; @003f v2 = iconst.i32 1 ;; @0041 jump block1 ;; ;; block1: -;; @0041 return v3 ; v3 = 1 +;; @0041 return v2 ; v2 = 1 ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0044 v3 = iconst.i32 2 +;; @0044 v2 = iconst.i32 2 ;; @0046 jump block1 ;; ;; block1: -;; @0046 return v3 ; v3 = 2 +;; @0046 return v2 ; v2 = 2 ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0049 v3 = iconst.i32 3 +;; @0049 v2 = iconst.i32 3 ;; @004b jump block1 ;; ;; block1: -;; @004b return v3 ; v3 = 3 +;; @004b return v2 ; v2 = 3 ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 1610612744 "VMFuncRef+0x8" +;; region5 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): -;; @0050 v4 = iconst.i32 16 -;; @0050 v5 = icmp uge v2, v4 ; v4 = 16 -;; @0050 v6 = uextend.i64 v2 -;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; @0050 v8 = iconst.i64 3 -;; @0050 v9 = ishl v6, v8 ; v8 = 3 -;; @0050 v10 = iadd v7, v9 -;; @0050 v11 = iconst.i64 0 -;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 -;; @0050 v13 = load.i64 user6 aligned region1 v12 -;; @0050 v14 = iconst.i64 -2 -;; @0050 v15 = band v13, v14 ; v14 = -2 -;; @0050 brif v13, block3(v15), block2 +;; @0050 v3 = iconst.i32 16 +;; @0050 v4 = icmp uge v2, v3 ; v3 = 16 +;; @0050 v5 = uextend.i64 v2 +;; @0050 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @0050 v7 = iconst.i64 3 +;; @0050 v8 = ishl v5, v7 ; v7 = 3 +;; @0050 v9 = iadd v6, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user6 aligned region3 v11 +;; @0050 v13 = iconst.i64 -2 +;; @0050 v14 = band v12, v13 ; v13 = -2 +;; @0050 brif v12, block3(v14), block2 ;; ;; block2 cold: -;; @0050 v17 = iconst.i32 0 -;; @0050 v18 = uextend.i64 v2 -;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 -;; @0050 jump block3(v19) +;; @0050 v16 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v16, v17) ; v16 = 0 +;; @0050 jump block3(v18) ;; -;; block3(v16: i64): -;; @0050 v20 = load.i64 user7 aligned readonly v16+8 -;; @0050 v21 = load.i64 notrap aligned readonly v16+24 -;; @0050 v22 = call_indirect sig0, v20(v21, v0) +;; block3(v15: i64): +;; @0050 v19 = load.i64 user7 aligned readonly region4 v15+8 +;; @0050 v20 = load.i64 notrap aligned readonly region5 v15+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v22 +;; @0053 return v21 ;; } diff --git a/tests/disas/call-indirect-mutable-keeps-sigcheck.wat b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat index 03318a349ef7..2b1f8cf7a110 100644 --- a/tests/disas/call-indirect-mutable-keeps-sigcheck.wat +++ b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat @@ -31,57 +31,60 @@ (elem (i32.const 0) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @004d v3 = iconst.i32 1 +;; @004d v2 = iconst.i32 1 ;; @004f jump block1 ;; ;; block1: -;; @004f return v3 ; v3 = 1 +;; @004f return v2 ; v2 = 1 ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0052 v3 = iconst.i32 2 +;; @0052 v2 = iconst.i32 2 ;; @0054 jump block1 ;; ;; block1: -;; @0054 return v3 ; v3 = 2 +;; @0054 return v2 ; v2 = 2 ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0057 v3 = iconst.i32 3 +;; @0057 v2 = iconst.i32 3 ;; @0059 jump block1 ;; ;; block1: -;; @0059 return v3 ; v3 = 3 +;; @0059 return v2 ; v2 = 3 ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i32) -> i64 tail ;; fn0 = colocated u805306368:6 sig0 ;; stack_limit = gv2 @@ -92,7 +95,7 @@ ;; @0060 v5 = iconst.i32 10 ;; @0060 v6 = icmp uge v2, v5 ; v5 = 10 ;; @0060 v7 = uextend.i64 v2 -;; @0060 v8 = load.i64 notrap aligned readonly can_move v0+48 +;; @0060 v8 = load.i64 notrap aligned readonly can_move region2 v0+48 ;; @0060 v9 = iconst.i64 3 ;; @0060 v10 = ishl v7, v9 ; v9 = 3 ;; @0060 v11 = iadd v8, v10 @@ -100,7 +103,7 @@ ;; @0060 v13 = select_spectre_guard v6, v12, v11 ; v12 = 0 ;; @0060 v14 = iconst.i64 1 ;; @0060 v15 = bor v4, v14 ; v14 = 1 -;; @0060 store user6 aligned region1 v15, v13 +;; @0060 store user6 aligned region3 v15, v13 ;; @0062 jump block1 ;; ;; block1: @@ -109,51 +112,55 @@ ;; ;; function u0:4(i64 vmctx, i64, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; region2 = 40 "VMContext+0x28" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 40 "VMContext+0x28" +;; region5 = 1677721600 "TypeIdsArray+0x0" +;; region6 = 1610612752 "VMFuncRef+0x10" +;; region7 = 1610612744 "VMFuncRef+0x8" +;; region8 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): -;; @0067 v4 = iconst.i32 10 -;; @0067 v5 = icmp uge v2, v4 ; v4 = 10 -;; @0067 v6 = uextend.i64 v2 -;; @0067 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; @0067 v8 = iconst.i64 3 -;; @0067 v9 = ishl v6, v8 ; v8 = 3 -;; @0067 v10 = iadd v7, v9 -;; @0067 v11 = iconst.i64 0 -;; @0067 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 -;; @0067 v13 = load.i64 user6 aligned region1 v12 -;; @0067 v14 = iconst.i64 -2 -;; @0067 v15 = band v13, v14 ; v14 = -2 -;; @0067 brif v13, block3(v15), block2 +;; @0067 v3 = iconst.i32 10 +;; @0067 v4 = icmp uge v2, v3 ; v3 = 10 +;; @0067 v5 = uextend.i64 v2 +;; @0067 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @0067 v7 = iconst.i64 3 +;; @0067 v8 = ishl v5, v7 ; v7 = 3 +;; @0067 v9 = iadd v6, v8 +;; @0067 v10 = iconst.i64 0 +;; @0067 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 +;; @0067 v12 = load.i64 user6 aligned region3 v11 +;; @0067 v13 = iconst.i64 -2 +;; @0067 v14 = band v12, v13 ; v13 = -2 +;; @0067 brif v12, block3(v14), block2 ;; ;; block2 cold: -;; @0067 v17 = iconst.i32 0 -;; @0067 v18 = uextend.i64 v2 -;; @0067 v19 = call fn0(v0, v17, v18) ; v17 = 0 -;; @0067 jump block3(v19) -;; -;; block3(v16: i64): -;; @0067 v20 = load.i64 notrap aligned readonly can_move region2 v0+40 -;; @0067 v21 = load.i32 notrap aligned readonly can_move v20 -;; @0067 v22 = load.i32 user7 aligned readonly v16+16 -;; @0067 v23 = icmp eq v22, v21 -;; @0067 v24 = uextend.i32 v23 -;; @0067 trapz v24, user8 -;; @0067 v25 = load.i64 notrap aligned readonly v16+8 -;; @0067 v26 = load.i64 notrap aligned readonly v16+24 -;; @0067 v27 = call_indirect sig0, v25(v26, v0) +;; @0067 v16 = iconst.i32 0 +;; @0067 v17 = uextend.i64 v2 +;; @0067 v18 = call fn0(v0, v16, v17) ; v16 = 0 +;; @0067 jump block3(v18) +;; +;; block3(v15: i64): +;; @0067 v19 = load.i64 notrap aligned readonly can_move region4 v0+40 +;; @0067 v20 = load.i32 notrap aligned readonly can_move region5 v19 +;; @0067 v21 = load.i32 user7 aligned readonly region6 v15+16 +;; @0067 v22 = icmp eq v21, v20 +;; @0067 v23 = uextend.i32 v22 +;; @0067 trapz v23, user8 +;; @0067 v24 = load.i64 notrap aligned readonly region7 v15+8 +;; @0067 v25 = load.i64 notrap aligned readonly region8 v15+24 +;; @0067 v26 = call_indirect sig0, v24(v25, v0) ;; @006a jump block1 ;; ;; block1: -;; @006a return v27 +;; @006a return v26 ;; } diff --git a/tests/disas/gc/call-indirect-final-type.wat b/tests/disas/gc/call-indirect-final-type.wat index 13ffa96bec62..9bc2dbe79046 100644 --- a/tests/disas/gc/call-indirect-final-type.wat +++ b/tests/disas/gc/call-indirect-final-type.wat @@ -17,80 +17,88 @@ ) ;; function u0:0(i64 vmctx, i64, i32, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; region2 = 40 "VMContext+0x28" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 40 "VMContext+0x28" +;; region5 = 1677721600 "TypeIdsArray+0x0" +;; region6 = 1610612752 "VMFuncRef+0x10" +;; region7 = 1610612744 "VMFuncRef+0x8" +;; region8 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64, i32) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32, v3: i32): -;; @002b v12 = iconst.i64 0 -;; @002b v14 = load.i64 user6 aligned region1 v12 ; v12 = 0 -;; @002b v15 = iconst.i64 -2 -;; @002b v16 = band v14, v15 ; v15 = -2 -;; @002b brif v14, block3(v16), block2 +;; @002b v11 = iconst.i64 0 +;; @002b v13 = load.i64 user6 aligned region3 v11 ; v11 = 0 +;; @002b v14 = iconst.i64 -2 +;; @002b v15 = band v13, v14 ; v14 = -2 +;; @002b brif v13, block3(v15), block2 ;; ;; block2 cold: -;; @002b v5 = iconst.i32 0 -;; @002b v7 = uextend.i64 v3 -;; @002b v20 = call fn0(v0, v5, v7) ; v5 = 0 -;; @002b jump block3(v20) +;; @002b v4 = iconst.i32 0 +;; @002b v6 = uextend.i64 v3 +;; @002b v19 = call fn0(v0, v4, v6) ; v4 = 0 +;; @002b jump block3(v19) ;; -;; block3(v17: i64): -;; @002b v23 = load.i32 user7 aligned readonly v17+16 -;; @002b v21 = load.i64 notrap aligned readonly can_move region2 v0+40 -;; @002b v22 = load.i32 notrap aligned readonly can_move v21 -;; @002b v24 = icmp eq v23, v22 -;; @002b trapz v24, user8 -;; @002b v26 = load.i64 notrap aligned readonly v17+8 -;; @002b v27 = load.i64 notrap aligned readonly v17+24 -;; @002b v28 = call_indirect sig0, v26(v27, v0, v2) +;; block3(v16: i64): +;; @002b v22 = load.i32 user7 aligned readonly region6 v16+16 +;; @002b v20 = load.i64 notrap aligned readonly can_move region4 v0+40 +;; @002b v21 = load.i32 notrap aligned readonly can_move region5 v20 +;; @002b v23 = icmp eq v22, v21 +;; @002b trapz v23, user8 +;; @002b v25 = load.i64 notrap aligned readonly region7 v16+8 +;; @002b v26 = load.i64 notrap aligned readonly region8 v16+24 +;; @002b v27 = call_indirect sig0, v25(v26, v0, v2) ;; @002e jump block1 ;; ;; block1: -;; @002e return v28 +;; @002e return v27 ;; } ;; ;; function u0:1(i64 vmctx, i64, i32, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; region2 = 40 "VMContext+0x28" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 40 "VMContext+0x28" +;; region5 = 1677721600 "TypeIdsArray+0x0" +;; region6 = 1610612752 "VMFuncRef+0x10" +;; region7 = 1610612744 "VMFuncRef+0x8" +;; region8 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64, i32) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32, v3: i32): -;; @0035 v12 = iconst.i64 0 -;; @0035 v14 = load.i64 user6 aligned region1 v12 ; v12 = 0 -;; @0035 v15 = iconst.i64 -2 -;; @0035 v16 = band v14, v15 ; v15 = -2 -;; @0035 brif v14, block3(v16), block2 +;; @0035 v11 = iconst.i64 0 +;; @0035 v13 = load.i64 user6 aligned region3 v11 ; v11 = 0 +;; @0035 v14 = iconst.i64 -2 +;; @0035 v15 = band v13, v14 ; v14 = -2 +;; @0035 brif v13, block3(v15), block2 ;; ;; block2 cold: -;; @0035 v5 = iconst.i32 0 -;; @0035 v7 = uextend.i64 v3 -;; @0035 v20 = call fn0(v0, v5, v7) ; v5 = 0 -;; @0035 jump block3(v20) +;; @0035 v4 = iconst.i32 0 +;; @0035 v6 = uextend.i64 v3 +;; @0035 v19 = call fn0(v0, v4, v6) ; v4 = 0 +;; @0035 jump block3(v19) ;; -;; block3(v17: i64): -;; @0035 v23 = load.i32 user7 aligned readonly v17+16 -;; @0035 v21 = load.i64 notrap aligned readonly can_move region2 v0+40 -;; @0035 v22 = load.i32 notrap aligned readonly can_move v21 -;; @0035 v24 = icmp eq v23, v22 -;; @0035 trapz v24, user8 -;; @0035 v26 = load.i64 notrap aligned readonly v17+8 -;; @0035 v27 = load.i64 notrap aligned readonly v17+24 -;; @0035 return_call_indirect sig0, v26(v27, v0, v2) +;; block3(v16: i64): +;; @0035 v22 = load.i32 user7 aligned readonly region6 v16+16 +;; @0035 v20 = load.i64 notrap aligned readonly can_move region4 v0+40 +;; @0035 v21 = load.i32 notrap aligned readonly can_move region5 v20 +;; @0035 v23 = icmp eq v22, v21 +;; @0035 trapz v23, user8 +;; @0035 v25 = load.i64 notrap aligned readonly region7 v16+8 +;; @0035 v26 = load.i64 notrap aligned readonly region8 v16+24 +;; @0035 return_call_indirect sig0, v25(v26, v0, v2) ;; } diff --git a/tests/disas/indirect-call-no-caching.wat b/tests/disas/indirect-call-no-caching.wat index 1a2e852558bb..3d4cc4448291 100644 --- a/tests/disas/indirect-call-no-caching.wat +++ b/tests/disas/indirect-call-no-caching.wat @@ -22,89 +22,94 @@ (elem (i32.const 1) func $f1 $f2 $f3)) ;; function u0:0(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @003f v3 = iconst.i32 1 +;; @003f v2 = iconst.i32 1 ;; @0041 jump block1 ;; ;; block1: -;; @0041 return v3 ; v3 = 1 +;; @0041 return v2 ; v2 = 1 ;; } ;; ;; function u0:1(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0044 v3 = iconst.i32 2 +;; @0044 v2 = iconst.i32 2 ;; @0046 jump block1 ;; ;; block1: -;; @0046 return v3 ; v3 = 2 +;; @0046 return v2 ; v2 = 2 ;; } ;; ;; function u0:2(i64 vmctx, i64) -> i32 tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): -;; @0049 v3 = iconst.i32 3 +;; @0049 v2 = iconst.i32 3 ;; @004b jump block1 ;; ;; block1: -;; @004b return v3 ; v3 = 3 +;; @004b return v2 ; v2 = 3 ;; } ;; ;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 1610612744 "VMFuncRef+0x8" +;; region5 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64) -> i32 tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64, v2: i32): -;; @0050 v4 = iconst.i32 10 -;; @0050 v5 = icmp uge v2, v4 ; v4 = 10 -;; @0050 v6 = uextend.i64 v2 -;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; @0050 v8 = iconst.i64 3 -;; @0050 v9 = ishl v6, v8 ; v8 = 3 -;; @0050 v10 = iadd v7, v9 -;; @0050 v11 = iconst.i64 0 -;; @0050 v12 = select_spectre_guard v5, v11, v10 ; v11 = 0 -;; @0050 v13 = load.i64 user6 aligned region1 v12 -;; @0050 v14 = iconst.i64 -2 -;; @0050 v15 = band v13, v14 ; v14 = -2 -;; @0050 brif v13, block3(v15), block2 +;; @0050 v3 = iconst.i32 10 +;; @0050 v4 = icmp uge v2, v3 ; v3 = 10 +;; @0050 v5 = uextend.i64 v2 +;; @0050 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @0050 v7 = iconst.i64 3 +;; @0050 v8 = ishl v5, v7 ; v7 = 3 +;; @0050 v9 = iadd v6, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user6 aligned region3 v11 +;; @0050 v13 = iconst.i64 -2 +;; @0050 v14 = band v12, v13 ; v13 = -2 +;; @0050 brif v12, block3(v14), block2 ;; ;; block2 cold: -;; @0050 v17 = iconst.i32 0 -;; @0050 v18 = uextend.i64 v2 -;; @0050 v19 = call fn0(v0, v17, v18) ; v17 = 0 -;; @0050 jump block3(v19) +;; @0050 v16 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v16, v17) ; v16 = 0 +;; @0050 jump block3(v18) ;; -;; block3(v16: i64): -;; @0050 v20 = load.i64 user7 aligned readonly v16+8 -;; @0050 v21 = load.i64 notrap aligned readonly v16+24 -;; @0050 v22 = call_indirect sig0, v20(v21, v0) +;; block3(v15: i64): +;; @0050 v19 = load.i64 user7 aligned readonly region4 v15+8 +;; @0050 v20 = load.i64 notrap aligned readonly region5 v15+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v22 +;; @0053 return v21 ;; } diff --git a/tests/disas/readonly-funcrefs.wat b/tests/disas/readonly-funcrefs.wat index e341fbcc4dba..b21bbcbbae47 100644 --- a/tests/disas/readonly-funcrefs.wat +++ b/tests/disas/readonly-funcrefs.wat @@ -20,9 +20,10 @@ ;; function u0:0(i64 vmctx, i64) tail { ;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): @@ -34,12 +35,14 @@ ;; ;; function u0:1(i64 vmctx, i64, i32) tail { ;; region0 = 8 "VMContext+0x8" -;; region1 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region4 = 1610612744 "VMFuncRef+0x8" +;; region5 = 1610612760 "VMFuncRef+0x18" ;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly region0 gv0+8 -;; gv2 = load.i64 notrap aligned gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 ;; sig0 = (i64 vmctx, i64) tail ;; sig1 = (i64 vmctx, i32, i64) -> i64 tail ;; fn0 = colocated u805306368:7 sig1 @@ -49,13 +52,13 @@ ;; @0031 v3 = iconst.i32 2 ;; @0031 v4 = icmp uge v2, v3 ; v3 = 2 ;; @0031 v10 = iconst.i64 0 -;; @0031 v6 = load.i64 notrap aligned readonly can_move v0+48 +;; @0031 v6 = load.i64 notrap aligned readonly can_move region2 v0+48 ;; @0031 v5 = uextend.i64 v2 ;; @0031 v7 = iconst.i64 3 ;; @0031 v8 = ishl v5, v7 ; v7 = 3 ;; @0031 v9 = iadd v6, v8 ;; @0031 v11 = select_spectre_guard v4, v10, v9 ; v10 = 0 -;; @0031 v12 = load.i64 user6 aligned region1 v11 +;; @0031 v12 = load.i64 user6 aligned region3 v11 ;; @0031 v13 = iconst.i64 -2 ;; @0031 v14 = band v12, v13 ; v13 = -2 ;; @0031 brif v12, block3(v14), block2 @@ -66,8 +69,8 @@ ;; @0031 jump block3(v18) ;; ;; block3(v15: i64): -;; @0031 v19 = load.i64 user7 aligned readonly v15+8 -;; @0031 v20 = load.i64 notrap aligned readonly v15+24 +;; @0031 v19 = load.i64 user7 aligned readonly region4 v15+8 +;; @0031 v20 = load.i64 notrap aligned readonly region5 v15+24 ;; @0031 call_indirect sig0, v19(v20, v0) ;; @0034 jump block1 ;; diff --git a/tests/disas/startup-elem-active.wat b/tests/disas/startup-elem-active.wat index dcd928363d66..5fdf9990a768 100644 --- a/tests/disas/startup-elem-active.wat +++ b/tests/disas/startup-elem-active.wat @@ -46,59 +46,22 @@ ;; } ;; ;; function u2415919104:0(i64 vmctx, i64) tail { -<<<<<<< HEAD ;; region0 = 671088640 "VMTableDefinition+0x0" -;; region1 = 671088648 "VMTableDefinition+0x8" -;; region2 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; ;; block0(v0: i64, v1: i64): -;; v4 = load.i64 notrap aligned region1 v0+56 -;; v5 = ireduce.i32 v4 -;; v6 = uextend.i64 v5 -;; v78 = iconst.i64 4 -;; v84 = icmp ult v6, v78 ; v78 = 4 -;; trapnz v84, user6 -;; v13 = load.i64 notrap aligned region0 v0+48 -;; v95 = iconst.i32 21 -;; v2 = iconst.i32 1 -;; v106 = icmp ule v5, v2 ; v2 = 1 -;; v71 = iconst.i64 0 -;; v17 = iadd v13, v78 ; v78 = 4 -;; v34 = select_spectre_guard v106, v71, v17 ; v71 = 0 -;; store user6 aligned region2 v95, v34 ; v95 = 21 -;; v109 = iconst.i32 23 -;; v115 = iconst.i32 2 -;; v121 = icmp ule v5, v115 ; v115 = 2 -;; v123 = iconst.i64 8 -;; v49 = iadd v13, v123 ; v123 = 8 -;; v51 = select_spectre_guard v121, v71, v49 ; v71 = 0 -;; store user6 aligned region2 v109, v51 ; v109 = 23 -;; v125 = iconst.i32 25 -;; v3 = iconst.i32 3 -;; v136 = icmp ule v5, v3 ; v3 = 3 -;; v138 = iconst.i64 12 -;; v66 = iadd v13, v138 ; v138 = 12 -;; v68 = select_spectre_guard v136, v71, v66 ; v71 = 0 -;; store user6 aligned region2 v125, v68 ; v125 = 25 -======= -;; region0 = 1342177280 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" -;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move gv0+48 -;; -;; block0(v0: i64, v1: i64): -;; v100 = iconst.i32 21 -;; v12 = load.i64 notrap aligned readonly can_move v0+48 -;; v79 = iconst.i64 4 -;; v16 = iadd v12, v79 ; v79 = 4 -;; store user6 aligned region0 v100, v16 ; v100 = 21 -;; v117 = iconst.i32 23 -;; v134 = iconst.i64 8 -;; v46 = iadd v12, v134 ; v134 = 8 -;; store user6 aligned region0 v117, v46 ; v117 = 23 -;; v136 = iconst.i32 25 -;; v152 = iconst.i64 12 -;; v62 = iadd v12, v152 ; v152 = 12 -;; store user6 aligned region0 v136, v62 ; v136 = 25 ->>>>>>> 8ca462d0d4 (port call_indirect elisions to upstream table_initialization model) +;; v96 = iconst.i32 21 +;; v12 = load.i64 notrap aligned readonly can_move region0 v0+48 +;; v75 = iconst.i64 4 +;; v16 = iadd v12, v75 ; v75 = 4 +;; store user6 aligned region1 v96, v16 ; v96 = 21 +;; v113 = iconst.i32 23 +;; v130 = iconst.i64 8 +;; v46 = iadd v12, v130 ; v130 = 8 +;; store user6 aligned region1 v113, v46 ; v113 = 23 +;; v132 = iconst.i32 25 +;; v148 = iconst.i64 12 +;; v62 = iadd v12, v148 ; v148 = 12 +;; store user6 aligned region1 v132, v62 ; v132 = 25 ;; return ;; } diff --git a/tests/disas/startup-table-initial-value.wat b/tests/disas/startup-table-initial-value.wat index c7de859100cd..3bdc39250ed9 100644 --- a/tests/disas/startup-table-initial-value.wat +++ b/tests/disas/startup-table-initial-value.wat @@ -40,54 +40,25 @@ ;; } ;; ;; function u2415919104:0(i64 vmctx, i64) tail { -<<<<<<< HEAD ;; region0 = 671088640 "VMTableDefinition+0x0" -;; region1 = 671088648 "VMTableDefinition+0x8" -;; region2 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region1 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" ;; ;; block0(v0: i64, v1: i64): -;; v9 = load.i64 notrap aligned region1 v0+56 -;; v10 = ireduce.i32 v9 -;; v11 = uextend.i64 v10 -;; v39 = iconst.i64 10 -;; v51 = icmp ult v11, v39 ; v39 = 10 -;; trapnz v51, user6 -;; v18 = load.i64 notrap aligned region0 v0+48 +;; v17 = load.i64 notrap aligned readonly can_move region0 v0+48 ;; v3 = iconst.i32 1 -;; v81 = iconst.i64 36 -;; v83 = iadd v18, v81 ; v81 = 36 -;; v20 = iconst.i64 4 -;; jump block1(v18) -;; -;; block1(v29: i64): -;; v86 = iconst.i32 1 -;; store notrap aligned region2 v86, v29 ; v86 = 1 -;; v87 = iadd.i64 v18, v81 ; v81 = 36 -;; v88 = icmp eq v29, v87 -;; v89 = iconst.i64 4 -;; v90 = iadd v29, v89 ; v89 = 4 -;; brif v88, block2, block1(v90) -======= -;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move gv0+48 -;; -;; block0(v0: i64, v1: i64): -;; v17 = load.i64 notrap aligned readonly can_move v0+48 -;; v3 = iconst.i32 1 -;; v84 = iconst.i64 36 -;; v86 = iadd v17, v84 ; v84 = 36 +;; v83 = iconst.i64 36 +;; v85 = iadd v17, v83 ; v83 = 36 ;; v19 = iconst.i64 4 ;; jump block1(v17) ;; ;; block1(v28: i64): -;; v89 = iconst.i32 1 -;; store notrap aligned v89, v28 ; v89 = 1 -;; v90 = iadd.i64 v17, v84 ; v84 = 36 -;; v91 = icmp eq v28, v90 -;; v92 = iconst.i64 4 -;; v93 = iadd v28, v92 ; v92 = 4 -;; brif v91, block2, block1(v93) ->>>>>>> 8ca462d0d4 (port call_indirect elisions to upstream table_initialization model) +;; v88 = iconst.i32 1 +;; store notrap aligned region1 v88, v28 ; v88 = 1 +;; v89 = iadd.i64 v17, v83 ; v83 = 36 +;; v90 = icmp eq v28, v89 +;; v91 = iconst.i64 4 +;; v92 = iadd v28, v91 ; v91 = 4 +;; brif v90, block2, block1(v92) ;; ;; block2: ;; return diff --git a/tests/misc_testsuite/immutable-table-call-indirect.wast b/tests/misc_testsuite/immutable-table-call-indirect.wast new file mode 100644 index 000000000000..3b40cb9ab534 --- /dev/null +++ b/tests/misc_testsuite/immutable-table-call-indirect.wast @@ -0,0 +1,71 @@ +;;! reference_types = true + +;; call_indirect through tables that are never grown, exported, or mutated. +;; Compilation may use a constant bound and elide null/signature checks on +;; these shapes; runtime behavior must be unchanged: in-bounds calls work, +;; and out-of-bounds, null-slot, and signature-mismatch accesses still trap. + +;; Mixed-signature immutable table with a null hole. +(module + (type $i2i (func (param i32) (result i32))) + (type $v2i (func (result i32))) + (table 5 funcref) + (elem (i32.const 0) $add1 $ten $add1) + + (func $add1 (type $i2i) (i32.add (local.get 0) (i32.const 1))) + (func $ten (type $v2i) (i32.const 10)) + + (func (export "call-i2i") (param i32 i32) (result i32) + (call_indirect (type $i2i) (local.get 1) (local.get 0))) + (func (export "call-v2i") (param i32) (result i32) + (call_indirect (type $v2i) (local.get 0)))) + +(assert_return (invoke "call-i2i" (i32.const 0) (i32.const 41)) (i32.const 42)) +(assert_return (invoke "call-i2i" (i32.const 2) (i32.const 7)) (i32.const 8)) +(assert_return (invoke "call-v2i" (i32.const 1)) (i32.const 10)) + +;; Signature mismatch still traps. +(assert_trap (invoke "call-i2i" (i32.const 1) (i32.const 0)) "indirect call type mismatch") +(assert_trap (invoke "call-v2i" (i32.const 0)) "indirect call type mismatch") + +;; Null slots still trap: slot 3 was never initialized. +(assert_trap (invoke "call-i2i" (i32.const 3) (i32.const 0)) "uninitialized element") +(assert_trap (invoke "call-v2i" (i32.const 4)) "uninitialized element") + +;; Out of bounds still traps against the constant bound. +(assert_trap (invoke "call-i2i" (i32.const 5) (i32.const 0)) "undefined element") +(assert_trap (invoke "call-i2i" (i32.const -1) (i32.const 0)) "undefined element") + +;; Uniform-signature immutable table, fully initialized. +(module + (type $v2i (func (result i32))) + (table 3 funcref) + (elem (i32.const 0) $a $b $c) + + (func $a (type $v2i) (i32.const 1)) + (func $b (type $v2i) (i32.const 2)) + (func $c (type $v2i) (i32.const 3)) + + (func (export "call") (param i32) (result i32) + (call_indirect (type $v2i) (local.get 0))) + (func (export "call-wrong-type") (param i32 i32) (result i32) + (call_indirect (param i32) (result i32) (local.get 1) (local.get 0)))) + +(assert_return (invoke "call" (i32.const 0)) (i32.const 1)) +(assert_return (invoke "call" (i32.const 1)) (i32.const 2)) +(assert_return (invoke "call" (i32.const 2)) (i32.const 3)) +(assert_trap (invoke "call" (i32.const 3)) "undefined element") + +;; A caller whose expected type differs from the table's uniform type must +;; still observe the mismatch. +(assert_trap (invoke "call-wrong-type" (i32.const 0) (i32.const 0)) "indirect call type mismatch") + +;; Same shapes through a declared-growable (no max) table never actually +;; grown: an empty never-grown table has no valid index. +(module + (table 0 100 funcref) + (func (export "call-empty") (param i32) + (call_indirect (local.get 0)))) + +(assert_trap (invoke "call-empty" (i32.const 0)) "undefined element") +(assert_trap (invoke "call-empty" (i32.const 99)) "undefined element") From b06489340b9f75eef62812e26333b672ecbea542 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 20:02:38 -0700 Subject: [PATCH 10/12] pulley: add call_indirect{1,2,3,4} fused indirect-call ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror of the direct-call `call{1,2,3,4}` family: each new op combines `xmov xN, argN` ABI fixups with the indirect call. Reads arg values before writing the ABI registers so the sequence is safe when an argN aliases the corresponding ABI register. `call_indirect1 dst, arg1`: x0 = state[arg1] lr = pc pc = state[dst] Saves up to N Pulley dispatches per call_indirect site (one per moved arg). In practice at least one — the callee vmctx ABI fixup. Cranelift wiring in the next commit. --- pulley/src/interp.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++ pulley/src/lib.rs | 11 +++++++ 2 files changed, 89 insertions(+) diff --git a/pulley/src/interp.rs b/pulley/src/interp.rs index 5b3f79445340..c7533bd5fc09 100644 --- a/pulley/src/interp.rs +++ b/pulley/src/interp.rs @@ -1425,6 +1425,84 @@ impl OpVisitor for Interpreter<'_> { ControlFlow::Continue(()) } + fn call_indirect1(&mut self, dst: XReg, arg1: XReg) -> ControlFlow { + // Phase-4 fusion: combines `xmov x0, arg1` with `call_indirect dst`. + // Read arg1 BEFORE writing x0 so this is safe even when `arg1 == x0`. + let arg1_val = self.state[arg1]; + let target = self.state[dst].get_ptr(); + let return_addr = self.pc.as_ptr(); + self.state.lr = return_addr.as_ptr(); + self.state[XReg::x0] = arg1_val; + // SAFETY: same as `call_indirect`. + unsafe { + self.pc = UnsafeBytecodeStream::new(NonNull::new_unchecked(target)); + } + ControlFlow::Continue(()) + } + + fn call_indirect2(&mut self, dst: XReg, arg1: XReg, arg2: XReg) -> ControlFlow { + let (a1, a2) = (self.state[arg1], self.state[arg2]); + let target = self.state[dst].get_ptr(); + let return_addr = self.pc.as_ptr(); + self.state.lr = return_addr.as_ptr(); + self.state[XReg::x0] = a1; + self.state[XReg::x1] = a2; + // SAFETY: same as `call_indirect`. + unsafe { + self.pc = UnsafeBytecodeStream::new(NonNull::new_unchecked(target)); + } + ControlFlow::Continue(()) + } + + fn call_indirect3( + &mut self, + dst: XReg, + arg1: XReg, + arg2: XReg, + arg3: XReg, + ) -> ControlFlow { + let (a1, a2, a3) = (self.state[arg1], self.state[arg2], self.state[arg3]); + let target = self.state[dst].get_ptr(); + let return_addr = self.pc.as_ptr(); + self.state.lr = return_addr.as_ptr(); + self.state[XReg::x0] = a1; + self.state[XReg::x1] = a2; + self.state[XReg::x2] = a3; + // SAFETY: same as `call_indirect`. + unsafe { + self.pc = UnsafeBytecodeStream::new(NonNull::new_unchecked(target)); + } + ControlFlow::Continue(()) + } + + fn call_indirect4( + &mut self, + dst: XReg, + arg1: XReg, + arg2: XReg, + arg3: XReg, + arg4: XReg, + ) -> ControlFlow { + let (a1, a2, a3, a4) = ( + self.state[arg1], + self.state[arg2], + self.state[arg3], + self.state[arg4], + ); + let target = self.state[dst].get_ptr(); + let return_addr = self.pc.as_ptr(); + self.state.lr = return_addr.as_ptr(); + self.state[XReg::x0] = a1; + self.state[XReg::x1] = a2; + self.state[XReg::x2] = a3; + self.state[XReg::x3] = a4; + // SAFETY: same as `call_indirect`. + unsafe { + self.pc = UnsafeBytecodeStream::new(NonNull::new_unchecked(target)); + } + ControlFlow::Continue(()) + } + fn jump(&mut self, offset: PcRelOffset) -> ControlFlow { self.pc_rel_jump::(offset) } diff --git a/pulley/src/lib.rs b/pulley/src/lib.rs index 36a09cb13a34..8552d21f74b5 100644 --- a/pulley/src/lib.rs +++ b/pulley/src/lib.rs @@ -115,6 +115,17 @@ macro_rules! for_each_op { /// Transfer control to the PC in `reg` and set `lr` to the PC just /// after this instruction. call_indirect = CallIndirect { reg: XReg }; + /// Like `call_indirect`, but also `x0 = arg1`. Saves one Pulley + /// dispatch vs `xmov x0, arg1; call_indirect reg` for the common + /// call_indirect pattern where one ABI register (usually `vmctx`) + /// is set up immediately before the indirect call. + call_indirect1 = CallIndirect1 { reg: XReg, arg1: XReg }; + /// Like `call_indirect`, but also `x0, x1 = arg1, arg2`. + call_indirect2 = CallIndirect2 { reg: XReg, arg1: XReg, arg2: XReg }; + /// Like `call_indirect`, but also `x0, x1, x2 = arg1, arg2, arg3`. + call_indirect3 = CallIndirect3 { reg: XReg, arg1: XReg, arg2: XReg, arg3: XReg }; + /// Like `call_indirect`, but also `x0, x1, x2, x3 = arg1, arg2, arg3, arg4`. + call_indirect4 = CallIndirect4 { reg: XReg, arg1: XReg, arg2: XReg, arg3: XReg, arg4: XReg }; /// Unconditionally transfer control to the PC at the given offset. jump = Jump { offset: PcRelOffset }; From bd5dca25b939647f8e98532eff5562941d037ea1 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 21 May 2026 20:02:38 -0700 Subject: [PATCH 11/12] cranelift/pulley: pass first 4 indirect-call args via call_indirectN Extend `Inst::IndirectCall`'s `info.dest` from `XReg` to `PulleyCallIndirect { target, args: SmallVec<[XReg; 4]> }`, parallel to `PulleyCall`. `gen_call_ind_info` pulls the first 0-4 integer args from `uses` (where they were going through regalloc's `reg_fixed_use`, synthesising an `xmov` each) into `args`, where they flow as free reg uses and the emitted `call_indirect{1,2,3,4}` opcode moves them at call time. The emit side picks the narrowest op after the same "drop args already in their ABI register" loop used by direct calls. Net effect is one fewer Pulley dispatch per moved arg at each call_indirect site: the ABI argument-move fixups regalloc used to emit as standalone `xmov` dispatches fold into the call op itself. Filetest snapshots updated for the new `dest` shape. --- .../src/isa/pulley_shared/inst/args.rs | 20 ++++++++++++ .../src/isa/pulley_shared/inst/emit.rs | 18 ++++++++++- .../codegen/src/isa/pulley_shared/inst/mod.rs | 15 +++++++-- .../src/isa/pulley_shared/lower/isle.rs | 32 ++++++++++++++++--- .../filetests/isa/pulley32/call.clif | 2 +- .../filetests/isa/pulley32/exceptions.clif | 2 +- .../filetests/isa/pulley32/preserve-all.clif | 4 +-- .../filetests/isa/pulley64/call.clif | 2 +- .../filetests/isa/pulley64/exceptions.clif | 2 +- .../filetests/isa/pulley64/preserve-all.clif | 4 +-- tests/disas/pulley/call.wat | 6 ++-- 11 files changed, 87 insertions(+), 20 deletions(-) diff --git a/cranelift/codegen/src/isa/pulley_shared/inst/args.rs b/cranelift/codegen/src/isa/pulley_shared/inst/args.rs index e97e3303ef99..79bafbe2fa39 100644 --- a/cranelift/codegen/src/isa/pulley_shared/inst/args.rs +++ b/cranelift/codegen/src/isa/pulley_shared/inst/args.rs @@ -577,6 +577,26 @@ pub struct PulleyCall { pub args: SmallVec<[XReg; 4]>, } +/// Payload of `CallInfo` for indirect-call instructions. +/// +/// Mirror of `PulleyCall` for `Inst::IndirectCall`: the call target is a +/// runtime register (the loaded `wasm_call` pointer at the call_indirect +/// dispatch tail), and the first 0–4 integer ABI args are passed as free +/// registers so the `call_indirect1/2/3/4` opcodes can move them into +/// `x0..x3` as part of the call (saving one `xmov` per arg on the hot +/// dispatch path). Remaining args live in `CallInfo::uses` with fixed +/// pregs, just as for `PulleyCall`. +#[derive(Clone, Debug)] +pub struct PulleyCallIndirect { + /// The register holding the call target (e.g. the `wasm_call` pointer + /// loaded out of a `VMFuncRef`). + pub target: XReg, + /// Up to 4 integer args destined for `x0..x3`. Tracked separately so + /// regalloc doesn't insert moves and the `call_indirectN` opcode moves + /// them itself. + pub args: SmallVec<[XReg; 4]>, +} + pub use super::super::lower::isle::generated_code::AddrO32; impl Copy for AddrO32 {} diff --git a/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs b/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs index 74bff5d97a7d..1f8e8251c0a8 100644 --- a/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs +++ b/cranelift/codegen/src/isa/pulley_shared/inst/emit.rs @@ -233,7 +233,23 @@ fn pulley_emit

( } Inst::IndirectCall { info } => { - enc::call_indirect(sink, info.dest); + // If x0..xN args are already in their correct ABI register + // (because regalloc allocated the producer's vreg there), drop + // them off the end so we can use a narrower `call_indirectN` + // op — mirror of the direct-call shrink loop above. + let target = info.dest.target; + let mut args = &info.dest.args[..]; + while !args.is_empty() && args.last().copied() == XReg::new(x_reg(args.len() - 1)) { + args = &args[..args.len() - 1]; + } + match args { + [] => enc::call_indirect(sink, target), + [x0] => enc::call_indirect1(sink, target, *x0), + [x0, x1] => enc::call_indirect2(sink, target, *x0, *x1), + [x0, x1, x2] => enc::call_indirect3(sink, target, *x0, *x1, *x2), + [x0, x1, x2, x3] => enc::call_indirect4(sink, target, *x0, *x1, *x2, *x3), + _ => unreachable!(), + } if let Some(s) = state.take_stack_map() { let offset = sink.cur_offset(); diff --git a/cranelift/codegen/src/isa/pulley_shared/inst/mod.rs b/cranelift/codegen/src/isa/pulley_shared/inst/mod.rs index 6bbe69795e51..59ac484f069c 100644 --- a/cranelift/codegen/src/isa/pulley_shared/inst/mod.rs +++ b/cranelift/codegen/src/isa/pulley_shared/inst/mod.rs @@ -206,14 +206,25 @@ fn pulley_get_operands(inst: &mut Inst, collector: &mut impl OperandVisitor) { } } Inst::IndirectCall { info } => { - collector.reg_use(&mut info.dest); let CallInfo { uses, defs, + dest, try_call_info, clobbers, .. } = &mut **info; + + // Phase-4: the target and the first up-to-4 integer args live + // in `dest` and are passed as free reg uses; the emitted + // `call_indirect{1,2,3,4}` op moves the args into x0..x3 at + // call time. Remaining args still flow through `uses` with + // fixed pregs as before. + let PulleyCallIndirect { target, args } = dest; + collector.reg_use(target); + for arg in args { + collector.reg_use(arg); + } for CallArgPair { vreg, preg } in uses { collector.reg_fixed_use(vreg, *preg); } @@ -723,7 +734,7 @@ impl Inst { } Inst::IndirectCall { info } => { - let callee = format_reg(*info.dest); + let callee = format_reg(*info.dest.target); let try_call = info .try_call_info .as_ref() diff --git a/cranelift/codegen/src/isa/pulley_shared/lower/isle.rs b/cranelift/codegen/src/isa/pulley_shared/lower/isle.rs index 3068fb1137ff..c065c45a2c6a 100644 --- a/cranelift/codegen/src/isa/pulley_shared/lower/isle.rs +++ b/cranelift/codegen/src/isa/pulley_shared/lower/isle.rs @@ -10,8 +10,8 @@ use crate::ir::{condcodes::*, immediates::*, types::*, *}; use crate::isa::CallConv; use crate::isa::pulley_shared::{ inst::{ - FReg, OperandSize, PulleyCall, ReturnCallInfo, VReg, WritableFReg, WritableVReg, - WritableXReg, XReg, + FReg, OperandSize, PulleyCall, PulleyCallIndirect, ReturnCallInfo, VReg, WritableFReg, + WritableVReg, WritableXReg, XReg, }, lower::{Cond, regs}, *, @@ -30,7 +30,7 @@ type Unit = (); type VecArgPair = Vec; type VecRetPair = Vec; type BoxCallInfo = Box>; -type BoxCallIndInfo = Box>; +type BoxCallIndInfo = Box>; type BoxCallIndirectHostInfo = Box>; type BoxReturnCallInfo = Box>; type BoxReturnCallIndInfo = Box>; @@ -124,7 +124,7 @@ where &mut self, sig: Sig, dest: Reg, - uses: CallArgList, + mut uses: CallArgList, defs: CallRetList, try_call_info: Option, ) -> BoxCallIndInfo { @@ -133,8 +133,30 @@ where self.lower_ctx .abi_mut() .accumulate_outgoing_args_size(stack_ret_space + stack_arg_space); + let call_conv = self.lower_ctx.sigs()[sig].call_conv(); - let dest = XReg::new(dest).unwrap(); + // Mirror of `gen_call_info`: take out the first four integer + // arguments (x0..x3) and pass them through the `args` list so the + // emitted `call_indirect{1,2,3,4}` op can move them at call time. + // Saves one Pulley dispatch per moved arg vs the previous "regalloc + // emits xmov; then `call_indirect`" sequence. + let mut args = SmallVec::new(); + uses.sort_by_key(|arg| arg.preg); + if call_conv != CallConv::PreserveAll { + uses.retain(|arg| { + if arg.preg != regs::x0() + && arg.preg != regs::x1() + && arg.preg != regs::x2() + && arg.preg != regs::x3() + { + return true; + } + args.push(XReg::new(arg.vreg).unwrap()); + false + }); + } + let target = XReg::new(dest).unwrap(); + let dest = PulleyCallIndirect { target, args }; Box::new( self.lower_ctx .gen_call_info(sig, dest, uses, defs, try_call_info, false), diff --git a/cranelift/filetests/filetests/isa/pulley32/call.clif b/cranelift/filetests/filetests/isa/pulley32/call.clif index c2dc9a09f6c9..aece47fc9a19 100644 --- a/cranelift/filetests/filetests/isa/pulley32/call.clif +++ b/cranelift/filetests/filetests/isa/pulley32/call.clif @@ -291,7 +291,7 @@ block0(v0: i32): ; VCode: ; push_frame ; block0: -; indirect_call x0, CallInfo { dest: XReg(p0i), uses: [], defs: [CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I64) }], clobbers: PRegSet { bits: [65534, 4294967295, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: None, patchable: false } +; indirect_call x0, CallInfo { dest: PulleyCallIndirect { target: XReg(p0i), args: [] }, uses: [], defs: [CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I64) }], clobbers: PRegSet { bits: [65534, 4294967295, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: None, patchable: false } ; pop_frame ; ret ; diff --git a/cranelift/filetests/filetests/isa/pulley32/exceptions.clif b/cranelift/filetests/filetests/isa/pulley32/exceptions.clif index 2d3dfef3e853..eeed198535d2 100644 --- a/cranelift/filetests/filetests/isa/pulley32/exceptions.clif +++ b/cranelift/filetests/filetests/isa/pulley32/exceptions.clif @@ -77,7 +77,7 @@ function %f2(i32, i32) -> i32, f32, f64 { ; block0: ; fconst64 f1, 4607182418800017408 ; fstore64 Slot(0), f1 // flags = notrap aligned -; indirect_call x1, CallInfo { dest: XReg(p1i), uses: [CallArgPair { vreg: p0i, preg: p0i }], defs: [CallRetPair { vreg: Writable { reg: p0f }, location: Reg(p0f, types::F32) }, CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I32) }, CallRetPair { vreg: Writable { reg: p1i }, location: Reg(p1i, types::I32) }], clobbers: PRegSet { bits: [4294967292, 4294967294, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: Some(TryCallInfo { continuation: MachLabel(1), exception_handlers: [Default(MachLabel(2))] }), patchable: false }; jump MachLabel(1); catch [default: MachLabel(2)] +; indirect_call x1, CallInfo { dest: PulleyCallIndirect { target: XReg(p1i), args: [XReg(p0i)] }, uses: [], defs: [CallRetPair { vreg: Writable { reg: p0f }, location: Reg(p0f, types::F32) }, CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I32) }, CallRetPair { vreg: Writable { reg: p1i }, location: Reg(p1i, types::I32) }], clobbers: PRegSet { bits: [4294967292, 4294967294, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: Some(TryCallInfo { continuation: MachLabel(1), exception_handlers: [Default(MachLabel(2))] }), patchable: false }; jump MachLabel(1); catch [default: MachLabel(2)] ; block1: ; xone x0 ; f1 = fload64 Slot(0) // flags = notrap aligned diff --git a/cranelift/filetests/filetests/isa/pulley32/preserve-all.clif b/cranelift/filetests/filetests/isa/pulley32/preserve-all.clif index 67b8297bdd2f..7733ce08fb51 100644 --- a/cranelift/filetests/filetests/isa/pulley32/preserve-all.clif +++ b/cranelift/filetests/filetests/isa/pulley32/preserve-all.clif @@ -15,8 +15,8 @@ block0(v0: i64): ; xmov x3, x0 ; xmov x1, x3 ; xmov x2, x3 -; indirect_call x3, CallInfo { dest: XReg(p3i), uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } -; indirect_call x3, CallInfo { dest: XReg(p3i), uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } +; indirect_call x3, CallInfo { dest: PulleyCallIndirect { target: XReg(p3i), args: [] }, uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } +; indirect_call x3, CallInfo { dest: PulleyCallIndirect { target: XReg(p3i), args: [] }, uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } ; pop_frame ; ret ; diff --git a/cranelift/filetests/filetests/isa/pulley64/call.clif b/cranelift/filetests/filetests/isa/pulley64/call.clif index 16b271835620..bd6f9bba825f 100644 --- a/cranelift/filetests/filetests/isa/pulley64/call.clif +++ b/cranelift/filetests/filetests/isa/pulley64/call.clif @@ -291,7 +291,7 @@ block0(v0: i64): ; VCode: ; push_frame ; block0: -; indirect_call x0, CallInfo { dest: XReg(p0i), uses: [], defs: [CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I64) }], clobbers: PRegSet { bits: [65534, 4294967295, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: None, patchable: false } +; indirect_call x0, CallInfo { dest: PulleyCallIndirect { target: XReg(p0i), args: [] }, uses: [], defs: [CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I64) }], clobbers: PRegSet { bits: [65534, 4294967295, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: None, patchable: false } ; pop_frame ; ret ; diff --git a/cranelift/filetests/filetests/isa/pulley64/exceptions.clif b/cranelift/filetests/filetests/isa/pulley64/exceptions.clif index 88c5528c1935..6a0b7b1577a1 100644 --- a/cranelift/filetests/filetests/isa/pulley64/exceptions.clif +++ b/cranelift/filetests/filetests/isa/pulley64/exceptions.clif @@ -79,7 +79,7 @@ function %f2(i32, i64) -> i32, f32, f64 { ; block0: ; fconst64 f1, 4607182418800017408 ; fstore64 Slot(0), f1 // flags = notrap aligned -; indirect_call x1, CallInfo { dest: XReg(p1i), uses: [CallArgPair { vreg: p0i, preg: p0i }], defs: [CallRetPair { vreg: Writable { reg: p0f }, location: Reg(p0f, types::F32) }, CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I64) }, CallRetPair { vreg: Writable { reg: p1i }, location: Reg(p1i, types::I64) }], clobbers: PRegSet { bits: [4294967292, 4294967294, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: Some(TryCallInfo { continuation: MachLabel(1), exception_handlers: [Default(MachLabel(2))] }), patchable: false }; jump MachLabel(1); catch [default: MachLabel(2)] +; indirect_call x1, CallInfo { dest: PulleyCallIndirect { target: XReg(p1i), args: [XReg(p0i)] }, uses: [], defs: [CallRetPair { vreg: Writable { reg: p0f }, location: Reg(p0f, types::F32) }, CallRetPair { vreg: Writable { reg: p0i }, location: Reg(p0i, types::I64) }, CallRetPair { vreg: Writable { reg: p1i }, location: Reg(p1i, types::I64) }], clobbers: PRegSet { bits: [4294967292, 4294967294, 4294967295, 0] }, callee_conv: Tail, caller_conv: Fast, callee_pop_size: 0, try_call_info: Some(TryCallInfo { continuation: MachLabel(1), exception_handlers: [Default(MachLabel(2))] }), patchable: false }; jump MachLabel(1); catch [default: MachLabel(2)] ; block1: ; xone x0 ; f1 = fload64 Slot(0) // flags = notrap aligned diff --git a/cranelift/filetests/filetests/isa/pulley64/preserve-all.clif b/cranelift/filetests/filetests/isa/pulley64/preserve-all.clif index 363a6c3538c7..0b13fcc98618 100644 --- a/cranelift/filetests/filetests/isa/pulley64/preserve-all.clif +++ b/cranelift/filetests/filetests/isa/pulley64/preserve-all.clif @@ -15,8 +15,8 @@ block0(v0: i64): ; xmov x3, x0 ; xmov x1, x3 ; xmov x2, x3 -; indirect_call x3, CallInfo { dest: XReg(p3i), uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } -; indirect_call x3, CallInfo { dest: XReg(p3i), uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } +; indirect_call x3, CallInfo { dest: PulleyCallIndirect { target: XReg(p3i), args: [] }, uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } +; indirect_call x3, CallInfo { dest: PulleyCallIndirect { target: XReg(p3i), args: [] }, uses: [CallArgPair { vreg: p0i, preg: p0i }, CallArgPair { vreg: p1i, preg: p1i }, CallArgPair { vreg: p2i, preg: p2i }, CallArgPair { vreg: p3i, preg: p3i }], defs: [], clobbers: PRegSet { bits: [0, 0, 0, 0] }, callee_conv: PreserveAll, caller_conv: SystemV, callee_pop_size: 0, try_call_info: None, patchable: false } ; pop_frame ; ret ; diff --git a/tests/disas/pulley/call.wat b/tests/disas/pulley/call.wat index 233ca7be3c35..d9bc3142fd99 100644 --- a/tests/disas/pulley/call.wat +++ b/tests/disas/pulley/call.wat @@ -8,9 +8,7 @@ ;; wasm[0]::function[1]: ;; push_frame ;; xload32le_o32 x3, x0, 28 -;; xmov x6, x0 -;; xload32le_o32 x0, x6, 36 -;; xmov x1, x6 -;; call_indirect x3 +;; xload32le_o32 x4, x0, 36 +;; call_indirect2 x3, x4, x0 ;; pop_frame ;; ret From 1a7b790b30d78c89a38165f8caac67c7cbf5a968 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Fri, 10 Jul 2026 14:36:55 -0700 Subject: [PATCH 12/12] tests: runtime coverage for call_indirectN arg bundling Exercises call_indirect at arg counts 0/1/4/6 with interleaved int and float args over an immutable funcref table, so the emitted call_indirect{1,2,3,4} ops (and the >4-arg fall-through to the normal ABI path) are checked for correct argument delivery on all four engine configs. --- .../pulley-call-indirect-arg-bundling.wast | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/misc_testsuite/pulley-call-indirect-arg-bundling.wast diff --git a/tests/misc_testsuite/pulley-call-indirect-arg-bundling.wast b/tests/misc_testsuite/pulley-call-indirect-arg-bundling.wast new file mode 100644 index 000000000000..8cbfb87995e9 --- /dev/null +++ b/tests/misc_testsuite/pulley-call-indirect-arg-bundling.wast @@ -0,0 +1,52 @@ +;; The Pulley `call_indirect{1,2,3,4}` ops bundle the first four integer ABI +;; arguments into the indirect-call op; a fifth+ integer arg, and any float +;; arg, must still be passed the normal way. Exercise every count across that +;; boundary through a call_indirect and check the results are unchanged. + +(module + (type $t0 (func (result i32))) + (type $t1 (func (param i32) (result i32))) + (type $t4 (func (param i32 i32 i32 i32) (result i32))) + (type $t6 (func (param i32 i32 i32 i32 i32 i32) (result i32))) + (type $tf (func (param i32 f64 i32 f64 i32) (result f64))) + + (table 5 5 funcref) + (elem (i32.const 0) $f0 $f1 $f4 $f6 $ff) + + (func $f0 (type $t0) (i32.const 100)) + (func $f1 (type $t1) (local.get 0)) + (func $f4 (type $t4) + (i32.add (i32.add (local.get 0) (local.get 1)) + (i32.add (local.get 2) (local.get 3)))) + (func $f6 (type $t6) + (i32.add + (i32.add (i32.add (local.get 0) (local.get 1)) (i32.add (local.get 2) (local.get 3))) + (i32.add (local.get 4) (local.get 5)))) + ;; Interleaved int/float args: the floats never occupy x0..x3, so the + ;; bundling must skip them and still sum the integers + floats correctly. + (func $ff (type $tf) + (f64.add + (f64.add (local.get 1) (local.get 3)) + (f64.convert_i32_s (i32.add (i32.add (local.get 0) (local.get 2)) (local.get 4))))) + + (func (export "c0") (result i32) + (call_indirect (type $t0) (i32.const 0))) + (func (export "c1") (param i32) (result i32) + (call_indirect (type $t1) (local.get 0) (i32.const 1))) + (func (export "c4") (param i32 i32 i32 i32) (result i32) + (call_indirect (type $t4) (local.get 0) (local.get 1) (local.get 2) (local.get 3) + (i32.const 2))) + (func (export "c6") (param i32 i32 i32 i32 i32 i32) (result i32) + (call_indirect (type $t6) (local.get 0) (local.get 1) (local.get 2) + (local.get 3) (local.get 4) (local.get 5) (i32.const 3))) + (func (export "cf") (param i32 f64 i32 f64 i32) (result f64) + (call_indirect (type $tf) (local.get 0) (local.get 1) (local.get 2) + (local.get 3) (local.get 4) (i32.const 4)))) + +(assert_return (invoke "c0") (i32.const 100)) +(assert_return (invoke "c1" (i32.const 42)) (i32.const 42)) +(assert_return (invoke "c4" (i32.const 1) (i32.const 2) (i32.const 3) (i32.const 4)) (i32.const 10)) +(assert_return (invoke "c6" (i32.const 1) (i32.const 2) (i32.const 3) + (i32.const 4) (i32.const 5) (i32.const 6)) (i32.const 21)) +(assert_return (invoke "cf" (i32.const 1) (f64.const 2.5) (i32.const 3) (f64.const 4.5) (i32.const 5)) + (f64.const 16))