diff --git a/cranelift/codegen/src/alias_analysis.rs b/cranelift/codegen/src/alias_analysis.rs index c15769fd70f9..626455a778e8 100644 --- a/cranelift/codegen/src/alias_analysis.rs +++ b/cranelift/codegen/src/alias_analysis.rs @@ -424,6 +424,32 @@ impl LastStores { } } + /// Roll this state back to the memory version from just before `dead`, + /// which is a store being removed from the function by dead-store + /// elimination. + /// + /// `prev_last_store` must be the last-store instruction that immediately + /// preceded `dead`, as recorded when `dead` itself was processed. + /// + /// Only `dead`'s own alias-region slot is restored. A store with no alias + /// region is treated as a fence by `update`, which clears *every* region + /// slot, and we do not undo that; in that case, we leave this state + /// alone. Similarly, stores marked observed while processing `dead` stay + /// observed. + fn undo_store(&mut self, func: &Function, dead: Inst, prev_last_store: PackedOption) { + debug_assert!(func.dfg.insts[dead].opcode().can_store()); + + let Some(region) = func.dfg.insts[dead].alias_region(&func.dfg) else { + return; + }; + + // Only roll back if `dead` really is the current last store to its + // region. + if self.regions[region].expand() == Some(dead) { + self.regions[region] = prev_last_store; + } + } + /// Get the last-store instruction for the given `inst`'s alias region, if /// any. fn get_last_store(&self, func: &Function, inst: Inst) -> PackedOption { @@ -527,6 +553,26 @@ struct MemoryLoc { extending_opcode: Option, } +/// What is known to be in memory at an associated `MemoryLoc`. +#[derive(Clone, Copy, Debug)] +struct KnownValue { + /// The value held at the associated `MemoryLoc`. + value: Value, + + /// The instruction that produced `value`: either the load that read it out + /// of memory or the store that wrote it there. + /// + /// Kept around for quick dominance checks. + def_inst: Inst, + + /// When this entry was created by a store, the last-store instruction that + /// immediately preceded that store: that is, the memory version this + /// location was at just *before* `def_inst` overwrote it. + /// + /// `None` for entries created by loads. + prev_last_store: Option>, +} + /// The result of processing an instruction through alias analysis. pub enum OptResult { /// No optimization applied. @@ -576,9 +622,7 @@ pub struct AliasAnalysis<'a> { /// Known memory-value equivalences. This is the result of the /// analysis. This is a mapping from (last store, address /// expression, offset, type) to SSA `Value`. - /// - /// We keep the defining inst around for quick dominance checks. - mem_values: FxHashMap, + mem_values: FxHashMap, } impl<'a> AliasAnalysis<'a> { @@ -754,7 +798,36 @@ impl<'a> AliasAnalysis<'a> { ty, extending_opcode: get_ext_opcode(opcode), }; - self.mem_values.remove(&dead_loc); + let dead_entry = self.mem_values.remove(&dead_loc); + + // Roll our last-store state back to the memory version + // just before the dead store, so that `state` describes + // memory as if the dead store had never happened. + // + // Our callers remove the dead store from the layout and + // then reprocess this overwriting store. Without the + // rollback, that reprocessing keys its `mem_values` + // lookup on the instruction we just removed, finds + // nothing, and so fails to notice that the overwriter + // has now become an idempotent store. Chains like + // + // v1 = load.i32 region0 v0 + // store region0 v2, v0 ;; dead + // store region0 v1, v0 ;; idempotent, once the + // ;; dead store is gone + // + // would then need a whole additional pass over the + // function to collapse each link. + // + // A missing entry means we never processed the dead + // store as a store in this pass (it can come from a + // precomputed `block_input` snapshot, for a predecessor + // block we have not walked yet), so we have no previous + // version to roll back to and simply don't. + if let Some(prev) = dead_entry.and_then(|e| e.prev_last_store) { + state.undo_store(func, last_store, prev); + } + return OptResult::DeadStore { dead: last_store, overwriter: inst, @@ -769,7 +842,12 @@ impl<'a> AliasAnalysis<'a> { ty, extending_opcode: get_ext_opcode(opcode), }; - if let Some((def_inst, known_value)) = self.mem_values.get(&check_loc).cloned() { + if let Some(KnownValue { + def_inst, + value: known_value, + .. + }) = self.mem_values.get(&check_loc).cloned() + { // Check for idempotent stores, where we are // storing the exact same value back to a location // that already has that value. @@ -806,7 +884,14 @@ impl<'a> AliasAnalysis<'a> { extending_opcode: get_ext_opcode(opcode), }; trace!(" --> updating known values in memory: {mem_loc:?} = {store_data}"); - self.mem_values.insert(mem_loc, (inst, store_data)); + self.mem_values.insert( + mem_loc, + KnownValue { + def_inst: inst, + value: store_data, + prev_last_store: Some(last_store), + }, + ); OptResult::None } else if opcode.can_load() { @@ -831,8 +916,9 @@ impl<'a> AliasAnalysis<'a> { // load (stores will always dominate though if // their `last_store` survives through // meet-points to this use-site). - let aliased = if let Some((def_inst, value)) = - self.mem_values.get(&mem_loc).cloned() + let aliased = if let Some(KnownValue { + def_inst, value, .. + }) = self.mem_values.get(&mem_loc).cloned() { trace!(" see known value {value} from {def_inst}"); if self.domtree.dominates(def_inst, inst, &func.layout) { @@ -851,7 +937,16 @@ impl<'a> AliasAnalysis<'a> { // as a new equivalent value. if aliased.is_none() { trace!(" --> inserting load result {load_result} at loc {mem_loc:?}"); - self.mem_values.insert(mem_loc, (inst, load_result)); + self.mem_values.insert( + mem_loc, + KnownValue { + def_inst: inst, + value: load_result, + // A load does not advance the memory version, so + // there is no previous version to roll back to. + prev_last_store: None, + }, + ); } match aliased { diff --git a/cranelift/codegen/src/inst_predicates.rs b/cranelift/codegen/src/inst_predicates.rs index 239e514f1715..244385cd536b 100644 --- a/cranelift/codegen/src/inst_predicates.rs +++ b/cranelift/codegen/src/inst_predicates.rs @@ -94,11 +94,27 @@ pub fn is_mergeable_for_egraph(func: &Function, inst: Inst) -> bool { /// Does the given instruction have any side-effect as per [has_side_effect], or else is a load, /// but not the get_pinned_reg opcode? +/// +/// Loads are included so that lowering colors them as side-effecting, which is +/// what keeps a load from being merged into a consumer across an intervening +/// store. Deciding whether a load has to be *emitted* is a different question; +/// see [`must_lower_even_if_unused`]. pub fn has_lowering_side_effect(func: &Function, inst: Inst) -> bool { let op = func.dfg.insts[inst].opcode(); op != Opcode::GetPinnedReg && (has_side_effect(func, inst) || op.can_load()) } +/// Must lowering emit the given instruction even when none of its results are +/// used? +/// +/// This is [has_lowering_side_effect] without its "or is a load" clause: a load +/// that is defined not to trap has no effect of its own, so if nothing wants the +/// value it read there is no reason to emit it. +pub fn must_lower_even_if_unused(func: &Function, inst: Inst) -> bool { + let op = func.dfg.insts[inst].opcode(); + op != Opcode::GetPinnedReg && has_side_effect(func, inst) +} + /// Is the given instruction a constant value (`iconst`, `fconst`) that can be /// represented in 64 bits? pub fn is_constant_64bit(func: &Function, inst: Inst) -> Option { diff --git a/cranelift/codegen/src/machinst/lower.rs b/cranelift/codegen/src/machinst/lower.rs index ad2ffd99e1f0..c006c6b64083 100644 --- a/cranelift/codegen/src/machinst/lower.rs +++ b/cranelift/codegen/src/machinst/lower.rs @@ -6,7 +6,9 @@ // top of it, e.g. the side-effect/coloring analysis and the scan support. use crate::entity::SecondaryMap; -use crate::inst_predicates::{has_lowering_side_effect, is_constant_64bit}; +use crate::inst_predicates::{ + has_lowering_side_effect, is_constant_64bit, must_lower_even_if_unused, +}; use crate::ir::{ ArgumentPurpose, Block, BlockArg, Constant, ConstantData, DataFlowGraph, ExternalName, Function, GlobalValue, GlobalValueData, Immediate, Inst, InstructionData, RelSourceLoc, SigRef, @@ -750,14 +752,19 @@ impl<'func, I: VCodeInst> Lower<'func, I> { // Are any outputs used at least once? let value_needed = self.is_any_inst_result_needed(inst); + + // Do we have to emit this instruction even though nothing uses its + // results? Note that this is not the same question as + // `has_side_effect` above: loads are colored as side-effecting so + // that load merging cannot move one across a store, but a load that + // is defined not to trap can simply be dropped when it is dead. + let must_lower = must_lower_even_if_unused(self.f, inst); + trace!( - "lower_clif_block: block {} inst {} ({:?}) is_branch {} side_effect {} value_needed {}", - block, - inst, - data, + "lower_clif_block: {block}, {inst}, ({data:?}), is_branch {}, \ + has_side_effect {has_side_effect}, must_lower {must_lower}, \ + value_needed {value_needed}", data.opcode().is_branch(), - has_side_effect, - value_needed, ); // Update scan state to color prior to this inst (as we are scanning @@ -777,11 +784,11 @@ impl<'func, I: VCodeInst> Lower<'func, I> { // order, and therefore **before** in reversed order. // Only emit value label aliases if the instruction will be lowered // (otherwise we want to keep using the earlier label instead). - self.emit_value_label_live_range_start_for_inst(inst, has_side_effect || value_needed); + self.emit_value_label_live_range_start_for_inst(inst, must_lower || value_needed); // Normal instruction: codegen if the instruction is side-effecting // or any of its outputs is used. - if has_side_effect || value_needed { + if must_lower || value_needed { trace!("lowering: inst {}: {}", inst, self.f.dfg.display_inst(inst)); let temp_regs = match backend.lower(self, inst) { Some(regs) => regs, diff --git a/cranelift/filetests/filetests/alias/check-unset-reset-flag.clif b/cranelift/filetests/filetests/alias/check-unset-reset-flag.clif index af926b0a0bf8..08188dd75cb8 100644 --- a/cranelift/filetests/filetests/alias/check-unset-reset-flag.clif +++ b/cranelift/filetests/filetests/alias/check-unset-reset-flag.clif @@ -21,7 +21,7 @@ block0(v0: i64, v1: i32): ; block0(v0: i64, v1: i32): ; v2 = load.i64 notrap aligned region0 v0 ; trapz v2, user42 -; store notrap aligned region0 v2, v0 ; v4 = iadd v1, v1 ; return v4 ; } + diff --git a/cranelift/filetests/filetests/alias/dead-store-then-idempotent-store.clif b/cranelift/filetests/filetests/alias/dead-store-then-idempotent-store.clif new file mode 100644 index 000000000000..02e5ecd5b8a5 --- /dev/null +++ b/cranelift/filetests/filetests/alias/dead-store-then-idempotent-store.clif @@ -0,0 +1,181 @@ +test optimize precise-output +set opt_level=speed +target x86_64 + +;; Removing a dead store must expose the *previous* memory version to the store +;; that overwrote it, so that a save/clear/restore sequence collapses entirely in +;; a single pass rather than one link per pass. +function %save_clear_restore(i64) { + region0 = 0 "flags" +block0(v0: i64): + v1 = load.i32 notrap aligned region0 v0 + v2 = iconst.i32 0 + store notrap aligned region0 v2, v0 + store notrap aligned region0 v1, v0 + return +} + +; function %save_clear_restore(i64) fast { +; region0 = 0 "flags" +; +; block0(v0: i64): +; v1 = load.i32 notrap aligned region0 v0 +; return +; } + +;; The same, but with several dead stores between the load and the restore. +function %save_clobber_many_restore(i64, i32, i32) { + region0 = 0 "flags" +block0(v0: i64, v1: i32, v2: i32): + v3 = load.i32 notrap aligned region0 v0 + store notrap aligned region0 v1, v0 + store notrap aligned region0 v2, v0 + store notrap aligned region0 v1, v0 + store notrap aligned region0 v3, v0 + return +} + +; function %save_clobber_many_restore(i64, i32, i32) fast { +; region0 = 0 "flags" +; +; block0(v0: i64, v1: i32, v2: i32): +; v3 = load.i32 notrap aligned region0 v0 +; return +; } + +;; Two independent flags, each in its own alias region, are both collapsed. +;; +;; Note that the accesses are interleaved: unwinding one region's dead store +;; must not disturb the other region's last-store state. +function %two_regions_interleaved(i64, i64) { + region0 = 0 "flags0" + region1 = 1 "flags1" +block0(v0: i64, v1: i64): + v2 = load.i32 notrap aligned region0 v0 + v3 = load.i32 notrap aligned region1 v1 + v4 = iconst.i32 0 + store notrap aligned region0 v4, v0 + store notrap aligned region1 v4, v1 + store notrap aligned region0 v2, v0 + store notrap aligned region1 v3, v1 + return +} + +; function %two_regions_interleaved(i64, i64) fast { +; region0 = 0 "flags0" +; region1 = 1 "flags1" +; +; block0(v0: i64, v1: i64): +; v2 = load.i32 notrap aligned region0 v0 +; v3 = load.i32 notrap aligned region1 v1 +; return +; } + +;; The restore is folded across intervening blocks, so long as nothing in them +;; observes the flag. +function %save_clear_restore_cross_block(i64) { + region0 = 0 "flags" +block0(v0: i64): + v1 = load.i32 notrap aligned region0 v0 + v2 = iconst.i32 0 + store notrap aligned region0 v2, v0 + jump block1 + +block1: + jump block2 + +block2: + store notrap aligned region0 v1, v0 + return +} + +; function %save_clear_restore_cross_block(i64) fast { +; region0 = 0 "flags" +; +; block0(v0: i64): +; v1 = load.i32 notrap aligned region0 v0 +; jump block1 +; +; block1: +; jump block2 +; +; block2: +; return +; } + +;; Negative test: a call between the clear and the restore observes the cleared +;; flag, so neither store may be removed. +function %call_observes_cleared_flag(i64) { + region0 = 0 "flags" + fn0 = %g(i64) +block0(v0: i64): + v1 = load.i32 notrap aligned region0 v0 + v2 = iconst.i32 0 + store notrap aligned region0 v2, v0 + call fn0(v0) + store notrap aligned region0 v1, v0 + return +} + +; function %call_observes_cleared_flag(i64) fast { +; region0 = 0 "flags" +; sig0 = (i64) fast +; fn0 = %g sig0 +; +; block0(v0: i64): +; v1 = load.i32 notrap aligned region0 v0 +; v2 = iconst.i32 0 +; store notrap aligned region0 v2, v0 ; v2 = 0 +; call fn0(v0) +; store notrap aligned region0 v1, v0 +; return +; } + +;; Negative test: the final store writes a value other than the saved one, so it +;; is not idempotent. Only the dead middle store is removed. +function %restore_wrong_value(i64, i32) { + region0 = 0 "flags" +block0(v0: i64, v1: i32): + v2 = load.i32 notrap aligned region0 v0 + v3 = iconst.i32 0 + store notrap aligned region0 v3, v0 + store notrap aligned region0 v1, v0 + return +} + +; function %restore_wrong_value(i64, i32) fast { +; region0 = 0 "flags" +; +; block0(v0: i64, v1: i32): +; v2 = load.i32 notrap aligned region0 v0 +; store notrap aligned region0 v1, v0 +; return +; } + +;; Negative test: rolling back to the previous memory version must not resurrect +;; knowledge across a store to a *different* address in the same region. The +;; region's last-store slot is per-region, not per-address, so after the store to +;; `v0+8` the analysis no longer knows what is at `v0`, and the final store to +;; `v0` cannot be proven idempotent. +function %same_region_different_address(i64, i32) { + region0 = 0 "flags" +block0(v0: i64, v1: i32): + v2 = load.i32 notrap aligned region0 v0 + v3 = iconst.i32 0 + store notrap aligned region0 v3, v0 + store notrap aligned region0 v1, v0+8 + store notrap aligned region0 v2, v0 + return +} + +; function %same_region_different_address(i64, i32) fast { +; region0 = 0 "flags" +; +; block0(v0: i64, v1: i32): +; v2 = load.i32 notrap aligned region0 v0 +; v3 = iconst.i32 0 +; store notrap aligned region0 v3, v0 ; v3 = 0 +; store notrap aligned region0 v1, v0+8 +; store notrap aligned region0 v2, v0 +; return +; } diff --git a/cranelift/filetests/filetests/isa/aarch64/dead-notrap-load.clif b/cranelift/filetests/filetests/isa/aarch64/dead-notrap-load.clif new file mode 100644 index 000000000000..bd17c43e9ce0 --- /dev/null +++ b/cranelift/filetests/filetests/isa/aarch64/dead-notrap-load.clif @@ -0,0 +1,111 @@ +test compile precise-output +target aarch64 + +;; A `notrap` load has no side effect of its own, so when nothing uses the value +;; it read, lowering drops it. + +function %dead_notrap_load(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned v0 + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +;; Extending loads too. + +function %dead_notrap_uload8(i64) { +block0(v0: i64): + v1 = uload8.i32 notrap aligned v0 + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +;; Chains collapse within the single backward pass over the block: dropping the +;; second load is what makes the first one dead. + +function %dead_notrap_load_chain(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned readonly v0+176 + v2 = load.i32 notrap aligned v1 + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +;; The address computation survives if it is live for some other reason. + +function %dead_notrap_load_live_address(i64) -> i64 { +block0(v0: i64): + v1 = iconst.i64 8 + v2 = iadd v0, v1 + v3 = load.i64 notrap aligned v2 + return v2 +} + +; VCode: +; block0: +; add x0, x0, #8 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; add x0, x0, #8 +; ret + +;; A load without `notrap` is defined to trap on inaccessible memory, so it is +;; still emitted even though its result is unused. + +function %dead_trapping_load(i64) { +block0(v0: i64): + v1 = load.i64 aligned v0 + return +} + +; VCode: +; block0: +; ldr x2, [x0] +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ldr x2, [x0] ; trap: heap_oob +; ret + +;; Atomic loads are always side-effecting, `notrap` or not. + +function %dead_atomic_load(i64) { +block0(v0: i64): + v1 = atomic_load.i64 notrap aligned v0 + return +} + +; VCode: +; block0: +; ldar x2, [x0] +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ldar x2, [x0] +; ret + diff --git a/cranelift/filetests/filetests/isa/riscv64/dead-notrap-load.clif b/cranelift/filetests/filetests/isa/riscv64/dead-notrap-load.clif new file mode 100644 index 000000000000..76c2791a86c0 --- /dev/null +++ b/cranelift/filetests/filetests/isa/riscv64/dead-notrap-load.clif @@ -0,0 +1,113 @@ +test compile precise-output +target riscv64 + +;; A `notrap` load has no side effect of its own, so when nothing uses the value +;; it read, lowering drops it. + +function %dead_notrap_load(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned v0 + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +;; Extending loads too. + +function %dead_notrap_uload8(i64) { +block0(v0: i64): + v1 = uload8.i32 notrap aligned v0 + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +;; Chains collapse within the single backward pass over the block: dropping the +;; second load is what makes the first one dead. + +function %dead_notrap_load_chain(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned readonly v0+176 + v2 = load.i32 notrap aligned v1 + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +;; The address computation survives if it is live for some other reason. + +function %dead_notrap_load_live_address(i64) -> i64 { +block0(v0: i64): + v1 = iconst.i64 8 + v2 = iadd v0, v1 + v3 = load.i64 notrap aligned v2 + return v2 +} + +; VCode: +; block0: +; addi a0,a0,8 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; addi a0, a0, 8 +; ret + +;; A load without `notrap` is defined to trap on inaccessible memory, so it is +;; still emitted even though its result is unused. + +function %dead_trapping_load(i64) { +block0(v0: i64): + v1 = load.i64 aligned v0 + return +} + +; VCode: +; block0: +; ld a0,0(a0) +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ld a0, 0(a0) ; trap: heap_oob +; ret + +;; Atomic loads are always side-effecting, `notrap` or not. + +function %dead_atomic_load(i64) { +block0(v0: i64): + v1 = atomic_load.i64 notrap aligned v0 + return +} + +; VCode: +; block0: +; atomic_load.i64 a0,(a0) +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; fence rw, rw +; ld a0, 0(a0) ; trap: heap_oob +; fence r, rw +; ret + diff --git a/cranelift/filetests/filetests/isa/s390x/dead-notrap-load.clif b/cranelift/filetests/filetests/isa/s390x/dead-notrap-load.clif new file mode 100644 index 000000000000..941492749165 --- /dev/null +++ b/cranelift/filetests/filetests/isa/s390x/dead-notrap-load.clif @@ -0,0 +1,111 @@ +test compile precise-output +target s390x + +;; A `notrap` load has no side effect of its own, so when nothing uses the value +;; it read, lowering drops it. + +function %dead_notrap_load(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned v0 + return +} + +; VCode: +; block0: +; br %r14 +; +; Disassembled: +; block0: ; offset 0x0 +; br %r14 + +;; Extending loads too. + +function %dead_notrap_uload8(i64) { +block0(v0: i64): + v1 = uload8.i32 notrap aligned v0 + return +} + +; VCode: +; block0: +; br %r14 +; +; Disassembled: +; block0: ; offset 0x0 +; br %r14 + +;; Chains collapse within the single backward pass over the block: dropping the +;; second load is what makes the first one dead. + +function %dead_notrap_load_chain(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned readonly v0+176 + v2 = load.i32 notrap aligned v1 + return +} + +; VCode: +; block0: +; br %r14 +; +; Disassembled: +; block0: ; offset 0x0 +; br %r14 + +;; The address computation survives if it is live for some other reason. + +function %dead_notrap_load_live_address(i64) -> i64 { +block0(v0: i64): + v1 = iconst.i64 8 + v2 = iadd v0, v1 + v3 = load.i64 notrap aligned v2 + return v2 +} + +; VCode: +; block0: +; aghi %r2, 8 +; br %r14 +; +; Disassembled: +; block0: ; offset 0x0 +; aghi %r2, 8 +; br %r14 + +;; A load without `notrap` is defined to trap on inaccessible memory, so it is +;; still emitted even though its result is unused. + +function %dead_trapping_load(i64) { +block0(v0: i64): + v1 = load.i64 aligned v0 + return +} + +; VCode: +; block0: +; lg %r2, 0(%r2) +; br %r14 +; +; Disassembled: +; block0: ; offset 0x0 +; lg %r2, 0(%r2) ; trap: heap_oob +; br %r14 + +;; Atomic loads are always side-effecting, `notrap` or not. + +function %dead_atomic_load(i64) { +block0(v0: i64): + v1 = atomic_load.i64 notrap aligned v0 + return +} + +; VCode: +; block0: +; lg %r2, 0(%r2) +; br %r14 +; +; Disassembled: +; block0: ; offset 0x0 +; lg %r2, 0(%r2) +; br %r14 + diff --git a/cranelift/filetests/filetests/isa/x64/dead-notrap-load.clif b/cranelift/filetests/filetests/isa/x64/dead-notrap-load.clif new file mode 100644 index 000000000000..64f82a18e4c0 --- /dev/null +++ b/cranelift/filetests/filetests/isa/x64/dead-notrap-load.clif @@ -0,0 +1,164 @@ +test compile precise-output +target x86_64 + +;; A `notrap` load has no side effect of its own, so when nothing uses the value +;; it read, lowering drops it. + +function %dead_notrap_load(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned v0 + return +} + +; VCode: +; pushq %rbp +; movq %rsp, %rbp +; block0: +; movq %rbp, %rsp +; popq %rbp +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; pushq %rbp +; movq %rsp, %rbp +; block1: ; offset 0x4 +; movq %rbp, %rsp +; popq %rbp +; retq + +;; Extending loads too. + +function %dead_notrap_uload8(i64) { +block0(v0: i64): + v1 = uload8.i32 notrap aligned v0 + return +} + +; VCode: +; pushq %rbp +; movq %rsp, %rbp +; block0: +; movq %rbp, %rsp +; popq %rbp +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; pushq %rbp +; movq %rsp, %rbp +; block1: ; offset 0x4 +; movq %rbp, %rsp +; popq %rbp +; retq + +;; Chains collapse within the single backward pass over the block: dropping the +;; second load is what makes the first one dead. + +function %dead_notrap_load_chain(i64) { +block0(v0: i64): + v1 = load.i64 notrap aligned readonly v0+176 + v2 = load.i32 notrap aligned v1 + return +} + +; VCode: +; pushq %rbp +; movq %rsp, %rbp +; block0: +; movq %rbp, %rsp +; popq %rbp +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; pushq %rbp +; movq %rsp, %rbp +; block1: ; offset 0x4 +; movq %rbp, %rsp +; popq %rbp +; retq + +;; The address computation survives if it is live for some other reason. + +function %dead_notrap_load_live_address(i64) -> i64 { +block0(v0: i64): + v1 = iconst.i64 8 + v2 = iadd v0, v1 + v3 = load.i64 notrap aligned v2 + return v2 +} + +; VCode: +; pushq %rbp +; movq %rsp, %rbp +; block0: +; leaq 8(%rdi), %rax +; movq %rbp, %rsp +; popq %rbp +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; pushq %rbp +; movq %rsp, %rbp +; block1: ; offset 0x4 +; leaq 8(%rdi), %rax +; movq %rbp, %rsp +; popq %rbp +; retq + +;; A load without `notrap` is defined to trap on inaccessible memory, so it is +;; still emitted even though its result is unused. + +function %dead_trapping_load(i64) { +block0(v0: i64): + v1 = load.i64 aligned v0 + return +} + +; VCode: +; pushq %rbp +; movq %rsp, %rbp +; block0: +; movq (%rdi), %rdx +; movq %rbp, %rsp +; popq %rbp +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; pushq %rbp +; movq %rsp, %rbp +; block1: ; offset 0x4 +; movq (%rdi), %rdx ; trap: heap_oob +; movq %rbp, %rsp +; popq %rbp +; retq + +;; Atomic loads are always side-effecting, `notrap` or not. + +function %dead_atomic_load(i64) { +block0(v0: i64): + v1 = atomic_load.i64 notrap aligned v0 + return +} + +; VCode: +; pushq %rbp +; movq %rsp, %rbp +; block0: +; movq (%rdi), %rdx +; movq %rbp, %rsp +; popq %rbp +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; pushq %rbp +; movq %rsp, %rbp +; block1: ; offset 0x4 +; movq (%rdi), %rdx +; movq %rbp, %rsp +; popq %rbp +; retq diff --git a/crates/cranelift/src/alias_region.rs b/crates/cranelift/src/alias_region.rs index 873f34b7a21b..89dc2a2a3096 100644 --- a/crates/cranelift/src/alias_region.rs +++ b/crates/cranelift/src/alias_region.rs @@ -76,8 +76,8 @@ enum AliasRegionKey { offset: u32, }, - /// An imported or exported memory access (shared across all - /// imported/exported memories). + /// An access of a memory that crosses a module boundary and whose + /// definition we do not statically know (shared across all such memories). PublicMemory, /// A defined memory access. @@ -88,8 +88,8 @@ enum AliasRegionKey { index: DefinedMemoryIndex, }, - /// An imported or exported table access (shared across all - /// imported/exported tables). + /// An access of a table that crosses a module boundary and whose definition + /// we do not statically know (shared across all such tables). PublicTable, /// A defined table access. @@ -100,8 +100,8 @@ enum AliasRegionKey { index: DefinedTableIndex, }, - /// An imported or exported global access (shared across all - /// imported/exported globals). + /// An access of a global that crosses a module boundary and whose definition + /// we do not statically know (shared across all such globals). PublicGlobal, /// A defined global access. @@ -722,14 +722,13 @@ where self.region(func, AliasRegionKey::GcHeap) } - /// Get the alias region for an imported or exported memory access (shared - /// across all imported/exported memories). + /// Get the alias region shared by all memories that cross a module boundary + /// and whose definition we do not statically know. pub fn public_memory_region(&mut self, func: &mut ir::Function) -> ir::AliasRegion { self.region(func, AliasRegionKey::PublicMemory) } - /// Get the alias region for accessing a defined memory that is not - /// exported. + /// Get the alias region for accessing a particular defined memory. pub fn defined_memory_region( &mut self, func: &mut ir::Function, @@ -739,14 +738,13 @@ where self.region(func, AliasRegionKey::DefinedMemory { module, index }) } - /// Get the alias region for an imported or exported table access (shared - /// across all imported/exported memories). + /// Get the alias region shared by all tables that cross a module boundary + /// and whose definition we do not statically know. pub fn public_table_region(&mut self, func: &mut ir::Function) -> ir::AliasRegion { self.region(func, AliasRegionKey::PublicTable) } - /// Get the alias region for accessing a defined table that is not - /// exported. + /// Get the alias region for accessing a particular defined table. pub fn defined_table_region( &mut self, func: &mut ir::Function, @@ -756,14 +754,13 @@ where self.region(func, AliasRegionKey::DefinedTable { module, index }) } - /// Get the alias region for an imported or exported global access (shared - /// across all imported/exported memories). + /// Get the alias region shared by all globals that cross a module boundary + /// and whose definition we do not statically know. pub fn public_global_region(&mut self, func: &mut ir::Function) -> ir::AliasRegion { self.region(func, AliasRegionKey::PublicGlobal) } - /// Get the alias region for accessing a defined global that is not - /// exported. + /// Get the alias region for accessing a particular defined global. pub fn defined_global_region( &mut self, func: &mut ir::Function, diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 64311df3e192..a6be5923757e 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -9,7 +9,7 @@ use crate::translate::{ }; use crate::trap::TranslateTrap; use crate::{ - BuiltinFunctionSignatures, TRAP_ARRAY_OUT_OF_BOUNDS, TRAP_GC_HEAP_CORRUPT, + BuiltinFunctionSignatures, Reachability, TRAP_ARRAY_OUT_OF_BOUNDS, TRAP_GC_HEAP_CORRUPT, TRAP_TABLE_OUT_OF_BOUNDS, }; use cranelift_codegen::cursor::FuncCursor; @@ -347,25 +347,39 @@ impl<'module_environment> FuncEnvironment<'module_environment> { func: &mut Function, memory: MemoryIndex, ) -> ir::AliasRegion { - if self.module.is_exported_memory(memory) { - // A function that operates on an exported defined memory can be - // inlined into a different module caller, where that that caller's - // module also imports that exported memory. That caller will access - // the memory with `AliasRegionKey::PublicMemory`, so we must also - // conservatively do the same here, even though we potentially know - // the precise static module index and defined memory index, because - // memory accessed with two different alias regions must not - // actually alias, or else we will get miscompiles. - self.alias_regions.public_memory_region(func) - } else { - match self.module.defined_memory_index(memory) { - Some(def) => self.alias_regions.defined_memory_region( - func, - self.translation.module_index(), - def, - ), - None => self.alias_regions.public_memory_region(func), + match self.module.defined_memory_index(memory) { + // A memory defined by this module. When it is exported, a function + // that operates on it can be inlined into a caller in a different + // module that imports that memory, and vice versa. That other module + // accesses the memory with `AliasRegionKey::PublicMemory` unless it + // statically knows that its import is always this memory, so we can + // only use this memory's precise region when every module that may + // import it does know that. Memory accessed with two different alias + // regions must not actually alias, or else we will get miscompiles. + Some(def) => { + if self.module.is_exported_memory(memory) + && !self.translation.memories_known_to_importers.contains(def) + { + self.alias_regions.public_memory_region(func) + } else { + self.alias_regions.defined_memory_region( + func, + self.translation.module_index(), + def, + ) + } } + + // A memory imported by this module: use the precise region when we + // statically know which defined memory always satisfies the import + // and everything else that imports it knows the same. + None => match self.translation.known_imported_memories[memory] { + Some(known) => { + self.alias_regions + .defined_memory_region(func, known.module, known.index) + } + None => self.alias_regions.public_memory_region(func), + }, } } @@ -374,18 +388,28 @@ impl<'module_environment> FuncEnvironment<'module_environment> { func: &mut Function, table: TableIndex, ) -> ir::AliasRegion { - if self.module.is_exported_table(table) { - // See the comment in `memory_alias_region` for details. - self.alias_regions.public_table_region(func) - } else { - match self.module.defined_table_index(table) { - Some(def) => self.alias_regions.defined_table_region( - func, - self.translation.module_index(), - def, - ), - None => self.alias_regions.public_table_region(func), + // See the comments in `memory_alias_region` for details. + match self.module.defined_table_index(table) { + Some(def) => { + if self.module.is_exported_table(table) + && !self.translation.tables_known_to_importers.contains(def) + { + self.alias_regions.public_table_region(func) + } else { + self.alias_regions.defined_table_region( + func, + self.translation.module_index(), + def, + ) + } } + None => match self.translation.known_imported_tables[table] { + Some(known) => { + self.alias_regions + .defined_table_region(func, known.module, known.index) + } + None => self.alias_regions.public_table_region(func), + }, } } @@ -394,18 +418,28 @@ impl<'module_environment> FuncEnvironment<'module_environment> { func: &mut Function, global: GlobalIndex, ) -> ir::AliasRegion { - if self.module.is_exported_global(global) { - // See the comment in `memory_alias_region` for details. - self.alias_regions.public_global_region(func) - } else { - match self.module.defined_global_index(global) { - Some(def) => self.alias_regions.defined_global_region( - func, - self.translation.module_index(), - def, - ), - None => self.alias_regions.public_global_region(func), + // See the comments in `memory_alias_region` for details. + match self.module.defined_global_index(global) { + Some(def) => { + if self.module.is_exported_global(global) + && !self.translation.globals_known_to_importers.contains(def) + { + self.alias_regions.public_global_region(func) + } else { + self.alias_regions.defined_global_region( + func, + self.translation.module_index(), + def, + ) + } } + None => match self.translation.known_imported_globals[global] { + Some(known) => { + self.alias_regions + .defined_global_region(func, known.module, known.index) + } + None => self.alias_regions.public_global_region(func), + }, } } @@ -1807,7 +1841,7 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { callee_index: FuncIndex, sig_ref: ir::SigRef, wasm_call_args: &[ir::Value], - ) -> WasmResult { + ) -> WasmResult> { let mut real_call_args = Vec::with_capacity(wasm_call_args.len() + 2); let caller_vmctx = self .builder @@ -1831,7 +1865,9 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { let callee = self .env .get_or_create_defined_func_ref(self.builder.func, def_func_index); - return Ok(self.direct_call_inst(callee, &real_call_args)); + return Ok(Reachability::Reachable( + self.direct_call_inst(callee, &real_call_args), + )); } // Handle direct calls to imported functions. We use an indirect call @@ -1873,9 +1909,11 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { let result = intrinsic_compiler .translate(*intrinsic, &real_call_args) .unwrap(); - Ok(result.into_iter().collect()) + Ok(Reachability::Reachable(result.into_iter().collect())) } else { - Ok(self.direct_call_inst(callee, &real_call_args)) + Ok(Reachability::Reachable( + self.direct_call_inst(callee, &real_call_args), + )) } } @@ -1887,37 +1925,53 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { let callee = self .env .get_or_create_imported_func_ref(self.builder.func, callee_index); - Ok(self.direct_call_inst(callee, &real_call_args)) + Ok(Reachability::Reachable( + self.direct_call_inst(callee, &real_call_args), + )) } - // The guest-to-guest sync fast path: these adapter intrinsics are - // lowered inline rather than called, but only when concurrency - // support is enabled (the deferred thread state only exists then) - // and this isn't a tail call (the deferred frame must outlive the - // call). Otherwise fall back to the indirect call, which is also - // the out-of-line slow path the inline `exit` branches to. + // Fused adapter intrinsics that are lowered inline rather than + // called. Some(KnownFunc::FactIntrinsic(intrinsic)) => { - if self.env.tunables.concurrency_support { - debug_assert!(!self.tail); - match intrinsic { - FactInlineIntrinsic::EnterSyncCall => { - return Ok(self.lower_fact_enter_sync_call(&real_call_args)); - } - FactInlineIntrinsic::ExitSyncCall => { - return Ok(self.lower_fact_exit_sync_call( - callee_index, - sig_ref, - &real_call_args, - )); - } + match intrinsic { + FactInlineIntrinsic::Trap => { + self.lower_fact_trap(&real_call_args); + return Ok(Reachability::Unreachable); + } + + // The guest-to-guest sync fast path: these adapter + // intrinsics are lowered inline rather than called, but + // only when concurrency support is enabled (the deferred + // thread state only exists then) and this isn't a tail call + // (the deferred frame must outlive the call). Otherwise + // fall back to the indirect call, which is also the + // out-of-line slow path the inline `exit` branches to. + FactInlineIntrinsic::EnterSyncCall if self.env.tunables.concurrency_support => { + debug_assert!(!self.tail); + return Ok(Reachability::Reachable( + self.lower_fact_enter_sync_call(&real_call_args), + )); + } + FactInlineIntrinsic::ExitSyncCall if self.env.tunables.concurrency_support => { + debug_assert!(!self.tail); + return Ok(Reachability::Reachable(self.lower_fact_exit_sync_call( + callee_index, + sig_ref, + &real_call_args, + ))); } + FactInlineIntrinsic::EnterSyncCall | FactInlineIntrinsic::ExitSyncCall => {} } let func_addr = self.env.alias_regions.vmctx_vmfunction_import_wasm_call( &mut self.builder.cursor(), vmctx, callee_index, ); - Ok(self.indirect_call_inst(sig_ref, func_addr, &real_call_args)) + Ok(Reachability::Reachable(self.indirect_call_inst( + sig_ref, + func_addr, + &real_call_args, + ))) } Some(key) => panic!("unexpected kind of known-import function: {key:?}"), @@ -1931,7 +1985,11 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { vmctx, callee_index, ); - Ok(self.indirect_call_inst(sig_ref, func_addr, &real_call_args)) + Ok(Reachability::Reachable(self.indirect_call_inst( + sig_ref, + func_addr, + &real_call_args, + ))) } } } @@ -1950,6 +2008,37 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { abi == wasmtime_environ::Abi::Wasm && !self.tail && !self.env.tunables.debug_guest } + /// Inline lowering of a FACT adapter's `trap` intrinsic: raise the trap + /// directly rather than calling out to the host just to have it turn a + /// constant into an error. + fn lower_fact_trap(&mut self, real_call_args: &[ir::Value]) { + let &[_callee_vmctx, _caller_vmctx, trap_code] = real_call_args else { + panic!("wrong number of arguments for the FACT `trap` intrinsic"); + }; + + let def = self + .builder + .func + .dfg + .value_def(trap_code) + .inst() + .expect("FACT emits an instruction for this argument"); + let ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + } = self.builder.func.dfg.insts[def] + else { + panic!("FACT emits a UnaryImm for this argument") + }; + let byte = u8::try_from(imm.bits()) + .expect("FACT emits an immediate that fits in u8 for this argument"); + let trap = wasmtime_environ::Trap::from_u8(byte) + .expect("FACT emits a valid Trap discriminant for this argument"); + + self.env + .trap(self.builder, crate::env_trap_to_clif_trap(trap)); + } + /// Inline lowering of a FACT adapter's `enter-sync-call` intrinsic: push a /// `VMDeferredThread` onto an explicit stack slot and publish it as the /// store's current thread, deferring the heavyweight task bookkeeping the @@ -3406,6 +3495,8 @@ impl FuncEnvironment<'_> { ) } + /// Returns `None` when the call was lowered to an unconditional trap and so + /// everything after it is unreachable. See `Call::direct_call`. pub fn translate_call<'a>( &mut self, builder: &'a mut FunctionBuilder, @@ -3413,7 +3504,7 @@ impl FuncEnvironment<'_> { callee_index: FuncIndex, sig_ref: ir::SigRef, call_args: &[ir::Value], - ) -> WasmResult { + ) -> WasmResult> { Call::new(builder, self, srcloc).direct_call(callee_index, sig_ref, call_args) } @@ -3436,7 +3527,8 @@ impl FuncEnvironment<'_> { sig_ref: ir::SigRef, call_args: &[ir::Value], ) -> WasmResult<()> { - Call::new_tail(builder, self, srcloc).direct_call(callee_index, sig_ref, call_args)?; + let _ = + Call::new_tail(builder, self, srcloc).direct_call(callee_index, sig_ref, call_args)?; Ok(()) } @@ -6088,7 +6180,10 @@ impl FuncEnvironment<'_> { .signature .unwrap_module_type_index(); let sig_ref = self.get_or_create_interned_sig_ref(builder.func, ty); - self.translate_call(builder, Default::default(), func, sig_ref, &[])?; + match self.translate_call(builder, Default::default(), func, sig_ref, &[])? { + Reachability::Reachable(_) => {} + Reachability::Unreachable => return Ok(()), + } if self.tunables.consume_fuel { self.fuel_load_into_var(builder); } diff --git a/crates/cranelift/src/lib.rs b/crates/cranelift/src/lib.rs index 50c0b26cbe58..9e090c6f6a68 100644 --- a/crates/cranelift/src/lib.rs +++ b/crates/cranelift/src/lib.rs @@ -78,6 +78,16 @@ pub const TRAP_CAST_FAILURE: TrapCode = pub const TRAP_UNCAUGHT_EXCEPTION: TrapCode = TrapCode::unwrap_user(Trap::UncaughtException as u8 + TRAP_OFFSET); +/// The CLIF trap code for a Wasmtime trap code. +/// +/// This is the inverse of `clif_trap_to_env_trap`'s fallback arm, and is what +/// all of the `TRAP_*` constants above compute for their particular trap. Use +/// it when the trap isn't statically known, e.g. when it came out of an +/// adapter-module immediate. +const fn env_trap_to_clif_trap(trap: Trap) -> TrapCode { + TrapCode::unwrap_user(trap as u8 + TRAP_OFFSET) +} + /// Creates a new cranelift `Signature` with no wasm params/results for the /// given calling convention. /// diff --git a/crates/cranelift/src/translate/code_translator.rs b/crates/cranelift/src/translate/code_translator.rs index 6e1b49cb7070..088e0f2bd488 100644 --- a/crates/cranelift/src/translate/code_translator.rs +++ b/crates/cranelift/src/translate/code_translator.rs @@ -715,13 +715,16 @@ pub fn translate_operator( let mut args = environ.stacks.peekn(num_args).to_vec(); bitcast_wasm_params(environ, sig_ref, &mut args, builder); - let inst_results = environ.translate_call( - builder, - environ.next_srcloc, - function_index, - sig_ref, - &args, - )?; + let inst_results = unwrap_or_return_unreachable_state!( + environ, + environ.translate_call( + builder, + environ.next_srcloc, + function_index, + sig_ref, + &args, + )? + ); debug_assert_eq!( inst_results.len(), diff --git a/crates/environ/src/collections/entity_set.rs b/crates/environ/src/collections/entity_set.rs index af4a2d03efe5..fd699dba4c69 100644 --- a/crates/environ/src/collections/entity_set.rs +++ b/crates/environ/src/collections/entity_set.rs @@ -3,7 +3,7 @@ use wasmtime_core::error::OutOfMemory; /// Like `cranelift_entity::EntitySet` but enforces fallible allocation for all /// methods that allocate. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct TryEntitySet where K: EntityRef, @@ -11,6 +11,17 @@ where inner: cranelift_entity::EntitySet, } +impl Default for TryEntitySet +where + K: EntityRef, +{ + fn default() -> Self { + Self { + inner: Default::default(), + } + } +} + impl TryEntitySet where K: EntityRef, diff --git a/crates/environ/src/compile/module_environ.rs b/crates/environ/src/compile/module_environ.rs index 1c7961323aed..9a6e58b36958 100644 --- a/crates/environ/src/compile/module_environ.rs +++ b/crates/environ/src/compile/module_environ.rs @@ -4,12 +4,13 @@ use crate::module::{ }; use crate::prelude::*; use crate::{ - ConstExpr, ConstOp, DataIndex, DefinedFuncIndex, DefinedGlobalIndex, ElemIndex, - EngineOrModuleTypeIndex, EntityIndex, EntityType, FuncIndex, FuncKey, GlobalIndex, IndexType, - MemoryIndex, MemoryInitializer, ModuleInternedTypeIndex, ModuleStartup, ModuleTypesBuilder, - PanicOnOom as _, PassiveElemIndex, PrimaryMap, RuntimeDataIndex, StaticModuleIndex, TableIndex, - TableInitialValue, TableInitialization, Tag, TagIndex, Tunables, TypeConvert, TypeIndex, - WasmHeapTopType, WasmHeapType, WasmResult, WasmValType, WasmparserTypeConverter, + ConstExpr, ConstOp, DataIndex, DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, + DefinedTableIndex, ElemIndex, EngineOrModuleTypeIndex, EntityIndex, EntityType, FuncIndex, + FuncKey, GlobalIndex, IndexType, MemoryIndex, MemoryInitializer, ModuleInternedTypeIndex, + ModuleStartup, ModuleTypesBuilder, PanicOnOom as _, PassiveElemIndex, PrimaryMap, + RuntimeDataIndex, StaticModuleIndex, TableIndex, TableInitialValue, TableInitialization, Tag, + TagIndex, Tunables, TypeConvert, TypeIndex, WasmHeapTopType, WasmHeapType, WasmResult, + WasmValType, WasmparserTypeConverter, }; use alloc::borrow::Cow; use cranelift_entity::SecondaryMap; @@ -47,6 +48,9 @@ pub enum FactInlineIntrinsic { /// fall back to the out-of-line `exit-sync-call` libcall when the thread /// was promoted. ExitSyncCall, + /// `trap`: raise the trap named by the (always constant) trap-code + /// argument. + Trap, } /// A statically-known function import. @@ -70,6 +74,15 @@ impl From for KnownFunc { } } +/// A statically-known import of a core Wasm global, memory, or table. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct KnownEntity { + /// The module that defines this entity. + pub module: StaticModuleIndex, + /// The entity's index in the defining module's defined-entity index space. + pub index: T, +} + /// The result of translating via `ModuleEnvironment`. /// /// Function bodies are not yet translated, and data initializers have not yet @@ -105,6 +118,47 @@ pub struct ModuleTranslation<'data> { /// `FuncKey::FactInlineIntrinsic`s. pub known_imported_functions: SecondaryMap>, + /// For each imported global, memory, or table, the single statically-known + /// defined entity that always satisfies that import, if any. + /// + /// This is used to access the entity via the defining module's precise + /// `AliasRegionKey::Defined{Global,Memory,Table}` region instead of the + /// conservative `AliasRegionKey::Public{Global,Memory,Table}` region that is + /// shared by every entity of that kind which crosses a module boundary. + /// + /// XXX: Being known requires more here than it does for functions: it is + /// not enough that *this* module's import is always the same entity, + /// *every* module that may import that entity must also always import that + /// same entity. Otherwise a function from one of those other modules, which + /// accesses the entity via the conservative region, could be inlined next + /// to one of our accesses via the precise region, and accessing the same + /// memory through two different alias regions is invalid. + pub known_imported_globals: SecondaryMap>>, + + /// Same as `known_imported_globals`, but for memories. + pub known_imported_memories: SecondaryMap>>, + + /// Same as `known_imported_globals`, but for tables. + pub known_imported_tables: SecondaryMap>>, + + /// For each global defined by this module, whether every module that may + /// import this global always imports exactly this global. + /// + /// When this holds, accesses of the global may use its precise + /// `AliasRegionKey::DefinedGlobal` region even when the global is exported, + /// because every module that can reach it agrees on that same region. This + /// is vacuously true of globals that nothing in the component imports. + /// + /// This can only be determined by looking at the whole component, so it is + /// always `false` for standalone modules. + pub globals_known_to_importers: TryEntitySet, + + /// Same as [`Self::globals_known_to_importers`], but for memories. + pub memories_known_to_importers: TryEntitySet, + + /// Same as [`Self::globals_known_to_importers`], but for tables. + pub tables_known_to_importers: TryEntitySet, + /// A list of type signatures which are considered exported from this /// module, or those that can possibly be called. This list is sorted, and /// trampolines for each of these signatures are required. @@ -226,6 +280,12 @@ impl<'data> ModuleTranslation<'data> { wasm_module_offset: 0, function_body_inputs: PrimaryMap::default(), known_imported_functions: SecondaryMap::default(), + known_imported_globals: SecondaryMap::default(), + known_imported_memories: SecondaryMap::default(), + known_imported_tables: SecondaryMap::default(), + globals_known_to_importers: TryEntitySet::default(), + memories_known_to_importers: TryEntitySet::default(), + tables_known_to_importers: TryEntitySet::default(), exported_signatures: Vec::default(), debuginfo: DebugInfoData::default(), has_unparsed_debuginfo: false, diff --git a/crates/environ/src/component/translate.rs b/crates/environ/src/component/translate.rs index c4d7ed007edf..f355051647a4 100644 --- a/crates/environ/src/component/translate.rs +++ b/crates/environ/src/component/translate.rs @@ -3,15 +3,16 @@ use crate::component::dfg::AbstractInstantiations; use crate::component::*; use crate::prelude::*; use crate::{ - EngineOrModuleTypeIndex, EntityIndex, FactInlineIntrinsic, FuncKey, ModuleEnvironment, + DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, EngineOrModuleTypeIndex, + EntityIndex, FactInlineIntrinsic, FuncKey, KnownEntity, ModuleEnvironment, ModuleInternedTypeIndex, ModuleTranslation, ModuleTypesBuilder, PrimaryMap, ScopeVec, TagIndex, Tunables, TypeConvert, WasmHeapType, WasmResult, WasmValType, }; use core::str::FromStr; -use cranelift_entity::SecondaryMap; use cranelift_entity::packed_option::PackedOption; +use cranelift_entity::{EntityRef, SecondaryMap}; use indexmap::IndexMap; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::mem; use wasmparser::component_types::{ AliasableResourceId, ComponentCoreModuleTypeId, ComponentDefinedTypeId, ComponentEntityType, @@ -19,6 +20,7 @@ use wasmparser::component_types::{ }; use wasmparser::types::Types; use wasmparser::{Chunk, ComponentExternName, Encoding, Parser, Payload, Validator}; +use wasmtime_core::alloc::PanicOnOom; mod adapt; pub use self::adapt::*; @@ -549,12 +551,17 @@ impl<'a, 'data> Translator<'a, 'data> { let translation = component.finish(self.types.types_mut_for_inlining(), self.result.types_ref())?; - self.analyze_function_imports(&translation); + self.analyze_imports(&translation); Ok((translation, self.static_modules)) } - fn analyze_function_imports(&mut self, translation: &ComponentTranslation) { + /// Record everything we statically know about each module's imports. + /// + /// See `ModuleTranslation::known_imported_functions` and + /// `ModuleTranslation::known_imported_globals` for how we can optimize + /// lowering based on this information. + fn analyze_imports(&mut self, translation: &ComponentTranslation) { // First, abstract interpret the initializers to create a map from each // static module to its abstract set of instantiations. let mut instantiations = SecondaryMap::::new(); @@ -583,105 +590,211 @@ impl<'a, 'data> Translator<'a, 'data> { } } + // Third, find the globals, memories, and tables whose identity is not + // statically known to everything that can access them. Note that this + // is a property of the whole component and not of a single module's + // instantiations; see `ModuleTranslation::known_imported_globals` for + // details. + let ambiguous = ambiguous_entities(translation, &instantiations, &instance_to_module); + + // Fourth, record which of each module's own defined entities all of + // their importers agree on, which lets those modules use a precise alias + // region for them even when they are exported. + for (module, translation) in self.static_modules.iter_mut() { + for i in 0..translation.module.num_defined_globals() { + let index = DefinedGlobalIndex::new(i); + let global = translation.module.global_index(index); + if !ambiguous.contains(&(module, EntityIndex::Global(global))) { + translation + .globals_known_to_importers + .insert(index) + .panic_on_oom(); + } + } + for i in 0..translation.module.num_defined_memories() { + let index = DefinedMemoryIndex::new(i); + let memory = translation.module.memory_index(index); + if !ambiguous.contains(&(module, EntityIndex::Memory(memory))) { + translation + .memories_known_to_importers + .insert(index) + .panic_on_oom(); + } + } + for i in 0..translation.module.num_defined_tables() { + let index = DefinedTableIndex::new(i); + let table = translation.module.table_index(index); + if !ambiguous.contains(&(module, EntityIndex::Table(table))) { + translation + .tables_known_to_importers + .insert(index) + .panic_on_oom(); + } + } + } + // Finally, iterate over our instantiations and record statically-known - // function imports so that they can get translated into direct calls - // (and eventually get inlined) rather than indirect calls through the - // imports table. + // imports: function imports so that they can get translated into direct + // calls (and eventually get inlined) rather than indirect calls through + // the imports table; and global, memory, and table imports so that they + // can get precise alias regions instead of the conservative regions + // shared by everything that crosses a module boundary. for (module, instantiations) in instantiations.iter() { let args = match instantiations { dfg::AbstractInstantiations::Many | dfg::AbstractInstantiations::None => continue, dfg::AbstractInstantiations::One(args) => args, }; - let mut imported_func_counter = 0_u32; for (i, arg) in args.iter().enumerate() { - // Only consider function imports. - let (_, _, crate::types::EntityType::Function(_)) = - self.static_modules[module].module.import(i).unwrap() - else { - continue; - }; - - let imported_func = FuncIndex::from_u32(imported_func_counter); - imported_func_counter += 1; - debug_assert!( - self.static_modules[module] - .module - .defined_func_index(imported_func) - .is_none() - ); - - let known_func = match arg { - CoreDef::InstanceFlags(_) => unreachable!("instance flags are not a function"), - CoreDef::TaskMayBlock => unreachable!("task_may_block is not a function"), - - // We could in theory inline these trampolines, so it could - // potentially make sense to record that we know this - // imported function is this particular trampoline. However, - // everything else is based around (module, - // defined-function) pairs and these trampolines don't fit - // that paradigm. Also, inlining trampolines gets really - // tricky when we consider the stack pointer, frame pointer, - // and return address note-taking that they do for the - // purposes of stack walking. We could, with enough effort, - // turn them into direct calls even though we probably - // wouldn't ever inline them, but it just doesn't seem worth - // the effort. - // - // That said, a couple of adapter trampolines are lowered - // inline during translation. We record these here so - // `FuncEnvironment` recognizes them. All other trampolines - // remain indirect calls. - CoreDef::Trampoline(index) => match translation.trampolines[*index] { - Trampoline::EnterSyncCall => FactInlineIntrinsic::EnterSyncCall.into(), - Trampoline::ExitSyncCall => FactInlineIntrinsic::ExitSyncCall.into(), - _ => continue, - }, - - // This import is a compile-time builtin intrinsic, we - // should inline its implementation during function - // translation. - CoreDef::UnsafeIntrinsic(i) => FuncKey::UnsafeIntrinsic(Abi::Wasm, *i).into(), - - // This imported function is an export from another - // instance, a perfect candidate for becoming an inlinable - // direct call! - CoreDef::Export(export) => { - let Some(arg_module) = &instance_to_module[export.instance].expand() else { - // Instance of a dynamic module that is not part of - // this component, not a statically-known module - // inside this component. We have to do an indirect - // call. - continue; - }; - - let ExportItem::Index(EntityIndex::Function(arg_func)) = &export.item + // Record that this global, memory, or table import is always the + // same defined entity, when we know that and when everything + // else that imports that entity knows it too. + macro_rules! record_known_entity { + ($variant:ident, $imported:expr, $defined_index:ident, $known:ident) => {{ + let Some((arg_module, EntityIndex::$variant(arg_entity))) = + unambiguous_entity(&instance_to_module, &ambiguous, arg) else { - unreachable!("function imports must be functions") + continue; }; - - let Some(arg_module_def_func) = self.static_modules[*arg_module] + let Some(index) = self.static_modules[arg_module] .module - .defined_func_index(*arg_func) + .$defined_index(arg_entity) else { - // TODO: we should ideally follow re-export chains - // to bottom out the instantiation argument in - // either a definition or an import at the root - // component boundary. In practice, this pattern is - // rare, so following these chains is left for the - // Future. continue; }; + assert!(self.static_modules[module].$known[$imported].is_none()); + self.static_modules[module].$known[$imported] = Some(KnownEntity { + module: arg_module, + index, + }); + }}; + } + + match self.static_modules[module].module.import_index(i).unwrap() { + EntityIndex::Function(imported_func) => { + debug_assert!( + self.static_modules[module] + .module + .defined_func_index(imported_func) + .is_none() + ); + + let known_func = match arg { + CoreDef::InstanceFlags(_) => { + unreachable!("instance flags are not a function") + } + CoreDef::TaskMayBlock => { + unreachable!("task_may_block is not a function") + } + + // We could in theory inline these trampolines, so it + // could potentially make sense to record that we + // know this imported function is this particular + // trampoline. However, everything else is based + // around (module, defined-function) pairs and these + // trampolines don't fit that paradigm. Also, + // inlining trampolines gets really tricky when we + // consider the stack pointer, frame pointer, and + // return address note-taking that they do for the + // purposes of stack walking. We could, with enough + // effort, turn them into direct calls even though we + // probably wouldn't ever inline them, but it just + // doesn't seem worth the effort. + // + // That said, a couple of adapter trampolines are + // lowered inline during translation. We record these + // here so `FuncEnvironment` recognizes them. All + // other trampolines remain indirect calls. + CoreDef::Trampoline(index) => match translation.trampolines[*index] { + Trampoline::EnterSyncCall => { + FactInlineIntrinsic::EnterSyncCall.into() + } + Trampoline::ExitSyncCall => { + FactInlineIntrinsic::ExitSyncCall.into() + } + Trampoline::Trap => FactInlineIntrinsic::Trap.into(), + _ => continue, + }, + + // This import is a compile-time builtin intrinsic, + // we should inline its implementation during + // function translation. + CoreDef::UnsafeIntrinsic(i) => { + FuncKey::UnsafeIntrinsic(Abi::Wasm, *i).into() + } + + // This imported function is an export from another + // instance, a perfect candidate for becoming an + // inlinable direct call! + CoreDef::Export(export) => { + let Some((arg_module, arg_entity)) = + resolve_core_export(&instance_to_module, export) + else { + // Instance of a dynamic module that is not + // part of this component, not a + // statically-known module inside this + // component. We have to do an indirect call. + continue; + }; + + let EntityIndex::Function(arg_func) = arg_entity else { + unreachable!("function imports must be functions") + }; + + let Some(arg_module_def_func) = self.static_modules[arg_module] + .module + .defined_func_index(arg_func) + else { + // TODO: we should ideally follow re-export + // chains to bottom out the instantiation + // argument in either a definition or an + // import at the root component boundary. In + // practice, this pattern is rare, so + // following these chains is left for the + // Future. + continue; + }; + + FuncKey::DefinedWasmFunction(arg_module, arg_module_def_func).into() + } + }; - FuncKey::DefinedWasmFunction(*arg_module, arg_module_def_func).into() + assert!( + self.static_modules[module].known_imported_functions[imported_func] + .is_none() + ); + self.static_modules[module].known_imported_functions[imported_func] = + Some(known_func); } - }; - assert!( - self.static_modules[module].known_imported_functions[imported_func].is_none() - ); - self.static_modules[module].known_imported_functions[imported_func] = - Some(known_func); + // Note that a global import is not necessarily satisfied by a + // wasm global: it can also be one of the component-model + // flags that live in the `VMComponentContext`, which have + // nothing to do with defined-global alias regions. + EntityIndex::Global(imported_global) => record_known_entity!( + Global, + imported_global, + defined_global_index, + known_imported_globals + ), + + EntityIndex::Memory(imported_memory) => record_known_entity!( + Memory, + imported_memory, + defined_memory_index, + known_imported_memories + ), + + EntityIndex::Table(imported_table) => record_known_entity!( + Table, + imported_table, + defined_table_index, + known_imported_tables + ), + + // Tags don't have alias regions of their own. + EntityIndex::Tag(_) => {} + } } } } @@ -1813,3 +1926,121 @@ mod pre_inlining { } } use pre_inlining::PreInliningComponentTypes; + +/// Resolve a `CoreExport` to the static module that defines it and the entity +/// index it refers to within that module, when we can see through it statically. +fn resolve_core_export( + instance_to_module: &PrimaryMap>, + export: &CoreExport, +) -> Option<(StaticModuleIndex, EntityIndex)> { + // This can be an instance of a dynamic module that is not part of this + // component, rather than a statically-known module inside of it. + let module = instance_to_module[export.instance].expand()?; + match &export.item { + ExportItem::Index(index) => Some((module, *index)), + // Names are only used for instances of modules whose shape we don't + // statically know, which we already filtered out. + ExportItem::Name(_) => None, + } +} + +/// Same as `resolve_core_export`, but for a `CoreDef` that must additionally be +/// unambiguous. +fn unambiguous_entity( + instance_to_module: &PrimaryMap>, + ambiguous: &HashSet<(StaticModuleIndex, EntityIndex)>, + def: &CoreDef, +) -> Option<(StaticModuleIndex, EntityIndex)> { + let CoreDef::Export(export) = def else { + return None; + }; + let entity = resolve_core_export(instance_to_module, export)?; + if ambiguous.contains(&entity) { + return None; + } + Some(entity) +} + +/// Find every core wasm entity in this component whose identity is *not* +/// statically known to every module that may import it. +/// +/// An entity is unambiguous when every argument it flows into belongs to a +/// module that we only ever instantiate with that same entity: +/// +/// * An argument to a module that we may instantiate differently elsewhere is +/// ambiguous because that module cannot statically know which one of these +/// entities it was given at runtime. +/// +/// * An argument to an imported module is ambiguous because that module is +/// compiled separately from this component, and it may re-export the entity +/// back to us under a name we cannot see through, which we may then hand to a +/// module whose imports we do otherwise know. +/// +/// Note that ambiguity is never partial: if a module importing an entity has to +/// conservatively tag its accesses with that entity's public alias region, then +/// the module defining the entity must do the same, or else inlining one of +/// them into the other would access the same bytes through two different alias +/// regions, which is invalid. +fn ambiguous_entities( + translation: &ComponentTranslation, + instantiations: &SecondaryMap>, + instance_to_module: &PrimaryMap>, +) -> HashSet<(StaticModuleIndex, EntityIndex)> { + let mut ambiguous = HashSet::default(); + + let mut mark = |def: &CoreDef| match def { + CoreDef::Export(export) => { + if let Some(entity) = resolve_core_export(instance_to_module, export) { + ambiguous.insert(entity); + } + } + + // None of these are entities that get an alias region keyed by a + // defining module and index. + CoreDef::InstanceFlags(_) + | CoreDef::Trampoline(_) + | CoreDef::UnsafeIntrinsic(_) + | CoreDef::TaskMayBlock => {} + }; + + for init in &translation.component.initializers { + match init { + GlobalInitializer::InstantiateModule(instantiation, _) => match instantiation { + InstantiateModule::Static(module, args) => { + // Arguments to modules that we only instantiate one way are + // exactly the references that keep an entity unambiguous, so + // they are the one case we do not mark here. Everything else + // gets whichever of a number of different entities it was + // handed at runtime, and so has to be conservative. + if !matches!(instantiations[*module], dfg::AbstractInstantiations::One(_)) { + for arg in args.iter() { + mark(arg); + } + } + } + + // We cannot see through an imported module's exports, so an + // entity we pass into one and that comes back out to a module + // whose imports we do know would be accessed via two different + // alias regions. + InstantiateModule::Import(_, args) => { + for arg in args.values().flat_map(|args| args.values()) { + mark(arg); + } + } + }, + + // The remaining initializers do not involve the global/table/memory + // alias regions. + GlobalInitializer::ExtractMemory(_) + | GlobalInitializer::ExtractTable(_) + | GlobalInitializer::ExtractRealloc(_) + | GlobalInitializer::ExtractCallback(_) + | GlobalInitializer::ExtractPostReturn(_) + | GlobalInitializer::Resource(_) + | GlobalInitializer::LowerImport { .. } => {} + } + } + + ambiguous +} diff --git a/crates/environ/src/module.rs b/crates/environ/src/module.rs index 98e551003d9b..90443ebf75f2 100644 --- a/crates/environ/src/module.rs +++ b/crates/environ/src/module.rs @@ -511,6 +511,13 @@ impl Module { } } + /// Get the entity index for this module's `i`th import. + pub fn import_index(&self, i: usize) -> Option { + match self.initializers.get(i)? { + Initializer::Import { index, .. } => Some(*index), + } + } + /// Returns the type of an item based on its index pub fn type_of(&self, index: EntityIndex) -> EntityType { match index { diff --git a/tests/disas/component-model/direct-adapter-calls-inlining.wat b/tests/disas/component-model/direct-adapter-calls-inlining.wat index 04435c56b5d6..b69739b04ee6 100644 --- a/tests/disas/component-model/direct-adapter-calls-inlining.wat +++ b/tests/disas/component-model/direct-adapter-calls-inlining.wat @@ -60,7 +60,6 @@ ;; region2 = 1207959576 "VMFunctionImport+0x18" ;; region3 = 1476395008 "VMGlobalImport+0x0" ;; region4 = 402653184 "PublicGlobal" -;; region5 = 1207959560 "VMFunctionImport+0x8" ;; gv0 = vmctx ;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 ;; gv2 = load.i64 notrap aligned region1 gv1+24 @@ -83,58 +82,41 @@ ;; block2: ;; jump block6 ;; -;; block8(v5: i64): -;; jump block5 -;; ;; block6: ;; @00ee v3 = load.i64 notrap aligned readonly can_move region2 v0+72 ;; v9 = load.i64 notrap aligned readonly can_move region3 v3+136 ;; v10 = load.i32 notrap aligned region4 v9 -;; brif v10, block9, block10 -;; -;; block10: -;; v24 = load.i64 notrap aligned readonly can_move region5 v3+88 -;; v23 = load.i64 notrap aligned readonly can_move region2 v3+104 -;; v22 = iconst.i32 23 -;; try_call_indirect v24(v23, v3, v22), sig1, block11, [ context v3, default: block8(exn0) ] ; v22 = 23 -;; -;; block11: -;; trap user12 +;; trapz v10, user26 +;; jump block9 ;; ;; block9: ;; v11 = load.i64 notrap aligned readonly can_move region3 v3+112 ;; v12 = load.i32 notrap aligned region4 v11 -;; store notrap aligned region4 v12, v11 +;; jump block12 +;; +;; block12: ;; jump block13 ;; ;; block13: -;; jump block14 +;; jump block11 ;; -;; block14: -;; jump block12 -;; -;; block12: +;; block11: ;; store.i32 notrap aligned region4 v10, v9 ;; jump block7 ;; ;; block7: ;; jump block4 ;; -;; block5: -;; v26 = iconst.i32 49 -;; call_indirect.i64 sig1, v24(v23, v3, v26) ; v26 = 49 -;; trap user12 -;; ;; block4: ;; jump block3 ;; ;; block3: -;; jump block15 +;; jump block14 ;; -;; block15: +;; block14: ;; @00f0 jump block1 ;; ;; block1: -;; v29 = iconst.i32 1276 -;; @00f0 return v29 ; v29 = 1276 +;; v24 = iconst.i32 1276 +;; @00f0 return v24 ; v24 = 1276 ;; } diff --git a/tests/disas/component-model/direct-adapter-calls-x64.wat b/tests/disas/component-model/direct-adapter-calls-x64.wat index cfa4fead7efa..c5cb56ee8fbe 100644 --- a/tests/disas/component-model/direct-adapter-calls-x64.wat +++ b/tests/disas/component-model/direct-adapter-calls-x64.wat @@ -87,7 +87,7 @@ ;; movq 0x18(%r10), %r10 ;; addq $0x60, %r10 ;; cmpq %rsp, %r10 -;; ja 0x147 +;; ja 0xfd ;; 79: subq $0x50, %rsp ;; movq %rbx, 0x20(%rsp) ;; movq %r12, 0x28(%rsp) @@ -96,37 +96,21 @@ ;; movq %r15, 0x40(%rsp) ;; movq %rdi, (%rsp) ;; movq (%rsp), %rdi -;; movq 0x88(%rdi), %rcx -;; movl (%rcx), %eax -;; movq %rcx, 0x10(%rsp) -;; testl %eax, %eax -;; movq %rax, 8(%rsp) -;; jne 0xd5 -;; b9: movq (%rsp), %rdi -;; movq 0x58(%rdi), %rax -;; movq 0x68(%rdi), %rdi -;; movl $0x17, %edx +;; movq 0x88(%rdi), %r10 +;; movl (%r10), %r11d +;; movq %r10, 0x10(%rsp) +;; testl %r11d, %r11d +;; movq %r11, 8(%rsp) +;; je 0xff +;; bb: movq (%rsp), %rdi +;; movq 0x48(%rdi), %rdi ;; movq (%rsp), %rsi -;; callq *%rax -;; ├─╼ exception frame offset: SP = FP - 0x50 -;; ╰─╼ exception handler: default handler, context at [SP+0x0], handler=0x132 -;; jmp 0x130 -;; d5: movq (%rsp), %rsi -;; movq 0x70(%rsi), %rax -;; movl (%rax), %ecx -;; movl %ecx, (%rax) -;; movq 0x48(%rsi), %rdi ;; callq 0 ;; ├─╼ exception frame offset: SP = FP - 0x50 -;; ╰─╼ exception handler: default handler, context at [SP+0x0], handler=0xef -;; jmp 0xf7 -;; ef: movq %rax, %rdx -;; jmp 0x132 -;; f7: movq %rax, %rdx -;; movq 8(%rsp), %rcx -;; movq 0x10(%rsp), %rax -;; movl %ecx, (%rax) -;; movq %rdx, %rax +;; ╰─╼ exception handler: default handler, context at [SP+0x0], handler=0xfb +;; movq 0x10(%rsp), %r10 +;; movq 8(%rsp), %r11 +;; movl %r11d, (%r10) ;; movq 0x20(%rsp), %rbx ;; movq 0x28(%rsp), %r12 ;; movq 0x30(%rsp), %r13 @@ -136,12 +120,6 @@ ;; movq %rbp, %rsp ;; popq %rbp ;; retq -;; 12b: jmp 0x132 -;; 130: ud2 -;; 132: movq (%rsp), %rsi -;; 136: movq 0x58(%rsi), %rcx -;; 13a: movq 0x68(%rsi), %rdi -;; 13e: movl $0x31, %edx -;; 143: callq *%rcx -;; 145: ud2 -;; 147: ud2 +;; fb: ud2 +;; fd: ud2 +;; ff: ud2 diff --git a/tests/disas/component-model/direct-adapter-calls.wat b/tests/disas/component-model/direct-adapter-calls.wat index e3ce501b14d6..ca4167811926 100644 --- a/tests/disas/component-model/direct-adapter-calls.wat +++ b/tests/disas/component-model/direct-adapter-calls.wat @@ -101,7 +101,6 @@ ;; region2 = 1476395008 "VMGlobalImport+0x0" ;; region3 = 402653184 "PublicGlobal" ;; region4 = 1207959576 "VMFunctionImport+0x18" -;; region5 = 1207959560 "VMFunctionImport+0x8" ;; gv0 = vmctx ;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 ;; gv2 = load.i64 notrap aligned region1 gv1+24 @@ -119,25 +118,16 @@ ;; block4: ;; @0082 v6 = load.i64 notrap aligned readonly can_move region2 v0+136 ;; @0082 v7 = load.i32 notrap aligned region3 v6 -;; @0086 brif v7, block7, block8 -;; -;; block8: -;; @008a v10 = load.i64 notrap aligned readonly can_move region5 v0+88 -;; @008a v9 = load.i64 notrap aligned readonly can_move region4 v0+104 -;; @0088 v8 = iconst.i32 23 -;; @008a try_call_indirect v10(v9, v0, v8), sig0, block9, [ context v0, default: block6(exn0) ] ; v8 = 23 -;; -;; block9: -;; @008c trap user12 +;; @0086 trapz v7, user26 +;; @0086 jump block7 ;; ;; block7: -;; @008e v11 = load.i64 notrap aligned readonly can_move region2 v0+112 -;; @008e v12 = load.i32 notrap aligned region3 v11 -;; @009a store notrap aligned region3 v12, v11 -;; @009c v16 = load.i64 notrap aligned readonly can_move region4 v0+72 -;; @009c try_call fn0(v16, v0, v2), sig1, block10(ret0), [ context v0, default: block6(exn0) ] +;; @008e v10 = load.i64 notrap aligned readonly can_move region2 v0+112 +;; @008e v11 = load.i32 notrap aligned region3 v10 +;; @009c v15 = load.i64 notrap aligned readonly can_move region4 v0+72 +;; @009c try_call fn0(v15, v0, v2), sig1, block9(ret0), [ context v0, default: block6(exn0) ] ;; -;; block10(v17: i32): +;; block9(v16: i32): ;; @00a8 store.i32 notrap aligned region3 v7, v6 ;; @00aa jump block5 ;; @@ -145,15 +135,11 @@ ;; @00ab jump block2 ;; ;; block3: -;; v24 = load.i64 notrap aligned readonly can_move region5 v0+88 -;; v25 = load.i64 notrap aligned readonly can_move region4 v0+104 -;; @00ae v21 = iconst.i32 49 -;; @00b0 call_indirect sig0, v24(v25, v0, v21) ; v21 = 49 -;; @00b2 trap user12 +;; @00b0 trap user52 ;; ;; block2: ;; @00b4 jump block1 ;; ;; block1: -;; @00b4 return v17 +;; @00b4 return v16 ;; } diff --git a/tests/disas/component-model/known-imported-adapter-memory.wat b/tests/disas/component-model/known-imported-adapter-memory.wat new file mode 100644 index 000000000000..d1a8738fd197 --- /dev/null +++ b/tests/disas/component-model/known-imported-adapter-memory.wat @@ -0,0 +1,266 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "function" +;;! flags = "-C inlining=n -Wconcurrency-support=n" + +;; Every access of memory contents below (in the modules that define and import +;; the memories, and in the adapter that copies the returned tuple from one to +;; the other) should use the same `DefinedMemory` region rather than the +;; conservative `PublicMemory` region. + +(component + (component $A + (core module $M + (memory (export "mem") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + (i32.const 0)) + (func (export "f") (param i32) (result i32) + (i32.store (i32.const 8) (local.get 0)) + (i32.store offset=4 (i32.const 8) (local.get 0)) + (i32.const 8)) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "a" u32) (result (tuple u32 u32)) + (canon lift (core func $m "f") + (memory $m "mem") + (realloc (func $m "realloc")))) + ) + + (instance $a (instantiate $A)) + + (component $B + (import "f" (func $f (param "a" u32) (result (tuple u32 u32)))) + + (core module $Mem + (memory (export "mem") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + (i32.const 0)) + ) + (core instance $mem (instantiate $Mem)) + + (core func $f' (canon lower (func $f) + (memory $mem "mem") + (realloc (func $mem "realloc")))) + + (core module $N + (import "" "mem" (memory 1)) + (import "" "f'" (func $f' (param i32 i32))) + (func (export "g") (result i32) + (call $f' (i32.const 42) (i32.const 0)) + (i32.load (i32.const 0))) + ) + (core instance $n (instantiate $N + (with "" (instance + (export "mem" (memory $mem "mem")) + (export "f'" (func $f')) + )) + )) + ) + + (instance $b (instantiate $B (with "f" (func $a "f")))) +) +;; function u0:0(i64 vmctx, i64, i32, i32, i32, i32) -> 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 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32, v5: i32): +;; @0055 jump block1 +;; +;; block1: +;; @0053 v6 = iconst.i32 0 +;; @0055 return v6 ; v6 = 0 +;; } +;; +;; function u0:1(i64 vmctx, i64, i32) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 603979776 "VMMemoryDefinition+0x0" +;; region3 = 603979784 "VMMemoryDefinition+0x8" +;; region4 = 201326592 "DefinedMemory(StaticModuleIndex(0), DefinedMemoryIndex(0))" +;; gv0 = vmctx +;; 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, v2: i32): +;; @005c v5 = load.i64 notrap aligned readonly can_move region2 v0+56 +;; v14 = iconst.i64 8 +;; @005c v6 = iadd v5, v14 ; v14 = 8 +;; @005c store little region4 v2, v6 +;; v16 = iconst.i64 12 +;; v21 = iadd v5, v16 ; v16 = 12 +;; @0063 store little region4 v2, v21 +;; @0068 jump block1 +;; +;; block1: +;; @0058 v3 = iconst.i32 8 +;; @0068 return v3 ; v3 = 8 +;; } +;; +;; function u1:0(i64 vmctx, i64, i32, i32, i32, i32) -> 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 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32, v5: i32): +;; @013e jump block1 +;; +;; block1: +;; @013c v6 = iconst.i32 0 +;; @013e return v6 ; v6 = 0 +;; } +;; +;; function u2:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1275068416 "VMMemoryImport+0x0" +;; region4 = 603979776 "VMMemoryDefinition+0x0" +;; region5 = 603979784 "VMMemoryDefinition+0x8" +;; region6 = 201588736 "DefinedMemory(StaticModuleIndex(1), DefinedMemoryIndex(0))" +;; gv0 = vmctx +;; 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 +;; fn0 = colocated u3:0 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @01af v4 = load.i64 notrap aligned readonly can_move region2 v0+96 +;; @01ab v2 = iconst.i32 42 +;; @01ad v3 = iconst.i32 0 +;; @01af call fn0(v4, v0, v2, v3) ; v2 = 42, v3 = 0 +;; @01b3 v7 = load.i64 notrap aligned readonly can_move region3 v0+48 +;; @01b3 v8 = load.i64 notrap aligned readonly can_move region4 v7 +;; @01b3 v10 = load.i32 little region6 v8 +;; @01b6 jump block1 +;; +;; block1: +;; @01b6 return v10 +;; } +;; +;; function u3:0(i64 vmctx, i64, i32, i32) tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1476395008 "VMGlobalImport+0x0" +;; region3 = 402653184 "PublicGlobal" +;; region4 = 1207959576 "VMFunctionImport+0x18" +;; region5 = 1275068416 "VMMemoryImport+0x0" +;; region6 = 603979776 "VMMemoryDefinition+0x0" +;; region7 = 603979784 "VMMemoryDefinition+0x8" +;; region8 = 201326592 "DefinedMemory(StaticModuleIndex(0), DefinedMemoryIndex(0))" +;; region9 = 201588736 "DefinedMemory(StaticModuleIndex(1), DefinedMemoryIndex(0))" +;; gv0 = vmctx +;; 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, i64, i32) -> i32 tail +;; fn0 = colocated u0:1 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32, v3: i32): +;; @00be jump block4 +;; +;; block6(v5: i64): +;; @00be jump block3 +;; +;; block4: +;; @00c5 v7 = load.i64 notrap aligned readonly can_move region2 v0+248 +;; @00c5 v8 = load.i32 notrap aligned region3 v7 +;; @00c9 trapz v8, user26 +;; @00c9 jump block7 +;; +;; block7: +;; @00d1 v11 = load.i64 notrap aligned readonly can_move region2 v0+224 +;; @00d1 v12 = load.i32 notrap aligned region3 v11 +;; @00df v16 = load.i64 notrap aligned readonly can_move region4 v0+184 +;; @00df try_call fn0(v16, v0, v2), sig1, block9(ret0), [ context v0, default: block6(exn0) ] +;; +;; block9(v17: i32): +;; @00b8 v4 = iconst.i32 0 +;; @00e5 store notrap aligned region3 v4, v7 ; v4 = 0 +;; @00e9 v20 = iconst.i32 3 +;; @00eb v21 = band v17, v20 ; v20 = 3 +;; @00ec trapnz v21, user36 +;; @00ec jump block11 +;; +;; block11: +;; @00f8 v24 = load.i64 notrap aligned readonly can_move region5 v0+48 +;; @00f8 v25 = load.i64 notrap aligned region7 v24+8 +;; @00f8 v26 = iconst.i64 16 +;; @00f8 v27 = ushr v25, v26 ; v26 = 16 +;; @00f8 v28 = ireduce.i32 v27 +;; @00fa v29 = uextend.i64 v28 +;; @00fd v31 = ishl v29, v26 ; v26 = 16 +;; @0100 v32 = uextend.i64 v17 +;; v85 = iconst.i64 8 +;; @0104 v35 = iadd v32, v85 ; v85 = 8 +;; @0105 v36 = icmp uge v31, v35 +;; @0106 brif v36, block12, block14 +;; +;; block14: +;; @0108 jump block13 +;; +;; block13: +;; @010b trap user4 +;; +;; block12: +;; v86 = iconst.i32 3 +;; v87 = band.i32 v3, v86 ; v86 = 3 +;; @0114 trapnz v87, user36 +;; @0114 jump block16 +;; +;; block16: +;; @0120 v44 = load.i64 notrap aligned readonly can_move region5 v0+72 +;; @0120 v45 = load.i64 notrap aligned region7 v44+8 +;; v88 = iconst.i64 16 +;; v89 = ushr v45, v88 ; v88 = 16 +;; @0120 v48 = ireduce.i32 v89 +;; @0122 v49 = uextend.i64 v48 +;; v90 = ishl v49, v88 ; v88 = 16 +;; @0128 v52 = uextend.i64 v3 +;; v91 = iconst.i64 8 +;; v92 = iadd v52, v91 ; v91 = 8 +;; @012d v56 = icmp uge v90, v92 +;; @012e brif v56, block17, block19 +;; +;; block19: +;; @0130 jump block18 +;; +;; block18: +;; @0133 trap user4 +;; +;; block17: +;; @013b v62 = load.i64 notrap aligned readonly can_move region6 v24 +;; @013b v63 = iadd v62, v32 +;; @013b v64 = load.i32 little region8 v63 +;; @013e v67 = load.i64 notrap aligned readonly can_move region6 v44 +;; @013e v68 = iadd v67, v52 +;; @013e store little region9 v64, v68 +;; @0146 v73 = iconst.i64 4 +;; @0146 v74 = iadd v63, v73 ; v73 = 4 +;; @0146 v75 = load.i32 little region8 v74 +;; @0149 v81 = iadd v68, v73 ; v73 = 4 +;; @0149 store little region9 v75, v81 +;; @014f store.i32 notrap aligned region3 v8, v7 +;; @0151 jump block5 +;; +;; block5: +;; @0152 jump block2 +;; +;; block3: +;; @0157 trap user52 +;; +;; block2: +;; @015b jump block1 +;; +;; block1: +;; @015b return +;; } diff --git a/tests/disas/component-model/known-imported-canonical-abi-memory.wat b/tests/disas/component-model/known-imported-canonical-abi-memory.wat new file mode 100644 index 000000000000..3cf1e1e84f42 --- /dev/null +++ b/tests/disas/component-model/known-imported-canonical-abi-memory.wat @@ -0,0 +1,66 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "function" +;;! flags = "-C inlining=n -Wconcurrency-support=n" + +;; `$M`'s memory is unambiguous despite being exported and used by component +;; model libcall intrinsics when transcoding strings, and `$M` gets the precise +;; `DefinedMemory` alias region for it. + +(component + (core module $M + (memory (export "mem") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + (i32.const 0) + ) + (func (export "f") (param i32 i32) + (i32.store (local.get 0) (local.get 1)) + ) + ) + + (core instance $m (instantiate $M)) + + (func (export "f") (param "s" string) + (canon lift (core func $m "f") + (memory $m "mem") + (realloc (func $m "realloc")) + ) + ) +) +;; function u0:0(i64 vmctx, i64, i32, i32, i32, i32) -> 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 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32, v5: i32): +;; @004a jump block1 +;; +;; block1: +;; @0048 v6 = iconst.i32 0 +;; @004a return v6 ; v6 = 0 +;; } +;; +;; function u0:1(i64 vmctx, i64, i32, i32) tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 603979776 "VMMemoryDefinition+0x0" +;; region3 = 603979784 "VMMemoryDefinition+0x8" +;; region4 = 201326592 "DefinedMemory(StaticModuleIndex(0), DefinedMemoryIndex(0))" +;; gv0 = vmctx +;; 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, v2: i32, v3: i32): +;; @0051 v5 = load.i64 notrap aligned readonly can_move region2 v0+56 +;; @0051 v4 = uextend.i64 v2 +;; @0051 v6 = iadd v5, v4 +;; @0051 store little region4 v3, v6 +;; @0054 jump block1 +;; +;; block1: +;; @0054 return +;; } diff --git a/tests/disas/component-model/known-imported-entities.wat b/tests/disas/component-model/known-imported-entities.wat new file mode 100644 index 000000000000..0a3df05d8397 --- /dev/null +++ b/tests/disas/component-model/known-imported-entities.wat @@ -0,0 +1,202 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "function" +;;! flags = "-C inlining=n -Wconcurrency-support=n" + +;; Module `$M` defines a memory, global, and table and module `$N` imports them. +;; Each module is instantiated exactly once, and nothing else in the component +;; can get its hands on those entities, so both modules statically know that +;; `$N`'s imports are always `$M`'s definitions. Both modules should therefore +;; use the precise `DefinedMemory`/`DefinedGlobal`/`DefinedTable` alias regions, +;; and use the same region as each other for the same entity, rather than +;; falling back to the conservative `PublicMemory`/`PublicGlobal`/`PublicTable` +;; regions. + +(component + (core module $M + (memory (export "mem") 1) + (global (export "g") (mut i32) (i32.const 0)) + (table (export "t") 1 funcref) + + (func (export "load-mem") (result i32) + (i32.load (i32.const 0))) + (func (export "get-global") (result i32) + (global.get 0)) + (func (export "get-table") (result funcref) + (table.get 0 (i32.const 0))) + ) + + (core instance $m (instantiate $M)) + + (core module $N + (import "" "mem" (memory 1)) + (import "" "g" (global (mut i32))) + (import "" "t" (table 1 funcref)) + + (func (export "load-mem") (result i32) + (i32.load (i32.const 0))) + (func (export "get-global") (result i32) + (global.get 0)) + (func (export "get-table") (result funcref) + (table.get 0 (i32.const 0))) + ) + + (core instance $n (instantiate $N (with "" (instance $m)))) +) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 603979776 "VMMemoryDefinition+0x0" +;; region3 = 603979784 "VMMemoryDefinition+0x8" +;; region4 = 201326592 "DefinedMemory(StaticModuleIndex(0), DefinedMemoryIndex(0))" +;; gv0 = vmctx +;; 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): +;; @0073 v4 = load.i64 notrap aligned readonly can_move region2 v0+56 +;; @0073 v6 = load.i32 little region4 v4 +;; @0076 jump block1 +;; +;; block1: +;; @0076 return v6 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 469762048 "DefinedGlobal(StaticModuleIndex(0), DefinedGlobalIndex(0))" +;; gv0 = vmctx +;; 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): +;; @0079 v2 = load.i32 notrap aligned region2 v0+96 +;; @007b jump block1 +;; +;; block1: +;; @007b return v2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i64 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 671088648 "VMTableDefinition+0x8" +;; region4 = 335544320 "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 +;; sig0 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0080 v3 = load.i64 notrap aligned region3 v0+80 +;; @0080 v7 = load.i64 notrap aligned region2 v0+72 +;; @0080 v4 = ireduce.i32 v3 +;; @007e v2 = iconst.i32 0 +;; v21 = icmp eq v4, v2 ; v2 = 0 +;; v24 = iconst.i64 0 +;; @0080 v12 = select_spectre_guard v21, v24, v7 ; v24 = 0 +;; @0080 v13 = load.i64 user6 aligned region4 v12 +;; @0080 v14 = iconst.i64 -2 +;; @0080 v15 = band v13, v14 ; v14 = -2 +;; @0080 brif v13, block3(v15), block2 +;; +;; block2 cold: +;; v25 = iconst.i32 0 +;; v26 = iconst.i64 0 +;; @0080 v19 = call fn0(v0, v25, v26) ; v25 = 0, v26 = 0 +;; @0080 jump block3(v19) +;; +;; block3(v16: i64): +;; @0082 jump block1 +;; +;; block1: +;; @0082 return v16 +;; } +;; +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1275068416 "VMMemoryImport+0x0" +;; region3 = 603979776 "VMMemoryDefinition+0x0" +;; region4 = 603979784 "VMMemoryDefinition+0x8" +;; region5 = 201326592 "DefinedMemory(StaticModuleIndex(0), DefinedMemoryIndex(0))" +;; gv0 = vmctx +;; 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): +;; @00f5 v4 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @00f5 v5 = load.i64 notrap aligned readonly can_move region3 v4 +;; @00f5 v7 = load.i32 little region5 v5 +;; @00f8 jump block1 +;; +;; block1: +;; @00f8 return v7 +;; } +;; +;; function u1:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1476395008 "VMGlobalImport+0x0" +;; region3 = 469762048 "DefinedGlobal(StaticModuleIndex(0), DefinedGlobalIndex(0))" +;; gv0 = vmctx +;; 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): +;; @00fb v2 = load.i64 notrap aligned readonly can_move region2 v0+96 +;; @00fb v3 = load.i32 notrap aligned region3 v2 +;; @00fd jump block1 +;; +;; block1: +;; @00fd return v3 +;; } +;; +;; function u1:2(i64 vmctx, i64) -> i64 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1342177280 "VMTableImport+0x0" +;; region3 = 671088640 "VMTableDefinition+0x0" +;; region4 = 671088648 "VMTableDefinition+0x8" +;; region5 = 335544320 "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 +;; sig0 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0102 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; @0102 v4 = load.i64 notrap aligned region4 v3+8 +;; @0102 v9 = load.i64 notrap aligned region3 v3 +;; @0102 v5 = ireduce.i32 v4 +;; @0100 v2 = iconst.i32 0 +;; v23 = icmp eq v5, v2 ; v2 = 0 +;; v26 = iconst.i64 0 +;; @0102 v14 = select_spectre_guard v23, v26, v9 ; v26 = 0 +;; @0102 v15 = load.i64 user6 aligned region5 v14 +;; @0102 v16 = iconst.i64 -2 +;; @0102 v17 = band v15, v16 ; v16 = -2 +;; @0102 brif v15, block3(v17), block2 +;; +;; block2 cold: +;; v27 = iconst.i32 0 +;; v28 = iconst.i64 0 +;; @0102 v21 = call fn0(v0, v27, v28) ; v27 = 0, v28 = 0 +;; @0102 jump block3(v21) +;; +;; block3(v18: i64): +;; @0104 jump block1 +;; +;; block1: +;; @0104 return v18 +;; } diff --git a/tests/disas/component-model/multiple-instantiations-makes-imports-unknown.wat b/tests/disas/component-model/multiple-instantiations-makes-imports-unknown.wat new file mode 100644 index 000000000000..07f20f75e633 --- /dev/null +++ b/tests/disas/component-model/multiple-instantiations-makes-imports-unknown.wat @@ -0,0 +1,300 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "function" +;;! flags = "-C inlining=n -Wconcurrency-support=n" + +;; Same as `known-imported-entities.wat` except that `$N` is instantiated twice +;; with the exports of two different modules, so `$N` does not always import the +;; same entities and we cannot statically know what its imports are. Both the +;; defining modules and `$N` must fall back to the conservative `PublicMemory` / +;; `PublicGlobal` / `PublicTable` alias regions: if only `$N` did, then inlining +;; one of `$M1`'s or `$M2`'s functions into one of `$N`'s functions would end up +;; accessing the same entity through two different alias regions, which is +;; invalid. + +(component + (core module $M1 + (memory (export "mem") 1) + (global (export "g") (mut i32) (i32.const 0)) + (table (export "t") 1 funcref) + + (func (export "load-mem") (result i32) + (i32.load (i32.const 0)) + ) + (func (export "get-global") (result i32) + (global.get 0) + ) + (func (export "get-table") (result funcref) + (table.get 0 (i32.const 0)) + ) + ) + (core instance $m1 (instantiate $M1)) + + (core module $M2 + (memory (export "mem") 1) + (global (export "g") (mut i32) (i32.const 0)) + (table (export "t") 1 funcref) + + (func (export "load-mem") (result i32) + (i32.load (i32.const 0)) + ) + (func (export "get-global") (result i32) + (global.get 0) + ) + (func (export "get-table") (result funcref) + (table.get 0 (i32.const 0)) + ) + ) + (core instance $m2 (instantiate $M2)) + + (core module $N + (import "" "mem" (memory 1)) + (import "" "g" (global (mut i32))) + (import "" "t" (table 1 funcref)) + + (func (export "load-mem") (result i32) + (i32.load (i32.const 0)) + ) + (func (export "get-global") (result i32) + (global.get 0) + ) + (func (export "get-table") (result funcref) + (table.get 0 (i32.const 0)) + ) + ) + (core instance $n1 (instantiate $N (with "" (instance $m1)))) + (core instance $n2 (instantiate $N (with "" (instance $m2)))) +) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 603979776 "VMMemoryDefinition+0x0" +;; region3 = 603979784 "VMMemoryDefinition+0x8" +;; region4 = 134217728 "PublicMemory" +;; gv0 = vmctx +;; 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): +;; @0073 v4 = load.i64 notrap aligned readonly can_move region2 v0+56 +;; @0073 v6 = load.i32 little region4 v4 +;; @0076 jump block1 +;; +;; block1: +;; @0076 return v6 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 402653184 "PublicGlobal" +;; gv0 = vmctx +;; 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): +;; @0079 v2 = load.i32 notrap aligned region2 v0+96 +;; @007b jump block1 +;; +;; block1: +;; @007b return v2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i64 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 671088648 "VMTableDefinition+0x8" +;; region4 = 268435456 "PublicTable" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; sig0 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0080 v3 = load.i64 notrap aligned region3 v0+80 +;; @0080 v7 = load.i64 notrap aligned region2 v0+72 +;; @0080 v4 = ireduce.i32 v3 +;; @007e v2 = iconst.i32 0 +;; v21 = icmp eq v4, v2 ; v2 = 0 +;; v24 = iconst.i64 0 +;; @0080 v12 = select_spectre_guard v21, v24, v7 ; v24 = 0 +;; @0080 v13 = load.i64 user6 aligned region4 v12 +;; @0080 v14 = iconst.i64 -2 +;; @0080 v15 = band v13, v14 ; v14 = -2 +;; @0080 brif v13, block3(v15), block2 +;; +;; block2 cold: +;; v25 = iconst.i32 0 +;; v26 = iconst.i64 0 +;; @0080 v19 = call fn0(v0, v25, v26) ; v25 = 0, v26 = 0 +;; @0080 jump block3(v19) +;; +;; block3(v16: i64): +;; @0082 jump block1 +;; +;; block1: +;; @0082 return v16 +;; } +;; +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 603979776 "VMMemoryDefinition+0x0" +;; region3 = 603979784 "VMMemoryDefinition+0x8" +;; region4 = 134217728 "PublicMemory" +;; gv0 = vmctx +;; 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): +;; @0100 v4 = load.i64 notrap aligned readonly can_move region2 v0+56 +;; @0100 v6 = load.i32 little region4 v4 +;; @0103 jump block1 +;; +;; block1: +;; @0103 return v6 +;; } +;; +;; function u1:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 402653184 "PublicGlobal" +;; gv0 = vmctx +;; 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): +;; @0106 v2 = load.i32 notrap aligned region2 v0+96 +;; @0108 jump block1 +;; +;; block1: +;; @0108 return v2 +;; } +;; +;; function u1:2(i64 vmctx, i64) -> i64 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 671088640 "VMTableDefinition+0x0" +;; region3 = 671088648 "VMTableDefinition+0x8" +;; region4 = 268435456 "PublicTable" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; sig0 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @010d v3 = load.i64 notrap aligned region3 v0+80 +;; @010d v7 = load.i64 notrap aligned region2 v0+72 +;; @010d v4 = ireduce.i32 v3 +;; @010b v2 = iconst.i32 0 +;; v21 = icmp eq v4, v2 ; v2 = 0 +;; v24 = iconst.i64 0 +;; @010d v12 = select_spectre_guard v21, v24, v7 ; v24 = 0 +;; @010d v13 = load.i64 user6 aligned region4 v12 +;; @010d v14 = iconst.i64 -2 +;; @010d v15 = band v13, v14 ; v14 = -2 +;; @010d brif v13, block3(v15), block2 +;; +;; block2 cold: +;; v25 = iconst.i32 0 +;; v26 = iconst.i64 0 +;; @010d v19 = call fn0(v0, v25, v26) ; v25 = 0, v26 = 0 +;; @010d jump block3(v19) +;; +;; block3(v16: i64): +;; @010f jump block1 +;; +;; block1: +;; @010f return v16 +;; } +;; +;; function u2:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1275068416 "VMMemoryImport+0x0" +;; region3 = 603979776 "VMMemoryDefinition+0x0" +;; region4 = 603979784 "VMMemoryDefinition+0x8" +;; region5 = 134217728 "PublicMemory" +;; gv0 = vmctx +;; 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): +;; @0183 v4 = load.i64 notrap aligned readonly can_move region2 v0+48 +;; @0183 v5 = load.i64 notrap aligned readonly can_move region3 v4 +;; @0183 v7 = load.i32 little region5 v5 +;; @0186 jump block1 +;; +;; block1: +;; @0186 return v7 +;; } +;; +;; function u2:1(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1476395008 "VMGlobalImport+0x0" +;; region3 = 402653184 "PublicGlobal" +;; gv0 = vmctx +;; 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): +;; @0189 v2 = load.i64 notrap aligned readonly can_move region2 v0+96 +;; @0189 v3 = load.i32 notrap aligned region3 v2 +;; @018b jump block1 +;; +;; block1: +;; @018b return v3 +;; } +;; +;; function u2:2(i64 vmctx, i64) -> i64 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1342177280 "VMTableImport+0x0" +;; region3 = 671088640 "VMTableDefinition+0x0" +;; region4 = 671088648 "VMTableDefinition+0x8" +;; region5 = 268435456 "PublicTable" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; sig0 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0190 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; @0190 v4 = load.i64 notrap aligned region4 v3+8 +;; @0190 v9 = load.i64 notrap aligned region3 v3 +;; @0190 v5 = ireduce.i32 v4 +;; @018e v2 = iconst.i32 0 +;; v23 = icmp eq v5, v2 ; v2 = 0 +;; v26 = iconst.i64 0 +;; @0190 v14 = select_spectre_guard v23, v26, v9 ; v26 = 0 +;; @0190 v15 = load.i64 user6 aligned region5 v14 +;; @0190 v16 = iconst.i64 -2 +;; @0190 v17 = band v15, v16 ; v16 = -2 +;; @0190 brif v15, block3(v17), block2 +;; +;; block2 cold: +;; v27 = iconst.i32 0 +;; v28 = iconst.i64 0 +;; @0190 v21 = call fn0(v0, v27, v28) ; v27 = 0, v28 = 0 +;; @0190 jump block3(v21) +;; +;; block3(v18: i64): +;; @0192 jump block1 +;; +;; block1: +;; @0192 return v18 +;; } diff --git a/tests/disas/component-model/sync-adapter-calls-x64.wat b/tests/disas/component-model/sync-adapter-calls-x64.wat new file mode 100644 index 000000000000..918098dce113 --- /dev/null +++ b/tests/disas/component-model/sync-adapter-calls-x64.wat @@ -0,0 +1,96 @@ +;;! target = "x86_64" +;;! test = "compile" +;;! filter = "wasm[1]" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +(component + (component $A + (core module $M + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42)) + ) + ) + + (core instance $m (instantiate $M)) + + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + + (core func $f' (canon lower (func $f))) + + (core module $N + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1234)) + ) + ) + + (core instance $n + (instantiate $N + (with "" (instance (export "f'" (func $f')))) + ) + ) + + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b + (instantiate $B + (with "f" (func $a "f")) + ) + ) + + (export "g" (func $b "g")) +) + +;; wasm[1]::function[1]: +;; pushq %rbp +;; movq %rsp, %rbp +;; movq 8(%rdi), %r10 +;; movq 0x18(%r10), %r10 +;; addq $0x20, %r10 +;; cmpq %rsp, %r10 +;; ja 0xe6 +;; 39: subq $0x20, %rsp +;; movq 0x48(%rdi), %rdi +;; movq 0xc8(%rdi), %rax +;; movl (%rax), %ecx +;; testl %ecx, %ecx +;; je 0xe8 +;; 52: movq 0xe0(%rdi), %rdx +;; movl (%rdx), %esi +;; movl $0, (%rdx) +;; movq 8(%rdi), %rdi +;; movq 0x88(%rdi), %r8 +;; leaq (%rsp), %r10 +;; movq %r8, (%rsp) +;; movl $2, 8(%rsp) +;; movl $0, 0xc(%rsp) +;; movl $1, 0x10(%rsp) +;; movl 0x80(%rdi), %r9d +;; movl %r9d, 0x14(%rsp) +;; movl $0, 0x80(%rdi) +;; movl 0x84(%rdi), %r11d +;; movl %r11d, 0x18(%rsp) +;; movl $0, 0x84(%rdi) +;; movq %r10, 0x88(%rdi) +;; movq %r8, 0x88(%rdi) +;; movl %r9d, 0x80(%rdi) +;; movl %r11d, 0x84(%rdi) +;; movl %ecx, (%rax) +;; movl %esi, (%rdx) +;; movl $0x4fc, %eax +;; addq $0x20, %rsp +;; movq %rbp, %rsp +;; popq %rbp +;; retq +;; e6: ud2 +;; e8: ud2 diff --git a/tests/disas/component-model/sync-adapter-calls.wat b/tests/disas/component-model/sync-adapter-calls.wat index 36536000f4cc..024d7db65d88 100644 --- a/tests/disas/component-model/sync-adapter-calls.wat +++ b/tests/disas/component-model/sync-adapter-calls.wat @@ -1,6 +1,6 @@ ;;! target = "x86_64" ;;! test = "optimize" -;;! filter = "function" +;;! filter = "wasm[1]--function" ;;! flags = "-C inlining=y -Wconcurrency-support=y" (component @@ -50,23 +50,7 @@ (export "g" (func $b "g")) ) -;; function u0:0(i64 vmctx, i64, i32) -> 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 -;; stack_limit = gv2 -;; -;; block0(v0: i64, v1: i64, v2: i32): -;; @003b jump block1 -;; -;; block1: -;; @0038 v3 = iconst.i32 42 -;; v4 = iadd.i32 v2, v3 ; v3 = 42 -;; @003b return v4 -;; } -;; + ;; function u1:0(i64 vmctx, i64) -> i32 tail { ;; ss0 = explicit_slot 32, align = 8 ;; region0 = 8 "VMContext+0x8" @@ -108,23 +92,12 @@ ;; block2: ;; jump block6 ;; -;; block8(v5: i64): -;; jump block5 -;; ;; block6: ;; @00ee v3 = load.i64 notrap aligned readonly can_move region2 v0+72 ;; v9 = load.i64 notrap aligned readonly can_move region3 v3+200 ;; v10 = load.i32 notrap aligned region4 v9 -;; brif v10, block9, block10 -;; -;; block10: -;; v53 = load.i64 notrap aligned readonly can_move region14 v3+88 -;; v52 = load.i64 notrap aligned readonly can_move region2 v3+104 -;; v51 = iconst.i32 23 -;; try_call_indirect v53(v52, v3, v51), sig1, block11, [ context v3, default: block8(exn0) ] ; v51 = 23 -;; -;; block11: -;; trap user12 +;; trapz v10, user26 +;; jump block9 ;; ;; block9: ;; v11 = load.i64 notrap aligned readonly can_move region3 v3+224 @@ -149,25 +122,24 @@ ;; store notrap aligned region5 v19, v20+136 ;; v26 = load.i64 notrap aligned readonly can_move region3 v3+176 ;; v27 = load.i32 notrap aligned region4 v26 -;; store notrap aligned region4 v27, v26 +;; jump block16 +;; +;; block16: ;; jump block17 ;; ;; block17: -;; jump block18 +;; jump block11 ;; -;; block18: +;; block11: ;; jump block12 ;; ;; block12: -;; jump block13 -;; -;; block13: ;; store.i64 notrap aligned region5 v21, v20+136 ;; store.i32 notrap aligned region10 v22, v20+128 ;; store.i32 notrap aligned region12 v24, v20+132 -;; jump block15 +;; jump block14 ;; -;; block15: +;; block14: ;; store.i32 notrap aligned region4 v10, v9 ;; store.i32 notrap aligned region4 v12, v11 ;; jump block7 @@ -175,138 +147,16 @@ ;; block7: ;; jump block4 ;; -;; block5: -;; v61 = load.i64 notrap aligned readonly can_move region14 v3+88 -;; v62 = load.i64 notrap aligned readonly can_move region2 v3+104 -;; v48 = iconst.i32 49 -;; call_indirect sig1, v61(v62, v3, v48) ; v48 = 49 -;; trap user12 -;; ;; block4: ;; jump block3 ;; ;; block3: -;; jump block19 +;; jump block18 ;; -;; block19: +;; block18: ;; @00f0 jump block1 ;; ;; block1: -;; v54 = iconst.i32 1276 -;; @00f0 return v54 ; v54 = 1276 -;; } -;; -;; function u2:0(i64 vmctx, i64, i32) -> i32 tail { -;; ss0 = explicit_slot 32, align = 8 -;; region0 = 8 "VMContext+0x8" -;; region1 = 67108888 "VMStoreContext+0x18" -;; region2 = 1476395008 "VMGlobalImport+0x0" -;; region3 = 402653184 "PublicGlobal" -;; region4 = 1207959576 "VMFunctionImport+0x18" -;; region5 = 1207959560 "VMFunctionImport+0x8" -;; region6 = 67109000 "VMStoreContext+0x88" -;; region7 = 1006632960 "VMDeferredThread+0x0" -;; region8 = 1006632968 "VMDeferredThread+0x8" -;; region9 = 1006632972 "VMDeferredThread+0xc" -;; region10 = 1006632976 "VMDeferredThread+0x10" -;; region11 = 67108992 "VMStoreContext+0x80" -;; region12 = 1006632980 "VMDeferredThread+0x14" -;; region13 = 67108996 "VMStoreContext+0x84" -;; region14 = 1006632984 "VMDeferredThread+0x18" -;; gv0 = vmctx -;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 -;; gv2 = load.i64 notrap aligned region1 gv1+24 -;; gv3 = vmctx -;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 -;; gv5 = load.i64 notrap aligned region1 gv4+24 -;; sig0 = (i64 vmctx, i64, i32) tail -;; sig1 = (i64 vmctx, i64, i32, i32, i32) tail -;; sig2 = (i64 vmctx, i64, i32) -> i32 tail -;; sig3 = (i64 vmctx, i64) tail -;; fn0 = colocated u0:0 sig2 -;; stack_limit = gv2 -;; -;; block0(v0: i64, v1: i64, v2: i32): -;; @00cf jump block4 -;; -;; block6(v4: i64): -;; @00cf jump block3 -;; -;; block4: -;; @00d6 v6 = load.i64 notrap aligned readonly can_move region2 v0+200 -;; @00d6 v7 = load.i32 notrap aligned region3 v6 -;; @00da brif v7, block7, block8 -;; -;; block8: -;; @00de v10 = load.i64 notrap aligned readonly can_move region5 v0+88 -;; @00de v9 = load.i64 notrap aligned readonly can_move region4 v0+104 -;; @00dc v8 = iconst.i32 23 -;; @00de try_call_indirect v10(v9, v0, v8), sig0, block9, [ context v0, default: block6(exn0) ] ; v8 = 23 -;; -;; block9: -;; @00e0 trap user12 -;; -;; block7: -;; @00e2 v11 = load.i64 notrap aligned readonly can_move region2 v0+224 -;; @00e2 v12 = load.i32 notrap aligned region3 v11 -;; @00c9 v3 = iconst.i32 0 -;; @00e8 store notrap aligned region3 v3, v11 ; v3 = 0 -;; @00f0 v20 = load.i64 notrap aligned readonly can_move region0 v0+8 -;; @00f0 v21 = load.i64 notrap aligned region6 v20+136 -;; @00f0 v19 = stack_addr.i64 ss0 -;; @00f0 store notrap aligned region7 v21, v19 -;; @00ea v15 = iconst.i32 2 -;; @00f0 store notrap aligned region8 v15, v19+8 ; v15 = 2 -;; @00f0 store notrap aligned region9 v3, v19+12 ; v3 = 0 -;; @00ee v17 = iconst.i32 1 -;; @00f0 store notrap aligned region10 v17, v19+16 ; v17 = 1 -;; @00f0 v22 = load.i32 notrap aligned region11 v20+128 -;; @00f0 store notrap aligned region12 v22, v19+20 -;; @00f0 store notrap aligned region11 v3, v20+128 ; v3 = 0 -;; @00f0 v24 = load.i32 notrap aligned region13 v20+132 -;; @00f0 store notrap aligned region14 v24, v19+24 -;; @00f0 store notrap aligned region13 v3, v20+132 ; v3 = 0 -;; @00f0 store notrap aligned region6 v19, v20+136 -;; @00f2 v26 = load.i64 notrap aligned readonly can_move region2 v0+176 -;; @00f2 v27 = load.i32 notrap aligned region3 v26 -;; @00fe store notrap aligned region3 v27, v26 -;; @0100 jump block15 -;; -;; block15: -;; jump block16 -;; -;; block16: -;; jump block10 -;; -;; block10: -;; @0104 jump block11 -;; -;; block11: -;; @0104 store.i64 notrap aligned region6 v21, v20+136 -;; @0104 store.i32 notrap aligned region11 v22, v20+128 -;; @0104 store.i32 notrap aligned region13 v24, v20+132 -;; @0104 jump block13 -;; -;; block13: -;; @010e store.i32 notrap aligned region3 v7, v6 -;; @0112 store.i32 notrap aligned region3 v12, v11 -;; @0114 jump block5 -;; -;; block5: -;; @0115 jump block2 -;; -;; block3: -;; v55 = load.i64 notrap aligned readonly can_move region5 v0+88 -;; v56 = load.i64 notrap aligned readonly can_move region4 v0+104 -;; @0118 v49 = iconst.i32 49 -;; @011a call_indirect sig0, v55(v56, v0, v49) ; v49 = 49 -;; @011c trap user12 -;; -;; block2: -;; @011e jump block1 -;; -;; block1: -;; v52 = iconst.i32 42 -;; v53 = iadd.i32 v2, v52 ; v52 = 42 -;; @011e return v53 +;; v52 = iconst.i32 1276 +;; @00f0 return v52 ; v52 = 1276 ;; }