// Licensing scope: see ../../LICENSE and this directory's LICENSE and NOTICE.
// Copyright 2023 WebAssembly Community Group participants.
// Retained historical upstream notice; current derivation remains unconfirmed.
// Conservative attribution from pinned Binaryen version_130 counterpart:
// Copyright 2016 WebAssembly Community Group participants
// Counterpart: src/passes/OptimizeInstructions.cpp (5d704ad5); origin remains unconfirmed.
// Potentially incorporated Binaryen expression retains Apache-2.0 terms.
// Original Starshine contributions use the scoped MIT grant in ../../LICENSE.
// Modified for Starshine through 2026: implementation and validation use
// Starshine's representations and APIs; this is not an unchanged upstream file.
// Conservative notice retention does not establish copying or independence.
///|
pub fn optimize_instructions_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new(
"optimize-instructions",
// SSA is intentionally lazy: only stored-scalar equality/default candidates
// need it. Eager SSA would add unrelated work and constrain shared DAG input.
requires=[@ir.HotAnalysis::use_def(), @ir.HotAnalysis::effects()],
invalidates=[
@ir.HotAnalysis::cfg(),
@ir.HotAnalysis::dominance(),
@ir.HotAnalysis::liveness(),
@ir.HotAnalysis::use_def(),
@ir.HotAnalysis::effects(),
@ir.HotAnalysis::loop_info(),
@ir.HotAnalysis::ssa(),
],
)
}
///|
pub fn optimize_instructions_summary() -> String {
"Fold safe exact-instruction peepholes such as shift masks and compare-to-zero patterns."
}
///|
priv struct OptimizeInstructionsLocalFacts {
sign_ext_state_by_local : Array[OiSignExtLocalStateFact]
i32_unsigned_max_state_by_local : Array[OiI32UnsignedMaxLocalStateFact]
i64_unsigned_max_state_by_local : Array[OiI64UnsignedMaxLocalStateFact]
i32_max_by_node : Array[Int32?]
i64_max_by_node : Array[Int64?]
i32_max_ready : @bitset.BitSet
i64_max_ready : @bitset.BitSet
assigned_i32_max_by_node : Array[Int32?]
assigned_i64_max_by_node : Array[Int64?]
assigned_i32_max_ready : @bitset.BitSet
assigned_i64_max_ready : @bitset.BitSet
mut sign_ext_ready : Bool
mut unsigned_max_ready : Bool
mut label_used_revision : Int32
mut label_used : @bitset.BitSet
mut boolean_exit_labels : @bitset.BitSet?
}
///|
priv enum OiSignExtLocalStateKind {
// Binaryen `LocalScanner` keeps a per-local summary that becomes unusable
// after conflicting or unsupported assignments. Keep that state explicit
// instead of encoding unknown/known/conflict as 0/positive/-1 bits.
UnknownSignExtLocalState
KnownSignExtLocalState
ConflictingSignExtLocalState
}
///|
priv struct OiSignExtLocalStateFact {
kind : OiSignExtLocalStateKind
bits : Int32
}
///|
fn OiSignExtLocalStateFact::unknown() -> OiSignExtLocalStateFact {
{ kind: UnknownSignExtLocalState, bits: 0, }
}
///|
fn OiSignExtLocalStateFact::from_assignment_bits(
bits : Int32,
) -> OiSignExtLocalStateFact {
if bits > 0 {
{ kind: KnownSignExtLocalState, bits, }
} else {
{ kind: ConflictingSignExtLocalState, bits: 0, }
}
}
///|
fn OiSignExtLocalStateFact::merge_assignment_bits(
self : OiSignExtLocalStateFact,
bits : Int32,
) -> OiSignExtLocalStateFact {
let candidate = OiSignExtLocalStateFact::from_assignment_bits(bits)
match self.kind {
UnknownSignExtLocalState => candidate
KnownSignExtLocalState =>
match candidate.kind {
KnownSignExtLocalState if self.bits == candidate.bits => self
_ => { kind: ConflictingSignExtLocalState, bits: 0, }
}
ConflictingSignExtLocalState => self
}
}
///|
fn OiSignExtLocalStateFact::final_bits(self : OiSignExtLocalStateFact) -> Int32 {
match self.kind {
KnownSignExtLocalState => self.bits
UnknownSignExtLocalState | ConflictingSignExtLocalState => 0
}
}
///|
fn OiSignExtLocalStateFact::finalized(
self : OiSignExtLocalStateFact,
) -> OiSignExtLocalStateFact {
match self.kind {
KnownSignExtLocalState => self
UnknownSignExtLocalState | ConflictingSignExtLocalState =>
OiSignExtLocalStateFact::unknown()
}
}
///|
priv struct OiI32UnsignedMaxLocalStateFact {
// Local unsigned-max summaries start at zero for known i32 locals, grow to
// the largest proven assigned maximum, and fall back to None for unsupported
// or conflicting assignments instead of storing a -1 sentinel.
max_value : Int32?
}
///|
fn OiI32UnsignedMaxLocalStateFact::unknown() -> OiI32UnsignedMaxLocalStateFact {
{ max_value: None, }
}
///|
fn OiI32UnsignedMaxLocalStateFact::zero() -> OiI32UnsignedMaxLocalStateFact {
{ max_value: Some(0), }
}
///|
fn OiI32UnsignedMaxLocalStateFact::merge_assignment_max(
self : OiI32UnsignedMaxLocalStateFact,
max_value : Int32?,
) -> OiI32UnsignedMaxLocalStateFact {
match (self.max_value, max_value) {
(Some(current), Some(value)) if value >= 0 =>
{ max_value: Some(if value > current { value } else { current }), }
_ => OiI32UnsignedMaxLocalStateFact::unknown()
}
}
///|
priv struct OiI64UnsignedMaxLocalStateFact {
// Mirrors the i32 local unsigned-max state for i64 locals without the old
// -1L sentinel payload.
max_value : Int64?
}
///|
fn OiI64UnsignedMaxLocalStateFact::unknown() -> OiI64UnsignedMaxLocalStateFact {
{ max_value: None, }
}
///|
fn OiI64UnsignedMaxLocalStateFact::zero() -> OiI64UnsignedMaxLocalStateFact {
{ max_value: Some(0L), }
}
///|
fn OiI64UnsignedMaxLocalStateFact::merge_assignment_max(
self : OiI64UnsignedMaxLocalStateFact,
max_value : Int64?,
) -> OiI64UnsignedMaxLocalStateFact {
match (self.max_value, max_value) {
(Some(current), Some(value)) if value >= 0L =>
{ max_value: Some(if value > current { value } else { current }), }
_ => OiI64UnsignedMaxLocalStateFact::unknown()
}
}
///|
priv struct OiLocalAssignmentPayloadFact {
local_id : Int32
value_id : Int32
val_type : @lib.ValType
}
///|
priv struct OiLocalAssignmentFact {
payload : OiLocalAssignmentPayloadFact?
}
///|
fn OptimizeInstructionsLocalFacts::new(
local_count : Int32,
node_count : Int32,
) -> OptimizeInstructionsLocalFacts {
{
sign_ext_state_by_local: Array::make(
local_count,
OiSignExtLocalStateFact::unknown(),
),
i32_unsigned_max_state_by_local: Array::make(
local_count,
OiI32UnsignedMaxLocalStateFact::unknown(),
),
i64_unsigned_max_state_by_local: Array::make(
local_count,
OiI64UnsignedMaxLocalStateFact::unknown(),
),
i32_max_by_node: Array::make(node_count, None),
i64_max_by_node: Array::make(node_count, None),
i32_max_ready: @bitset.BitSet::new(node_count),
i64_max_ready: @bitset.BitSet::new(node_count),
assigned_i32_max_by_node: Array::make(node_count, None),
assigned_i64_max_by_node: Array::make(node_count, None),
assigned_i32_max_ready: @bitset.BitSet::new(node_count),
assigned_i64_max_ready: @bitset.BitSet::new(node_count),
sign_ext_ready: false,
unsigned_max_ready: false,
label_used_revision: -1,
label_used: @bitset.BitSet::new(0),
boolean_exit_labels: None,
}
}
///|
fn OptimizeInstructionsLocalFacts::sign_ext_bits_for_get(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
) -> Int32 {
if local_id >= 0 && local_id < self.sign_ext_state_by_local.length() {
self.sign_ext_state_by_local[local_id].final_bits()
} else {
0
}
}
///|
fn OptimizeInstructionsLocalFacts::merge_sign_ext_bits_for_local(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
bits : Int32,
) -> Unit {
if local_id < 0 || local_id >= self.sign_ext_state_by_local.length() {
return
}
self.sign_ext_state_by_local[local_id] = self.sign_ext_state_by_local[local_id].merge_assignment_bits(
bits,
)
}
///|
fn OptimizeInstructionsLocalFacts::finalize_sign_ext_bits(
self : OptimizeInstructionsLocalFacts,
) -> Unit {
for i = 0; i < self.sign_ext_state_by_local.length(); i = i + 1 {
self.sign_ext_state_by_local[i] = self.sign_ext_state_by_local[i].finalized()
}
}
///|
fn OptimizeInstructionsLocalFacts::i32_unsigned_max_for_get(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
) -> Int32? {
if self.unsigned_max_ready &&
local_id >= 0 &&
local_id < self.i32_unsigned_max_state_by_local.length() {
self.i32_unsigned_max_state_by_local[local_id].max_value
} else {
None
}
}
///|
fn OptimizeInstructionsLocalFacts::i64_unsigned_max_for_get(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
) -> Int64? {
if self.unsigned_max_ready &&
local_id >= 0 &&
local_id < self.i64_unsigned_max_state_by_local.length() {
self.i64_unsigned_max_state_by_local[local_id].max_value
} else {
None
}
}
///|
fn OptimizeInstructionsLocalFacts::seed_i32_unsigned_max_local(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
) -> Unit {
if local_id >= 0 && local_id < self.i32_unsigned_max_state_by_local.length() {
self.i32_unsigned_max_state_by_local[local_id] = OiI32UnsignedMaxLocalStateFact::zero()
}
}
///|
fn OptimizeInstructionsLocalFacts::seed_i64_unsigned_max_local(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
) -> Unit {
if local_id >= 0 && local_id < self.i64_unsigned_max_state_by_local.length() {
self.i64_unsigned_max_state_by_local[local_id] = OiI64UnsignedMaxLocalStateFact::zero()
}
}
///|
fn OptimizeInstructionsLocalFacts::merge_i32_unsigned_max_for_local(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
max_value : Int32?,
) -> Unit {
if local_id < 0 || local_id >= self.i32_unsigned_max_state_by_local.length() {
return
}
self.i32_unsigned_max_state_by_local[local_id] = self.i32_unsigned_max_state_by_local[local_id].merge_assignment_max(
max_value,
)
}
///|
fn OptimizeInstructionsLocalFacts::merge_i64_unsigned_max_for_local(
self : OptimizeInstructionsLocalFacts,
local_id : Int32,
max_value : Int64?,
) -> Unit {
if local_id < 0 || local_id >= self.i64_unsigned_max_state_by_local.length() {
return
}
self.i64_unsigned_max_state_by_local[local_id] = self.i64_unsigned_max_state_by_local[local_id].merge_assignment_max(
max_value,
)
}
///|
fn OiLocalAssignmentFact::empty() -> OiLocalAssignmentFact {
{ payload: None, }
}
///|
fn OiLocalAssignmentFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiLocalAssignmentFact {
// Binaryen's `LocalScanner` records local-set/tee assignments as facts that
// later integer rewrites query, rather than each rewrite owning a separate
// tree scanner. Keep Starshine's represented local integer assignment shape
// explicit and shared by sign-extension and unsigned-max summaries. Unsupported
// shapes carry no payload instead of sentinel local/value ids.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return OiLocalAssignmentFact::empty()
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => ()
_ => return OiLocalAssignmentFact::empty()
}
let local_id = node.imm0
if local_id < @ir.hot_local_param_count(func) || node.child_count != 1 {
return OiLocalAssignmentFact::empty()
}
match @ir.hot_local_type(func, local_id) {
Some(val_type) =>
{
payload: Some({
local_id,
value_id: func.children[node.child_base],
val_type,
}),
}
None => OiLocalAssignmentFact::empty()
}
}
///|
fn OiLocalAssignmentFact::sign_ext_integer_assignment(
self : OiLocalAssignmentFact,
) -> OiLocalAssignmentPayloadFact? {
match self.payload {
Some(payload) if payload.val_type == @lib.ValType::i32() ||
payload.val_type == @lib.ValType::i64() => Some(payload)
_ => None
}
}
///|
fn OiLocalAssignmentFact::i32_assignment(
self : OiLocalAssignmentFact,
) -> OiLocalAssignmentPayloadFact? {
match self.payload {
Some(payload) if payload.val_type == @lib.ValType::i32() => Some(payload)
_ => None
}
}
///|
fn OiLocalAssignmentFact::i64_assignment(
self : OiLocalAssignmentFact,
) -> OiLocalAssignmentPayloadFact? {
match self.payload {
Some(payload) if payload.val_type == @lib.ValType::i64() => Some(payload)
_ => None
}
}
///|
priv enum OiRefNullabilityKind {
UnknownRefNullability
KnownNullRef
KnownNonNullResultType
KnownNonNullLocalType
KnownNonNullRefFunc
KnownNonNullRefI31
}
///|
priv struct OiRefNullabilityFact {
kind : OiRefNullabilityKind
}
///|
fn OiRefNullabilityFact::unknown() -> OiRefNullabilityFact {
{ kind: UnknownRefNullability, }
}
///|
fn OiRefNullabilityFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
ref_type : @lib.RefType?,
) -> OiRefNullabilityFact {
let node = @ir.hot_node_get(func, node_id)
let kind = match node.op {
@ir.HotOp::RefNull => KnownNullRef
_ =>
match ref_type {
Some(ref_type) if ref_type.is_non_nullable() => KnownNonNullResultType
_ =>
match node.op {
@ir.HotOp::LocalGet =>
match @ir.hot_local_type(func, node.imm0) {
Some(@lib.ValType::RefTypeValType(ref_type)) if ref_type.is_non_nullable() =>
KnownNonNullLocalType
_ => UnknownRefNullability
}
@ir.HotOp::RefFunc => KnownNonNullRefFunc
@ir.HotOp::Heap =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefI31) => KnownNonNullRefI31
_ => UnknownRefNullability
}
_ => UnknownRefNullability
}
}
}
{ kind, }
}
///|
fn OiRefNullabilityFact::proves_known_null(self : OiRefNullabilityFact) -> Bool {
match self.kind {
KnownNullRef => true
_ => false
}
}
///|
fn OiRefNullabilityFact::proves_known_non_null(
self : OiRefNullabilityFact,
) -> Bool {
match self.kind {
KnownNonNullResultType
| KnownNonNullLocalType
| KnownNonNullRefFunc
| KnownNonNullRefI31 => true
UnknownRefNullability | KnownNullRef => false
}
}
///|
priv struct OiRefFact {
ref_type : @lib.RefType?
nullability_fact : OiRefNullabilityFact
}
///|
fn OiRefFact::unknown() -> OiRefFact {
{ ref_type: None, nullability_fact: OiRefNullabilityFact::unknown(), }
}
///|
fn OiRefFact::from_node(func : @ir.HotFunc, node_id : Int32) -> OiRefFact {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiRefFact::unknown()
}
let ref_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[@lib.ValType::RefTypeValType(ref_type)] => Some(ref_type)
_ => None
}
{
ref_type,
nullability_fact: OiRefNullabilityFact::from_node(func, node_id, ref_type),
}
}
///|
fn OiRefFact::proves_known_null(self : OiRefFact) -> Bool {
self.nullability_fact.proves_known_null()
}
///|
fn OiRefFact::proves_known_non_null(self : OiRefFact) -> Bool {
self.nullability_fact.proves_known_non_null()
}
///|
fn OiRefFact::proves_type_non_nullable(self : OiRefFact) -> Bool {
match self.ref_type {
Some(ref_type) => ref_type.is_non_nullable()
None => false
}
}
///|
priv enum OiRefIsNullResultKind {
RefIsNullUnknownResult
RefIsNullKnownNonNullOperand
}
///|
priv struct OiRefIsNullResultFact {
kind : OiRefIsNullResultKind
}
///|
fn OiRefIsNullResultFact::from_ref_fact(
operand_fact : OiRefFact,
) -> OiRefIsNullResultFact {
let kind = if operand_fact.proves_known_non_null() {
RefIsNullKnownNonNullOperand
} else {
// Binaryen v130 folds ref.is_null from non-nullability, but does not turn
// known-null value/dataflow facts into a constant in this direct pass.
RefIsNullUnknownResult
}
{ kind, }
}
///|
fn OiRefIsNullResultFact::known_result(
self : OiRefIsNullResultFact,
) -> @lib.I32? {
match self.kind {
RefIsNullKnownNonNullOperand => Some(@lib.I32(0))
RefIsNullUnknownResult => None
}
}
///|
priv enum OiRefIsNullOperandErasureKind {
RefIsNullOperandErasable
RefIsNullOperandPreserveEffectsWithDrop
}
///|
priv struct OiRefIsNullOperandErasureFact {
kind : OiRefIsNullOperandErasureKind
}
///|
fn OiRefIsNullOperandErasureFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
) -> OiRefIsNullOperandErasureFact {
// Binaryen v130 `visitRefIsNull` erases the operand only when the operand's
// effects are pure; otherwise it materializes the known boolean after a drop.
// Keep that as a selected effect-preservation outcome rather than a raw
// purity boolean on the rewrite fact.
let kind = if optimize_instructions_node_effects_are_pure(
ctx, func, operand_id,
) {
RefIsNullOperandErasable
} else {
RefIsNullOperandPreserveEffectsWithDrop
}
{ kind, }
}
///|
fn OiRefIsNullOperandErasureFact::proves_operand_erasable(
self : OiRefIsNullOperandErasureFact,
) -> Bool {
match self.kind {
RefIsNullOperandErasable => true
RefIsNullOperandPreserveEffectsWithDrop => false
}
}
///|
priv struct OiRefIsNullFact {
operand_id : Int32
result_fact : OiRefIsNullResultFact
erasure_fact : OiRefIsNullOperandErasureFact
}
///|
fn OiRefIsNullFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefIsNullFact? {
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
let operand_id = @ir.hot_child_get(func, node_id, 0)
// Binaryen v130 `visitRefIsNull` makes the direct decision from reference
// nullability/source facts, then preserves operand effects with a drop when
// the operand cannot be erased. Keep those as named facts so the visitor does
// not own a one-off ref-null / non-null / purity cascade.
Some({
operand_id,
result_fact: OiRefIsNullResultFact::from_ref_fact(
OiRefFact::from_node(func, operand_id),
),
erasure_fact: OiRefIsNullOperandErasureFact::from_operand(
ctx, func, operand_id,
),
})
}
///|
priv struct OiRefIsNullSkipCastFact {
replacement_operand_id : Int32
}
///|
fn OiRefIsNullSkipCastFact::from_fact(
ctx : HotPassContext,
func : @ir.HotFunc,
fact : OiRefIsNullFact,
) -> OiRefIsNullSkipCastFact? {
// Binaryen v130 `visitRefIsNull` calls `skipCast(curr->value)` only under
// traps-never-happen and only after direct known-null / known-non-null folds.
// Keep the cast-chain traversal as a fact so the visitor and rewrite plan do
// not grow a raw opcode scanner for nullable reference casts.
if !ctx.traps_never_happen {
return None
}
let mut current_id = fact.operand_id
let mut changed = false
while current_id >= 0 && @ir.hot_node_is_live(func, current_id) {
let node = @ir.hot_node_get(func, current_id)
match node.op {
@ir.HotOp::RefCast if node.child_count == 1 => {
current_id = @ir.hot_child_get(func, current_id, 0)
changed = true
continue
}
@ir.HotOp::Heap if node.child_count == 1 =>
match @ir.hot_node_exact_instr(func, current_id) {
Some(@lib.Instruction::RefAsNonNull) => {
current_id = @ir.hot_child_get(func, current_id, 0)
changed = true
continue
}
_ => ()
}
_ => ()
}
break
}
if changed {
Some({ replacement_operand_id: current_id, })
} else {
None
}
}
///|
priv enum OiRefIsNullRewritePlanKind {
RefIsNullRewriteToConst
RefIsNullDropOperandThenConst
RefIsNullSkipCast
}
///|
priv struct OiRefIsNullRewritePlanFact {
kind : OiRefIsNullRewritePlanKind
operand_id : Int32
replacement_operand_id : Int32
const_result : @lib.I32
}
///|
fn OiRefIsNullRewritePlanFact::from_fact(
ctx : HotPassContext,
func : @ir.HotFunc,
fact : OiRefIsNullFact,
) -> OiRefIsNullRewritePlanFact? {
match fact.result_fact.known_result() {
Some(value) =>
Some({
kind: if fact.erasure_fact.proves_operand_erasable() {
RefIsNullRewriteToConst
} else {
RefIsNullDropOperandThenConst
},
operand_id: fact.operand_id,
replacement_operand_id: fact.operand_id,
const_result: value,
})
None =>
match OiRefIsNullSkipCastFact::from_fact(ctx, func, fact) {
Some(skip_fact) =>
Some({
kind: RefIsNullSkipCast,
operand_id: fact.operand_id,
replacement_operand_id: skip_fact.replacement_operand_id,
const_result: @lib.I32(0),
})
None => None
}
}
}
///|
fn OiRefIsNullRewritePlanFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefIsNullRewritePlanFact? {
match OiRefIsNullFact::from_node(ctx, func, node_id) {
Some(fact) => OiRefIsNullRewritePlanFact::from_fact(ctx, func, fact)
None => None
}
}
///|
fn OiRefIsNullRewritePlanFact::replace_node_with_ref_is_null(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
operand_id : Int32,
) -> Unit {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::ref_is_null(),
operand_id,
)
}
///|
priv struct OiSynthesizedRefIsNullFact {
operand_id : Int32
}
///|
fn OiSynthesizedRefIsNullFact::from_operand(
operand_id : Int32,
) -> OiSynthesizedRefIsNullFact {
{ operand_id, }
}
///|
fn OiSynthesizedRefIsNullFact::apply(
self : OiSynthesizedRefIsNullFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
// A producer such as ref.eq may synthesize ref.is_null after the ordinary
// RefIsNull post-order slot has passed. Reuse the regular typed result and
// operand-effect plan immediately instead of admitting downstream shapes.
OiRefIsNullRewritePlanFact::replace_node_with_ref_is_null(
ctx,
func,
node_id,
self.operand_id,
)
match OiRefIsNullRewritePlanFact::from_node(ctx, func, node_id) {
Some(plan) => ignore(plan.apply(ctx, func, node_id))
None => ()
}
true
}
///|
fn OiRefIsNullRewritePlanFact::apply(
self : OiRefIsNullRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
RefIsNullRewriteToConst => {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.const_result,
)
true
}
RefIsNullDropOperandThenConst => {
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
self.operand_id,
self.const_result,
)
true
}
RefIsNullSkipCast => {
OiRefIsNullRewritePlanFact::replace_node_with_ref_is_null(
ctx,
func,
node_id,
self.replacement_operand_id,
)
true
}
}
}
///|
priv enum OiRefLiteralKind {
NoRefLiteral
I31RefLiteral
FuncRefLiteral
}
///|
priv struct OiRefLiteralFact {
kind : OiRefLiteralKind
}
///|
fn OiRefLiteralFact::none() -> OiRefLiteralFact {
{ kind: NoRefLiteral, }
}
///|
fn OiRefLiteralFact::i31() -> OiRefLiteralFact {
{ kind: I31RefLiteral, }
}
///|
fn OiRefLiteralFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefLiteralFact {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiRefLiteralFact::none()
}
let node = @ir.hot_node_get(func, node_id)
let kind = match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefI31) => I31RefLiteral
_ =>
match node.op {
@ir.HotOp::RefFunc => FuncRefLiteral
_ => NoRefLiteral
}
}
{ kind, }
}
///|
priv enum OiRefLiteralTargetKind {
NoRefLiteralTargetProof
RefLiteralMatchesTarget
I31RefLiteralMissesTarget
}
///|
priv struct OiRefLiteralTargetFact {
kind : OiRefLiteralTargetKind
}
///|
fn OiRefLiteralTargetFact::from_literal(
literal_fact : OiRefLiteralFact,
target : @lib.HeapType,
) -> OiRefLiteralTargetFact {
// Binaryen v130 treats direct `ref.func` and `ref.i31` operands as literal
// source facts before cast/test/equality target checks. Keep the target proof
// as its own fact so consumers do not each own a literal-kind-to-heap-target
// decision table.
let kind = match literal_fact.kind {
FuncRefLiteral =>
if OiRefFuncLiteralTargetFact::from_target(target).proves_match_target() {
RefLiteralMatchesTarget
} else {
NoRefLiteralTargetProof
}
I31RefLiteral => {
let target_fact = OiI31HeapTargetFact::from_target(target)
if target_fact.proves_match_target() {
RefLiteralMatchesTarget
} else if target_fact.proves_miss_target() {
I31RefLiteralMissesTarget
} else {
NoRefLiteralTargetProof
}
}
NoRefLiteral => NoRefLiteralTargetProof
}
{ kind, }
}
///|
fn OiRefLiteralTargetFact::proves_match_target(
self : OiRefLiteralTargetFact,
) -> Bool {
match self.kind {
RefLiteralMatchesTarget => true
_ => false
}
}
///|
fn OiRefLiteralTargetFact::proves_i31_miss_target(
self : OiRefLiteralTargetFact,
) -> Bool {
match self.kind {
I31RefLiteralMissesTarget => true
_ => false
}
}
///|
priv enum OiNullTrapScopeKind {
NullTrapOperandTrapsOnNull
NullTrapOperandDoesNotTrapOnNull
}
///|
priv struct OiNullTrapScopeFact {
kind : OiNullTrapScopeKind
}
///|
priv struct OiNullTrapFact {
scope_fact : OiNullTrapScopeFact
operand_nullability_fact : OiRefNullabilityFact
}
///|
priv enum OiNullTrapRewriteKind {
NullTrapNoRewrite
NullTrapReplaceParentWithUnreachable
NullTrapSkipExplicitNonNullCheck
}
///|
priv struct OiNullTrapRewriteFact {
kind : OiNullTrapRewriteKind
}
///|
fn OiNullTrapScopeFact::from_traps_on_null(
caller_scope_traps : Bool,
) -> OiNullTrapScopeFact {
{
kind: if caller_scope_traps {
NullTrapOperandTrapsOnNull
} else {
NullTrapOperandDoesNotTrapOnNull
},
}
}
///|
fn OiNullTrapScopeFact::proves_traps_on_null(
self : OiNullTrapScopeFact,
) -> Bool {
match self.kind {
NullTrapOperandTrapsOnNull => true
NullTrapOperandDoesNotTrapOnNull => false
}
}
///|
fn OiNullTrapFact::for_operand(
func : @ir.HotFunc,
operand_id : Int32,
caller_scope_traps~ : Bool,
) -> OiNullTrapFact {
// Binaryen v130's `trapOnNull` callers decide whether a null operand traps in
// the parent context before asking nullability-specific rewrite questions.
// Keep that caller scope as a selected fact so the rewrite fact is not backed
// by a raw trapping boolean.
let ref_fact = OiRefFact::from_node(func, operand_id)
{
scope_fact: OiNullTrapScopeFact::from_traps_on_null(caller_scope_traps),
operand_nullability_fact: ref_fact.nullability_fact,
}
}
///|
fn OiNullTrapRewriteFact::from_trap_fact(
trap_fact : OiNullTrapFact,
) -> OiNullTrapRewriteFact {
// Binaryen v130 routes both `ref.as_non_null` and descriptor/call_ref
// null-trap rewrites through `trapOnNull`: a definitely null trapping
// operand makes the parent unreachable, while a definitely non-null operand
// can skip the explicit check. Keep that ordered decision as one fact instead
// of exposing two helper-style `can_*` predicates over the same nullability
// proof.
let kind = if trap_fact.scope_fact.proves_traps_on_null() &&
trap_fact.operand_nullability_fact.proves_known_null() {
NullTrapReplaceParentWithUnreachable
} else if trap_fact.operand_nullability_fact.proves_known_non_null() {
NullTrapSkipExplicitNonNullCheck
} else {
NullTrapNoRewrite
}
{ kind, }
}
///|
fn optimize_instructions_region_tail_id(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> Int32? {
let root_count = @ir.hot_region_root_count(func, region_ref)
if root_count == 0 {
return None
}
let tail_id = @ir.hot_region_get(func, region_ref, root_count - 1)
if @ir.hot_node_is_live(func, tail_id) {
Some(tail_id)
} else {
None
}
}
///|
priv enum OiRegionFallthroughExitKind {
RegionMayFallThrough
RegionExplicitUnreachableTail
}
///|
priv struct OiRegionFallthroughExitFact {
kind : OiRegionFallthroughExitKind
tail_id : Int32?
}
///|
fn OiRegionFallthroughExitFact::from_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> OiRegionFallthroughExitFact {
// Binaryen's represented one-sided fallthrough paths treat a region as
// stopping only when the live tail is an explicit `unreachable`. Keep that
// as a source-backed fact instead of a boolean helper shared by unrelated
// descriptor/call_ref/array fallthrough scanners.
match optimize_instructions_region_tail_id(func, region_ref) {
Some(tail_id) =>
match @ir.hot_node_get(func, tail_id).op {
@ir.HotOp::Unreachable =>
{ kind: RegionExplicitUnreachableTail, tail_id: Some(tail_id), }
_ => { kind: RegionMayFallThrough, tail_id: Some(tail_id), }
}
None => { kind: RegionMayFallThrough, tail_id: None, }
}
}
///|
fn OiRegionFallthroughExitFact::proves_explicit_no_fallthrough(
self : OiRegionFallthroughExitFact,
) -> Bool {
ignore(self.tail_id)
match self.kind {
RegionExplicitUnreachableTail => true
RegionMayFallThrough => false
}
}
///|
priv enum OiIfOneSidedFallthroughRegionKind {
IfNoOneSidedFallthroughRegion
IfThenStopsElseFallsThroughRegion
IfElseStopsThenFallsThroughRegion
}
///|
priv struct OiIfOneSidedFallthroughRegionFact {
kind : OiIfOneSidedFallthroughRegionKind
then_exit : OiRegionFallthroughExitFact
else_exit : OiRegionFallthroughExitFact
fallthrough_region : @ir.HotRegionRef?
}
///|
fn OiIfOneSidedFallthroughRegionFact::from_regions(
func : @ir.HotFunc,
then_ref : @ir.HotRegionRef,
else_ref : @ir.HotRegionRef,
) -> OiIfOneSidedFallthroughRegionFact {
// Binaryen v130 `Properties::getImmediateFallthrough` and Starshine's
// represented array-default fallthrough search both descend through an `if`
// only when exactly one arm explicitly stops. Capture the arm-choice as one
// fact so callers do not each carry parallel then/else stop booleans.
let then_exit = OiRegionFallthroughExitFact::from_region(func, then_ref)
let else_exit = OiRegionFallthroughExitFact::from_region(func, else_ref)
let then_stops = then_exit.proves_explicit_no_fallthrough()
let else_stops = else_exit.proves_explicit_no_fallthrough()
let (kind, fallthrough_region) = if then_stops && !else_stops {
(IfThenStopsElseFallsThroughRegion, Some(else_ref))
} else if else_stops && !then_stops {
(IfElseStopsThenFallsThroughRegion, Some(then_ref))
} else {
(IfNoOneSidedFallthroughRegion, None)
}
{ kind, then_exit, else_exit, fallthrough_region, }
}
///|
fn OiIfOneSidedFallthroughRegionFact::proven_fallthrough_region(
self : OiIfOneSidedFallthroughRegionFact,
) -> @ir.HotRegionRef? {
ignore(self.then_exit)
ignore(self.else_exit)
match self.kind {
IfThenStopsElseFallsThroughRegion | IfElseStopsThenFallsThroughRegion =>
self.fallthrough_region
IfNoOneSidedFallthroughRegion => None
}
}
///|
priv enum OiFallthroughTailKind {
FallthroughTailNoProof
FallthroughTailLiveNonBranch
FallthroughTailBrOnReferenceChild
}
///|
priv struct OiFallthroughTailFact {
kind : OiFallthroughTailKind
}
///|
fn OiFallthroughTailFact::empty() -> OiFallthroughTailFact {
{ kind: FallthroughTailNoProof, }
}
///|
fn OiFallthroughTailFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiFallthroughTailFact {
// Binaryen's Properties::getImmediateFallthrough treats br_on_* as letting
// their reference child fall through. Starshine's represented region path
// keeps that selected source outcome as a tail fact so descriptor/ref
// fallthrough typing does not own an opcode helper or parallel live/arity/
// branch booleans.
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiFallthroughTailFact::empty()
}
let child_count = @ir.hot_child_count(func, node_id)
let kind = match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail =>
if child_count > 0 {
FallthroughTailBrOnReferenceChild
} else {
FallthroughTailNoProof
}
_ => FallthroughTailLiveNonBranch
}
{ kind, }
}
///|
fn OiFallthroughTailFact::proves_branch_to_fallthrough_child(
self : OiFallthroughTailFact,
) -> Bool {
match self.kind {
FallthroughTailBrOnReferenceChild => true
FallthroughTailNoProof | FallthroughTailLiveNonBranch => false
}
}
///|
priv enum OiRefFallthroughTailUseKind {
NoRefFallthroughTail
BranchFreeRefFallthroughTail
BrOnReferenceChildRefFallthroughTail
BranchControlBlockedRefFallthroughTail
}
///|
priv struct OiRefFallthroughTailUseFact {
kind : OiRefFallthroughTailUseKind
region_branch_control : OiDescriptorBranchControlRegionFact
prefix_branch_control : OiDescriptorBranchControlRegionFact
tail_fact : OiFallthroughTailFact
}
///|
fn OiRefFallthroughTailUseFact::empty() -> OiRefFallthroughTailUseFact {
{
kind: NoRefFallthroughTail,
region_branch_control: OiDescriptorBranchControlRegionFact::empty(),
prefix_branch_control: OiDescriptorBranchControlRegionFact::empty(),
tail_fact: OiFallthroughTailFact::empty(),
}
}
///|
fn OiRefFallthroughTailUseFact::from_facts(
region_branch_control : OiDescriptorBranchControlRegionFact,
prefix_branch_control : OiDescriptorBranchControlRegionFact,
tail_fact : OiFallthroughTailFact,
) -> OiRefFallthroughTailUseFact {
// Binaryen's Properties::getFallthroughType can use a branch-free tail, or a
// br_on_* reference child when no earlier prefix branch control would escape.
// Keep that region-control outcome explicit instead of carrying parallel
// has-branch / prefix-has-branch / tail-branches booleans on the region fact.
let has_branch_control = region_branch_control.proves_has_branch_control()
let prefix_has_branch_control = prefix_branch_control.proves_has_branch_control()
let tail_branches_to_fallthrough_child = tail_fact.proves_branch_to_fallthrough_child()
let kind = if !has_branch_control {
BranchFreeRefFallthroughTail
} else if tail_branches_to_fallthrough_child && !prefix_has_branch_control {
BrOnReferenceChildRefFallthroughTail
} else {
BranchControlBlockedRefFallthroughTail
}
{ kind, region_branch_control, prefix_branch_control, tail_fact, }
}
///|
fn OiRefFallthroughTailUseFact::proves_usable_tail(
self : OiRefFallthroughTailUseFact,
) -> Bool {
ignore(self.region_branch_control)
ignore(self.prefix_branch_control)
ignore(self.tail_fact)
match self.kind {
BranchFreeRefFallthroughTail | BrOnReferenceChildRefFallthroughTail => true
NoRefFallthroughTail | BranchControlBlockedRefFallthroughTail => false
}
}
///|
priv struct OiRefFallthroughRegionFact {
root_count : Int32
tail_id : Int32?
tail_use : OiRefFallthroughTailUseFact
ref_type : @lib.RefType?
}
///|
fn OiRefFallthroughRegionFact::empty() -> OiRefFallthroughRegionFact {
{
root_count: 0,
tail_id: None,
tail_use: OiRefFallthroughTailUseFact::empty(),
ref_type: None,
}
}
///|
fn OiRefFallthroughRegionFact::from_region(
ctx : HotPassContext,
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> OiRefFallthroughRegionFact {
// Binaryen's Properties::getFallthroughType descends through block/loop/if
// tails, but the represented descriptor path must not hide earlier branch
// control unless the live tail itself is the branch-on-reference fallthrough.
// Carry the region-control result and resulting type as facts instead of
// scattered fallthrough helpers or sentinel tail ids.
let root_count = @ir.hot_region_root_count(func, region_ref)
let tail_id = match optimize_instructions_region_tail_id(func, region_ref) {
Some(tail_id) => tail_id
None => return OiRefFallthroughRegionFact::empty()
}
let tail_use = OiRefFallthroughTailUseFact::from_facts(
OiDescriptorBranchControlRegionFact::from_region(
func,
region_ref.holder,
region_ref,
),
OiDescriptorBranchControlRegionFact::from_region_prefix_excluding_tail(
func,
region_ref.holder,
region_ref,
),
OiFallthroughTailFact::from_node(func, tail_id),
)
let ref_type = if tail_use.proves_usable_tail() {
OiRefFallthroughFact::from_node(ctx, func, tail_id).ref_type
} else {
None
}
{ root_count, tail_id: Some(tail_id), tail_use, ref_type, }
}
///|
fn OiRefFallthroughRegionFact::proven_ref_type(
self : OiRefFallthroughRegionFact,
) -> @lib.RefType? {
ignore(self.root_count)
match self.tail_id {
Some(_) if self.tail_use.proves_usable_tail() => self.ref_type
_ => None
}
}
///|
priv struct OiRefFallthroughFact {
ref_type : @lib.RefType?
}
///|
fn optimize_instructions_node_ref_result_type(
func : @ir.HotFunc,
node_id : Int32,
) -> @lib.RefType? {
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[@lib.ValType::RefTypeValType(ref_type)] => Some(ref_type)
_ => None
}
}
///|
priv enum OiHeapSubtypeKind {
HeapSubtypeSameHeap
HeapSubtypeNoModuleContext
HeapSubtypeKnownModuleSupertype
HeapSubtypeNoKnownModuleSupertype
}
///|
priv struct OiHeapSubtypeFact {
kind : OiHeapSubtypeKind
}
///|
fn OiHeapSubtypeFact::from_heaps(
ctx : HotPassContext,
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiHeapSubtypeFact {
// Binaryen's GC cast checks treat same-heap success separately from the
// module-supertype walk. Keep the missing-module and no-known-supertype
// outcomes explicit so strict-subtype payloads do not depend on one raw
// `known` boolean.
if source == target {
return { kind: HeapSubtypeSameHeap, }
}
let module_ctx = match ctx.module_ctx {
Some(module_ctx) => module_ctx
None => return { kind: HeapSubtypeNoModuleContext, }
}
let kind = if OiHeapSubtypeFact::proves_subtype_with_fuel(
module_ctx, source, target, 64,
) {
HeapSubtypeKnownModuleSupertype
} else {
HeapSubtypeNoKnownModuleSupertype
}
{ kind, }
}
///|
fn optimize_instructions_module_heap_matches(
module_ctx : @ir.HotModuleContext,
source : @lib.HeapType,
target : @lib.HeapType,
) -> Bool {
// Canonical identity includes recursion-group size and member position.
// Comparing only indices or flattened definitions loses that identity.
let env = @validate.Env::{
..@validate.Env::new().with_types(module_ctx.subtypes),
rec_stack: module_ctx.type_groups,
}
@validate.Match::matches(source, target, env)
}
///|
fn OiHeapSubtypeFact::proves_subtype_with_fuel(
module_ctx : @ir.HotModuleContext,
source : @lib.HeapType,
target : @lib.HeapType,
fuel : Int32,
) -> Bool {
if optimize_instructions_module_heap_matches(module_ctx, source, target) {
return true
}
if fuel <= 0 {
return false
}
let source_idx = match source {
@lib.HeapType(source_idx) => source_idx
_ => return false
}
match @ir.hot_module_resolve_subtype(module_ctx, source_idx) {
Some(subtype) => {
for super_idx in subtype.super_types() {
if OiHeapSubtypeFact::proves_subtype_with_fuel(
module_ctx,
@lib.HeapType::new(super_idx),
target,
fuel - 1,
) {
return true
}
}
false
}
None => false
}
}
///|
fn OiHeapSubtypeFact::proves_known_subtype(self : OiHeapSubtypeFact) -> Bool {
match self.kind {
HeapSubtypeSameHeap | HeapSubtypeKnownModuleSupertype => true
HeapSubtypeNoModuleContext | HeapSubtypeNoKnownModuleSupertype => false
}
}
///|
priv enum OiI31HeapTargetKind {
I31HeapTargetNoKnownRelation
I31HeapTargetMatches
I31HeapTargetMisses
}
///|
priv struct OiI31HeapTargetFact {
kind : OiI31HeapTargetKind
}
///|
fn OiI31HeapTargetFact::from_target(
target : @lib.HeapType,
) -> OiI31HeapTargetFact {
// Binaryen's GC cast checks treat i31 as a value under i31/eq/any and
// disjoint from aggregate or concrete heaps. Keep that abstract-heap relation
// as one selected outcome used by ref.test/ref.cast/ref.eq literal and heap
// facts instead of parallel helper-level i31 match/miss predicates.
let kind = match target {
@lib.HeapType::AbsHeapTypeHeapType(abs) =>
if abs == @lib.AbsHeapType::i31() ||
abs == @lib.AbsHeapType::eq() ||
abs == @lib.AbsHeapType::any() {
I31HeapTargetMatches
} else if abs == @lib.AbsHeapType::struct_() ||
abs == @lib.AbsHeapType::array() {
I31HeapTargetMisses
} else {
I31HeapTargetNoKnownRelation
}
@lib.HeapType::SharedAbsHeapTypeHeapType(_)
| @lib.HeapType::HeapType(_)
| @lib.HeapType::DefTypeHeapType(_) => I31HeapTargetMisses
}
{ kind, }
}
///|
fn OiI31HeapTargetFact::proves_match_target(self : OiI31HeapTargetFact) -> Bool {
match self.kind {
I31HeapTargetMatches => true
I31HeapTargetNoKnownRelation | I31HeapTargetMisses => false
}
}
///|
fn OiI31HeapTargetFact::proves_miss_target(self : OiI31HeapTargetFact) -> Bool {
match self.kind {
I31HeapTargetMisses => true
I31HeapTargetNoKnownRelation | I31HeapTargetMatches => false
}
}
///|
priv enum OiRefFuncLiteralTargetKind {
RefFuncLiteralNoTargetMatch
RefFuncLiteralMatchesFuncTarget
}
///|
priv struct OiRefFuncLiteralTargetFact {
kind : OiRefFuncLiteralTargetKind
}
///|
fn OiRefFuncLiteralTargetFact::from_target(
target : @lib.HeapType,
) -> OiRefFuncLiteralTargetFact {
// Preserve the existing direct `ref.func` literal target proof exactly: this
// narrow literal fact only recognizes the concrete func heap. Broader func ->
// any compatibility remains in the module heap-target fact used for typed
// producers. Keep the literal-target relation as a selected source fact
// instead of a direct match boolean.
let kind = match target {
@lib.HeapType::AbsHeapTypeHeapType(abs) if abs == @lib.AbsHeapType::func() =>
RefFuncLiteralMatchesFuncTarget
_ => RefFuncLiteralNoTargetMatch
}
{ kind, }
}
///|
fn OiRefFuncLiteralTargetFact::proves_match_target(
self : OiRefFuncLiteralTargetFact,
) -> Bool {
match self.kind {
RefFuncLiteralMatchesFuncTarget => true
RefFuncLiteralNoTargetMatch => false
}
}
///|
priv enum OiAbsStructArrayHeapRelationKind {
AbsStructArrayHeapNoKnownRelation
AbsStructArrayHeapMatchesTarget
AbsStructArrayHeapDisjointTarget
}
///|
priv struct OiAbsStructArrayHeapRelationFact {
kind : OiAbsStructArrayHeapRelationKind
}
///|
fn OiAbsStructArrayHeapRelationFact::from_heaps(
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiAbsStructArrayHeapRelationFact {
// Binaryen's abstract struct/array lattice facts are separate from full
// module subtype walks: struct/array values flow to eq/any, and struct and
// array are disjoint from one another. Keep the selected lattice outcome in
// one fact instead of parallel match/disjoint booleans.
let kind = match (source, target) {
(
@lib.HeapType::AbsHeapTypeHeapType(source_abs),
@lib.HeapType::AbsHeapTypeHeapType(target_abs),
) => {
let aggregate_source = source_abs == @lib.AbsHeapType::struct_() ||
source_abs == @lib.AbsHeapType::array()
if aggregate_source &&
(
target_abs == @lib.AbsHeapType::eq() ||
target_abs == @lib.AbsHeapType::any()
) {
AbsStructArrayHeapMatchesTarget
} else if (
source_abs == @lib.AbsHeapType::struct_() &&
target_abs == @lib.AbsHeapType::array()
) ||
(
source_abs == @lib.AbsHeapType::array() &&
target_abs == @lib.AbsHeapType::struct_()
) {
AbsStructArrayHeapDisjointTarget
} else {
AbsStructArrayHeapNoKnownRelation
}
}
_ => AbsStructArrayHeapNoKnownRelation
}
{ kind, }
}
///|
fn OiAbsStructArrayHeapRelationFact::proves_match_target(
self : OiAbsStructArrayHeapRelationFact,
) -> Bool {
match self.kind {
AbsStructArrayHeapMatchesTarget => true
AbsStructArrayHeapNoKnownRelation | AbsStructArrayHeapDisjointTarget =>
false
}
}
///|
fn OiAbsStructArrayHeapRelationFact::proves_disjoint_target(
self : OiAbsStructArrayHeapRelationFact,
) -> Bool {
match self.kind {
AbsStructArrayHeapDisjointTarget => true
AbsStructArrayHeapNoKnownRelation | AbsStructArrayHeapMatchesTarget => false
}
}
///|
priv enum OiConcreteCompAbsTargetKind {
ConcreteCompAbsTargetNoMatch
ConcreteCompAbsTargetStructMatchesStructEqAny
ConcreteCompAbsTargetArrayMatchesArrayEqAny
ConcreteCompAbsTargetFuncMatchesFuncAny
}
///|
priv struct OiConcreteCompAbsTargetFact {
kind : OiConcreteCompAbsTargetKind
}
///|
fn OiConcreteCompAbsTargetFact::from_comp_abs(
comp : @lib.CompType,
target : @lib.AbsHeapType,
) -> OiConcreteCompAbsTargetFact {
// Binaryen's GC cast-check heap walk maps a concrete composite source to the
// abstract heap supertypes it can inhabit. Keep the selected concrete-comp
// outcome explicit before module subtype traversal consumes it.
let kind = match comp {
@lib.StructCompType(_) =>
if target == @lib.AbsHeapType::struct_() ||
target == @lib.AbsHeapType::eq() ||
target == @lib.AbsHeapType::any() {
ConcreteCompAbsTargetStructMatchesStructEqAny
} else {
ConcreteCompAbsTargetNoMatch
}
@lib.ArrayCompType(_) =>
if target == @lib.AbsHeapType::array() ||
target == @lib.AbsHeapType::eq() ||
target == @lib.AbsHeapType::any() {
ConcreteCompAbsTargetArrayMatchesArrayEqAny
} else {
ConcreteCompAbsTargetNoMatch
}
@lib.FuncCompType(_, _) =>
if target == @lib.AbsHeapType::func() || target == @lib.AbsHeapType::any() {
ConcreteCompAbsTargetFuncMatchesFuncAny
} else {
ConcreteCompAbsTargetNoMatch
}
@lib.ContCompType(_) => ConcreteCompAbsTargetNoMatch
}
{ kind, }
}
///|
fn OiConcreteCompAbsTargetFact::proves_match_target(
self : OiConcreteCompAbsTargetFact,
) -> Bool {
match self.kind {
ConcreteCompAbsTargetStructMatchesStructEqAny
| ConcreteCompAbsTargetArrayMatchesArrayEqAny
| ConcreteCompAbsTargetFuncMatchesFuncAny => true
ConcreteCompAbsTargetNoMatch => false
}
}
///|
priv enum OiModuleHeapSourceKind {
ModuleHeapSourceSameHeap
ModuleHeapSourceAbstractPair
ModuleHeapSourceConcreteHeap
ModuleHeapSourceUnsupported
}
///|
priv struct OiModuleHeapSourceFact {
kind : OiModuleHeapSourceKind
}
///|
fn OiModuleHeapSourceFact::from_heaps(
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiModuleHeapSourceFact {
let kind = if source == target {
ModuleHeapSourceSameHeap
} else {
match (source, target) {
(
@lib.HeapType::AbsHeapTypeHeapType(_),
@lib.HeapType::AbsHeapTypeHeapType(_),
) => ModuleHeapSourceAbstractPair
(@lib.HeapType::HeapType(_), _) => ModuleHeapSourceConcreteHeap
_ => ModuleHeapSourceUnsupported
}
}
{ kind, }
}
///|
priv enum OiModuleHeapSubtypeTargetKind {
ModuleHeapSubtypeTargetNoMatch
ModuleHeapSubtypeTargetConcreteCompAbs
ModuleHeapSubtypeTargetSupertypeWalk
}
///|
priv struct OiModuleHeapSubtypeTargetFact {
kind : OiModuleHeapSubtypeTargetKind
}
///|
fn OiModuleHeapSubtypeTargetFact::from_subtype(
module_ctx : @ir.HotModuleContext,
subtype : @lib.SubType,
target : @lib.HeapType,
fuel : Int32,
) -> OiModuleHeapSubtypeTargetFact {
if fuel <= 0 {
return { kind: ModuleHeapSubtypeTargetNoMatch, }
}
let (_, supers, comp) = match subtype {
@lib.CompTypeSubType(_, comp) => (false, [], comp)
@lib.SubType(is_final, supers, _, comp) => (is_final, supers, comp)
}
match target {
@lib.HeapType::AbsHeapTypeHeapType(abs) =>
if OiConcreteCompAbsTargetFact::from_comp_abs(comp, abs).proves_match_target() {
return { kind: ModuleHeapSubtypeTargetConcreteCompAbs, }
}
_ => ()
}
for super_idx in supers {
if OiModuleHeapTargetFact::from_heaps(
module_ctx,
@lib.HeapType::new(super_idx),
target,
fuel - 1,
).proves_match_target() {
return { kind: ModuleHeapSubtypeTargetSupertypeWalk, }
}
}
{ kind: ModuleHeapSubtypeTargetNoMatch, }
}
///|
fn OiModuleHeapSubtypeTargetFact::proves_match_target(
self : OiModuleHeapSubtypeTargetFact,
) -> Bool {
match self.kind {
ModuleHeapSubtypeTargetConcreteCompAbs
| ModuleHeapSubtypeTargetSupertypeWalk => true
ModuleHeapSubtypeTargetNoMatch => false
}
}
///|
priv enum OiModuleHeapTargetKind {
ModuleHeapTargetNoKnownMatch
ModuleHeapTargetNoFuel
ModuleHeapTargetSameHeap
ModuleHeapTargetAbstractPair
ModuleHeapTargetConcreteSubtype
}
///|
priv struct OiModuleHeapTargetEvidenceFact {
kind : OiModuleHeapTargetKind
source : OiModuleHeapSourceFact
}
///|
fn OiModuleHeapTargetEvidenceFact::proves_source_classified(
self : OiModuleHeapTargetEvidenceFact,
) -> Bool {
match self.source.kind {
ModuleHeapSourceSameHeap
| ModuleHeapSourceAbstractPair
| ModuleHeapSourceConcreteHeap
| ModuleHeapSourceUnsupported => true
}
}
///|
priv struct OiModuleHeapTargetFact {
evidence : OiModuleHeapTargetEvidenceFact
}
///|
fn OiModuleHeapTargetFact::from_heaps(
module_ctx : @ir.HotModuleContext,
source : @lib.HeapType,
target : @lib.HeapType,
fuel : Int32,
) -> OiModuleHeapTargetFact {
// Source-backed equivalent of Binaryen's heap-target compatibility walk: fast
// abstract-heap lattice facts first, then bounded module subtype traversal.
let source_fact = OiModuleHeapSourceFact::from_heaps(source, target)
if optimize_instructions_module_heap_matches(module_ctx, source, target) {
return {
evidence: { kind: ModuleHeapTargetSameHeap, source: source_fact, },
}
}
if fuel <= 0 {
return { evidence: { kind: ModuleHeapTargetNoFuel, source: source_fact, }, }
}
let kind = match (source, target) {
(
@lib.HeapType::AbsHeapTypeHeapType(source_abs),
@lib.HeapType::AbsHeapTypeHeapType(target_abs),
) =>
if source_abs == target_abs ||
(
source_abs == @lib.AbsHeapType::i31() &&
OiI31HeapTargetFact::from_target(target).proves_match_target()
) ||
OiAbsStructArrayHeapRelationFact::from_heaps(source, target).proves_match_target() ||
(
source_abs == @lib.AbsHeapType::eq() &&
target_abs == @lib.AbsHeapType::any()
) ||
(
source_abs == @lib.AbsHeapType::func() &&
target_abs == @lib.AbsHeapType::any()
) {
ModuleHeapTargetAbstractPair
} else {
ModuleHeapTargetNoKnownMatch
}
(@lib.HeapType::HeapType(source_idx), _) =>
match @ir.hot_module_resolve_subtype(module_ctx, source_idx) {
Some(subtype) =>
if OiModuleHeapSubtypeTargetFact::from_subtype(
module_ctx,
subtype,
target,
fuel - 1,
).proves_match_target() {
ModuleHeapTargetConcreteSubtype
} else {
ModuleHeapTargetNoKnownMatch
}
None => ModuleHeapTargetNoKnownMatch
}
_ => ModuleHeapTargetNoKnownMatch
}
{ evidence: { kind, source: source_fact, }, }
}
///|
fn OiModuleHeapTargetFact::proves_match_target(
self : OiModuleHeapTargetFact,
) -> Bool {
if not(self.evidence.proves_source_classified()) {
return false
}
match self.evidence.kind {
ModuleHeapTargetSameHeap
| ModuleHeapTargetAbstractPair
| ModuleHeapTargetConcreteSubtype => true
ModuleHeapTargetNoKnownMatch | ModuleHeapTargetNoFuel => false
}
}
///|
priv enum OiConcreteSourceTargetOverlapKind {
ConcreteSourceTargetOverlapUnknownNoModule
ConcreteSourceTargetOverlapUnknownNonConcreteSource
ConcreteSourceTargetOverlapKnownSourceTargetMatch
ConcreteSourceTargetOverlapKnownSharedSubtype
ConcreteSourceTargetOverlapNoTargetOverlap
}
///|
priv struct OiConcreteSourceTargetOverlapFact {
kind : OiConcreteSourceTargetOverlapKind
}
///|
fn OiConcreteSourceTargetOverlapFact::from_heaps(
module_ctx : @ir.HotModuleContext?,
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiConcreteSourceTargetOverlapFact {
// Binaryen proves a ref.test/ref.cast miss for concrete sources by checking
// that no declared subtype can simultaneously flow to both source and target.
// Keep the expensive module walk behind an explicit fact rather than a
// caller-owned subtype scanner.
let ctx = match module_ctx {
Some(ctx) => ctx
None => return { kind: ConcreteSourceTargetOverlapUnknownNoModule, }
}
match source {
@lib.HeapType::HeapType(_) => ()
_ => return { kind: ConcreteSourceTargetOverlapUnknownNonConcreteSource, }
}
if OiModuleHeapTargetFact::from_heaps(ctx, source, target, 512).proves_match_target() {
return { kind: ConcreteSourceTargetOverlapKnownSourceTargetMatch, }
}
let subtype_count = @ir.hot_module_subtype_count(ctx)
for i = 0; i < subtype_count; i = i + 1 {
let heap = @lib.HeapType::new(@lib.TypeIdx::new(i.reinterpret_as_uint()))
if OiModuleHeapTargetFact::from_heaps(ctx, heap, source, 512).proves_match_target() &&
OiModuleHeapTargetFact::from_heaps(ctx, heap, target, 512).proves_match_target() {
return { kind: ConcreteSourceTargetOverlapKnownSharedSubtype, }
}
}
{ kind: ConcreteSourceTargetOverlapNoTargetOverlap, }
}
///|
fn OiConcreteSourceTargetOverlapFact::proves_no_target_overlap(
self : OiConcreteSourceTargetOverlapFact,
) -> Bool {
match self.kind {
ConcreteSourceTargetOverlapNoTargetOverlap => true
ConcreteSourceTargetOverlapUnknownNoModule
| ConcreteSourceTargetOverlapUnknownNonConcreteSource
| ConcreteSourceTargetOverlapKnownSourceTargetMatch
| ConcreteSourceTargetOverlapKnownSharedSubtype => false
}
}
///|
priv enum OiFinalHeapKind {
FinalHeapConcreteSubtype
FinalHeapCompTypeSubtype
FinalHeapNonFinalConcreteSubtype
FinalHeapUnresolvedConcreteHeap
FinalHeapNonConcreteHeap
}
///|
priv struct OiFinalHeapFact {
kind : OiFinalHeapKind
}
///|
fn OiFinalHeapFact::from_heap(
module_ctx : @ir.HotModuleContext,
heap : @lib.HeapType,
) -> OiFinalHeapFact {
// Binaryen's GC cast checks use final heap evidence only for module-defined
// concrete heaps whose resolved subtype is final or whose composite type is
// already represented as a concrete comp subtype. Keep the non-final,
// unresolved, and non-concrete cases explicit so exact-target consumers do
// not depend on a raw boolean payload.
let kind = match heap {
@lib.HeapType::HeapType(type_idx) =>
match @ir.hot_module_resolve_subtype(module_ctx, type_idx) {
Some(@lib.CompTypeSubType(_, _)) => FinalHeapCompTypeSubtype
Some(@lib.SubType(true, _, _, _)) => FinalHeapConcreteSubtype
Some(@lib.SubType(false, _, _, _)) => FinalHeapNonFinalConcreteSubtype
None => FinalHeapUnresolvedConcreteHeap
}
_ => FinalHeapNonConcreteHeap
}
{ kind, }
}
///|
fn OiFinalHeapFact::proves_final_heap(self : OiFinalHeapFact) -> Bool {
match self.kind {
FinalHeapConcreteSubtype | FinalHeapCompTypeSubtype => true
FinalHeapNonFinalConcreteSubtype
| FinalHeapUnresolvedConcreteHeap
| FinalHeapNonConcreteHeap => false
}
}
///|
priv enum OiHeapRelationMatchKind {
HeapRelationNoKnownMatch
HeapRelationSameHeap
HeapRelationI31MatchesTarget
HeapRelationAbsStructArrayMatchesTarget
HeapRelationModuleHeapMatchesTarget
}
///|
priv struct OiHeapRelationMatchFact {
kind : OiHeapRelationMatchKind
}
///|
fn OiHeapRelationMatchFact::from_heaps(
ctx : HotPassContext,
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiHeapRelationMatchFact {
// Binaryen's GC cast utilities distinguish cheap exact/abstract target proofs
// from the module-subtyping walk. Preserve that order here so downstream facts
// can ask whether a match was known without full module subtyping.
let kind = if source == target {
HeapRelationSameHeap
} else if source == @lib.HeapType::abs(@lib.AbsHeapType::i31()) &&
OiI31HeapTargetFact::from_target(target).proves_match_target() {
HeapRelationI31MatchesTarget
} else if OiAbsStructArrayHeapRelationFact::from_heaps(source, target).proves_match_target() {
HeapRelationAbsStructArrayMatchesTarget
} else {
match ctx.module_ctx {
Some(module_ctx) if OiModuleHeapTargetFact::from_heaps(
module_ctx, source, target, 512,
).proves_match_target() => HeapRelationModuleHeapMatchesTarget
_ => HeapRelationNoKnownMatch
}
}
{ kind, }
}
///|
fn OiHeapRelationMatchFact::proves_known_match_without_full_subtyping(
self : OiHeapRelationMatchFact,
) -> Bool {
match self.kind {
HeapRelationSameHeap
| HeapRelationI31MatchesTarget
| HeapRelationAbsStructArrayMatchesTarget => true
_ => false
}
}
///|
fn OiHeapRelationMatchFact::proves_known_match_target(
self : OiHeapRelationMatchFact,
) -> Bool {
match self.kind {
HeapRelationNoKnownMatch => false
_ => true
}
}
///|
fn OiHeapRelationMatchFact::proves_module_heap_match(
self : OiHeapRelationMatchFact,
) -> Bool {
match self.kind {
HeapRelationModuleHeapMatchesTarget => true
_ => false
}
}
///|
priv enum OiHeapRelationMissKind {
HeapRelationNoKnownMiss
HeapRelationI31CannotMatchTarget
HeapRelationBottomTargetCannotMatchSource
HeapRelationAbsStructArrayDisjoint
HeapRelationConcreteSourceNoTargetOverlap
}
///|
priv struct OiHeapRelationMissFact {
kind : OiHeapRelationMissKind
}
///|
fn optimize_instructions_heap_is_bottom(heap : @lib.HeapType) -> Bool {
heap == @lib.HeapType::abs(@lib.AbsHeapType::none()) ||
heap == @lib.HeapType::abs(@lib.AbsHeapType::no_extern()) ||
heap == @lib.HeapType::abs(@lib.AbsHeapType::no_func()) ||
heap == @lib.HeapType::abs(@lib.AbsHeapType::no_exn()) ||
heap == @lib.HeapType::abs(@lib.AbsHeapType::no_cont()) ||
heap == @lib.HeapType::shared_abs(@lib.AbsHeapType::none()) ||
heap == @lib.HeapType::shared_abs(@lib.AbsHeapType::no_extern()) ||
heap == @lib.HeapType::shared_abs(@lib.AbsHeapType::no_func()) ||
heap == @lib.HeapType::shared_abs(@lib.AbsHeapType::no_exn()) ||
heap == @lib.HeapType::shared_abs(@lib.AbsHeapType::no_cont())
}
///|
fn OiHeapRelationMissFact::from_heaps(
ctx : HotPassContext,
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiHeapRelationMissFact {
// Binaryen's impossible-cast checks are a separate relation from successful
// target matching. Keep the miss reason explicit so ref.test/ref.cast/ref.eq
// consumers do not own parallel match/miss boolean fields.
let kind = if source == @lib.HeapType::abs(@lib.AbsHeapType::i31()) &&
OiI31HeapTargetFact::from_target(target).proves_miss_target() {
HeapRelationI31CannotMatchTarget
} else if source != target && optimize_instructions_heap_is_bottom(target) {
HeapRelationBottomTargetCannotMatchSource
} else if OiAbsStructArrayHeapRelationFact::from_heaps(source, target).proves_disjoint_target() {
HeapRelationAbsStructArrayDisjoint
} else if OiConcreteSourceTargetOverlapFact::from_heaps(
ctx.module_ctx,
source,
target,
).proves_no_target_overlap() {
HeapRelationConcreteSourceNoTargetOverlap
} else {
HeapRelationNoKnownMiss
}
{ kind, }
}
///|
fn OiHeapRelationMissFact::proves_known_miss_target(
self : OiHeapRelationMissFact,
) -> Bool {
match self.kind {
HeapRelationNoKnownMiss => false
HeapRelationI31CannotMatchTarget
| HeapRelationBottomTargetCannotMatchSource
| HeapRelationAbsStructArrayDisjoint
| HeapRelationConcreteSourceNoTargetOverlap => true
}
}
///|
fn OiHeapRelationMissFact::proves_abs_struct_array_disjoint(
self : OiHeapRelationMissFact,
) -> Bool {
match self.kind {
HeapRelationAbsStructArrayDisjoint => true
HeapRelationNoKnownMiss
| HeapRelationI31CannotMatchTarget
| HeapRelationBottomTargetCannotMatchSource
| HeapRelationConcreteSourceNoTargetOverlap => false
}
}
///|
priv enum OiHeapIdentityKind {
HeapIdentityDifferentOrUnknown
HeapIdentitySame
}
///|
priv struct OiHeapIdentityFact {
kind : OiHeapIdentityKind
}
///|
fn OiHeapIdentityFact::from_heaps(
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiHeapIdentityFact {
// Binaryen's GC cast check first observes exact heap equality before asking
// broader subtype or overlap questions. Keep that source-order identity
// outcome explicit instead of storing another raw relation boolean.
{
kind: if source == target {
HeapIdentitySame
} else {
HeapIdentityDifferentOrUnknown
},
}
}
///|
fn OiHeapIdentityFact::proves_same_heap(self : OiHeapIdentityFact) -> Bool {
match self.kind {
HeapIdentitySame => true
HeapIdentityDifferentOrUnknown => false
}
}
///|
priv enum OiConcreteHeapPairKind {
ConcreteHeapPairNoProof
ConcreteHeapPairBothConcrete
}
///|
priv struct OiConcreteHeapPairFact {
kind : OiConcreteHeapPairKind
}
///|
fn OiConcreteHeapPairFact::from_heaps(
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiConcreteHeapPairFact {
// Strict concrete-subtype success and exact-target miss facts are only valid
// for two module-defined concrete heaps. Represent that relation as a named
// fact rather than a parallel boolean on the aggregate heap relation.
let kind = match (source, target) {
(@lib.HeapType::HeapType(_), @lib.HeapType::HeapType(_)) =>
ConcreteHeapPairBothConcrete
_ => ConcreteHeapPairNoProof
}
{ kind, }
}
///|
fn OiConcreteHeapPairFact::proves_both_concrete_heaps(
self : OiConcreteHeapPairFact,
) -> Bool {
match self.kind {
ConcreteHeapPairBothConcrete => true
ConcreteHeapPairNoProof => false
}
}
///|
priv struct OiHeapRelationFact {
match_fact : OiHeapRelationMatchFact
miss_fact : OiHeapRelationMissFact
identity_fact : OiHeapIdentityFact
subtype_fact : OiHeapSubtypeFact
concrete_pair_fact : OiConcreteHeapPairFact
}
///|
fn OiHeapRelationFact::from_heaps(
ctx : HotPassContext,
source : @lib.HeapType,
target : @lib.HeapType,
) -> OiHeapRelationFact {
{
match_fact: OiHeapRelationMatchFact::from_heaps(ctx, source, target),
miss_fact: OiHeapRelationMissFact::from_heaps(ctx, source, target),
identity_fact: OiHeapIdentityFact::from_heaps(source, target),
subtype_fact: OiHeapSubtypeFact::from_heaps(ctx, source, target),
concrete_pair_fact: OiConcreteHeapPairFact::from_heaps(source, target),
}
}
///|
fn OiHeapRelationFact::known_match_without_full_subtyping(
self : OiHeapRelationFact,
) -> Bool {
self.match_fact.proves_known_match_without_full_subtyping()
}
///|
fn OiHeapRelationFact::known_match_target(self : OiHeapRelationFact) -> Bool {
self.match_fact.proves_known_match_target()
}
///|
fn OiHeapRelationFact::known_miss_target(self : OiHeapRelationFact) -> Bool {
self.miss_fact.proves_known_miss_target()
}
///|
fn OiHeapRelationFact::known_abs_struct_array_disjoint(
self : OiHeapRelationFact,
) -> Bool {
self.miss_fact.proves_abs_struct_array_disjoint()
}
///|
fn OiHeapRelationFact::proves_same_heap(self : OiHeapRelationFact) -> Bool {
self.identity_fact.proves_same_heap()
}
///|
fn OiHeapRelationFact::proves_known_subtype(self : OiHeapRelationFact) -> Bool {
self.subtype_fact.proves_known_subtype()
}
///|
fn OiHeapRelationFact::proves_both_concrete_heaps(
self : OiHeapRelationFact,
) -> Bool {
self.concrete_pair_fact.proves_both_concrete_heaps()
}
///|
priv enum OiHeapSubtypeTargetPayloadKind {
UnknownHeapSubtypeTargetPayload
SameHeapSubtypeTargetPayload
StrictConcreteSubtypeTargetPayload
}
///|
priv struct OiHeapSubtypeTargetNullabilityFact {
nullable : Bool
}
///|
priv struct OiHeapSubtypeTargetExactnessFact {
exact : Bool
}
///|
priv struct OiHeapSubtypeTargetPayloadFact {
kind : OiHeapSubtypeTargetPayloadKind
nullability_fact : OiHeapSubtypeTargetNullabilityFact
exactness_fact : OiHeapSubtypeTargetExactnessFact
}
///|
fn OiHeapSubtypeTargetPayloadFact::from_heap_relation(
relation : OiHeapRelationFact,
source_nullable : Bool,
source_exact : Bool,
) -> OiHeapSubtypeTargetPayloadFact {
// Binaryen's descriptor/ref.test producer relation carries one successful
// heap-compatibility payload: either the source is the target heap, or a
// strict concrete subtype flows to the target. Keep the exactness adjustment
// (`strict subtype` cannot prove exact target instances) in this explicit
// fact instead of a relation helper returning tuple sentinels.
if relation.proves_same_heap() {
{
kind: SameHeapSubtypeTargetPayload,
nullability_fact: { nullable: source_nullable, },
exactness_fact: { exact: source_exact, },
}
} else if relation.proves_both_concrete_heaps() &&
!relation.proves_same_heap() &&
relation.proves_known_subtype() {
{
kind: StrictConcreteSubtypeTargetPayload,
nullability_fact: { nullable: source_nullable, },
exactness_fact: { exact: false, },
}
} else {
{
kind: UnknownHeapSubtypeTargetPayload,
nullability_fact: { nullable: false, },
exactness_fact: { exact: false, },
}
}
}
///|
fn OiHeapSubtypeTargetPayloadFact::proves_known_payload(
self : OiHeapSubtypeTargetPayloadFact,
) -> Bool {
match self.kind {
SameHeapSubtypeTargetPayload | StrictConcreteSubtypeTargetPayload => true
UnknownHeapSubtypeTargetPayload => false
}
}
///|
fn OiHeapSubtypeTargetPayloadFact::proven_nullable(
self : OiHeapSubtypeTargetPayloadFact,
) -> Bool {
self.nullability_fact.nullable
}
///|
fn OiHeapSubtypeTargetPayloadFact::proven_exact(
self : OiHeapSubtypeTargetPayloadFact,
) -> Bool {
self.exactness_fact.exact
}
///|
priv enum OiHeapStrictConcreteTargetMatchKind {
UnknownStrictConcreteTargetMatch
StrictConcreteSubtypeModuleTargetMatch
}
///|
priv struct OiHeapStrictConcreteTargetMatchEvidenceFact {
relation : OiHeapRelationFact
}
///|
priv struct OiHeapStrictConcreteTargetMatchFact {
kind : OiHeapStrictConcreteTargetMatchKind
evidence_fact : OiHeapStrictConcreteTargetMatchEvidenceFact?
}
///|
fn OiHeapStrictConcreteTargetMatchFact::from_heap_relation(
relation : OiHeapRelationFact,
) -> OiHeapStrictConcreteTargetMatchFact {
// Source-backed split for Binaryen's exact-target miss proof: a strict
// concrete subtype that also module-matches the target cannot be an exact
// instance of the target heap. Keep the relation evidence nested here instead
// of collapsing those proofs into a direct boolean payload.
if relation.proves_both_concrete_heaps() &&
!relation.proves_same_heap() &&
relation.match_fact.proves_module_heap_match() {
{
kind: StrictConcreteSubtypeModuleTargetMatch,
evidence_fact: Some({ relation, }),
}
} else {
{ kind: UnknownStrictConcreteTargetMatch, evidence_fact: None, }
}
}
///|
fn OiHeapStrictConcreteTargetMatchFact::proves_target_match(
self : OiHeapStrictConcreteTargetMatchFact,
) -> Bool {
match (self.kind, self.evidence_fact) {
(StrictConcreteSubtypeModuleTargetMatch, Some(evidence)) =>
evidence.relation.proves_both_concrete_heaps() &&
!evidence.relation.proves_same_heap() &&
evidence.relation.match_fact.proves_module_heap_match()
_ => false
}
}
///|
priv enum OiHeapNullableExactOnlyNullTargetKind {
UnknownNullableExactOnlyNullTargetMatch
NullableExactOnlyNullTargetMatch
}
///|
priv struct OiHeapNullableExactOnlyNullSourceNullabilityFact {
nullable : Bool
}
///|
priv struct OiHeapNullableExactOnlyNullSourceExactnessFact {
exact : Bool
}
///|
priv struct OiHeapNullableExactOnlyNullTargetNullabilityFact {
nullable : Bool
}
///|
priv struct OiHeapNullableExactOnlyNullTargetEvidenceFact {
relation : OiHeapRelationFact
source_nullability_fact : OiHeapNullableExactOnlyNullSourceNullabilityFact
source_exactness_fact : OiHeapNullableExactOnlyNullSourceExactnessFact
target_nullability_fact : OiHeapNullableExactOnlyNullTargetNullabilityFact
}
///|
priv struct OiHeapNullableExactOnlyNullTargetFact {
kind : OiHeapNullableExactOnlyNullTargetKind
evidence_fact : OiHeapNullableExactOnlyNullTargetEvidenceFact?
}
///|
fn OiHeapNullableExactOnlyNullTargetFact::from_heap_relation(
relation : OiHeapRelationFact,
source_nullable : Bool,
source_exact : Bool,
target_nullable : Bool,
) -> OiHeapNullableExactOnlyNullTargetFact {
// Binaryen's nullable-exact check can only fold to a null test when the
// nullable source is a different concrete heap from a nullable exact target;
// an exact source or module target match proves any successful cast must be
// the null case. Keep those source, target, and heap-relation payloads nested
// instead of collapsing the proof to a direct boolean.
let evidence_fact = {
relation,
source_nullability_fact: { nullable: source_nullable, },
source_exactness_fact: { exact: source_exact, },
target_nullability_fact: { nullable: target_nullable, },
}
if target_nullable &&
source_nullable &&
relation.proves_both_concrete_heaps() &&
!relation.proves_same_heap() &&
(source_exact || relation.match_fact.proves_module_heap_match()) {
{
kind: NullableExactOnlyNullTargetMatch,
evidence_fact: Some(evidence_fact),
}
} else {
{ kind: UnknownNullableExactOnlyNullTargetMatch, evidence_fact: None, }
}
}
///|
fn OiHeapNullableExactOnlyNullTargetFact::proves_target_match(
self : OiHeapNullableExactOnlyNullTargetFact,
) -> Bool {
match (self.kind, self.evidence_fact) {
(NullableExactOnlyNullTargetMatch, Some(evidence)) =>
evidence.target_nullability_fact.nullable &&
evidence.source_nullability_fact.nullable &&
evidence.relation.proves_both_concrete_heaps() &&
!evidence.relation.proves_same_heap() &&
(
evidence.source_exactness_fact.exact ||
evidence.relation.match_fact.proves_module_heap_match()
)
_ => false
}
}
///|
fn optimize_instructions_ref_fallthrough_lower_bound(
ctx : HotPassContext,
outer : @lib.RefType,
inner : @lib.RefType,
) -> @lib.RefType? {
let outer_heap = outer.get_heap_type()
let inner_heap = inner.get_heap_type()
let nullable = outer.is_nullable() && inner.is_nullable()
if inner_heap == outer_heap {
return Some(
@lib.RefType::new(
nullable,
inner_heap,
exact=outer.is_exact() || inner.is_exact(),
),
)
}
if outer.is_exact() || inner.is_exact() {
return None
}
if OiHeapRelationFact::from_heaps(ctx, inner_heap, outer_heap).known_match_target() {
return Some(@lib.RefType::new(nullable, inner_heap, exact=inner.is_exact()))
}
if OiHeapRelationFact::from_heaps(ctx, outer_heap, inner_heap).known_match_target() {
return Some(@lib.RefType::new(nullable, outer_heap, exact=outer.is_exact()))
}
None
}
///|
fn OiRefFallthroughFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefFallthroughFact {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return { ref_type: None, }
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::RefCast if node.child_count == 1 => {
let child_id = @ir.hot_child_get(func, node_id, 0)
match
(
optimize_instructions_node_ref_result_type(func, node_id),
OiRefFallthroughFact::from_node(ctx, func, child_id).ref_type,
) {
(Some(outer), Some(inner)) =>
match
optimize_instructions_ref_fallthrough_lower_bound(ctx, outer, inner) {
Some(ref_type) => return { ref_type: Some(ref_type), }
None => ()
}
_ => ()
}
}
@ir.HotOp::Heap if node.child_count == 1 =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull) => {
let child_id = @ir.hot_child_get(func, node_id, 0)
match
(
optimize_instructions_node_ref_result_type(func, node_id),
OiRefFallthroughFact::from_node(ctx, func, child_id).ref_type,
) {
(Some(outer), Some(inner)) =>
match
optimize_instructions_ref_fallthrough_lower_bound(
ctx, outer, inner,
) {
Some(ref_type) => return { ref_type: Some(ref_type), }
None => ()
}
_ => ()
}
}
_ => ()
}
@ir.HotOp::LocalTee if node.child_count == 1 => {
let child_id = @ir.hot_child_get(func, node_id, 0)
match OiRefFallthroughFact::from_node(ctx, func, child_id).ref_type {
Some(ref_type) => return { ref_type: Some(ref_type), }
None => ()
}
}
@ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail if node.child_count > 0 => {
let child_id = @ir.hot_child_get(func, node_id, node.child_count - 1)
match
(
optimize_instructions_node_ref_result_type(func, node_id),
OiRefFallthroughFact::from_node(ctx, func, child_id).ref_type,
) {
(Some(outer), Some(inner)) =>
match
optimize_instructions_ref_fallthrough_lower_bound(ctx, outer, inner) {
Some(ref_type) => return { ref_type: Some(ref_type), }
None => ()
}
_ => ()
}
}
@ir.HotOp::Block | @ir.HotOp::Loop => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
match
OiRefFallthroughRegionFact::from_region(ctx, func, body_ref).proven_ref_type() {
Some(ref_type) => return { ref_type: Some(ref_type), }
None => ()
}
}
@ir.HotOp::If =>
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) => {
let then_ref = @ir.hot_region_then_ref(func, node_id)
let one_sided_fact = OiIfOneSidedFallthroughRegionFact::from_regions(
func, then_ref, else_ref,
)
match one_sided_fact.proven_fallthrough_region() {
Some(region_ref) =>
match
OiRefFallthroughRegionFact::from_region(ctx, func, region_ref).proven_ref_type() {
Some(ref_type) => return { ref_type: Some(ref_type), }
None => ()
}
None => ()
}
}
None => ()
}
_ => ()
}
{ ref_type: optimize_instructions_node_ref_result_type(func, node_id), }
}
///|
priv struct OiRefProducerFact {
ref_type : @lib.RefType?
}
///|
fn OiRefProducerFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefProducerFact {
let node = @ir.hot_node_get(func, node_id)
let ref_type = match node.op {
@ir.HotOp::Call if node.imm0 >= 0 =>
match ctx.module_ctx {
Some(module_ctx) =>
match
@ir.hot_module_resolve_func_type(
module_ctx,
@lib.FuncIdx::new(node.imm0.reinterpret_as_uint()),
) {
Some(@lib.FuncType(_, [@lib.ValType::RefTypeValType(ref_type)])) =>
Some(ref_type)
_ => None
}
None => None
}
@ir.HotOp::CallIndirect | @ir.HotOp::CallRef =>
match ctx.module_ctx {
Some(module_ctx) => {
let sig = @ir.hot_call_sig_get(func, node.imm0)
match
@ir.hot_module_resolve_type_func_type(module_ctx, sig.type_idx) {
Some(@lib.FuncType(_, [@lib.ValType::RefTypeValType(ref_type)])) =>
Some(ref_type)
_ => None
}
}
None => None
}
_ => OiRefFallthroughFact::from_node(ctx, func, node_id).ref_type
}
{ ref_type, }
}
///|
priv enum OiRefProducerSubtypeRelationKind {
UnknownRefProducerSubtypeRelation
MatchesOrStrictSubtypesRefProducerTargetHeap
}
///|
priv struct OiRefProducerSubtypeNullabilityFact {
nullable : Bool
}
///|
priv struct OiRefProducerSubtypeExactnessFact {
exact : Bool
}
///|
priv struct OiRefProducerSubtypeRelationPayloadFact {
nullability_fact : OiRefProducerSubtypeNullabilityFact
exactness_fact : OiRefProducerSubtypeExactnessFact
}
///|
priv struct OiRefProducerSubtypeRelationFact {
kind : OiRefProducerSubtypeRelationKind
payload : OiRefProducerSubtypeRelationPayloadFact?
}
///|
fn OiRefProducerSubtypeRelationFact::unknown() -> OiRefProducerSubtypeRelationFact {
{ kind: UnknownRefProducerSubtypeRelation, payload: None, }
}
///|
fn OiRefProducerSubtypeRelationFact::from_heap_relation(
relation : OiHeapRelationFact,
source_nullable : Bool,
source_exact : Bool,
) -> OiRefProducerSubtypeRelationFact {
// Binaryen v130 descriptor tests and descriptor equality casts share the
// `matches || strict-subtype` producer-target outcome from
// `GCTypeUtils::evaluateCastCheck`. Keep that outcome as one kind with a
// tightened nullable/exact payload fact instead of parallel proof booleans on
// the broader producer relation.
let payload_fact = OiHeapSubtypeTargetPayloadFact::from_heap_relation(
relation, source_nullable, source_exact,
)
if payload_fact.proves_known_payload() {
{
kind: MatchesOrStrictSubtypesRefProducerTargetHeap,
payload: Some({
nullability_fact: { nullable: payload_fact.proven_nullable(), },
exactness_fact: { exact: payload_fact.proven_exact(), },
}),
}
} else {
OiRefProducerSubtypeRelationFact::unknown()
}
}
///|
fn OiRefProducerSubtypeRelationFact::proves_matches_heap(
self : OiRefProducerSubtypeRelationFact,
) -> Bool {
match (self.kind, self.payload) {
(MatchesOrStrictSubtypesRefProducerTargetHeap, Some(_)) => true
_ => false
}
}
///|
fn OiRefProducerSubtypeRelationFact::proves_nullable(
self : OiRefProducerSubtypeRelationFact,
) -> Bool {
match self.payload {
Some(payload) => payload.nullability_fact.nullable
None => false
}
}
///|
fn OiRefProducerSubtypeRelationFact::proves_exact(
self : OiRefProducerSubtypeRelationFact,
) -> Bool {
match self.payload {
Some(payload) => payload.exactness_fact.exact
None => false
}
}
///|
priv enum OiRefTargetIdentityKind {
RefTargetNullableExactTargetTypeIdentity
RefTargetExactHeapIdentity
RefTargetExactNonNullHeapIdentity
RefTargetFinalHeapIdentity
}
///|
priv struct OiRefTargetIdentityProofFact {
kind : OiRefTargetIdentityKind
}
///|
fn OiRefTargetIdentityProofFact::when_proven(
kind : OiRefTargetIdentityKind,
proven : Bool,
) -> OiRefTargetIdentityProofFact? {
if proven {
Some({ kind, })
} else {
None
}
}
///|
fn OiRefTargetIdentityProofFact::proves_kind(
self : OiRefTargetIdentityProofFact,
kind : OiRefTargetIdentityKind,
) -> Bool {
match (self.kind, kind) {
(
RefTargetNullableExactTargetTypeIdentity,
RefTargetNullableExactTargetTypeIdentity,
) => true
(RefTargetExactHeapIdentity, RefTargetExactHeapIdentity) => true
(RefTargetExactNonNullHeapIdentity, RefTargetExactNonNullHeapIdentity) =>
true
(RefTargetFinalHeapIdentity, RefTargetFinalHeapIdentity) => true
_ => false
}
}
///|
priv struct OiRefProducerTargetIdentityFact {
exact_heap_fact : OiRefTargetIdentityProofFact?
exact_non_null_heap_fact : OiRefTargetIdentityProofFact?
final_heap_fact : OiRefTargetIdentityProofFact?
}
///|
priv enum OiRefTargetMatchKind {
RefTargetNullableMatch
RefTargetNonNullMatch
RefTargetExactTestMatch
RefTargetFinalExactTestMatch
RefTargetNullableExactOnlyNullMatch
}
///|
priv struct OiRefTargetMatchProofFact {
kind : OiRefTargetMatchKind
}
///|
fn OiRefTargetMatchProofFact::when_proven(
kind : OiRefTargetMatchKind,
proven : Bool,
) -> OiRefTargetMatchProofFact? {
if proven {
Some({ kind, })
} else {
None
}
}
///|
fn OiRefTargetMatchProofFact::proves_kind(
self : OiRefTargetMatchProofFact,
kind : OiRefTargetMatchKind,
) -> Bool {
match (self.kind, kind) {
(RefTargetNullableMatch, RefTargetNullableMatch) => true
(RefTargetNonNullMatch, RefTargetNonNullMatch) => true
(RefTargetExactTestMatch, RefTargetExactTestMatch) => true
(RefTargetFinalExactTestMatch, RefTargetFinalExactTestMatch) => true
(RefTargetNullableExactOnlyNullMatch, RefTargetNullableExactOnlyNullMatch) =>
true
_ => false
}
}
///|
priv struct OiRefProducerTargetMatchFact {
nullable_match_fact : OiRefTargetMatchProofFact?
non_null_match_fact : OiRefTargetMatchProofFact?
exact_test_match_fact : OiRefTargetMatchProofFact?
final_exact_test_match_fact : OiRefTargetMatchProofFact?
nullable_exact_only_null_fact : OiRefTargetMatchProofFact?
}
///|
priv enum OiRefTargetMissKind {
RefTargetMissesTarget
RefTargetMissesExactTarget
RefTargetMissesExactTargetByStrictSubtype
RefTargetNullOrMissForNonNullTarget
}
///|
priv struct OiRefTargetMissProofFact {
kind : OiRefTargetMissKind
}
///|
fn OiRefTargetMissProofFact::when_proven(
kind : OiRefTargetMissKind,
proven : Bool,
) -> OiRefTargetMissProofFact? {
if proven {
Some({ kind, })
} else {
None
}
}
///|
fn OiRefTargetMissProofFact::proves_kind(
self : OiRefTargetMissProofFact,
kind : OiRefTargetMissKind,
) -> Bool {
match (self.kind, kind) {
(RefTargetMissesTarget, RefTargetMissesTarget) => true
(RefTargetMissesExactTarget, RefTargetMissesExactTarget) => true
(
RefTargetMissesExactTargetByStrictSubtype,
RefTargetMissesExactTargetByStrictSubtype,
) => true
(RefTargetNullOrMissForNonNullTarget, RefTargetNullOrMissForNonNullTarget) =>
true
_ => false
}
}
///|
priv struct OiRefProducerTargetMissFact {
target_miss_fact : OiRefTargetMissProofFact?
exact_target_miss_fact : OiRefTargetMissProofFact?
strict_subtype_exact_target_miss_fact : OiRefTargetMissProofFact?
}
///|
priv struct OiRefProducerTargetRelationFact {
identity_fact : OiRefProducerTargetIdentityFact
match_fact : OiRefProducerTargetMatchFact
miss_fact : OiRefProducerTargetMissFact
subtype_relation : OiRefProducerSubtypeRelationFact
}
///|
fn OiRefProducerTargetRelationFact::from_producer(
ctx : HotPassContext,
producer_fact : OiRefProducerFact,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiRefProducerTargetRelationFact {
// Binaryen v130's call, call_ref, and fallthrough reference producers feed the
// same source/target relation table as ordinary source types. Keep the
// producer-derived target proof as an explicit fact so descriptor and ref.test
// consumers no longer re-run ad hoc match/miss/exact helper predicates.
match producer_fact.ref_type {
Some(ref_type) => {
let source = ref_type.get_heap_type()
let relation = OiHeapRelationFact::from_heaps(ctx, source, target)
let subtype_relation = OiRefProducerSubtypeRelationFact::from_heap_relation(
relation,
ref_type.is_nullable(),
ref_type.is_exact(),
)
let nullable_exact_only_null_fact = OiHeapNullableExactOnlyNullTargetFact::from_heap_relation(
relation,
ref_type.is_nullable(),
ref_type.is_exact(),
target_nullable,
)
let strict_concrete_target_match_fact = OiHeapStrictConcreteTargetMatchFact::from_heap_relation(
relation,
)
let final_heap = match ctx.module_ctx {
Some(module_ctx) =>
source == target &&
OiFinalHeapFact::from_heap(module_ctx, target).proves_final_heap()
None => false
}
{
identity_fact: {
exact_heap_fact: OiRefTargetIdentityProofFact::when_proven(
RefTargetExactHeapIdentity,
ref_type.is_exact() && source == target,
),
exact_non_null_heap_fact: OiRefTargetIdentityProofFact::when_proven(
RefTargetExactNonNullHeapIdentity,
ref_type.is_non_nullable() &&
ref_type.is_exact() &&
source == target,
),
final_heap_fact: OiRefTargetIdentityProofFact::when_proven(
RefTargetFinalHeapIdentity,
final_heap,
),
},
match_fact: {
nullable_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableMatch,
ref_type.is_nullable() && relation.known_match_target(),
),
non_null_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNonNullMatch,
ref_type.is_non_nullable() && relation.known_match_target(),
),
exact_test_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetExactTestMatch,
ref_type.is_exact() &&
source == target &&
(target_nullable || ref_type.is_non_nullable()),
),
final_exact_test_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetFinalExactTestMatch,
(target_nullable || ref_type.is_non_nullable()) && final_heap,
),
nullable_exact_only_null_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableExactOnlyNullMatch,
nullable_exact_only_null_fact.proves_target_match(),
),
},
miss_fact: {
target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesTarget,
relation.known_miss_target() &&
(ref_type.is_non_nullable() || !target_nullable),
),
exact_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesExactTarget,
ref_type.is_exact() &&
source != target &&
(ref_type.is_non_nullable() || !target_nullable),
),
strict_subtype_exact_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesExactTargetByStrictSubtype,
(ref_type.is_non_nullable() || !target_nullable) &&
strict_concrete_target_match_fact.proves_target_match(),
),
},
subtype_relation,
}
}
None =>
{
identity_fact: {
exact_heap_fact: None,
exact_non_null_heap_fact: None,
final_heap_fact: None,
},
match_fact: {
nullable_match_fact: None,
non_null_match_fact: None,
exact_test_match_fact: None,
final_exact_test_match_fact: None,
nullable_exact_only_null_fact: None,
},
miss_fact: {
target_miss_fact: None,
exact_target_miss_fact: None,
strict_subtype_exact_target_miss_fact: None,
},
subtype_relation: OiRefProducerSubtypeRelationFact::unknown(),
}
}
}
///|
fn OiRefProducerTargetRelationFact::proves_exact_heap_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.identity_fact.exact_heap_fact {
Some(fact) => fact.proves_kind(RefTargetExactHeapIdentity)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_exact_non_null_heap_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.identity_fact.exact_non_null_heap_fact {
Some(fact) => fact.proves_kind(RefTargetExactNonNullHeapIdentity)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_final_heap_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.identity_fact.final_heap_fact {
Some(fact) => fact.proves_kind(RefTargetFinalHeapIdentity)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_nullable_matches_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.match_fact.nullable_match_fact {
Some(fact) => fact.proves_kind(RefTargetNullableMatch)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_non_null_matches_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.match_fact.non_null_match_fact {
Some(fact) => fact.proves_kind(RefTargetNonNullMatch)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_matches_exact_test_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.match_fact.exact_test_match_fact {
Some(fact) => fact.proves_kind(RefTargetExactTestMatch)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_matches_final_exact_test_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.match_fact.final_exact_test_match_fact {
Some(fact) => fact.proves_kind(RefTargetFinalExactTestMatch)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_nullable_exact_target_only_as_null(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.match_fact.nullable_exact_only_null_fact {
Some(fact) => fact.proves_kind(RefTargetNullableExactOnlyNullMatch)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_misses_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.miss_fact.target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesTarget)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_misses_exact_target(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.miss_fact.exact_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesExactTarget)
None => false
}
}
///|
fn OiRefProducerTargetRelationFact::proves_misses_exact_target_by_strict_subtype(
self : OiRefProducerTargetRelationFact,
) -> Bool {
match self.miss_fact.strict_subtype_exact_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesExactTargetByStrictSubtype)
None => false
}
}
///|
priv struct OiDescriptorTypeFact {
exact_described_heap : @lib.HeapType?
}
///|
fn OiDescriptorTypeFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorTypeFact {
let module_ctx = match ctx.module_ctx {
Some(module_ctx) => module_ctx
None => return { exact_described_heap: None, }
}
let descriptor_idx = match
OiRefProducerFact::from_node(ctx, func, node_id).ref_type {
Some(ref_type) if ref_type.is_non_nullable() && ref_type.is_exact() =>
match ref_type.get_heap_type() {
@lib.HeapType(descriptor_idx) => descriptor_idx
_ => return { exact_described_heap: None, }
}
_ => return { exact_described_heap: None, }
}
let exact_described_heap = match
@ir.hot_module_resolve_subtype(module_ctx, descriptor_idx) {
Some(subtype) =>
match subtype.describes_type() {
Some(described_idx) => Some(@lib.HeapType::new(described_idx))
None => None
}
None => None
}
{ exact_described_heap, }
}
///|
fn OiDescriptorTypeFact::exactly_describes(
self : OiDescriptorTypeFact,
heap : @lib.HeapType,
) -> Bool {
match self.exact_described_heap {
Some(described_heap) => described_heap == heap
None => false
}
}
///|
priv struct OiDescriptorOperandFact {
type_fact : OiDescriptorTypeFact
trap_fact : OiDescriptorOperandTrapFact
erase_fact : OiDescriptorOperandEraseFact
skip_localization_fact : OiDescriptorOperandSkipLocalizationFact
exactness_drop_fact : OiDescriptorExactnessDropFact
}
///|
priv enum OiDescriptorExactnessDropKind {
DescriptorExactnessPreserved
DescriptorExactnessDropEscapingRefGetDesc
}
///|
priv struct OiDescriptorExactnessDropEscapingRefGetDescFact {
get_desc_fact : OiRefGetDescFact
branch_control_fact : OiDescriptorBranchControlFact
}
///|
fn OiDescriptorExactnessDropEscapingRefGetDescFact::proves_may_drop_exactness(
self : OiDescriptorExactnessDropEscapingRefGetDescFact,
) -> Bool {
self.get_desc_fact.child_id >= 0 &&
self.branch_control_fact.proves_has_escaping_branch_control()
}
///|
priv struct OiDescriptorExactnessDropFact {
kind : OiDescriptorExactnessDropKind
escaping_ref_get_desc_fact : OiDescriptorExactnessDropEscapingRefGetDescFact?
}
///|
fn OiDescriptorExactnessDropFact::from_operand(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorExactnessDropFact {
// Binaryen's descriptor-cast TNH skip may drop exactness when the descriptor
// operand is a ref.get_desc whose preserved children can escape through
// branch control. Keep that guard as a nested fact over the descriptor operand
// and the shared branch-control summary instead of helper-level predicates or
// parallel booleans.
let escaping_ref_get_desc_fact = match
OiRefGetDescFact::from_node(func, node_id) {
Some(get_desc_fact) => {
let branch_control_fact = OiDescriptorBranchControlFact::from_node(
func, node_id, node_id,
)
if branch_control_fact.proves_has_escaping_branch_control() {
Some({ get_desc_fact, branch_control_fact, })
} else {
None
}
}
None => None
}
let kind = match escaping_ref_get_desc_fact {
Some(_) => DescriptorExactnessDropEscapingRefGetDesc
None => DescriptorExactnessPreserved
}
{ kind, escaping_ref_get_desc_fact, }
}
///|
fn OiDescriptorExactnessDropFact::proves_may_drop_exactness(
self : OiDescriptorExactnessDropFact,
) -> Bool {
match (self.kind, self.escaping_ref_get_desc_fact) {
(DescriptorExactnessDropEscapingRefGetDesc, Some(fact)) =>
fact.proves_may_drop_exactness()
_ => false
}
}
///|
fn OiDescriptorOperandFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDescriptorOperandFact {
{
type_fact: OiDescriptorTypeFact::from_node(ctx, func, node_id),
trap_fact: OiDescriptorOperandTrapFact::from_operand(func, node_id),
erase_fact: OiDescriptorOperandEraseFact::from_operand(
ctx, func, dead, node_id,
),
skip_localization_fact: OiDescriptorOperandSkipLocalizationFact::from_operand(
func, node_id,
),
exactness_drop_fact: OiDescriptorExactnessDropFact::from_operand(
func, node_id,
),
}
}
///|
fn OiDescriptorOperandFact::proves_traps_without_effects(
self : OiDescriptorOperandFact,
) -> Bool {
self.trap_fact.proves_traps_without_effects()
}
///|
fn OiDescriptorOperandFact::proves_erases_without_effects(
self : OiDescriptorOperandFact,
) -> Bool {
self.erase_fact.proves_erases_without_effects()
}
///|
fn OiDescriptorOperandFact::proves_should_localize_when_skipped(
self : OiDescriptorOperandFact,
) -> Bool {
self.skip_localization_fact.proves_should_localize_when_skipped()
}
///|
fn OiDescriptorOperandFact::proves_may_drop_exactness_for_tnh_escaping_ref_get_desc(
self : OiDescriptorOperandFact,
) -> Bool {
self.exactness_drop_fact.proves_may_drop_exactness()
}
///|
priv enum OiRefGetDescTrapKind {
RefGetDescTrapNone
RefGetDescTrapChildUnreachable
RefGetDescTrapExplicitKnownNullChild
}
///|
priv struct OiRefGetDescExplicitKnownNullChildFact {
child_id : Int32
}
///|
fn OiRefGetDescExplicitKnownNullChildFact::proves_valid_payload(
self : OiRefGetDescExplicitKnownNullChildFact,
) -> Bool {
self.child_id >= 0
}
///|
priv struct OiRefGetDescTrapFact {
kind : OiRefGetDescTrapKind
explicit_known_null_child_fact : OiRefGetDescExplicitKnownNullChildFact?
}
///|
fn OiRefGetDescTrapFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefGetDescTrapFact {
// Binaryen v130 treats `ref.get_desc(unreachable)` and
// `ref.get_desc(ref.as_non_null(ref.null ...))` as descriptor operands that
// can trap before any side effect. Record the selected trap proof separately
// from the descriptor operand fact so callers do not ask `OiRefGetDescFact`
// for an ad hoc helper-level decision.
let get_desc_fact = match OiRefGetDescFact::from_node(func, node_id) {
Some(fact) => fact
None =>
return { kind: RefGetDescTrapNone, explicit_known_null_child_fact: None, }
}
if get_desc_fact.child_is_unreachable(func) {
return {
kind: RefGetDescTrapChildUnreachable,
explicit_known_null_child_fact: None,
}
}
match get_desc_fact.explicit_nullable_child() {
Some(nullable_child) =>
match @ir.hot_node_get(func, nullable_child).op {
@ir.HotOp::RefNull =>
{
kind: RefGetDescTrapExplicitKnownNullChild,
explicit_known_null_child_fact: Some({ child_id: nullable_child, }),
}
_ => { kind: RefGetDescTrapNone, explicit_known_null_child_fact: None, }
}
None => { kind: RefGetDescTrapNone, explicit_known_null_child_fact: None, }
}
}
///|
fn OiRefGetDescTrapFact::proves_traps_without_effects(
self : OiRefGetDescTrapFact,
) -> Bool {
match (self.kind, self.explicit_known_null_child_fact) {
(RefGetDescTrapChildUnreachable, _) => true
(RefGetDescTrapExplicitKnownNullChild, Some(fact)) =>
fact.proves_valid_payload()
_ => false
}
}
///|
priv enum OiDescriptorOperandTrapKind {
DescriptorOperandTrapNone
DescriptorOperandTrapDirectUnreachable
DescriptorOperandTrapRefGetDesc
}
///|
priv struct OiDescriptorOperandDirectUnreachableTrapFact {
node_id : Int32
}
///|
fn OiDescriptorOperandDirectUnreachableTrapFact::proves_valid_payload(
self : OiDescriptorOperandDirectUnreachableTrapFact,
) -> Bool {
self.node_id >= 0
}
///|
priv struct OiDescriptorOperandTrapFact {
kind : OiDescriptorOperandTrapKind
direct_unreachable_fact : OiDescriptorOperandDirectUnreachableTrapFact?
ref_get_desc_trap_fact : OiRefGetDescTrapFact?
}
///|
fn OiDescriptorOperandTrapFact::from_operand(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorOperandTrapFact {
// Descriptor casts/tests can become `unreachable` when the descriptor operand
// itself is already an unconditional trap with no preceding side effects.
// Lower the represented direct-unreachable and ref.get_desc trap cases through
// one ordered result kind plus nested source proof facts instead of a
// descriptor-operand helper cascade or parallel trap booleans.
let direct_unreachable_fact : OiDescriptorOperandDirectUnreachableTrapFact? = match
@ir.hot_node_get(func, node_id).op {
@ir.HotOp::Unreachable => Some({ node_id, })
_ => None
}
let ref_get_desc_trap_probe = OiRefGetDescTrapFact::from_node(func, node_id)
let ref_get_desc_trap_fact = if ref_get_desc_trap_probe.proves_traps_without_effects() {
Some(ref_get_desc_trap_probe)
} else {
None
}
let kind = match (direct_unreachable_fact, ref_get_desc_trap_fact) {
(Some(_), _) => DescriptorOperandTrapDirectUnreachable
(None, Some(_)) => DescriptorOperandTrapRefGetDesc
_ => DescriptorOperandTrapNone
}
{ kind, direct_unreachable_fact, ref_get_desc_trap_fact, }
}
///|
fn OiDescriptorOperandTrapFact::proves_traps_without_effects(
self : OiDescriptorOperandTrapFact,
) -> Bool {
match (self.kind, self.direct_unreachable_fact, self.ref_get_desc_trap_fact) {
(DescriptorOperandTrapDirectUnreachable, Some(fact), _) =>
fact.proves_valid_payload()
(DescriptorOperandTrapRefGetDesc, None, Some(fact)) =>
fact.proves_traps_without_effects()
_ => false
}
}
///|
priv enum OiDescriptorOperandSkipLocalizationKind {
DescriptorOperandSkipNoLocalization
DescriptorOperandSkipCallRoot
DescriptorOperandSkipCallRefRoot
DescriptorOperandSkipRefGetDescRoot
}
///|
priv struct OiDescriptorOperandSkipLocalizationFact {
kind : OiDescriptorOperandSkipLocalizationKind
}
///|
fn OiDescriptorOperandSkipLocalizationFact::from_operand(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorOperandSkipLocalizationFact {
// Binaryen's descriptor TNH skip path uses ChildLocalizer-like decisions to
// preserve descriptor operand evaluation when the descriptor is a call-like or
// ref.get_desc producer. Keep Starshine's represented producer subset as an
// explicit result kind so skip/replacement code does not own an opcode
// whitelist or parallel boolean sentinels.
let node = @ir.hot_node_get(func, node_id)
let kind = match node.op {
@ir.HotOp::Call => DescriptorOperandSkipCallRoot
@ir.HotOp::CallRef => DescriptorOperandSkipCallRefRoot
_ =>
match OiRefGetDescFact::from_node(func, node_id) {
Some(_) => DescriptorOperandSkipRefGetDescRoot
None => DescriptorOperandSkipNoLocalization
}
}
{ kind, }
}
///|
fn OiDescriptorOperandSkipLocalizationFact::proves_should_localize_when_skipped(
self : OiDescriptorOperandSkipLocalizationFact,
) -> Bool {
match self.kind {
DescriptorOperandSkipNoLocalization => false
DescriptorOperandSkipCallRoot => true
DescriptorOperandSkipCallRefRoot => true
DescriptorOperandSkipRefGetDescRoot => true
}
}
///|
priv enum OiRefTestStyleKind {
RefTestOrdinaryStyle
RefTestDescriptorStyle
}
///|
priv struct OiRefTestStyleFact {
kind : OiRefTestStyleKind
}
///|
fn OiRefTestStyleFact::ordinary() -> OiRefTestStyleFact {
{ kind: RefTestOrdinaryStyle, }
}
///|
fn OiRefTestStyleFact::descriptor() -> OiRefTestStyleFact {
{ kind: RefTestDescriptorStyle, }
}
///|
fn OiRefTestStyleFact::proves_descriptor_style(
self : OiRefTestStyleFact,
) -> Bool {
match self.kind {
RefTestOrdinaryStyle => false
RefTestDescriptorStyle => true
}
}
///|
priv struct OiRefTestFact {
nullable : Bool
exact : Bool
target : @lib.HeapType
operand_id : Int32
style_fact : OiRefTestStyleFact
}
///|
fn OiRefTestFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefTestFact? {
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
let (nullable, exact, target, style_fact) = match
@ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefTest(nullable, exact, target)) =>
(nullable, exact, target, OiRefTestStyleFact::ordinary())
Some(@lib.Instruction::RefTestDesc(nullable, target)) =>
(nullable, false, target, OiRefTestStyleFact::descriptor())
_ => return None
}
Some({
nullable,
exact,
target,
operand_id: @ir.hot_child_get(func, node_id, 0),
style_fact,
})
}
///|
fn OiRefTestFact::known_null_result(self : OiRefTestFact) -> @lib.I32 {
ignore(self.style_fact)
if self.nullable {
@lib.I32(1)
} else {
@lib.I32(0)
}
}
///|
priv struct OiRefTestTighteningFact {
narrowed_ref_type : @lib.RefType
}
///|
priv struct OiOrdinaryRefTestTighteningFact {
narrowed_ref_type : @lib.RefType
}
///|
priv struct OiDescriptorRefTestTighteningFact {
boundary_only : Bool
}
///|
fn OiOrdinaryRefTestTighteningFact::from_test(
ctx : HotPassContext,
func : @ir.HotFunc,
test_fact : OiRefTestFact,
) -> OiOrdinaryRefTestTighteningFact? {
// Binaryen v130 `visitRefTest` first intersects the ordinary test cast type
// with `getFallthroughType(ref)` using `Type::getGreatestLowerBound`, then
// evaluates the cast check. Keep the represented ordinary-test GLB/nullability
// rule as a fact over the test target and operand fallthrough type instead of
// an opcode-local immediate scanner.
let current_ref_type = @lib.RefType::new(
test_fact.nullable,
test_fact.target,
exact=test_fact.exact,
)
let fallthrough_ref_type = match
OiRefFallthroughFact::from_node(ctx, func, test_fact.operand_id).ref_type {
Some(ref_type) => ref_type
None => return None
}
match
optimize_instructions_ref_fallthrough_lower_bound(
ctx, current_ref_type, fallthrough_ref_type,
) {
Some(narrowed_ref_type) if narrowed_ref_type != current_ref_type =>
Some({ narrowed_ref_type, })
_ => None
}
}
///|
fn OiDescriptorRefTestTighteningFact::from_test(
test_fact : OiRefTestFact,
) -> OiDescriptorRefTestTighteningFact? {
// Binaryen applies the same `visitRefTest` fallthrough/GLB phase to descriptor
// tests, but Starshine cannot yet construct a validated descriptor-test GLB
// fixture for same-shape or bottom-target cases. Preserve the boundary as a
// named fact so descriptor tightening can be reopened here with valid fixtures
// instead of living as an implicit style escape in the ordinary ref-test path.
if test_fact.style_fact.proves_descriptor_style() {
Some({ boundary_only: true, })
} else {
None
}
}
///|
fn OiDescriptorRefTestTighteningFact::proves_boundary_only(
self : OiDescriptorRefTestTighteningFact,
) -> Bool {
self.boundary_only
}
///|
fn OiRefTestTighteningFact::from_test(
ctx : HotPassContext,
func : @ir.HotFunc,
test_fact : OiRefTestFact,
) -> OiRefTestTighteningFact? {
if test_fact.style_fact.proves_descriptor_style() {
match OiDescriptorRefTestTighteningFact::from_test(test_fact) {
Some(fact) => ignore(fact.proves_boundary_only())
None => ()
}
return None
}
match OiOrdinaryRefTestTighteningFact::from_test(ctx, func, test_fact) {
Some(fact) => Some({ narrowed_ref_type: fact.narrowed_ref_type, })
None => None
}
}
///|
priv struct OiRefTestOperandFact {
ref_fact : OiRefFact
producer_fact : OiRefProducerFact
literal_fact : OiRefLiteralFact
source_type : @lib.RefType?
}
///|
fn OiRefTestOperandFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
) -> OiRefTestOperandFact {
let ref_fact = OiRefFact::from_node(func, operand_id)
let producer_fact = OiRefProducerFact::from_node(ctx, func, operand_id)
{
ref_fact,
producer_fact,
literal_fact: OiRefLiteralFact::from_node(func, operand_id),
source_type: ref_fact.ref_type,
}
}
///|
fn OiRefTestOperandFact::proves_known_null(self : OiRefTestOperandFact) -> Bool {
self.ref_fact.proves_known_null()
}
///|
priv struct OiRefTestSourceTargetIdentityFact {
exact_heap_fact : OiRefTargetIdentityProofFact?
final_heap_fact : OiRefTargetIdentityProofFact?
}
///|
priv struct OiRefTestSourceTargetMatchFact {
nullable_match_fact : OiRefTargetMatchProofFact?
non_null_match_fact : OiRefTargetMatchProofFact?
exact_test_match_fact : OiRefTargetMatchProofFact?
final_exact_test_match_fact : OiRefTargetMatchProofFact?
nullable_exact_only_null_fact : OiRefTargetMatchProofFact?
}
///|
priv struct OiRefTestSourceTargetMissFact {
target_miss_fact : OiRefTargetMissProofFact?
non_null_target_miss_fact : OiRefTargetMissProofFact?
exact_target_miss_fact : OiRefTargetMissProofFact?
strict_subtype_exact_target_miss_fact : OiRefTargetMissProofFact?
}
///|
priv struct OiRefTestSourceTargetRelationFact {
identity_fact : OiRefTestSourceTargetIdentityFact
match_fact : OiRefTestSourceTargetMatchFact
miss_fact : OiRefTestSourceTargetMissFact
}
///|
fn OiRefTestSourceTargetRelationFact::from_operand(
ctx : HotPassContext,
operand_fact : OiRefTestOperandFact,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiRefTestSourceTargetRelationFact {
// Binaryen v130 `GCTypeUtils::evaluateCastCheck` first reasons about the
// operand's static source type and direct literals before it combines those
// proofs with fallthrough producer facts. Keep that source/target relation as
// its own fact so `OiRefTestOperandFact` remains discovery-only.
let literal_target = OiRefLiteralTargetFact::from_literal(
operand_fact.literal_fact,
target,
)
match operand_fact.source_type {
Some(ref_type) => {
let source = ref_type.get_heap_type()
let relation = OiHeapRelationFact::from_heaps(ctx, source, target)
let nullable_exact_only_null_fact = OiHeapNullableExactOnlyNullTargetFact::from_heap_relation(
relation,
ref_type.is_nullable(),
ref_type.is_exact(),
target_nullable,
)
let strict_concrete_target_match_fact = OiHeapStrictConcreteTargetMatchFact::from_heap_relation(
relation,
)
let final_heap = match ctx.module_ctx {
Some(module_ctx) =>
source == target &&
OiFinalHeapFact::from_heap(module_ctx, target).proves_final_heap()
None => false
}
{
identity_fact: {
exact_heap_fact: OiRefTargetIdentityProofFact::when_proven(
RefTargetExactHeapIdentity,
ref_type.is_exact() && source == target,
),
final_heap_fact: OiRefTargetIdentityProofFact::when_proven(
RefTargetFinalHeapIdentity,
final_heap,
),
},
match_fact: {
nullable_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableMatch,
ref_type.is_nullable() &&
relation.known_match_without_full_subtyping(),
),
non_null_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNonNullMatch,
literal_target.proves_match_target() ||
(
ref_type.is_non_nullable() &&
relation.known_match_without_full_subtyping()
),
),
exact_test_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetExactTestMatch,
ref_type.is_exact() &&
source == target &&
(target_nullable || ref_type.is_non_nullable()),
),
final_exact_test_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetFinalExactTestMatch,
(target_nullable || ref_type.is_non_nullable()) && final_heap,
),
nullable_exact_only_null_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableExactOnlyNullMatch,
nullable_exact_only_null_fact.proves_target_match(),
),
},
miss_fact: {
target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesTarget,
literal_target.proves_i31_miss_target() ||
(ref_type.is_non_nullable() && relation.known_miss_target()),
),
non_null_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetNullOrMissForNonNullTarget,
relation.known_miss_target(),
),
exact_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesExactTarget,
ref_type.is_exact() &&
source != target &&
(ref_type.is_non_nullable() || !target_nullable),
),
strict_subtype_exact_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesExactTargetByStrictSubtype,
(ref_type.is_non_nullable() || !target_nullable) &&
strict_concrete_target_match_fact.proves_target_match(),
),
},
}
}
None =>
{
identity_fact: { exact_heap_fact: None, final_heap_fact: None, },
match_fact: {
nullable_match_fact: None,
non_null_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNonNullMatch,
literal_target.proves_match_target(),
),
exact_test_match_fact: None,
final_exact_test_match_fact: None,
nullable_exact_only_null_fact: None,
},
miss_fact: {
target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesTarget,
literal_target.proves_i31_miss_target(),
),
non_null_target_miss_fact: None,
exact_target_miss_fact: None,
strict_subtype_exact_target_miss_fact: None,
},
}
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_exact_heap_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.identity_fact.exact_heap_fact {
Some(fact) => fact.proves_kind(RefTargetExactHeapIdentity)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_final_heap_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.identity_fact.final_heap_fact {
Some(fact) => fact.proves_kind(RefTargetFinalHeapIdentity)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_nullable_matches_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.match_fact.nullable_match_fact {
Some(fact) => fact.proves_kind(RefTargetNullableMatch)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_non_null_matches_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.match_fact.non_null_match_fact {
Some(fact) => fact.proves_kind(RefTargetNonNullMatch)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_matches_exact_test_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.match_fact.exact_test_match_fact {
Some(fact) => fact.proves_kind(RefTargetExactTestMatch)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_matches_final_exact_test_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.match_fact.final_exact_test_match_fact {
Some(fact) => fact.proves_kind(RefTargetFinalExactTestMatch)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_nullable_exact_target_only_as_null(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.match_fact.nullable_exact_only_null_fact {
Some(fact) => fact.proves_kind(RefTargetNullableExactOnlyNullMatch)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_misses_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesTarget)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_null_or_miss_for_non_null_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.non_null_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetNullOrMissForNonNullTarget)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_misses_exact_target(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.exact_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesExactTarget)
None => false
}
}
///|
fn OiRefTestSourceTargetRelationFact::proves_misses_exact_target_by_strict_subtype(
self : OiRefTestSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.strict_subtype_exact_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesExactTargetByStrictSubtype)
None => false
}
}
///|
priv struct OiRefTestOperandTargetRelationFact {
source_relation : OiRefTestSourceTargetRelationFact
producer_relation : OiRefProducerTargetRelationFact
}
///|
fn OiRefTestOperandTargetRelationFact::from_operand(
ctx : HotPassContext,
operand_fact : OiRefTestOperandFact,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiRefTestOperandTargetRelationFact {
// Binaryen v130 ordinary `ref.test` lowers `GCTypeUtils::evaluateCastCheck`
// source/target outcomes into Success, Failure, SuccessOnlyIfNull, and
// SuccessOnlyIfNonNull. Keep Starshine's direct source-type and fallthrough
// producer proofs in one relation fact before result-kind planning instead of
// leaving the evaluation table to recompute parallel predicate cascades.
{
source_relation: OiRefTestSourceTargetRelationFact::from_operand(
ctx, operand_fact, target, target_nullable,
),
producer_relation: OiRefProducerTargetRelationFact::from_producer(
ctx,
operand_fact.producer_fact,
target,
target_nullable,
),
}
}
///|
fn OiRefTestOperandTargetRelationFact::proves_success_only_if_non_null(
self : OiRefTestOperandTargetRelationFact,
nullable : Bool,
exact : Bool,
) -> Bool {
not(nullable) &&
(
not(exact) ||
self.source_relation.proves_exact_heap_target() ||
self.source_relation.proves_final_heap_target() ||
self.producer_relation.proves_exact_heap_target() ||
self.producer_relation.proves_final_heap_target()
) &&
(
self.source_relation.proves_nullable_matches_target() ||
self.producer_relation.proves_nullable_matches_target()
)
}
///|
fn OiRefTestOperandTargetRelationFact::proves_success_only_if_null(
self : OiRefTestOperandTargetRelationFact,
exact : Bool,
) -> Bool {
exact &&
(
self.source_relation.proves_nullable_exact_target_only_as_null() ||
self.producer_relation.proves_nullable_exact_target_only_as_null()
)
}
///|
fn OiRefTestOperandTargetRelationFact::proves_known_success(
self : OiRefTestOperandTargetRelationFact,
nullable : Bool,
exact : Bool,
) -> Bool {
(
not(exact) &&
(
self.source_relation.proves_non_null_matches_target() ||
self.producer_relation.proves_non_null_matches_target()
)
) ||
(
exact &&
(
self.source_relation.proves_matches_exact_test_target() ||
self.source_relation.proves_matches_final_exact_test_target() ||
self.producer_relation.proves_matches_exact_test_target() ||
self.producer_relation.proves_matches_final_exact_test_target()
)
) ||
(
not(exact) &&
nullable &&
(
self.source_relation.proves_nullable_matches_target() ||
self.producer_relation.proves_nullable_matches_target()
)
)
}
///|
fn OiRefTestOperandTargetRelationFact::proves_known_failure(
self : OiRefTestOperandTargetRelationFact,
nullable : Bool,
exact : Bool,
) -> Bool {
self.source_relation.proves_misses_target() ||
self.producer_relation.proves_misses_target() ||
(
exact &&
(
self.source_relation.proves_misses_exact_target() ||
self.source_relation.proves_misses_exact_target_by_strict_subtype() ||
self.producer_relation.proves_misses_exact_target() ||
self.producer_relation.proves_misses_exact_target_by_strict_subtype()
)
) ||
(
not(nullable) &&
self.source_relation.proves_null_or_miss_for_non_null_target()
)
}
///|
priv enum OiRefTestEvaluationKind {
RefTestUnknown
RefTestSuccess
RefTestFailure
RefTestSuccessOnlyIfNull
RefTestSuccessOnlyIfNonNull
}
///|
priv struct OiRefTestEvaluationFact {
kind : OiRefTestEvaluationKind
}
///|
priv struct OiDescriptorRefTestEvaluationFact {
kind : OiRefTestEvaluationKind
}
///|
priv struct OiDescriptorRefTestMissFact {
boundary_only : Bool
}
///|
fn OiDescriptorRefTestMissFact::from_operand(
ctx : HotPassContext,
operand_fact : OiRefTestOperandFact,
target : @lib.HeapType,
nullable : Bool,
) -> OiDescriptorRefTestMissFact? {
// Binaryen v130 evaluates descriptor tests through the same cast-check table
// that can prove ordinary `ref.test` misses. Starshine currently validates
// `ref.test_desc` targets against the operand type before OI runs, so a public
// disjoint descriptor-test miss fixture is rejected locally. Keep that miss
// reopening point as an explicit descriptor-test fact rather than hiding it in
// the descriptor evaluation's `Unknown` fallback.
ignore(nullable)
let candidate_miss = match operand_fact.source_type {
Some(ref_type) =>
OiHeapRelationFact::from_heaps(ctx, ref_type.get_heap_type(), target).known_miss_target()
None => false
}
if candidate_miss {
Some({ boundary_only: true, })
} else {
None
}
}
///|
fn OiDescriptorRefTestMissFact::proves_boundary_only(
self : OiDescriptorRefTestMissFact,
) -> Bool {
self.boundary_only
}
///|
fn OiDescriptorRefTestEvaluationFact::from_operand(
ctx : HotPassContext,
operand_fact : OiRefTestOperandFact,
target : @lib.HeapType,
nullable : Bool,
) -> OiDescriptorRefTestEvaluationFact {
// Binaryen v130 routes `ref.test_desc` through the same ref-test visitor and
// cast-check result family, but Starshine's custom-descriptor surface only
// represents the source-backed success / success-if-non-null descriptor match
// subset today. Keep that descriptor-specific evaluation as its own fact so
// future descriptor miss, GLB, and bottom-target widening can extend this
// table without reintroducing an opcode-local proof branch.
let relation_fact = OiRefProducerTargetRelationFact::from_producer(
ctx,
operand_fact.producer_fact,
target,
nullable,
)
let kind = if relation_fact.subtype_relation.proves_matches_heap() {
if !nullable && relation_fact.subtype_relation.proves_nullable() {
RefTestSuccessOnlyIfNonNull
} else {
RefTestSuccess
}
} else {
match
OiDescriptorRefTestMissFact::from_operand(
ctx, operand_fact, target, nullable,
) {
Some(fact) => ignore(fact.proves_boundary_only())
None => ()
}
RefTestUnknown
}
{ kind, }
}
///|
fn OiRefTestEvaluationFact::from_test(
ctx : HotPassContext,
func : @ir.HotFunc,
test_fact : OiRefTestFact,
) -> OiRefTestEvaluationFact {
// Binaryen v130 `visitRefTest` asks `GCTypeUtils::evaluateCastCheck` for a
// source-level result (`Success`, `Failure`, `SuccessOnlyIfNull`,
// `SuccessOnlyIfNonNull`, or `Unknown`) after looking through fallthrough
// producers. Keep that classification as a fact so the visitor does not own a
// scanner-like boolean cascade for null, exact, final, descriptor, and
// fallthrough test cases.
let nullable = test_fact.nullable
let exact = test_fact.exact
let target = test_fact.target
let operand_fact = OiRefTestOperandFact::from_node(
ctx,
func,
test_fact.operand_id,
)
if operand_fact.proves_known_null() {
return {
kind: if test_fact.known_null_result() == @lib.I32(1) {
RefTestSuccess
} else {
RefTestFailure
},
}
}
if test_fact.style_fact.proves_descriptor_style() {
return {
kind: OiDescriptorRefTestEvaluationFact::from_operand(
ctx, operand_fact, target, nullable,
).kind,
}
}
let relation_fact = OiRefTestOperandTargetRelationFact::from_operand(
ctx, operand_fact, target, nullable,
)
if relation_fact.proves_success_only_if_non_null(nullable, exact) {
return { kind: RefTestSuccessOnlyIfNonNull, }
}
if relation_fact.proves_success_only_if_null(exact) {
return { kind: RefTestSuccessOnlyIfNull, }
}
if relation_fact.proves_known_success(nullable, exact) {
return { kind: RefTestSuccess, }
}
if relation_fact.proves_known_failure(nullable, exact) {
return { kind: RefTestFailure, }
}
{ kind: RefTestUnknown, }
}
///|
priv enum OiRefTestResultKind {
RefTestResultUnknown
RefTestResultConst
RefTestResultRefIsNonNull
RefTestResultRefIsNull
}
///|
priv struct OiRefTestResultFact {
kind : OiRefTestResultKind
const_result : @lib.I32?
}
///|
fn OiRefTestResultFact::from_evaluation(
evaluation : OiRefTestEvaluationFact,
) -> OiRefTestResultFact {
match evaluation.kind {
RefTestSuccess =>
{ kind: RefTestResultConst, const_result: Some(@lib.I32(1)), }
RefTestFailure =>
{ kind: RefTestResultConst, const_result: Some(@lib.I32(0)), }
RefTestSuccessOnlyIfNull =>
{ kind: RefTestResultRefIsNull, const_result: None, }
RefTestSuccessOnlyIfNonNull =>
{ kind: RefTestResultRefIsNonNull, const_result: None, }
RefTestUnknown => { kind: RefTestResultUnknown, const_result: None, }
}
}
///|
fn OiRefTestResultFact::from_test(
ctx : HotPassContext,
func : @ir.HotFunc,
test_fact : OiRefTestFact,
) -> OiRefTestResultFact {
let evaluation = OiRefTestEvaluationFact::from_test(ctx, func, test_fact)
OiRefTestResultFact::from_evaluation(evaluation)
}
///|
priv enum OiRefTestConstOperandPreservationKind {
RefTestConstPreserveOperandWithDrop
}
///|
priv struct OiRefTestConstOperandPreservationFact {
kind : OiRefTestConstOperandPreservationKind
operand_id : Int32
}
///|
fn OiRefTestConstOperandPreservationFact::from_operand(
operand_id : Int32,
) -> OiRefTestConstOperandPreservationFact {
// Binaryen v130 `visitRefTest` materializes both known Success and Failure as
// `drop(ref); i32.const`. This is an output-shape contract, not merely an
// effect fallback: even direct ref.null and local.get operands remain
// explicit. Keep that unconditional preservation decision in one typed fact
// so candidate discovery and generic purity facts cannot erase the operand.
{ kind: RefTestConstPreserveOperandWithDrop, operand_id, }
}
///|
fn OiRefTestConstOperandPreservationFact::proves_operand_drop_required(
self : OiRefTestConstOperandPreservationFact,
) -> Bool {
match self.kind {
RefTestConstPreserveOperandWithDrop => self.operand_id >= 0
}
}
///|
priv enum OiRefTestRewritePlanKind {
RefTestTightenTarget
RefTestRewriteToNonNullCheck
RefTestRewriteToNullCheck
RefTestDropOperandThenConst
}
///|
priv struct OiRefTestRewritePlanFact {
kind : OiRefTestRewritePlanKind
test_fact : OiRefTestFact
const_result : @lib.I32?
narrowed_ref_type : @lib.RefType?
const_operand_preservation_fact : OiRefTestConstOperandPreservationFact?
}
///|
fn OiRefTestRewritePlanFact::from_test(
ctx : HotPassContext,
func : @ir.HotFunc,
test_fact : OiRefTestFact,
) -> OiRefTestRewritePlanFact? {
// Binaryen v130 `visitRefTest` turns the source-level cast-check result into
// either a null predicate rewrite or an i32 constant while preserving operand
// effects when the operand cannot be erased. Keep the replacement decision as
// a plan fact rather than a visitor-local cascade over evaluation outcomes and
// purity predicates.
let (active_test_fact, pending_tightening) = match
OiRefTestTighteningFact::from_test(ctx, func, test_fact) {
Some(tightening_fact) => {
let narrowed_ref_type = tightening_fact.narrowed_ref_type
(
{
nullable: narrowed_ref_type.is_nullable(),
exact: narrowed_ref_type.is_exact(),
target: narrowed_ref_type.get_heap_type(),
operand_id: test_fact.operand_id,
style_fact: test_fact.style_fact,
},
Some(narrowed_ref_type),
)
}
None => (test_fact, None)
}
let result_fact = OiRefTestResultFact::from_test(ctx, func, active_test_fact)
match result_fact.kind {
RefTestResultRefIsNonNull =>
Some({
kind: RefTestRewriteToNonNullCheck,
test_fact: active_test_fact,
const_result: None,
narrowed_ref_type: None,
const_operand_preservation_fact: None,
})
RefTestResultRefIsNull =>
Some({
kind: RefTestRewriteToNullCheck,
test_fact: active_test_fact,
const_result: None,
narrowed_ref_type: None,
const_operand_preservation_fact: None,
})
RefTestResultConst =>
match result_fact.const_result {
Some(value) => {
let operand_id = test_fact.operand_id
let preservation_fact = OiRefTestConstOperandPreservationFact::from_operand(
operand_id,
)
Some({
kind: RefTestDropOperandThenConst,
test_fact: active_test_fact,
const_result: Some(value),
narrowed_ref_type: None,
const_operand_preservation_fact: Some(preservation_fact),
})
}
None => None
}
RefTestResultUnknown =>
match pending_tightening {
Some(narrowed_ref_type) =>
Some({
kind: RefTestTightenTarget,
test_fact: active_test_fact,
const_result: None,
narrowed_ref_type: Some(narrowed_ref_type),
const_operand_preservation_fact: None,
})
None => None
}
}
}
///|
fn OiRefTestRewritePlanFact::replace_with_ref_is_null(
self : OiRefTestRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
OiRefIsNullRewritePlanFact::replace_node_with_ref_is_null(
ctx,
func,
node_id,
self.test_fact.operand_id,
)
}
///|
fn OiRefTestRewritePlanFact::replace_with_ref_is_non_null_test(
self : OiRefTestRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
let operand_id = self.test_fact.operand_id
let is_null = @ir.hot_build_unary(func, @lib.ValType::i32(), operand_id)
@ir.hot_node_exact_instr_set(func, is_null, @lib.Instruction::ref_is_null())
let non_null = @ir.hot_build_unary(func, @lib.ValType::i32(), is_null)
@ir.hot_node_exact_instr_set(func, non_null, @lib.Instruction::i32_eqz())
let replacement = @ir.hot_node_get(func, non_null)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_delete_node(func, non_null)
pass_mark_mutated(ctx, func)
}
///|
fn OiRefTestRewritePlanFact::apply(
self : OiRefTestRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let operand_id = self.test_fact.operand_id
match self.kind {
RefTestTightenTarget => {
let narrowed_ref_type = match self.narrowed_ref_type {
Some(ref_type) => ref_type
None => return false
}
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(
@lib.Instruction::ref_test_type(narrowed_ref_type),
),
)
let narrowed = @ir.hot_build_node(
func,
@ir.hot_node_get(func, node_id).op,
@ir.hot_type_intern_value(func, @lib.ValType::i32()),
imm1=payload_id,
children=[operand_id],
)
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, narrowed))
@ir.hot_delete_node(func, narrowed)
pass_mark_mutated(ctx, func)
true
}
RefTestRewriteToNonNullCheck => {
self.replace_with_ref_is_non_null_test(ctx, func, node_id)
true
}
RefTestRewriteToNullCheck => {
self.replace_with_ref_is_null(ctx, func, node_id)
true
}
RefTestDropOperandThenConst =>
match (self.const_result, self.const_operand_preservation_fact) {
(Some(value), Some(preservation_fact)) =>
if preservation_fact.proves_operand_drop_required() {
optimize_instructions_replace_with_drop_then_const_i32(
ctx, func, node_id, operand_id, value,
)
true
} else {
false
}
_ => false
}
}
}
///|
priv struct OiRefCastFact {
nullable : Bool
exact : Bool
target : @lib.HeapType
operand_id : Int32
}
///|
fn OiRefCastFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefCastFact? {
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
let (nullable, exact, target) = match
@ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefCast(nullable, exact, target)) =>
(nullable, exact, target)
_ => return None
}
Some({
nullable,
exact,
target,
operand_id: @ir.hot_child_get(func, node_id, 0),
})
}
///|
priv struct OiRefCastOperandTargetFact {
node_op : @ir.HotOp
source_type : @lib.RefType?
local_ref_type : @lib.RefType?
literal_fact : OiRefLiteralFact
}
///|
fn OiRefCastOperandTargetFact::from_operand(
func : @ir.HotFunc,
operand_id : Int32,
) -> OiRefCastOperandTargetFact {
let node = @ir.hot_node_get(func, operand_id)
let source_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, operand_id)) {
[@lib.ValType::RefTypeValType(ref_type)] => Some(ref_type)
_ => None
}
let local_ref_type = match node.op {
@ir.HotOp::LocalGet =>
match @ir.hot_local_type(func, node.imm0) {
Some(@lib.ValType::RefTypeValType(ref_type)) => Some(ref_type)
_ => None
}
_ => None
}
{
node_op: node.op,
source_type,
local_ref_type,
literal_fact: OiRefLiteralFact::from_node(func, operand_id),
}
}
///|
priv enum OiRefCastSourceTargetDirectNullKind {
RefCastSourceTargetNotDirectNull
RefCastSourceTargetDirectNull
}
///|
priv struct OiRefCastSourceTargetDirectNullFact {
kind : OiRefCastSourceTargetDirectNullKind
}
///|
fn OiRefCastSourceTargetDirectNullFact::from_node_op(
node_op : @ir.HotOp,
) -> OiRefCastSourceTargetDirectNullFact {
// Binaryen v130 `visitRefCast` handles a direct `ref.null` operand before the
// broader source-type relation. Keep that source proof as a selected direct
// null kind instead of carrying a raw boolean alongside the other relation
// payloads.
let kind = match node_op {
@ir.HotOp::RefNull => RefCastSourceTargetDirectNull
_ => RefCastSourceTargetNotDirectNull
}
{ kind, }
}
///|
fn OiRefCastSourceTargetDirectNullFact::proves_direct_null_operand(
self : OiRefCastSourceTargetDirectNullFact,
) -> Bool {
match self.kind {
RefCastSourceTargetDirectNull => true
RefCastSourceTargetNotDirectNull => false
}
}
///|
priv struct OiRefCastLocalTargetMatchFact {
non_null_match_fact : OiRefTargetMatchProofFact?
nullable_match_fact : OiRefTargetMatchProofFact?
}
///|
priv struct OiRefCastLocalTargetMissFact {
target_miss_fact : OiRefTargetMissProofFact?
non_null_target_miss_fact : OiRefTargetMissProofFact?
}
///|
priv struct OiRefCastLocalTargetRelationFact {
match_fact : OiRefCastLocalTargetMatchFact
miss_fact : OiRefCastLocalTargetMissFact
}
///|
fn OiRefCastLocalTargetRelationFact::empty() -> OiRefCastLocalTargetRelationFact {
{
match_fact: { non_null_match_fact: None, nullable_match_fact: None, },
miss_fact: { target_miss_fact: None, non_null_target_miss_fact: None, },
}
}
///|
fn OiRefCastLocalTargetRelationFact::from_target_fact(
ctx : HotPassContext,
target_fact : OiRefCastOperandTargetFact,
target : @lib.HeapType,
) -> OiRefCastLocalTargetRelationFact {
// Binaryen v130 `visitRefCast` consults the operand-local static type before
// source-type folds, so keep the local.get source/target proofs in a nested
// relation fact instead of carrying four parallel scanner booleans through the
// broader ref.cast source relation.
match target_fact.node_op {
@ir.HotOp::LocalGet =>
match target_fact.local_ref_type {
Some(ref_type) => {
let relation = OiHeapRelationFact::from_heaps(
ctx,
ref_type.get_heap_type(),
target,
)
{
match_fact: {
non_null_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNonNullMatch,
ref_type.is_non_nullable() &&
relation.known_match_without_full_subtyping(),
),
nullable_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableMatch,
ref_type.is_nullable() &&
relation.known_match_without_full_subtyping(),
),
},
miss_fact: {
target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesTarget,
ref_type.is_non_nullable() && relation.known_miss_target(),
),
non_null_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetNullOrMissForNonNullTarget,
relation.known_miss_target(),
),
},
}
}
None => OiRefCastLocalTargetRelationFact::empty()
}
_ => OiRefCastLocalTargetRelationFact::empty()
}
}
///|
fn OiRefCastLocalTargetRelationFact::proves_non_null_matches_target(
self : OiRefCastLocalTargetRelationFact,
) -> Bool {
match self.match_fact.non_null_match_fact {
Some(fact) => fact.proves_kind(RefTargetNonNullMatch)
None => false
}
}
///|
fn OiRefCastLocalTargetRelationFact::proves_nullable_matches_target(
self : OiRefCastLocalTargetRelationFact,
) -> Bool {
match self.match_fact.nullable_match_fact {
Some(fact) => fact.proves_kind(RefTargetNullableMatch)
None => false
}
}
///|
fn OiRefCastLocalTargetRelationFact::proves_misses_target(
self : OiRefCastLocalTargetRelationFact,
) -> Bool {
match self.miss_fact.target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesTarget)
None => false
}
}
///|
fn OiRefCastLocalTargetRelationFact::proves_null_or_miss_for_non_null_target(
self : OiRefCastLocalTargetRelationFact,
) -> Bool {
match self.miss_fact.non_null_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetNullOrMissForNonNullTarget)
None => false
}
}
///|
priv struct OiRefCastSourceTargetIdentityFact {
nullable_exact_target_type_fact : OiRefTargetIdentityProofFact?
exact_target_heap_fact : OiRefTargetIdentityProofFact?
}
///|
priv struct OiRefCastSourceTargetMatchFact {
non_null_match_fact : OiRefTargetMatchProofFact?
nullable_match_fact : OiRefTargetMatchProofFact?
nullable_exact_only_null_fact : OiRefTargetMatchProofFact?
}
///|
priv struct OiRefCastSourceTargetMissFact {
target_miss_fact : OiRefTargetMissProofFact?
non_null_target_miss_fact : OiRefTargetMissProofFact?
exact_target_miss_fact : OiRefTargetMissProofFact?
strict_subtype_exact_target_miss_fact : OiRefTargetMissProofFact?
}
///|
priv struct OiRefCastSourceTargetRelationFact {
direct_null_fact : OiRefCastSourceTargetDirectNullFact
identity_fact : OiRefCastSourceTargetIdentityFact
match_fact : OiRefCastSourceTargetMatchFact
miss_fact : OiRefCastSourceTargetMissFact
}
///|
fn OiRefCastSourceTargetRelationFact::from_target_fact(
ctx : HotPassContext,
target_fact : OiRefCastOperandTargetFact,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiRefCastSourceTargetRelationFact {
// Binaryen v130 `GCTypeUtils::evaluateCastCheck` reasons about direct nulls,
// operand source types, local static types, and direct literals before
// `visitRefCast` decides whether the operand can be forwarded, trapped, or
// lowered to a non-null check. Keep those source/target proofs in one fact so
// `OiRefCastOperandTargetFact` remains discovery-only.
let literal_target = OiRefLiteralTargetFact::from_literal(
target_fact.literal_fact,
target,
)
let direct_null_fact = OiRefCastSourceTargetDirectNullFact::from_node_op(
target_fact.node_op,
)
let local_relation = OiRefCastLocalTargetRelationFact::from_target_fact(
ctx, target_fact, target,
)
match target_fact.source_type {
Some(ref_type) => {
let source = ref_type.get_heap_type()
let relation = OiHeapRelationFact::from_heaps(ctx, source, target)
let nullable_exact_only_null_fact = OiHeapNullableExactOnlyNullTargetFact::from_heap_relation(
relation,
ref_type.is_nullable(),
ref_type.is_exact(),
target_nullable,
)
let strict_concrete_target_match_fact = OiHeapStrictConcreteTargetMatchFact::from_heap_relation(
relation,
)
{
direct_null_fact,
identity_fact: {
nullable_exact_target_type_fact: OiRefTargetIdentityProofFact::when_proven(
RefTargetNullableExactTargetTypeIdentity,
ref_type.is_nullable() && source == target,
),
exact_target_heap_fact: OiRefTargetIdentityProofFact::when_proven(
RefTargetExactHeapIdentity,
ref_type.is_exact() && source == target,
),
},
match_fact: {
non_null_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNonNullMatch,
literal_target.proves_match_target() ||
local_relation.proves_non_null_matches_target() ||
(
ref_type.is_non_nullable() &&
relation.known_match_without_full_subtyping()
),
),
nullable_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableMatch,
local_relation.proves_nullable_matches_target() ||
(ref_type.is_nullable() && relation.known_match_target()),
),
nullable_exact_only_null_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableExactOnlyNullMatch,
nullable_exact_only_null_fact.proves_target_match(),
),
},
miss_fact: {
target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesTarget,
literal_target.proves_i31_miss_target() ||
local_relation.proves_misses_target(),
),
non_null_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetNullOrMissForNonNullTarget,
local_relation.proves_null_or_miss_for_non_null_target(),
),
exact_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesExactTarget,
ref_type.is_exact() &&
source != target &&
(ref_type.is_non_nullable() || !target_nullable),
),
strict_subtype_exact_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesExactTargetByStrictSubtype,
(ref_type.is_non_nullable() || !target_nullable) &&
strict_concrete_target_match_fact.proves_target_match(),
),
},
}
}
None =>
{
direct_null_fact,
identity_fact: {
nullable_exact_target_type_fact: None,
exact_target_heap_fact: None,
},
match_fact: {
non_null_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNonNullMatch,
literal_target.proves_match_target(),
),
nullable_match_fact: OiRefTargetMatchProofFact::when_proven(
RefTargetNullableMatch,
local_relation.proves_nullable_matches_target(),
),
nullable_exact_only_null_fact: None,
},
miss_fact: {
target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetMissesTarget,
literal_target.proves_i31_miss_target() ||
local_relation.proves_misses_target(),
),
non_null_target_miss_fact: OiRefTargetMissProofFact::when_proven(
RefTargetNullOrMissForNonNullTarget,
local_relation.proves_null_or_miss_for_non_null_target(),
),
exact_target_miss_fact: None,
strict_subtype_exact_target_miss_fact: None,
},
}
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_direct_null_operand(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
self.direct_null_fact.proves_direct_null_operand()
}
///|
fn OiRefCastSourceTargetRelationFact::proves_nullable_exact_target_type(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.identity_fact.nullable_exact_target_type_fact {
Some(fact) => fact.proves_kind(RefTargetNullableExactTargetTypeIdentity)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_exact_target_heap(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.identity_fact.exact_target_heap_fact {
Some(fact) => fact.proves_kind(RefTargetExactHeapIdentity)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_known_match_target(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.match_fact.non_null_match_fact {
Some(fact) => fact.proves_kind(RefTargetNonNullMatch)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_nullable_matches_target(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.match_fact.nullable_match_fact {
Some(fact) => fact.proves_kind(RefTargetNullableMatch)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_nullable_exact_target_only_as_null(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.match_fact.nullable_exact_only_null_fact {
Some(fact) => fact.proves_kind(RefTargetNullableExactOnlyNullMatch)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_known_miss_target(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesTarget)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_null_or_miss_for_non_null_target(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.non_null_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetNullOrMissForNonNullTarget)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_misses_exact_target(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.exact_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesExactTarget)
None => false
}
}
///|
fn OiRefCastSourceTargetRelationFact::proves_misses_exact_target_by_strict_subtype(
self : OiRefCastSourceTargetRelationFact,
) -> Bool {
match self.miss_fact.strict_subtype_exact_target_miss_fact {
Some(fact) => fact.proves_kind(RefTargetMissesExactTargetByStrictSubtype)
None => false
}
}
///|
priv struct OiRefCastOperandTargetRelationFact {
source_relation : OiRefCastSourceTargetRelationFact
}
///|
fn OiRefCastOperandTargetRelationFact::from_target_fact(
ctx : HotPassContext,
target_fact : OiRefCastOperandTargetFact,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiRefCastOperandTargetRelationFact {
// Binaryen v130 `visitRefCast` asks `GCTypeUtils::evaluateCastCheck` for the
// source/target relation before deciding how to preserve operand effects.
// Keep Starshine's represented relation proofs nested so the cast plan and
// descriptor exact-miss plan consume one relation fact rather than parallel
// source/local/literal predicate fields.
{
source_relation: OiRefCastSourceTargetRelationFact::from_target_fact(
ctx, target_fact, target, target_nullable,
),
}
}
///|
priv enum OiRefCastOperandErasureKind {
RefCastOperandEraseTriviallyPure
RefCastOperandErasePureEffects
RefCastOperandPreserveEffects
}
///|
priv struct OiRefCastOperandErasureFact {
kind : OiRefCastOperandErasureKind
}
///|
fn OiRefCastOperandErasureFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
) -> OiRefCastOperandErasureFact {
// Binaryen only drops a trapping ref.cast operand when its effects need not
// be preserved. Keep that effect-preservation proof separate from the
// source/target relation so miss rewrites cannot accidentally widen erasure.
let trivially_pure = OiTriviallyPureOperandFact::proves_node_pure(
func, operand_id,
)
let effects_pure = optimize_instructions_node_effects_are_pure(
ctx, func, operand_id,
)
let kind = if trivially_pure {
RefCastOperandEraseTriviallyPure
} else if effects_pure {
RefCastOperandErasePureEffects
} else {
RefCastOperandPreserveEffects
}
{ kind, }
}
///|
fn OiRefCastOperandErasureFact::proves_erases_without_effects(
self : OiRefCastOperandErasureFact,
) -> Bool {
match self.kind {
RefCastOperandEraseTriviallyPure | RefCastOperandErasePureEffects => true
RefCastOperandPreserveEffects => false
}
}
///|
priv struct OiRefCastOperandFact {
target_relation_fact : OiRefCastOperandTargetRelationFact
erasure_fact : OiRefCastOperandErasureFact
}
///|
fn OiRefCastOperandFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiRefCastOperandFact {
// Binaryen v130 `visitRefCast` centralizes cast-check knowledge in
// `GCTypeUtils::evaluateCastCheck` and then separately asks whether operand
// effects must be preserved before trapping. Keep those as nested facts so
// ref.cast and descriptor exact-miss plans do not own parallel helper-level
// predicate cascades.
let target_fact = OiRefCastOperandTargetFact::from_operand(func, operand_id)
{
target_relation_fact: OiRefCastOperandTargetRelationFact::from_target_fact(
ctx, target_fact, target, target_nullable,
),
erasure_fact: OiRefCastOperandErasureFact::from_operand(
ctx, func, operand_id,
),
}
}
///|
fn OiRefCastOperandFact::proves_operand_erases_without_effects(
self : OiRefCastOperandFact,
) -> Bool {
self.erasure_fact.proves_erases_without_effects()
}
///|
fn OiRefCastOperandFact::proves_exact_target_miss(
self : OiRefCastOperandFact,
) -> Bool {
let relation = self.target_relation_fact.source_relation
relation.proves_misses_exact_target() ||
relation.proves_misses_exact_target_by_strict_subtype()
}
///|
priv enum OiRefCastOperandRewriteKind {
RefCastOperandDirectNullForward
RefCastOperandDirectNullTrap
RefCastOperandAsNonNull
RefCastOperandNullrefCast
RefCastOperandForward
RefCastOperandEraseTrap
RefCastOperandPreserveTrap
}
///|
priv struct OiRefCastOperandRewriteFact {
kind : OiRefCastOperandRewriteKind
}
///|
fn OiRefCastOperandRewriteFact::from_operand_fact(
operand_fact : OiRefCastOperandFact,
nullable : Bool,
exact : Bool,
) -> OiRefCastOperandRewriteFact? {
// Binaryen v130 `visitRefCast` asks `GCTypeUtils::evaluateCastCheck` for a
// known source/target outcome, then chooses the first matching result in this
// order: direct-null forwarding/trapping, non-null check, nullable-exact
// null-only materialization, operand forwarding, and finally trap emission
// with or without operand preservation. Capture that ordered decision as a
// result fact instead of exposing several helper-shaped `can_*` predicates on
// `OiRefCastOperandFact`.
let relation = operand_fact.target_relation_fact.source_relation
if relation.proves_direct_null_operand() {
return Some({
kind: if nullable {
RefCastOperandDirectNullForward
} else {
RefCastOperandDirectNullTrap
},
})
}
if !nullable &&
(!exact || relation.proves_exact_target_heap()) &&
relation.proves_nullable_exact_target_type() {
return Some({ kind: RefCastOperandAsNonNull, })
}
if exact && relation.proves_nullable_exact_target_only_as_null() {
return Some({ kind: RefCastOperandNullrefCast, })
}
if (!exact || relation.proves_exact_target_heap()) &&
(
relation.proves_known_match_target() ||
(nullable && relation.proves_nullable_matches_target())
) {
return Some({ kind: RefCastOperandForward, })
}
let traps = relation.proves_known_miss_target() ||
(exact && operand_fact.proves_exact_target_miss()) ||
(!nullable && relation.proves_null_or_miss_for_non_null_target())
if traps {
return Some({
kind: if operand_fact.proves_operand_erases_without_effects() {
RefCastOperandEraseTrap
} else {
RefCastOperandPreserveTrap
},
})
}
None
}
///|
priv struct OiRefCastTighteningFact {
narrowed_ref_type : @lib.RefType
}
///|
priv struct OiRefCastChildCastFact {
replacement_operand_id : Int32
}
///|
priv struct OiRefCastChildNonNullFact {
replacement_operand_id : Int32
narrowed_ref_type : @lib.RefType
}
///|
fn OiRefCastTighteningFact::from_cast(
ctx : HotPassContext,
func : @ir.HotFunc,
cast_fact : OiRefCastFact,
) -> OiRefCastTighteningFact? {
// Binaryen v130 `visitRefCast` first intersects ordinary cast result type
// with `getFallthroughType(ref)` using `Type::getGreatestLowerBound`, then
// re-optimizes the cast before known-result and child-wrapper folds. Keep the
// represented GLB/nullability rule as a fact over the cast target and operand
// fallthrough type instead of another visitor-local ref-type scanner.
let current_ref_type = @lib.RefType::new(
cast_fact.nullable,
cast_fact.target,
exact=cast_fact.exact,
)
let fallthrough_ref_type = match
OiRefFallthroughFact::from_node(ctx, func, cast_fact.operand_id).ref_type {
Some(ref_type) => ref_type
None => return None
}
match
optimize_instructions_ref_fallthrough_lower_bound(
ctx, current_ref_type, fallthrough_ref_type,
) {
Some(narrowed_ref_type) if narrowed_ref_type != current_ref_type =>
Some({ narrowed_ref_type, })
_ => None
}
}
///|
priv enum OiRefCastChildSubtypeKind {
RefCastChildSubtypeMatch
RefCastChildSubtypeNullableMismatch
RefCastChildSubtypeExactMismatch
RefCastChildSubtypeHeapMismatch
}
///|
priv struct OiRefCastChildSubtypeFact {
kind : OiRefCastChildSubtypeKind
}
///|
fn OiRefCastChildSubtypeFact::from_types(
ctx : HotPassContext,
parent_ref_type : @lib.RefType,
child_ref_type : @lib.RefType,
) -> OiRefCastChildSubtypeFact {
// Binaryen v130 `visitRefCast` removes an ordinary child cast only when the
// parent cast result type is a subtype of the child cast result type. Keep the
// represented `Type::isSubType(curr->type, child->type)` decision as a named
// fact with explicit nullable/exact/heap mismatch outcomes instead of a
// helper-style subtype predicate hidden under the child-cast rewrite.
if child_ref_type.is_non_nullable() && parent_ref_type.is_nullable() {
return { kind: RefCastChildSubtypeNullableMismatch, }
}
let parent_heap = parent_ref_type.get_heap_type()
let child_heap = child_ref_type.get_heap_type()
if child_ref_type.is_exact() {
return {
kind: if parent_ref_type.is_exact() && parent_heap == child_heap {
RefCastChildSubtypeMatch
} else {
RefCastChildSubtypeExactMismatch
},
}
}
{
kind: if OiHeapRelationFact::from_heaps(ctx, parent_heap, child_heap).known_match_target() {
RefCastChildSubtypeMatch
} else {
RefCastChildSubtypeHeapMismatch
},
}
}
///|
fn OiRefCastChildSubtypeFact::proves_parent_subtypes_child(
self : OiRefCastChildSubtypeFact,
) -> Bool {
match self.kind {
RefCastChildSubtypeMatch => true
RefCastChildSubtypeNullableMismatch
| RefCastChildSubtypeExactMismatch
| RefCastChildSubtypeHeapMismatch => false
}
}
///|
fn OiRefCastChildCastFact::from_cast(
ctx : HotPassContext,
func : @ir.HotFunc,
cast_fact : OiRefCastFact,
) -> OiRefCastChildCastFact? {
// Binaryen v130 `visitRefCast` removes an ordinary child `RefCast` after all
// known-result folds when the parent cast is at least as strong as the child
// cast (`Type::isSubType(curr->type, child->type)`). Keep that source rule as
// a fact over current/child ref types instead of a rewrite-local opcode scan;
// descriptor child casts remain excluded until Starshine has the child
// localization needed to drop/reorder their descriptor operands.
let child_id = cast_fact.operand_id
if child_id < 0 || !@ir.hot_node_is_live(func, child_id) {
return None
}
match @ir.hot_node_get(func, child_id).op {
@ir.HotOp::RefCast => ()
_ => return None
}
let child_fact = match OiRefCastFact::from_node(func, child_id) {
Some(fact) => fact
None => return None
}
let parent_type = @lib.RefType::new(
cast_fact.nullable,
cast_fact.target,
exact=cast_fact.exact,
)
let child_type = @lib.RefType::new(
child_fact.nullable,
child_fact.target,
exact=child_fact.exact,
)
let subtype_fact = OiRefCastChildSubtypeFact::from_types(
ctx, parent_type, child_type,
)
if subtype_fact.proves_parent_subtypes_child() {
Some({ replacement_operand_id: child_fact.operand_id, })
} else {
None
}
}
///|
fn OiRefCastChildNonNullFact::from_cast(
func : @ir.HotFunc,
cast_fact : OiRefCastFact,
) -> OiRefCastChildNonNullFact? {
// Binaryen v130 `visitRefCast` removes an ordinary child `ref.as_non_null`
// after known-result and child-cast folds by retyping the parent cast as
// non-null and rewiring it to the `ref.as_non_null` operand. Keep the child
// wrapper classification, replacement operand, and narrowed target type in a
// fact so the ref.cast plan does not own another opcode-local scanner.
let child_id = cast_fact.operand_id
if child_id < 0 || !@ir.hot_node_is_live(func, child_id) {
return None
}
match @ir.hot_node_get(func, child_id).op {
@ir.HotOp::Heap => ()
_ => return None
}
match @ir.hot_node_exact_instr(func, child_id) {
Some(@lib.Instruction::RefAsNonNull) => ()
_ => return None
}
if @ir.hot_child_count(func, child_id) != 1 {
return None
}
Some({
replacement_operand_id: @ir.hot_child_get(func, child_id, 0),
narrowed_ref_type: @lib.RefType::new(
false,
cast_fact.target,
exact=cast_fact.exact,
),
})
}
///|
priv enum OiRefCastRewritePlanKind {
RefCastTightenTarget
RefCastForwardOperand
RefCastUnreachable
RefCastDropOperandThenUnreachable
RefCastAsNonNull
RefCastNullrefCast
RefCastSkipChildCast
RefCastSkipChildNonNull
}
///|
priv struct OiRefCastRewritePayloadFact {
replacement_operand_id : Int32?
narrowed_ref_type : @lib.RefType?
}
///|
fn OiRefCastRewritePayloadFact::empty() -> OiRefCastRewritePayloadFact {
{ replacement_operand_id: None, narrowed_ref_type: None, }
}
///|
fn OiRefCastRewritePayloadFact::replacement_operand(
replacement_operand_id : Int32,
) -> OiRefCastRewritePayloadFact {
{
replacement_operand_id: Some(replacement_operand_id),
narrowed_ref_type: None,
}
}
///|
fn OiRefCastRewritePayloadFact::replacement_operand_and_type(
replacement_operand_id : Int32,
narrowed_ref_type : @lib.RefType,
) -> OiRefCastRewritePayloadFact {
{
replacement_operand_id: Some(replacement_operand_id),
narrowed_ref_type: Some(narrowed_ref_type),
}
}
///|
priv struct OiRefCastRewritePlanFact {
kind : OiRefCastRewritePlanKind
cast_fact : OiRefCastFact
payload : OiRefCastRewritePayloadFact
}
///|
fn OiRefCastRewritePlanFact::from_cast(
ctx : HotPassContext,
func : @ir.HotFunc,
cast_fact : OiRefCastFact,
) -> OiRefCastRewritePlanFact? {
// Binaryen v130 `visitRefCast` first tightens ordinary cast type with
// fallthrough/GLB information, then routes source-level cast outcomes through
// `GCTypeUtils::evaluateCastCheck` and chooses whether to forward the operand,
// keep only a null check, materialize null, or trap. Keep the represented
// Starshine subset as a plan fact fed by cast/fallthrough and operand facts so
// the visitor does not own a scanner-like cascade of local/ref/null/exactness
// predicates. When a tightening fact unlocks an existing known-result or child
// rewrite, use the tightened cast fact immediately; Binaryen reaches the same
// state by replacing and re-optimizing the cast.
let (active_cast_fact, pending_tightening) = match
OiRefCastTighteningFact::from_cast(ctx, func, cast_fact) {
Some(tightening_fact) => {
let narrowed_ref_type = tightening_fact.narrowed_ref_type
(
{
nullable: narrowed_ref_type.is_nullable(),
exact: narrowed_ref_type.is_exact(),
target: narrowed_ref_type.get_heap_type(),
operand_id: cast_fact.operand_id,
},
Some(narrowed_ref_type),
)
}
None => (cast_fact, None)
}
let nullable = active_cast_fact.nullable
let exact = active_cast_fact.exact
let operand_fact = OiRefCastOperandFact::from_operand(
ctx,
func,
active_cast_fact.operand_id,
active_cast_fact.target,
nullable,
)
match
OiRefCastOperandRewriteFact::from_operand_fact(
operand_fact, nullable, exact,
) {
Some(rewrite_fact) => {
let kind = match rewrite_fact.kind {
RefCastOperandDirectNullForward => RefCastForwardOperand
RefCastOperandDirectNullTrap => RefCastUnreachable
RefCastOperandAsNonNull => RefCastAsNonNull
RefCastOperandNullrefCast => RefCastNullrefCast
RefCastOperandForward => RefCastForwardOperand
RefCastOperandEraseTrap => RefCastUnreachable
RefCastOperandPreserveTrap => RefCastDropOperandThenUnreachable
}
return Some({
kind,
cast_fact: active_cast_fact,
payload: OiRefCastRewritePayloadFact::empty(),
})
}
None => ()
}
match OiRefCastChildCastFact::from_cast(ctx, func, active_cast_fact) {
Some(child_fact) =>
return Some({
kind: RefCastSkipChildCast,
cast_fact: active_cast_fact,
payload: OiRefCastRewritePayloadFact::replacement_operand(
child_fact.replacement_operand_id,
),
})
None => ()
}
match OiRefCastChildNonNullFact::from_cast(func, active_cast_fact) {
Some(child_fact) =>
return Some({
kind: RefCastSkipChildNonNull,
cast_fact: active_cast_fact,
payload: OiRefCastRewritePayloadFact::replacement_operand_and_type(
child_fact.replacement_operand_id,
child_fact.narrowed_ref_type,
),
})
None => ()
}
match pending_tightening {
Some(narrowed_ref_type) =>
Some({
kind: RefCastTightenTarget,
cast_fact: active_cast_fact,
payload: OiRefCastRewritePayloadFact::replacement_operand_and_type(
active_cast_fact.operand_id,
narrowed_ref_type,
),
})
None => None
}
}
///|
fn OiRefCastRewritePlanFact::apply(
self : OiRefCastRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let operand_id = self.cast_fact.operand_id
match self.kind {
RefCastTightenTarget => {
let replacement_operand_id = match self.payload.replacement_operand_id {
Some(id) => id
None => return false
}
let narrowed_ref_type = match self.payload.narrowed_ref_type {
Some(ref_type) => ref_type
None => return false
}
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(
@lib.Instruction::ref_cast_type(narrowed_ref_type),
),
)
let narrowed = @ir.hot_build_node(
func,
@ir.hot_node_get(func, node_id).op,
@ir.hot_type_intern_value(
func,
@lib.ValType::ref_type(narrowed_ref_type),
),
imm1=payload_id,
children=[replacement_operand_id],
)
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, narrowed))
@ir.hot_delete_node(func, narrowed)
pass_mark_mutated(ctx, func)
true
}
RefCastForwardOperand => {
optimize_instructions_replace_with_node_id(ctx, func, node_id, operand_id)
true
}
RefCastUnreachable => {
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
}
RefCastDropOperandThenUnreachable =>
optimize_instructions_replace_with_drop_then_unreachable(
ctx, func, node_id, operand_id,
)
RefCastAsNonNull =>
optimize_instructions_replace_ref_cast_with_ref_as_non_null(
ctx, func, node_id, operand_id,
)
RefCastNullrefCast =>
optimize_instructions_replace_ref_cast_with_nullref_cast(
ctx, func, node_id, operand_id,
)
RefCastSkipChildCast => {
let replacement_operand_id = match self.payload.replacement_operand_id {
Some(id) => id
None => return false
}
@ir.hot_child_set(func, node_id, 0, replacement_operand_id)
pass_mark_mutated(ctx, func)
true
}
RefCastSkipChildNonNull => {
let replacement_operand_id = match self.payload.replacement_operand_id {
Some(id) => id
None => return false
}
let narrowed_ref_type = match self.payload.narrowed_ref_type {
Some(ref_type) => ref_type
None => return false
}
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(
@lib.Instruction::ref_cast_type(narrowed_ref_type),
),
)
let narrowed = @ir.hot_build_node(
func,
@ir.hot_node_get(func, node_id).op,
@ir.hot_type_intern_value(
func,
@lib.ValType::ref_type(narrowed_ref_type),
),
imm1=payload_id,
children=[replacement_operand_id],
)
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, narrowed))
@ir.hot_delete_node(func, narrowed)
pass_mark_mutated(ctx, func)
true
}
}
}
///|
priv struct OiDescriptorCastFact {
node_id : Int32
nullable : Bool
exact : Bool
target : @lib.HeapType
operand_id : Int32
descriptor_id : Int32
}
///|
fn OiDescriptorCastFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorCastFact? {
if @ir.hot_child_count(func, node_id) != 2 {
return None
}
let (nullable, exact, target) = match
@ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefCastDescEq(nullable, exact, target)) =>
(nullable, exact, target)
_ => return None
}
Some({
node_id,
nullable,
exact,
target,
operand_id: @ir.hot_child_get(func, node_id, 0),
descriptor_id: @ir.hot_child_get(func, node_id, 1),
})
}
///|
priv enum OiDescriptorCastTighteningOperandKind {
DescriptorCastTighteningOperandUnknown
DescriptorCastTighteningOperandFallthroughNonNull
}
///|
priv struct OiDescriptorCastTighteningOperandFact {
kind : OiDescriptorCastTighteningOperandKind
}
///|
fn OiDescriptorCastTighteningOperandFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
) -> OiDescriptorCastTighteningOperandFact {
// Binaryen v130 descriptor-cast nullability tightening is driven by the
// reference operand fallthrough type. Keep that source proof as a fact instead
// of carrying a direct non-null boolean on the aggregate tightening record.
match OiRefFallthroughFact::from_node(ctx, func, operand_id).ref_type {
Some(ref_type) if ref_type.is_non_nullable() =>
{ kind: DescriptorCastTighteningOperandFallthroughNonNull, }
_ => { kind: DescriptorCastTighteningOperandUnknown, }
}
}
///|
fn OiDescriptorCastTighteningOperandFact::proves_fallthrough_non_null(
self : OiDescriptorCastTighteningOperandFact,
) -> Bool {
match self.kind {
DescriptorCastTighteningOperandFallthroughNonNull => true
DescriptorCastTighteningOperandUnknown => false
}
}
///|
priv struct OiDescriptorCastTighteningFact {
cast_fact : OiDescriptorCastFact
operand_fact : OiDescriptorCastTighteningOperandFact
narrowed_ref_type : @lib.RefType
}
///|
fn OiDescriptorCastTighteningFact::from_cast(
ctx : HotPassContext,
func : @ir.HotFunc,
cast_fact : OiDescriptorCastFact,
) -> OiDescriptorCastTighteningFact? {
// Binaryen v130 `visitRefCast` tightens descriptor-cast nullability before
// known-result cleanup: descriptor casts cannot change their heap target from
// the reference operand because the descriptor child controls that, but a
// non-null fallthrough reference makes every successful result non-null. Keep
// this separate from descriptor success/exactness facts because it does not
// require proving that the operand heap already matches or subtypes the target.
if !cast_fact.nullable {
return None
}
let operand_fact = OiDescriptorCastTighteningOperandFact::from_operand(
ctx,
func,
cast_fact.operand_id,
)
if !operand_fact.proves_fallthrough_non_null() {
return None
}
Some({
cast_fact,
operand_fact,
narrowed_ref_type: @lib.RefType::new(
false,
cast_fact.target,
exact=cast_fact.exact,
),
})
}
///|
priv struct OiDescriptorCastTighteningRewritePlanFact {
tightening_fact : OiDescriptorCastTighteningFact
}
///|
fn OiDescriptorCastTighteningRewritePlanFact::from_fact(
tightening_fact : OiDescriptorCastTighteningFact,
) -> OiDescriptorCastTighteningRewritePlanFact? {
if !tightening_fact.operand_fact.proves_fallthrough_non_null() {
return None
}
Some({ tightening_fact, })
}
///|
fn OiDescriptorCastTighteningRewritePlanFact::apply(
self : OiDescriptorCastTighteningRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let cast_fact = self.tightening_fact.cast_fact
let narrowed_ref_type = self.tightening_fact.narrowed_ref_type
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(
@lib.Instruction::ref_cast_desc_eq_type(narrowed_ref_type),
),
)
let narrowed = @ir.hot_build_node(
func,
@ir.hot_node_get(func, cast_fact.node_id).op,
@ir.hot_type_intern_value(func, @lib.ValType::ref_type(narrowed_ref_type)),
imm1=payload_id,
children=[cast_fact.operand_id, cast_fact.descriptor_id],
)
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, narrowed))
@ir.hot_delete_node(func, narrowed)
pass_mark_mutated(ctx, func)
true
}
///|
priv enum OiDescriptorExactMissDropBeforeUnreachableKind {
DescriptorExactMissDropUnsupported
DescriptorExactMissDropStraightLine
DescriptorExactMissDropBlockBeforeUnreachable
DescriptorExactMissDropCallBeforeUnreachable
}
///|
priv struct OiDescriptorExactMissDropBeforeUnreachableFact {
kind : OiDescriptorExactMissDropBeforeUnreachableKind
}
///|
fn OiDescriptorExactMissDropBeforeUnreachableFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorExactMissDropBeforeUnreachableFact {
// Binaryen v130 lowers statically-impossible descriptor equality casts with
// `getDroppedChildrenAndAppend(..., unreachable, IgnoreParentEffects)`. Keep
// Starshine's represented source-order preservation subset as an explicit
// result fact: ordinary subtrees must be straight-line, and only block/direct
// call roots are currently materialized as dropped values before the
// exact-miss replacement trap.
let node = @ir.hot_node_get(func, node_id)
let control_or_exit_root = match node.op {
@ir.HotOp::Block
| @ir.HotOp::Loop
| @ir.HotOp::If
| @ir.HotOp::Try
| @ir.HotOp::TryTable
| @ir.HotOp::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail
| @ir.HotOp::Return
| @ir.HotOp::ReturnCall
| @ir.HotOp::ReturnCallIndirect
| @ir.HotOp::ReturnCallRef
| @ir.HotOp::Throw
| @ir.HotOp::ThrowRef => true
_ => false
}
let mut all_children_straight_line = true
if !control_or_exit_root {
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if !OiDescriptorExactMissDropBeforeUnreachableFact::from_node(
func,
@ir.hot_child_get(func, node_id, slot),
).proves_straight_line() {
all_children_straight_line = false
}
}
}
let direct_call_root = match node.op {
@ir.HotOp::Call => true
_ => false
}
let mut block_region_straight_line = false
match node.op {
@ir.HotOp::Block => {
block_region_straight_line = true
let body = @ir.hot_region_body_ref(func, node_id)
for slot = 0
slot < @ir.hot_region_root_count(func, body)
slot = slot + 1 {
if !OiDescriptorExactMissDropBeforeUnreachableFact::from_node(
func,
@ir.hot_region_get(func, body, slot),
).proves_straight_line() {
block_region_straight_line = false
}
}
}
_ => ()
}
let kind = if block_region_straight_line {
DescriptorExactMissDropBlockBeforeUnreachable
} else if control_or_exit_root {
DescriptorExactMissDropUnsupported
} else if !all_children_straight_line {
DescriptorExactMissDropUnsupported
} else if direct_call_root {
DescriptorExactMissDropCallBeforeUnreachable
} else {
DescriptorExactMissDropStraightLine
}
{ kind, }
}
///|
fn OiDescriptorExactMissDropBeforeUnreachableFact::proves_straight_line(
self : OiDescriptorExactMissDropBeforeUnreachableFact,
) -> Bool {
match self.kind {
DescriptorExactMissDropStraightLine
| DescriptorExactMissDropCallBeforeUnreachable => true
DescriptorExactMissDropUnsupported
| DescriptorExactMissDropBlockBeforeUnreachable => false
}
}
///|
fn OiDescriptorExactMissDropBeforeUnreachableFact::proves_block_drop_before_unreachable(
self : OiDescriptorExactMissDropBeforeUnreachableFact,
) -> Bool {
match self.kind {
DescriptorExactMissDropBlockBeforeUnreachable => true
DescriptorExactMissDropUnsupported
| DescriptorExactMissDropStraightLine
| DescriptorExactMissDropCallBeforeUnreachable => false
}
}
///|
fn OiDescriptorExactMissDropBeforeUnreachableFact::proves_call_drop_before_unreachable(
self : OiDescriptorExactMissDropBeforeUnreachableFact,
) -> Bool {
match self.kind {
DescriptorExactMissDropCallBeforeUnreachable => true
DescriptorExactMissDropUnsupported
| DescriptorExactMissDropStraightLine
| DescriptorExactMissDropBlockBeforeUnreachable => false
}
}
///|
priv enum OiDescriptorExactMissPreservationKind {
DescriptorExactMissPreserveBothErase
DescriptorExactMissPreserveDescriptorEraseOperandDrop
DescriptorExactMissPreserveOperandEraseDescriptorDrop
DescriptorExactMissPreserveBothDrop
DescriptorExactMissPreserveUnsupported
}
///|
priv struct OiDescriptorExactMissPreservationFact {
kind : OiDescriptorExactMissPreservationKind
}
///|
fn OiDescriptorExactMissPreservationFact::from_operands(
descriptor_erases_without_effects : Bool,
ref_operand_erases_without_effects : Bool,
operand_drop_fact : OiDescriptorExactMissDropBeforeUnreachableFact,
descriptor_drop_fact : OiDescriptorExactMissDropBeforeUnreachableFact,
) -> OiDescriptorExactMissPreservationFact {
// Binaryen v130 preserves descriptor exact-miss source order by either erasing
// side-effect-free operands or dropping represented straight-line/call operands
// immediately before the replacement `unreachable`. Keep that preservation
// result as one ordered fact before eligibility chooses a rewrite shape.
let ref_operand_drops_before_unreachable = operand_drop_fact.proves_block_drop_before_unreachable() ||
operand_drop_fact.proves_call_drop_before_unreachable()
let descriptor_drops_before_unreachable = descriptor_drop_fact.proves_block_drop_before_unreachable() ||
descriptor_drop_fact.proves_call_drop_before_unreachable()
let kind = if descriptor_erases_without_effects &&
ref_operand_erases_without_effects {
DescriptorExactMissPreserveBothErase
} else if descriptor_erases_without_effects &&
ref_operand_drops_before_unreachable {
DescriptorExactMissPreserveDescriptorEraseOperandDrop
} else if ref_operand_erases_without_effects &&
descriptor_drops_before_unreachable {
DescriptorExactMissPreserveOperandEraseDescriptorDrop
} else if ref_operand_drops_before_unreachable &&
descriptor_drops_before_unreachable {
DescriptorExactMissPreserveBothDrop
} else {
DescriptorExactMissPreserveUnsupported
}
{ kind, }
}
///|
priv enum OiDescriptorExactTargetRelationKind {
DescriptorExactTargetRelationDescribesTarget
DescriptorExactTargetRelationUnsupported
}
///|
priv struct OiDescriptorExactTargetRelationFact {
kind : OiDescriptorExactTargetRelationKind
}
///|
fn OiDescriptorExactTargetRelationFact::from_descriptor_type(
descriptor_type_fact : OiDescriptorTypeFact,
target : @lib.HeapType,
) -> OiDescriptorExactTargetRelationFact {
let kind = if descriptor_type_fact.exactly_describes(target) {
DescriptorExactTargetRelationDescribesTarget
} else {
DescriptorExactTargetRelationUnsupported
}
{ kind, }
}
///|
fn OiDescriptorExactTargetRelationFact::proves_exactly_describes_target(
self : OiDescriptorExactTargetRelationFact,
) -> Bool {
match self.kind {
DescriptorExactTargetRelationDescribesTarget => true
DescriptorExactTargetRelationUnsupported => false
}
}
///|
priv struct OiDescriptorExactMissDescriptorRelationFact {
descriptor_type_fact : OiDescriptorTypeFact
target_relation_fact : OiDescriptorExactTargetRelationFact
}
///|
fn OiDescriptorExactMissDescriptorRelationFact::from_descriptor(
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
target : @lib.HeapType,
) -> OiDescriptorExactMissDescriptorRelationFact {
// Binaryen v130 descriptor equality exact-miss cleanup first proves that the
// descriptor operand exactly describes the cast target. Keep that descriptor
// source relation nested under the exact-miss fact instead of storing a
// parallel descriptor-match boolean beside the reference-operand miss proof.
let descriptor_type_fact = OiDescriptorTypeFact::from_node(
ctx, func, descriptor_id,
)
{
descriptor_type_fact,
target_relation_fact: OiDescriptorExactTargetRelationFact::from_descriptor_type(
descriptor_type_fact, target,
),
}
}
///|
fn OiDescriptorExactMissDescriptorRelationFact::proves_exactly_describes_target(
self : OiDescriptorExactMissDescriptorRelationFact,
) -> Bool {
ignore(self.descriptor_type_fact)
self.target_relation_fact.proves_exactly_describes_target()
}
///|
priv struct OiDescriptorExactMissOperandRelationFact {
ref_cast_operand_fact : OiRefCastOperandFact
}
///|
fn OiDescriptorExactMissOperandRelationFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiDescriptorExactMissOperandRelationFact {
// The sibling source relation is the ordinary ref.cast operand/target fact:
// exact descriptor misses are valid only when the reference operand is already
// known to miss the exact target. Keep that proof nested so exact-miss
// eligibility consumes descriptor and operand relations symmetrically.
{
ref_cast_operand_fact: OiRefCastOperandFact::from_operand(
ctx, func, operand_id, target, target_nullable,
),
}
}
///|
fn OiDescriptorExactMissOperandRelationFact::proves_exact_target_miss(
self : OiDescriptorExactMissOperandRelationFact,
) -> Bool {
self.ref_cast_operand_fact.proves_exact_target_miss()
}
///|
fn OiDescriptorExactMissOperandRelationFact::proves_operand_erases_without_effects(
self : OiDescriptorExactMissOperandRelationFact,
) -> Bool {
self.ref_cast_operand_fact.proves_operand_erases_without_effects()
}
///|
priv struct OiDescriptorExactMissFact {
cast_fact : OiDescriptorCastFact
descriptor_relation : OiDescriptorExactMissDescriptorRelationFact
operand_relation : OiDescriptorExactMissOperandRelationFact
preservation_fact : OiDescriptorExactMissPreservationFact
}
///|
fn OiDescriptorExactMissFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDescriptorExactMissFact? {
let cast_fact = match OiDescriptorCastFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let operand_drop_fact = OiDescriptorExactMissDropBeforeUnreachableFact::from_node(
func,
cast_fact.operand_id,
)
let descriptor_drop_fact = OiDescriptorExactMissDropBeforeUnreachableFact::from_node(
func,
cast_fact.descriptor_id,
)
let descriptor_relation = OiDescriptorExactMissDescriptorRelationFact::from_descriptor(
ctx,
func,
cast_fact.descriptor_id,
cast_fact.target,
)
let operand_relation = OiDescriptorExactMissOperandRelationFact::from_operand(
ctx,
func,
cast_fact.operand_id,
cast_fact.target,
cast_fact.nullable,
)
Some({
cast_fact,
descriptor_relation,
operand_relation,
preservation_fact: OiDescriptorExactMissPreservationFact::from_operands(
OiDescriptorOperandEraseFact::from_operand(
ctx,
func,
dead,
cast_fact.descriptor_id,
).proves_erases_without_effects(),
operand_relation.proves_operand_erases_without_effects(),
operand_drop_fact,
descriptor_drop_fact,
),
})
}
///|
priv enum OiDescriptorExactMissRewritePlanKind {
DescriptorExactMissUnreachable
DescriptorExactMissDropOperandThenUnreachable
DescriptorExactMissDropDescriptorThenUnreachable
DescriptorExactMissDropBothThenUnreachable
}
///|
priv enum OiDescriptorExactMissTrapModeKind {
DescriptorExactMissTrapModeAllowsCleanup
DescriptorExactMissTrapModeDisablesCleanup
}
///|
priv struct OiDescriptorExactMissTrapModeFact {
kind : OiDescriptorExactMissTrapModeKind
}
///|
fn OiDescriptorExactMissTrapModeFact::from_context(
ctx : HotPassContext,
) -> OiDescriptorExactMissTrapModeFact {
// Binaryen v130 descriptor-equality miss cleanup is a trap-producing rewrite,
// so it is only represented in normal trapping mode. Keep this mode gate
// separate from source/target proofs and source-order preservation outcomes.
if ctx.traps_never_happen || ctx.ignore_implicit_traps {
{ kind: DescriptorExactMissTrapModeDisablesCleanup, }
} else {
{ kind: DescriptorExactMissTrapModeAllowsCleanup, }
}
}
///|
priv enum OiDescriptorExactMissTargetEvidenceKind {
DescriptorExactMissTargetEvidenceSatisfied
DescriptorExactMissDescriptorNotExactTarget
DescriptorExactMissOperandNotMissTarget
}
///|
priv struct OiDescriptorExactMissTargetEvidenceFact {
kind : OiDescriptorExactMissTargetEvidenceKind
}
///|
fn OiDescriptorExactMissTargetEvidenceFact::from_miss(
miss_fact : OiDescriptorExactMissFact,
) -> OiDescriptorExactMissTargetEvidenceFact {
// The exact-miss rewrite needs both halves of Binaryen v130's
// descriptor-equality cast proof: the descriptor must exactly describe the
// target, and the reference operand must be known to miss that exact target.
if !miss_fact.descriptor_relation.proves_exactly_describes_target() {
return { kind: DescriptorExactMissDescriptorNotExactTarget, }
}
if !miss_fact.operand_relation.proves_exact_target_miss() {
return { kind: DescriptorExactMissOperandNotMissTarget, }
}
{ kind: DescriptorExactMissTargetEvidenceSatisfied, }
}
///|
priv enum OiDescriptorExactMissPreconditionKind {
DescriptorExactMissPreconditionSatisfied
DescriptorExactMissTrapsDisabled
DescriptorExactMissDescriptorPreconditionFailed
DescriptorExactMissOperandPreconditionFailed
}
///|
priv struct OiDescriptorExactMissPreconditionFact {
kind : OiDescriptorExactMissPreconditionKind
trap_mode_fact : OiDescriptorExactMissTrapModeFact
target_evidence_fact : OiDescriptorExactMissTargetEvidenceFact
}
///|
fn OiDescriptorExactMissPreconditionFact::from_miss(
ctx : HotPassContext,
miss_fact : OiDescriptorExactMissFact,
) -> OiDescriptorExactMissPreconditionFact {
// Keep the Binaryen v130 exact-miss preconditions factored ahead of the
// source-order preservation decision: trap mode decides whether a trap rewrite
// is legal, while target evidence decides whether the cast is a known miss.
let trap_mode_fact = OiDescriptorExactMissTrapModeFact::from_context(ctx)
let target_evidence_fact = OiDescriptorExactMissTargetEvidenceFact::from_miss(
miss_fact,
)
let kind = match trap_mode_fact.kind {
DescriptorExactMissTrapModeDisablesCleanup =>
DescriptorExactMissTrapsDisabled
DescriptorExactMissTrapModeAllowsCleanup =>
match target_evidence_fact.kind {
DescriptorExactMissTargetEvidenceSatisfied =>
DescriptorExactMissPreconditionSatisfied
DescriptorExactMissDescriptorNotExactTarget =>
DescriptorExactMissDescriptorPreconditionFailed
DescriptorExactMissOperandNotMissTarget =>
DescriptorExactMissOperandPreconditionFailed
}
}
{ kind, trap_mode_fact, target_evidence_fact, }
}
///|
fn OiDescriptorExactMissPreconditionFact::proves_exact_miss_rewrite_allowed(
self : OiDescriptorExactMissPreconditionFact,
) -> Bool {
let trap_mode_allows_cleanup = match self.trap_mode_fact.kind {
DescriptorExactMissTrapModeAllowsCleanup => true
DescriptorExactMissTrapModeDisablesCleanup => false
}
let target_evidence_satisfied = match self.target_evidence_fact.kind {
DescriptorExactMissTargetEvidenceSatisfied => true
DescriptorExactMissDescriptorNotExactTarget => false
DescriptorExactMissOperandNotMissTarget => false
}
match self.kind {
DescriptorExactMissPreconditionSatisfied =>
trap_mode_allows_cleanup && target_evidence_satisfied
DescriptorExactMissTrapsDisabled => false
DescriptorExactMissDescriptorPreconditionFailed => false
DescriptorExactMissOperandPreconditionFailed => false
}
}
///|
priv struct OiDescriptorExactMissRewritePlanFact {
kind : OiDescriptorExactMissRewritePlanKind
miss_fact : OiDescriptorExactMissFact
}
///|
fn OiDescriptorExactMissRewritePlanFact::from_miss(
ctx : HotPassContext,
miss_fact : OiDescriptorExactMissFact,
) -> OiDescriptorExactMissRewritePlanFact? {
// Binaryen v130 `visitRefCast`/descriptor-equality miss cleanup only runs in
// normal trapping mode. Keep the source-level replacement choice as a plan
// fact so descriptor casts do not hide a visitor-local drop/unreachable
// branch cascade.
let precondition_fact = OiDescriptorExactMissPreconditionFact::from_miss(
ctx, miss_fact,
)
if !precondition_fact.proves_exact_miss_rewrite_allowed() {
return None
}
let kind = match miss_fact.preservation_fact.kind {
DescriptorExactMissPreserveUnsupported => return None
DescriptorExactMissPreserveBothErase => DescriptorExactMissUnreachable
DescriptorExactMissPreserveDescriptorEraseOperandDrop =>
DescriptorExactMissDropOperandThenUnreachable
DescriptorExactMissPreserveOperandEraseDescriptorDrop =>
DescriptorExactMissDropDescriptorThenUnreachable
DescriptorExactMissPreserveBothDrop =>
DescriptorExactMissDropBothThenUnreachable
}
Some({ kind, miss_fact, })
}
///|
fn OiDescriptorExactMissRewritePlanFact::apply(
self : OiDescriptorExactMissRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let cast_fact = self.miss_fact.cast_fact
match self.kind {
DescriptorExactMissUnreachable => {
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
}
DescriptorExactMissDropOperandThenUnreachable =>
optimize_instructions_replace_with_drop_then_unreachable(
ctx,
func,
node_id,
cast_fact.operand_id,
)
DescriptorExactMissDropDescriptorThenUnreachable =>
optimize_instructions_replace_with_drop_then_unreachable(
ctx,
func,
node_id,
cast_fact.descriptor_id,
)
DescriptorExactMissDropBothThenUnreachable =>
optimize_instructions_replace_with_drop_drop_then_unreachable(
ctx,
func,
node_id,
cast_fact.operand_id,
cast_fact.descriptor_id,
)
}
}
///|
priv struct OiDescriptorOperandTargetRelationFact {
relation_fact : OiRefProducerTargetRelationFact
}
///|
fn OiDescriptorOperandTargetRelationFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiDescriptorOperandTargetRelationFact {
// Descriptor equality casts reuse Binaryen v130's source/target relation for
// the reference operand before TNH skip and result-narrowing decisions. Keep
// that producer-target relation nested here so descriptor consumers read one
// source-backed fact instead of parallel match/nullability/exactness booleans.
{
relation_fact: OiRefProducerTargetRelationFact::from_producer(
ctx,
OiRefProducerFact::from_node(ctx, func, operand_id),
target,
target_nullable,
),
}
}
///|
fn OiDescriptorOperandTargetRelationFact::proves_matches_or_subtypes_target(
self : OiDescriptorOperandTargetRelationFact,
) -> Bool {
self.relation_fact.subtype_relation.proves_matches_heap()
}
///|
fn OiDescriptorOperandTargetRelationFact::proves_nullable_target(
self : OiDescriptorOperandTargetRelationFact,
) -> Bool {
self.relation_fact.subtype_relation.proves_nullable()
}
///|
fn OiDescriptorOperandTargetRelationFact::proves_exact_target(
self : OiDescriptorOperandTargetRelationFact,
) -> Bool {
self.relation_fact.subtype_relation.proves_exact()
}
///|
fn OiDescriptorOperandTargetRelationFact::proves_exact_heap_target(
self : OiDescriptorOperandTargetRelationFact,
) -> Bool {
self.relation_fact.proves_exact_heap_target()
}
///|
priv struct OiDescriptorTnhSkipFact {
cast_fact : OiDescriptorCastFact
descriptor_fact : OiDescriptorOperandFact
operand_relation : OiDescriptorOperandTargetRelationFact
}
///|
fn OiDescriptorTnhSkipFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDescriptorTnhSkipFact? {
let cast_fact = match OiDescriptorCastFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
Some({
cast_fact,
descriptor_fact: OiDescriptorOperandFact::from_operand(
ctx,
func,
dead,
cast_fact.descriptor_id,
),
operand_relation: OiDescriptorOperandTargetRelationFact::from_operand(
ctx,
func,
cast_fact.operand_id,
cast_fact.target,
cast_fact.nullable,
),
})
}
///|
priv enum OiDescriptorTnhSkipTargetCompatibilityKind {
DescriptorTnhSkipTargetCompatible
DescriptorTnhSkipOperandMissesTarget
DescriptorTnhSkipTargetNullabilityMismatch
DescriptorTnhSkipTargetExactnessMismatch
}
///|
priv struct OiDescriptorTnhSkipTargetCompatibilityFact {
kind : OiDescriptorTnhSkipTargetCompatibilityKind
}
///|
fn OiDescriptorTnhSkipTargetCompatibilityFact::from_skip(
skip_fact : OiDescriptorTnhSkipFact,
) -> OiDescriptorTnhSkipTargetCompatibilityFact {
// Binaryen v130 descriptor equality cast skipping first requires the reference
// operand to satisfy the cast target, including nullability and exactness.
// Keep those target-side blockers as their own fact before descriptor
// exactness preservation is considered.
if !skip_fact.operand_relation.proves_matches_or_subtypes_target() {
return { kind: DescriptorTnhSkipOperandMissesTarget, }
}
let cast_fact = skip_fact.cast_fact
if !cast_fact.nullable && skip_fact.operand_relation.proves_nullable_target() {
return { kind: DescriptorTnhSkipTargetNullabilityMismatch, }
}
if cast_fact.exact && !skip_fact.operand_relation.proves_exact_target() {
return { kind: DescriptorTnhSkipTargetExactnessMismatch, }
}
{ kind: DescriptorTnhSkipTargetCompatible, }
}
///|
fn OiDescriptorTnhSkipTargetCompatibilityFact::proves_target_compatible(
self : OiDescriptorTnhSkipTargetCompatibilityFact,
) -> Bool {
match self.kind {
DescriptorTnhSkipTargetCompatible => true
DescriptorTnhSkipOperandMissesTarget => false
DescriptorTnhSkipTargetNullabilityMismatch => false
DescriptorTnhSkipTargetExactnessMismatch => false
}
}
///|
priv enum OiDescriptorTnhSkipDescriptorExactnessKind {
DescriptorTnhSkipNoExactDescriptorPayload
DescriptorTnhSkipExactDescriptorNotSubtypePayload
DescriptorTnhSkipExactDescriptorAlreadyExactTarget
DescriptorTnhSkipExactnessMayDropForEscapingRefGetDesc
DescriptorTnhSkipDescriptorExactnessPreserved
}
///|
priv struct OiDescriptorTnhSkipDescriptorExactnessFact {
kind : OiDescriptorTnhSkipDescriptorExactnessKind
}
///|
fn OiDescriptorTnhSkipDescriptorExactnessFact::from_skip(
ctx : HotPassContext,
func : @ir.HotFunc,
skip_fact : OiDescriptorTnhSkipFact,
) -> OiDescriptorTnhSkipDescriptorExactnessFact {
// Binaryen v130's TNH descriptor equality skip must not erase a descriptor
// operand whose exact-described heap is a stronger proof than the cast target.
// Store the exact-descriptor outcome separately from operand/target
// compatibility so the rewrite plan consumes source-backed preconditions.
let cast_fact = skip_fact.cast_fact
match skip_fact.descriptor_fact.type_fact.exact_described_heap {
Some(described_heap) => {
if !OiHeapRelationFact::from_heaps(ctx, described_heap, cast_fact.target).proves_known_subtype() {
return { kind: DescriptorTnhSkipExactDescriptorNotSubtypePayload, }
}
let described_relation = OiDescriptorOperandTargetRelationFact::from_operand(
ctx,
func,
cast_fact.operand_id,
described_heap,
false,
)
if described_relation.proves_exact_heap_target() {
return { kind: DescriptorTnhSkipExactDescriptorAlreadyExactTarget, }
}
if skip_fact.descriptor_fact.proves_may_drop_exactness_for_tnh_escaping_ref_get_desc() {
return { kind: DescriptorTnhSkipExactnessMayDropForEscapingRefGetDesc, }
}
{ kind: DescriptorTnhSkipDescriptorExactnessPreserved, }
}
None => { kind: DescriptorTnhSkipNoExactDescriptorPayload, }
}
}
///|
fn OiDescriptorTnhSkipDescriptorExactnessFact::proves_descriptor_exactness_may_be_skipped(
self : OiDescriptorTnhSkipDescriptorExactnessFact,
) -> Bool {
match self.kind {
DescriptorTnhSkipDescriptorExactnessPreserved => false
DescriptorTnhSkipNoExactDescriptorPayload => true
DescriptorTnhSkipExactDescriptorNotSubtypePayload => true
DescriptorTnhSkipExactDescriptorAlreadyExactTarget => true
DescriptorTnhSkipExactnessMayDropForEscapingRefGetDesc => true
}
}
///|
priv enum OiDescriptorTnhSkipPreconditionKind {
DescriptorTnhSkipTrapsActive
DescriptorTnhSkipTargetBlocked
DescriptorTnhSkipDescriptorExactnessBlocked
DescriptorTnhSkipAllowed
}
///|
priv struct OiDescriptorTnhSkipPreconditionFact {
kind : OiDescriptorTnhSkipPreconditionKind
target_fact : OiDescriptorTnhSkipTargetCompatibilityFact?
descriptor_exactness_fact : OiDescriptorTnhSkipDescriptorExactnessFact?
}
///|
fn OiDescriptorTnhSkipPreconditionFact::from_skip(
ctx : HotPassContext,
func : @ir.HotFunc,
skip_fact : OiDescriptorTnhSkipFact,
) -> OiDescriptorTnhSkipPreconditionFact {
// Binaryen v130 removes descriptor equality casts under TNH only after the
// operand/target relation is compatible and exact descriptor operands are not
// preserving a stronger proof. Keep those two source preconditions as nested
// facts instead of one eligibility result with local boolean checks.
if !ctx.traps_never_happen {
return {
kind: DescriptorTnhSkipTrapsActive,
target_fact: None,
descriptor_exactness_fact: None,
}
}
let target_fact = OiDescriptorTnhSkipTargetCompatibilityFact::from_skip(
skip_fact,
)
if !target_fact.proves_target_compatible() {
return {
kind: DescriptorTnhSkipTargetBlocked,
target_fact: Some(target_fact),
descriptor_exactness_fact: None,
}
}
let descriptor_exactness_fact = OiDescriptorTnhSkipDescriptorExactnessFact::from_skip(
ctx, func, skip_fact,
)
if !descriptor_exactness_fact.proves_descriptor_exactness_may_be_skipped() {
return {
kind: DescriptorTnhSkipDescriptorExactnessBlocked,
target_fact: Some(target_fact),
descriptor_exactness_fact: Some(descriptor_exactness_fact),
}
}
{
kind: DescriptorTnhSkipAllowed,
target_fact: Some(target_fact),
descriptor_exactness_fact: Some(descriptor_exactness_fact),
}
}
///|
fn OiDescriptorTnhSkipPreconditionFact::proves_tnh_skip_allowed(
self : OiDescriptorTnhSkipPreconditionFact,
) -> Bool {
match self.kind {
DescriptorTnhSkipAllowed => {
let target_allowed = match self.target_fact {
Some(fact) => fact.proves_target_compatible()
None => false
}
let descriptor_allowed = match self.descriptor_exactness_fact {
Some(fact) => fact.proves_descriptor_exactness_may_be_skipped()
None => false
}
target_allowed && descriptor_allowed
}
DescriptorTnhSkipTrapsActive => false
DescriptorTnhSkipTargetBlocked => false
DescriptorTnhSkipDescriptorExactnessBlocked => false
}
}
///|
priv enum OiDescriptorTnhSkipRewritePlanKind {
DescriptorTnhSkipForwardOperand
DescriptorTnhSkipLocalizeOperandThenDescriptor
DescriptorTnhSkipLocalSetDescriptorThenOperand
DescriptorTnhSkipDropDescriptorThenOperand
}
///|
priv struct OiDescriptorTnhSkipRewritePlanFact {
kind : OiDescriptorTnhSkipRewritePlanKind
skip_fact : OiDescriptorTnhSkipFact
}
///|
fn OiDescriptorTnhSkipRewritePlanFact::from_skip(
ctx : HotPassContext,
func : @ir.HotFunc,
skip_fact : OiDescriptorTnhSkipFact,
) -> OiDescriptorTnhSkipRewritePlanFact? {
// Binaryen v130 removes descriptor equality casts under TNH only after the
// reference operand is known to match and the descriptor check can be treated
// as non-trapping. Preserve the replacement-order decision as a fact: erase
// the descriptor, localize around ordered effects, localize descriptor-only
// values, or append a descriptor drop.
let precondition_fact = OiDescriptorTnhSkipPreconditionFact::from_skip(
ctx, func, skip_fact,
)
if !precondition_fact.proves_tnh_skip_allowed() {
return None
}
let cast_fact = skip_fact.cast_fact
let descriptor_fact = skip_fact.descriptor_fact
let kind = if descriptor_fact.proves_erases_without_effects() {
DescriptorTnhSkipForwardOperand
} else if OiDescriptorSkipOrderFact::from_node(func, cast_fact.operand_id).proves_must_preserve_operand_order() {
DescriptorTnhSkipLocalizeOperandThenDescriptor
} else if descriptor_fact.proves_should_localize_when_skipped() {
DescriptorTnhSkipLocalSetDescriptorThenOperand
} else {
DescriptorTnhSkipDropDescriptorThenOperand
}
Some({ kind, skip_fact, })
}
///|
fn OiDescriptorTnhSkipRewritePlanFact::apply(
self : OiDescriptorTnhSkipRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let cast_fact = self.skip_fact.cast_fact
match self.kind {
DescriptorTnhSkipForwardOperand => {
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
cast_fact.operand_id,
)
true
}
_ => {
ignore(
optimize_instructions_try_remove_ref_get_desc_child_null_check_traps_never_happen(
ctx,
func,
cast_fact.descriptor_id,
) ||
optimize_instructions_try_move_ref_get_desc_child_null_check(
ctx,
func,
cast_fact.descriptor_id,
),
)
match self.kind {
DescriptorTnhSkipLocalizeOperandThenDescriptor =>
optimize_instructions_replace_with_localized_node_then_descriptor_then_local_get(
ctx,
func,
node_id,
cast_fact.operand_id,
cast_fact.descriptor_id,
)
DescriptorTnhSkipLocalSetDescriptorThenOperand =>
optimize_instructions_replace_with_local_set_then_node_id(
ctx,
func,
node_id,
cast_fact.descriptor_id,
cast_fact.operand_id,
)
DescriptorTnhSkipDropDescriptorThenOperand =>
optimize_instructions_replace_with_drop_then_node_id(
ctx,
func,
node_id,
cast_fact.descriptor_id,
cast_fact.operand_id,
)
DescriptorTnhSkipForwardOperand => false
}
}
}
}
///|
priv enum OiDescriptorNullCastOperandKind {
DescriptorNullCastOperandKnownNull
DescriptorNullCastOperandNotKnownNull
}
///|
priv struct OiDescriptorNullCastOperandFact {
kind : OiDescriptorNullCastOperandKind
}
///|
fn OiDescriptorNullCastOperandFact::from_cast(
func : @ir.HotFunc,
cast_fact : OiDescriptorCastFact,
) -> OiDescriptorNullCastOperandFact {
// Binaryen v130 `optimizeKnownCastResult` reaches the descriptor-cast
// known-null path only after the reference operand has a known-null proof.
// Keep that source proof as a payload fact instead of a direct visitor-local
// predicate so later null-cast rewrite facts consume explicit evidence.
if OiRefFact::from_node(func, cast_fact.operand_id).proves_known_null() {
{ kind: DescriptorNullCastOperandKnownNull, }
} else {
{ kind: DescriptorNullCastOperandNotKnownNull, }
}
}
///|
fn OiDescriptorNullCastOperandFact::proves_known_null(
self : OiDescriptorNullCastOperandFact,
) -> Bool {
match self.kind {
DescriptorNullCastOperandKnownNull => true
DescriptorNullCastOperandNotKnownNull => false
}
}
///|
priv struct OiDescriptorNullCastFact {
cast_fact : OiDescriptorCastFact
operand_fact : OiDescriptorNullCastOperandFact
descriptor_fact : OiDescriptorOperandFact
}
///|
fn OiDescriptorNullCastFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDescriptorNullCastFact? {
let cast_fact = match OiDescriptorCastFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let operand_fact = OiDescriptorNullCastOperandFact::from_cast(func, cast_fact)
if !operand_fact.proves_known_null() {
return None
}
Some({
cast_fact,
operand_fact,
descriptor_fact: OiDescriptorOperandFact::from_operand(
ctx,
func,
dead,
cast_fact.descriptor_id,
),
})
}
///|
priv enum OiDescriptorNullCastRewritePlanKind {
DescriptorNullCastUnreachable
DescriptorNullCastTypedNull
DescriptorNullCastDropDescriptorThenTypedNull
DescriptorNullCastDropDescriptorThenUnreachable
}
///|
priv enum OiDescriptorNullCastRewriteScopeKind {
DescriptorNullCastDescriptorTrapScope
DescriptorNullCastFullKnownNullScope
}
///|
priv struct OiDescriptorNullCastRewriteScopeFact {
kind : OiDescriptorNullCastRewriteScopeKind
}
///|
fn OiDescriptorNullCastRewriteScopeFact::descriptor_trap_scope() -> OiDescriptorNullCastRewriteScopeFact {
// The descriptor-operand visitor only wants the no-effect trap proof: if the
// descriptor operand itself traps, the whole descriptor cast can become
// unreachable, but nullable typed-null materialization belongs to the full
// known-null cast visitor below.
{ kind: DescriptorNullCastDescriptorTrapScope, }
}
///|
fn OiDescriptorNullCastRewriteScopeFact::full_known_null_scope() -> OiDescriptorNullCastRewriteScopeFact {
// The ordinary descriptor known-null cast visitor may produce either typed
// nulls or unreachable replacements after descriptor effects are preserved.
{ kind: DescriptorNullCastFullKnownNullScope, }
}
///|
fn OiDescriptorNullCastRewriteScopeFact::proves_descriptor_trap_scope(
self : OiDescriptorNullCastRewriteScopeFact,
) -> Bool {
match self.kind {
DescriptorNullCastDescriptorTrapScope => true
DescriptorNullCastFullKnownNullScope => false
}
}
///|
priv struct OiDescriptorNullCastRewritePlanFact {
kind : OiDescriptorNullCastRewritePlanKind
null_fact : OiDescriptorNullCastFact
}
///|
fn OiDescriptorNullCastRewritePlanFact::from_null_cast(
null_fact : OiDescriptorNullCastFact,
scope_fact : OiDescriptorNullCastRewriteScopeFact,
) -> OiDescriptorNullCastRewritePlanFact? {
// Binaryen v130 `optimizeKnownCastResult` has a descriptor-cast special case
// for known-null reference operands: materialize the typed null for nullable
// casts, trap for non-nullable casts, and preserve the descriptor operand only
// when its evaluation cannot be erased. Keep both the caller scope and the
// source-order decision in facts so the visitor no longer owns a null/ref/
// descriptor erase cascade.
let cast_fact = null_fact.cast_fact
if !null_fact.operand_fact.proves_known_null() {
return None
}
let descriptor_fact = null_fact.descriptor_fact
if descriptor_fact.proves_traps_without_effects() {
return Some({ kind: DescriptorNullCastUnreachable, null_fact, })
}
if scope_fact.proves_descriptor_trap_scope() {
return None
}
let kind = if cast_fact.nullable {
if descriptor_fact.proves_erases_without_effects() {
DescriptorNullCastTypedNull
} else {
DescriptorNullCastDropDescriptorThenTypedNull
}
} else if descriptor_fact.proves_erases_without_effects() {
DescriptorNullCastUnreachable
} else {
DescriptorNullCastDropDescriptorThenUnreachable
}
Some({ kind, null_fact, })
}
///|
fn OiDescriptorNullCastRewritePlanFact::apply(
self : OiDescriptorNullCastRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let cast_fact = self.null_fact.cast_fact
match self.kind {
DescriptorNullCastUnreachable => {
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
}
DescriptorNullCastTypedNull => {
let typed_null_id = @ir.hot_build_ref_null(
func,
@lib.RefType::new(true, cast_fact.target, exact=cast_fact.exact),
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, typed_null_id,
)
true
}
DescriptorNullCastDropDescriptorThenTypedNull => {
let typed_null_id = @ir.hot_build_ref_null(
func,
@lib.RefType::new(true, cast_fact.target, exact=cast_fact.exact),
)
optimize_instructions_replace_with_drop_then_node_id(
ctx,
func,
node_id,
cast_fact.descriptor_id,
typed_null_id,
)
}
DescriptorNullCastDropDescriptorThenUnreachable =>
optimize_instructions_replace_with_drop_then_unreachable(
ctx,
func,
node_id,
cast_fact.descriptor_id,
)
}
}
///|
priv enum OiDescriptorExactSuccessTargetKind {
DescriptorExactSuccessTargetExactNonNull
DescriptorExactSuccessTargetNullable
DescriptorExactSuccessTargetInexact
}
///|
priv struct OiDescriptorExactSuccessTargetFact {
kind : OiDescriptorExactSuccessTargetKind
}
///|
fn OiDescriptorExactSuccessTargetFact::from_cast(
cast_fact : OiDescriptorCastFact,
) -> OiDescriptorExactSuccessTargetFact {
// Binaryen v130's descriptor equality cast skip under traps-never-happen
// first requires the cast target itself to be exact and non-null before the
// reference operand and descriptor facts can prove the same target. Keep that
// target precondition as a source-shaped payload instead of parallel nullable
// and exactness blockers in the exact-success aggregate precondition.
if cast_fact.nullable {
return { kind: DescriptorExactSuccessTargetNullable, }
}
if !cast_fact.exact {
return { kind: DescriptorExactSuccessTargetInexact, }
}
{ kind: DescriptorExactSuccessTargetExactNonNull, }
}
///|
priv enum OiDescriptorExactSuccessOperandKind {
DescriptorExactSuccessOperandExactNonNullTarget
DescriptorExactSuccessOperandUnsupported
}
///|
priv struct OiDescriptorExactSuccessOperandFact {
kind : OiDescriptorExactSuccessOperandKind
}
///|
fn OiDescriptorExactSuccessOperandFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_id : Int32,
target : @lib.HeapType,
target_nullable : Bool,
) -> OiDescriptorExactSuccessOperandFact {
// Binaryen v130's descriptor exact-success skip only applies after the
// reference operand is a call-like producer whose result is already exact,
// non-null, and on the descriptor target heap. Keep that operand proof as a
// separate source fact before TNH exact-success preconditions consume it.
let kind = match @ir.hot_node_get(func, operand_id).op {
@ir.HotOp::Call | @ir.HotOp::CallIndirect | @ir.HotOp::CallRef =>
if OiRefProducerTargetRelationFact::from_producer(
ctx,
OiRefProducerFact::from_node(ctx, func, operand_id),
target,
target_nullable,
).proves_exact_non_null_heap_target() {
DescriptorExactSuccessOperandExactNonNullTarget
} else {
DescriptorExactSuccessOperandUnsupported
}
_ => DescriptorExactSuccessOperandUnsupported
}
{ kind, }
}
///|
fn OiDescriptorExactSuccessOperandFact::proves_exact_non_null_target(
self : OiDescriptorExactSuccessOperandFact,
) -> Bool {
match self.kind {
DescriptorExactSuccessOperandExactNonNullTarget => true
DescriptorExactSuccessOperandUnsupported => false
}
}
///|
priv struct OiDescriptorExactSuccessDescriptorRelationFact {
descriptor_type_fact : OiDescriptorTypeFact
target_relation_fact : OiDescriptorExactTargetRelationFact
}
///|
fn OiDescriptorExactSuccessDescriptorRelationFact::from_descriptor(
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
target : @lib.HeapType,
) -> OiDescriptorExactSuccessDescriptorRelationFact {
// Binaryen v130 descriptor equality exact-success skipping under TNH needs a
// descriptor operand that exactly describes the cast target, just like the
// normal-mode exact-miss cleanup. Keep this descriptor source relation nested
// under the exact-success fact so preconditions consume source-backed operand
// and descriptor proofs rather than parallel booleans.
let descriptor_type_fact = OiDescriptorTypeFact::from_node(
ctx, func, descriptor_id,
)
{
descriptor_type_fact,
target_relation_fact: OiDescriptorExactTargetRelationFact::from_descriptor_type(
descriptor_type_fact, target,
),
}
}
///|
fn OiDescriptorExactSuccessDescriptorRelationFact::proves_exactly_describes_target(
self : OiDescriptorExactSuccessDescriptorRelationFact,
) -> Bool {
ignore(self.descriptor_type_fact)
self.target_relation_fact.proves_exactly_describes_target()
}
///|
priv struct OiDescriptorExactSuccessFact {
cast_fact : OiDescriptorCastFact
target_fact : OiDescriptorExactSuccessTargetFact
operand_fact : OiDescriptorExactSuccessOperandFact
descriptor_relation : OiDescriptorExactSuccessDescriptorRelationFact
}
///|
fn OiDescriptorExactSuccessFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorExactSuccessFact? {
let cast_fact = match OiDescriptorCastFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
Some({
cast_fact,
target_fact: OiDescriptorExactSuccessTargetFact::from_cast(cast_fact),
operand_fact: OiDescriptorExactSuccessOperandFact::from_operand(
ctx,
func,
cast_fact.operand_id,
cast_fact.target,
cast_fact.nullable,
),
descriptor_relation: OiDescriptorExactSuccessDescriptorRelationFact::from_descriptor(
ctx,
func,
cast_fact.descriptor_id,
cast_fact.target,
),
})
}
///|
priv enum OiDescriptorExactSuccessTrapModeKind {
DescriptorExactSuccessTrapModeActive
DescriptorExactSuccessTrapModeNeverHappens
}
///|
priv struct OiDescriptorExactSuccessTrapModeFact {
kind : OiDescriptorExactSuccessTrapModeKind
}
///|
fn OiDescriptorExactSuccessTrapModeFact::from_context(
ctx : HotPassContext,
) -> OiDescriptorExactSuccessTrapModeFact {
// Binaryen v130 only skips descriptor equality casts for exact successes in
// traps-never-happen mode. Keep that mode gate as its own precondition fact
// instead of folding it into the source-relation checks.
let kind = if ctx.traps_never_happen {
DescriptorExactSuccessTrapModeNeverHappens
} else {
DescriptorExactSuccessTrapModeActive
}
{ kind, }
}
///|
fn OiDescriptorExactSuccessTrapModeFact::proves_traps_never_happen(
self : OiDescriptorExactSuccessTrapModeFact,
) -> Bool {
match self.kind {
DescriptorExactSuccessTrapModeActive => false
DescriptorExactSuccessTrapModeNeverHappens => true
}
}
///|
priv enum OiDescriptorExactSuccessSourcePreconditionKind {
DescriptorExactSuccessOperandNotExactNonNullTarget
DescriptorExactSuccessDescriptorNotExactTarget
DescriptorExactSuccessSourceAllowed
}
///|
priv struct OiDescriptorExactSuccessSourcePreconditionFact {
operand_fact : OiDescriptorExactSuccessOperandFact
descriptor_relation : OiDescriptorExactSuccessDescriptorRelationFact
kind : OiDescriptorExactSuccessSourcePreconditionKind
}
///|
fn OiDescriptorExactSuccessSourcePreconditionFact::from_success(
success_fact : OiDescriptorExactSuccessFact,
) -> OiDescriptorExactSuccessSourcePreconditionFact {
// Binaryen v130's TNH exact-success skip needs both source operands to prove
// the same exact non-null target relation: the reference producer must
// already be exact/non-null, and the descriptor operand must exactly describe
// the cast target. Group those two source facts before the aggregate
// precondition combines them with trap mode and target-shape facts.
let operand_fact = success_fact.operand_fact
let descriptor_relation = success_fact.descriptor_relation
let kind = if !operand_fact.proves_exact_non_null_target() {
DescriptorExactSuccessOperandNotExactNonNullTarget
} else if !descriptor_relation.proves_exactly_describes_target() {
DescriptorExactSuccessDescriptorNotExactTarget
} else {
DescriptorExactSuccessSourceAllowed
}
{ operand_fact, descriptor_relation, kind, }
}
///|
fn OiDescriptorExactSuccessSourcePreconditionFact::proves_sources_allowed(
self : OiDescriptorExactSuccessSourcePreconditionFact,
) -> Bool {
ignore(self.operand_fact)
ignore(self.descriptor_relation)
match self.kind {
DescriptorExactSuccessOperandNotExactNonNullTarget => false
DescriptorExactSuccessDescriptorNotExactTarget => false
DescriptorExactSuccessSourceAllowed => true
}
}
///|
priv enum OiDescriptorExactSuccessPreconditionKind {
DescriptorExactSuccessPreconditionTrapsActive
DescriptorExactSuccessPreconditionNullableTarget
DescriptorExactSuccessPreconditionInexactTarget
DescriptorExactSuccessPreconditionSourceRejected
DescriptorExactSuccessPreconditionAllowed
}
///|
priv struct OiDescriptorExactSuccessPreconditionFact {
trap_mode_fact : OiDescriptorExactSuccessTrapModeFact
target_fact : OiDescriptorExactSuccessTargetFact
source_precondition : OiDescriptorExactSuccessSourcePreconditionFact
kind : OiDescriptorExactSuccessPreconditionKind
}
///|
fn OiDescriptorExactSuccessPreconditionFact::from_success(
ctx : HotPassContext,
success_fact : OiDescriptorExactSuccessFact,
) -> OiDescriptorExactSuccessPreconditionFact {
// Binaryen v130's descriptor equality cast skip under traps-never-happen is
// fact-driven: mode, target shape, reference producer, and descriptor relation
// all have to agree before replacement-order planning can preserve the two
// source evaluations. Keep that as a nested precondition fact, not a flat
// eligibility result.
let trap_mode_fact = OiDescriptorExactSuccessTrapModeFact::from_context(ctx)
let target_fact = success_fact.target_fact
let source_precondition = OiDescriptorExactSuccessSourcePreconditionFact::from_success(
success_fact,
)
let kind = if !trap_mode_fact.proves_traps_never_happen() {
DescriptorExactSuccessPreconditionTrapsActive
} else {
match target_fact.kind {
DescriptorExactSuccessTargetNullable =>
DescriptorExactSuccessPreconditionNullableTarget
DescriptorExactSuccessTargetInexact =>
DescriptorExactSuccessPreconditionInexactTarget
DescriptorExactSuccessTargetExactNonNull =>
if source_precondition.proves_sources_allowed() {
DescriptorExactSuccessPreconditionAllowed
} else {
DescriptorExactSuccessPreconditionSourceRejected
}
}
}
{ trap_mode_fact, target_fact, source_precondition, kind, }
}
///|
fn OiDescriptorExactSuccessPreconditionFact::proves_tnh_exact_success_allowed(
self : OiDescriptorExactSuccessPreconditionFact,
) -> Bool {
ignore(self.trap_mode_fact)
ignore(self.target_fact)
ignore(self.source_precondition)
match self.kind {
DescriptorExactSuccessPreconditionTrapsActive => false
DescriptorExactSuccessPreconditionNullableTarget => false
DescriptorExactSuccessPreconditionInexactTarget => false
DescriptorExactSuccessPreconditionSourceRejected => false
DescriptorExactSuccessPreconditionAllowed => true
}
}
///|
priv enum OiDescriptorExactSuccessRewritePlanKind {
DescriptorExactSuccessLocalizeOperandThenDescriptor
}
///|
priv struct OiDescriptorExactSuccessRewritePlanFact {
kind : OiDescriptorExactSuccessRewritePlanKind
success_fact : OiDescriptorExactSuccessFact
}
///|
fn OiDescriptorExactSuccessRewritePlanFact::from_success(
ctx : HotPassContext,
success_fact : OiDescriptorExactSuccessFact,
) -> OiDescriptorExactSuccessRewritePlanFact? {
// Binaryen v130 lets descriptor casts be skipped in traps-never-happen mode
// when the reference value and descriptor are both exact known matches. The
// represented Starshine subset is intentionally narrower than Binaryen's
// full `optimizeKnownCastResult`: it requires a direct call-style reference
// producer and an exact descriptor type fact, then preserves both operand
// evaluations through the same ChildLocalizer-shaped replacement.
let precondition_fact = OiDescriptorExactSuccessPreconditionFact::from_success(
ctx, success_fact,
)
if !precondition_fact.proves_tnh_exact_success_allowed() {
return None
}
Some({
kind: DescriptorExactSuccessLocalizeOperandThenDescriptor,
success_fact,
})
}
///|
fn OiDescriptorExactSuccessRewritePlanFact::apply(
self : OiDescriptorExactSuccessRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
DescriptorExactSuccessLocalizeOperandThenDescriptor =>
optimize_instructions_replace_with_localized_node_then_descriptor_then_local_get(
ctx,
func,
node_id,
self.success_fact.cast_fact.operand_id,
self.success_fact.cast_fact.descriptor_id,
)
}
}
///|
priv enum OiDescriptorDescribedSubtypeKind {
DescriptorDescribedSubtypeUnknown
DescriptorDescribedHeapSubtypesTarget
}
///|
priv struct OiDescriptorDescribedSubtypeFact {
kind : OiDescriptorDescribedSubtypeKind
}
///|
fn OiDescriptorDescribedSubtypeFact::from_exact_described_heap(
ctx : HotPassContext,
exact_described_heap : @lib.HeapType?,
target : @lib.HeapType,
) -> OiDescriptorDescribedSubtypeFact {
// Binaryen v130 descriptor-cast result narrowing may replace the cast target
// with the heap exactly described by the descriptor, but only when that heap is
// known to subtype the original target. Keep the subtype proof separate from
// the exact-described-heap payload instead of a parallel boolean.
match exact_described_heap {
Some(described_heap) if OiHeapRelationFact::from_heaps(
ctx, described_heap, target,
).proves_known_subtype() =>
{ kind: DescriptorDescribedHeapSubtypesTarget, }
_ => { kind: DescriptorDescribedSubtypeUnknown, }
}
}
///|
fn OiDescriptorDescribedSubtypeFact::proves_described_heap_subtypes_target(
self : OiDescriptorDescribedSubtypeFact,
) -> Bool {
match self.kind {
DescriptorDescribedHeapSubtypesTarget => true
DescriptorDescribedSubtypeUnknown => false
}
}
///|
priv enum OiDescriptorDescribedExactTargetKind {
DescriptorDescribedExactTargetUnknown
DescriptorExactlyDescribesTarget
}
///|
priv struct OiDescriptorDescribedExactTargetFact {
kind : OiDescriptorDescribedExactTargetKind
}
///|
fn OiDescriptorDescribedExactTargetFact::from_type_fact(
descriptor_type_fact : OiDescriptorTypeFact,
target : @lib.HeapType,
) -> OiDescriptorDescribedExactTargetFact {
// Binaryen v130 also preserves exactness when the descriptor exactly describes
// the cast target even if no narrower heap is selected. Carry that exact-target
// relation as its own source fact.
if descriptor_type_fact.exactly_describes(target) {
{ kind: DescriptorExactlyDescribesTarget, }
} else {
{ kind: DescriptorDescribedExactTargetUnknown, }
}
}
///|
fn OiDescriptorDescribedExactTargetFact::proves_exactly_describes_target(
self : OiDescriptorDescribedExactTargetFact,
) -> Bool {
match self.kind {
DescriptorExactlyDescribesTarget => true
DescriptorDescribedExactTargetUnknown => false
}
}
///|
priv struct OiDescriptorDescribedTargetRelationFact {
exact_described_heap : @lib.HeapType?
subtype_fact : OiDescriptorDescribedSubtypeFact
exact_target_fact : OiDescriptorDescribedExactTargetFact
}
///|
fn OiDescriptorDescribedTargetRelationFact::from_type_fact(
ctx : HotPassContext,
descriptor_type_fact : OiDescriptorTypeFact,
target : @lib.HeapType,
) -> OiDescriptorDescribedTargetRelationFact {
// Binaryen v130 descriptor-cast result narrowing first asks what heap the
// descriptor exactly describes, then whether that described heap can safely
// tighten the cast target. Keep that relation as a payload fact instead of
// parallel described-heap / subtype / exact-target fields on the rewrite fact.
let exact_described_heap = descriptor_type_fact.exact_described_heap
{
exact_described_heap,
subtype_fact: OiDescriptorDescribedSubtypeFact::from_exact_described_heap(
ctx, exact_described_heap, target,
),
exact_target_fact: OiDescriptorDescribedExactTargetFact::from_type_fact(
descriptor_type_fact, target,
),
}
}
///|
fn OiDescriptorDescribedTargetRelationFact::select_narrowed_target(
self : OiDescriptorDescribedTargetRelationFact,
target : @lib.HeapType,
) -> @lib.HeapType {
match self.exact_described_heap {
Some(described_heap) if self.subtype_fact.proves_described_heap_subtypes_target() =>
described_heap
_ => target
}
}
///|
fn OiDescriptorDescribedTargetRelationFact::proves_descriptor_exact_result(
self : OiDescriptorDescribedTargetRelationFact,
target : @lib.HeapType,
narrowed_target : @lib.HeapType,
) -> Bool {
narrowed_target != target ||
self.exact_target_fact.proves_exactly_describes_target()
}
///|
priv enum OiDescriptorNarrowedTargetKind {
DescriptorNarrowedTargetOriginal
DescriptorNarrowedTargetDescribedHeap
}
///|
priv struct OiDescriptorNarrowedTargetFact {
kind : OiDescriptorNarrowedTargetKind
target : @lib.HeapType
}
///|
fn OiDescriptorNarrowedTargetFact::from_cast(
cast_fact : OiDescriptorCastFact,
descriptor_relation : OiDescriptorDescribedTargetRelationFact,
) -> OiDescriptorNarrowedTargetFact {
// Binaryen v130 descriptor-cast result narrowing selects either the original
// cast target or the exact heap described by the descriptor when it is a safe
// subtype of the original target. Keep that heap selection separate from
// nullability and exactness so the narrowed result is built from source facts.
let target = descriptor_relation.select_narrowed_target(cast_fact.target)
if target == cast_fact.target {
{ kind: DescriptorNarrowedTargetOriginal, target, }
} else {
{ kind: DescriptorNarrowedTargetDescribedHeap, target, }
}
}
///|
priv enum OiDescriptorNarrowedNullabilityKind {
DescriptorNarrowedForcedNonNull
DescriptorNarrowedNonNullableCast
DescriptorNarrowedNullableOperand
DescriptorNarrowedNonNullableOperand
}
///|
priv struct OiDescriptorNarrowedNullabilityFact {
kind : OiDescriptorNarrowedNullabilityKind
nullable : Bool
}
///|
fn OiDescriptorNarrowedNullabilityFact::from_cast(
cast_fact : OiDescriptorCastFact,
force_non_null : Bool,
operand_relation : OiDescriptorOperandTargetRelationFact,
) -> OiDescriptorNarrowedNullabilityFact {
// Binaryen v130 descriptor-cast result nullability follows either the
// surrounding ref.as_non_null movement (force non-null) or the operand/target
// relation when the original descriptor cast is nullable. Keep those cases as
// selected facts instead of computing a bare conjunction in the result fact.
if force_non_null {
return { kind: DescriptorNarrowedForcedNonNull, nullable: false, }
}
if !cast_fact.nullable {
return { kind: DescriptorNarrowedNonNullableCast, nullable: false, }
}
if operand_relation.proves_nullable_target() {
{ kind: DescriptorNarrowedNullableOperand, nullable: true, }
} else {
{ kind: DescriptorNarrowedNonNullableOperand, nullable: false, }
}
}
///|
priv enum OiDescriptorNarrowedExactnessKind {
DescriptorNarrowedExactCast
DescriptorNarrowedExactDescriptorResult
DescriptorNarrowedInexactResult
}
///|
priv struct OiDescriptorNarrowedExactnessFact {
kind : OiDescriptorNarrowedExactnessKind
exact : Bool
}
///|
fn OiDescriptorNarrowedExactnessFact::from_cast(
cast_fact : OiDescriptorCastFact,
target_fact : OiDescriptorNarrowedTargetFact,
descriptor_relation : OiDescriptorDescribedTargetRelationFact,
) -> OiDescriptorNarrowedExactnessFact {
// Binaryen v130 preserves exactness either from the cast itself or from a
// descriptor relation that proves the narrowed target is exact. Keep the two
// source paths distinct before the final result type is assembled.
if cast_fact.exact {
return { kind: DescriptorNarrowedExactCast, exact: true, }
}
if descriptor_relation.proves_descriptor_exact_result(
cast_fact.target,
target_fact.target,
) {
{ kind: DescriptorNarrowedExactDescriptorResult, exact: true, }
} else {
{ kind: DescriptorNarrowedInexactResult, exact: false, }
}
}
///|
priv struct OiDescriptorNarrowedResultFact {
target_fact : OiDescriptorNarrowedTargetFact
nullability_fact : OiDescriptorNarrowedNullabilityFact
exactness_fact : OiDescriptorNarrowedExactnessFact
}
///|
fn OiDescriptorNarrowedResultFact::from_cast(
cast_fact : OiDescriptorCastFact,
force_non_null : Bool,
operand_relation : OiDescriptorOperandTargetRelationFact,
descriptor_relation : OiDescriptorDescribedTargetRelationFact,
) -> OiDescriptorNarrowedResultFact {
// Store the computed result type as nested fact payloads: target selection
// comes from the descriptor relation, nullability from operand/force-non-null
// facts, and exactness from either the cast or descriptor exact-result proof.
let target_fact = OiDescriptorNarrowedTargetFact::from_cast(
cast_fact, descriptor_relation,
)
let nullability_fact = OiDescriptorNarrowedNullabilityFact::from_cast(
cast_fact, force_non_null, operand_relation,
)
let exactness_fact = OiDescriptorNarrowedExactnessFact::from_cast(
cast_fact, target_fact, descriptor_relation,
)
{ target_fact, nullability_fact, exactness_fact, }
}
///|
fn OiDescriptorNarrowedResultFact::target(
self : OiDescriptorNarrowedResultFact,
) -> @lib.HeapType {
match self.target_fact.kind {
DescriptorNarrowedTargetOriginal => self.target_fact.target
DescriptorNarrowedTargetDescribedHeap => self.target_fact.target
}
}
///|
fn OiDescriptorNarrowedResultFact::nullable(
self : OiDescriptorNarrowedResultFact,
) -> Bool {
match self.nullability_fact.kind {
DescriptorNarrowedForcedNonNull => false
DescriptorNarrowedNonNullableCast => false
DescriptorNarrowedNullableOperand => self.nullability_fact.nullable
DescriptorNarrowedNonNullableOperand => false
}
}
///|
fn OiDescriptorNarrowedResultFact::exact(
self : OiDescriptorNarrowedResultFact,
) -> Bool {
match self.exactness_fact.kind {
DescriptorNarrowedExactCast => true
DescriptorNarrowedExactDescriptorResult => self.exactness_fact.exact
DescriptorNarrowedInexactResult => false
}
}
///|
priv enum OiDescriptorResultNarrowingControlKind {
DescriptorResultNarrowingDescriptorControlSafe
DescriptorResultNarrowingDescriptorControlEscapes
}
///|
priv struct OiDescriptorResultNarrowingControlFact {
kind : OiDescriptorResultNarrowingControlKind
}
///|
fn OiDescriptorResultNarrowingControlFact::from_descriptor(
func : @ir.HotFunc,
descriptor_id : Int32,
) -> OiDescriptorResultNarrowingControlFact {
// Binaryen v130 descriptor-cast result tightening refuses to reorder through
// descriptor operands with escaping branch control. Keep that descriptor-side
// source proof as its own fact instead of a direct boolean on the aggregate
// narrowing fact, so ordinary narrowing and surrounding ref.as_non_null
// movement consume the same control payload.
if OiDescriptorBranchControlFact::from_node(
func, descriptor_id, descriptor_id,
).proves_has_escaping_branch_control() {
{ kind: DescriptorResultNarrowingDescriptorControlEscapes, }
} else {
{ kind: DescriptorResultNarrowingDescriptorControlSafe, }
}
}
///|
fn OiDescriptorResultNarrowingControlFact::proves_descriptor_control_safe(
self : OiDescriptorResultNarrowingControlFact,
) -> Bool {
match self.kind {
DescriptorResultNarrowingDescriptorControlSafe => true
DescriptorResultNarrowingDescriptorControlEscapes => false
}
}
///|
priv struct OiDescriptorResultNarrowingFact {
cast_fact : OiDescriptorCastFact
descriptor_control_fact : OiDescriptorResultNarrowingControlFact
operand_relation : OiDescriptorOperandTargetRelationFact
descriptor_relation : OiDescriptorDescribedTargetRelationFact
narrowed_result : OiDescriptorNarrowedResultFact
}
///|
fn OiDescriptorResultNarrowingFact::from_cast(
ctx : HotPassContext,
func : @ir.HotFunc,
cast_fact : OiDescriptorCastFact,
force_non_null : Bool,
) -> OiDescriptorResultNarrowingFact {
// Binaryen v130's descriptor equality cast result tightening derives from
// source type facts: the reference operand must match or subtype the target
// for ordinary result narrowing, the descriptor operand may prove a narrower
// described heap, and descriptor subtrees with escaping branch control stay
// fail-closed. Store those decisions in one fact so `visitRefCast` and the
// surrounding ref.as_non_null movement do not duplicate helper-local scans.
let descriptor_control_fact = OiDescriptorResultNarrowingControlFact::from_descriptor(
func,
cast_fact.descriptor_id,
)
let operand_relation = OiDescriptorOperandTargetRelationFact::from_operand(
ctx,
func,
cast_fact.operand_id,
cast_fact.target,
cast_fact.nullable,
)
let descriptor_relation = OiDescriptorDescribedTargetRelationFact::from_type_fact(
ctx,
OiDescriptorTypeFact::from_node(ctx, func, cast_fact.descriptor_id),
cast_fact.target,
)
let narrowed_result = OiDescriptorNarrowedResultFact::from_cast(
cast_fact, force_non_null, operand_relation, descriptor_relation,
)
{
cast_fact,
descriptor_control_fact,
operand_relation,
descriptor_relation,
narrowed_result,
}
}
///|
fn OiDescriptorResultNarrowingFact::changes_result_type(
self : OiDescriptorResultNarrowingFact,
) -> Bool {
self.narrowed_result.nullable() != self.cast_fact.nullable ||
self.narrowed_result.exact() != self.cast_fact.exact ||
self.narrowed_result.target() != self.cast_fact.target
}
///|
priv enum OiDescriptorResultNarrowingSourcePreconditionKind {
DescriptorResultNarrowSourcePreconditionsReady
DescriptorResultNarrowSourceBlockedByEscapingDescriptorControl
DescriptorResultNarrowSourceBlockedByOperandRelation
}
///|
priv struct OiDescriptorResultNarrowingSourcePreconditionFact {
kind : OiDescriptorResultNarrowingSourcePreconditionKind
descriptor_control_fact : OiDescriptorResultNarrowingControlFact
operand_relation : OiDescriptorOperandTargetRelationFact?
}
///|
fn OiDescriptorResultNarrowingSourcePreconditionFact::from_narrowing(
narrowing_fact : OiDescriptorResultNarrowingFact,
) -> OiDescriptorResultNarrowingSourcePreconditionFact {
// Binaryen v130 descriptor-cast result tightening first needs source facts
// that make the descriptor-side ordering safe and prove the operand already
// matches or subtypes the cast target. Keep those source preconditions nested
// before checking whether the computed result type actually changes.
let descriptor_control_fact = narrowing_fact.descriptor_control_fact
if !descriptor_control_fact.proves_descriptor_control_safe() {
return {
kind: DescriptorResultNarrowSourceBlockedByEscapingDescriptorControl,
descriptor_control_fact,
operand_relation: None,
}
}
let operand_relation = narrowing_fact.operand_relation
if !operand_relation.proves_matches_or_subtypes_target() {
return {
kind: DescriptorResultNarrowSourceBlockedByOperandRelation,
descriptor_control_fact,
operand_relation: Some(operand_relation),
}
}
{
kind: DescriptorResultNarrowSourcePreconditionsReady,
descriptor_control_fact,
operand_relation: Some(operand_relation),
}
}
///|
fn OiDescriptorResultNarrowingSourcePreconditionFact::proves_sources_ready(
self : OiDescriptorResultNarrowingSourcePreconditionFact,
) -> Bool {
match self.kind {
DescriptorResultNarrowSourcePreconditionsReady => {
let descriptor_ready = self.descriptor_control_fact.proves_descriptor_control_safe()
let operand_ready = match self.operand_relation {
Some(fact) => fact.proves_matches_or_subtypes_target()
None => false
}
descriptor_ready && operand_ready
}
DescriptorResultNarrowSourceBlockedByEscapingDescriptorControl => false
DescriptorResultNarrowSourceBlockedByOperandRelation => false
}
}
///|
priv enum OiDescriptorResultNarrowingChangeKind {
DescriptorResultNarrowingChangesResult
DescriptorResultNarrowingUnchangedResult
}
///|
priv struct OiDescriptorResultNarrowingChangeFact {
kind : OiDescriptorResultNarrowingChangeKind
}
///|
fn OiDescriptorResultNarrowingChangeFact::from_narrowing(
narrowing_fact : OiDescriptorResultNarrowingFact,
) -> OiDescriptorResultNarrowingChangeFact {
// The source preconditions alone are not enough to mutate the cast: Binaryen's
// useful-type-info path only rebuilds the descriptor cast when the descriptor
// and operand facts produce a strictly narrower result type.
if narrowing_fact.changes_result_type() {
{ kind: DescriptorResultNarrowingChangesResult, }
} else {
{ kind: DescriptorResultNarrowingUnchangedResult, }
}
}
///|
fn OiDescriptorResultNarrowingChangeFact::proves_changes_result_type(
self : OiDescriptorResultNarrowingChangeFact,
) -> Bool {
match self.kind {
DescriptorResultNarrowingChangesResult => true
DescriptorResultNarrowingUnchangedResult => false
}
}
///|
priv enum OiDescriptorResultNarrowingPreconditionKind {
DescriptorResultNarrowBlockedBySourcePreconditions
DescriptorResultNarrowBlockedByUnchangedResult
DescriptorResultNarrowAllowed
}
///|
priv struct OiDescriptorResultNarrowingPreconditionFact {
kind : OiDescriptorResultNarrowingPreconditionKind
source_fact : OiDescriptorResultNarrowingSourcePreconditionFact
change_fact : OiDescriptorResultNarrowingChangeFact?
}
///|
fn OiDescriptorResultNarrowingPreconditionFact::from_narrowing(
narrowing_fact : OiDescriptorResultNarrowingFact,
) -> OiDescriptorResultNarrowingPreconditionFact {
// Keep the ordinary descriptor result-narrowing gate as an aggregate fact:
// source facts must be safe first, then the computed descriptor result must
// actually change. This avoids one flat eligibility result that mixed
// descriptor control, operand relation, and no-op mutation blockers.
ignore(narrowing_fact.descriptor_relation)
let source_fact = OiDescriptorResultNarrowingSourcePreconditionFact::from_narrowing(
narrowing_fact,
)
if !source_fact.proves_sources_ready() {
return {
kind: DescriptorResultNarrowBlockedBySourcePreconditions,
source_fact,
change_fact: None,
}
}
let change_fact = OiDescriptorResultNarrowingChangeFact::from_narrowing(
narrowing_fact,
)
if !change_fact.proves_changes_result_type() {
return {
kind: DescriptorResultNarrowBlockedByUnchangedResult,
source_fact,
change_fact: Some(change_fact),
}
}
{
kind: DescriptorResultNarrowAllowed,
source_fact,
change_fact: Some(change_fact),
}
}
///|
fn OiDescriptorResultNarrowingPreconditionFact::proves_result_narrowing_allowed(
self : OiDescriptorResultNarrowingPreconditionFact,
) -> Bool {
match self.kind {
DescriptorResultNarrowAllowed => {
let sources_ready = self.source_fact.proves_sources_ready()
let changes_result = match self.change_fact {
Some(fact) => fact.proves_changes_result_type()
None => false
}
sources_ready && changes_result
}
DescriptorResultNarrowBlockedBySourcePreconditions => false
DescriptorResultNarrowBlockedByUnchangedResult => false
}
}
///|
priv enum OiDescriptorRefAsNonNullMoveNullabilityKind {
DescriptorRefAsNonNullMoveNullableCast
DescriptorRefAsNonNullMoveNonNullableCast
}
///|
priv struct OiDescriptorRefAsNonNullMoveNullabilityFact {
kind : OiDescriptorRefAsNonNullMoveNullabilityKind
}
///|
fn OiDescriptorRefAsNonNullMoveNullabilityFact::from_narrowing(
narrowing_fact : OiDescriptorResultNarrowingFact,
) -> OiDescriptorRefAsNonNullMoveNullabilityFact {
if narrowing_fact.cast_fact.nullable {
{ kind: DescriptorRefAsNonNullMoveNullableCast, }
} else {
{ kind: DescriptorRefAsNonNullMoveNonNullableCast, }
}
}
///|
fn OiDescriptorRefAsNonNullMoveNullabilityFact::proves_cast_is_nullable(
self : OiDescriptorRefAsNonNullMoveNullabilityFact,
) -> Bool {
match self.kind {
DescriptorRefAsNonNullMoveNullableCast => true
DescriptorRefAsNonNullMoveNonNullableCast => false
}
}
///|
priv enum OiDescriptorRefAsNonNullMovePreconditionKind {
DescriptorRefAsNonNullMoveBlockedByNonNullableCast
DescriptorRefAsNonNullMoveBlockedByEscapingDescriptorControl
DescriptorRefAsNonNullMoveAllowed
}
///|
priv struct OiDescriptorRefAsNonNullMovePreconditionFact {
kind : OiDescriptorRefAsNonNullMovePreconditionKind
nullability_fact : OiDescriptorRefAsNonNullMoveNullabilityFact
descriptor_control_fact : OiDescriptorResultNarrowingControlFact?
}
///|
fn OiDescriptorRefAsNonNullMovePreconditionFact::from_narrowing(
narrowing_fact : OiDescriptorResultNarrowingFact,
) -> OiDescriptorRefAsNonNullMovePreconditionFact {
// Binaryen v130 can sink a surrounding `ref.as_non_null` into a descriptor
// cast only while the descriptor cast is still nullable and the descriptor
// operand has no escaping branch control. Keep this root-specific movement
// guard separate from ordinary descriptor result narrowing so the ref wrapper
// path does not consume operand heap-match facts it does not need.
ignore(narrowing_fact.operand_relation)
let nullability_fact = OiDescriptorRefAsNonNullMoveNullabilityFact::from_narrowing(
narrowing_fact,
)
if !nullability_fact.proves_cast_is_nullable() {
return {
kind: DescriptorRefAsNonNullMoveBlockedByNonNullableCast,
nullability_fact,
descriptor_control_fact: None,
}
}
let descriptor_control_fact = narrowing_fact.descriptor_control_fact
if !descriptor_control_fact.proves_descriptor_control_safe() {
return {
kind: DescriptorRefAsNonNullMoveBlockedByEscapingDescriptorControl,
nullability_fact,
descriptor_control_fact: Some(descriptor_control_fact),
}
}
{
kind: DescriptorRefAsNonNullMoveAllowed,
nullability_fact,
descriptor_control_fact: Some(descriptor_control_fact),
}
}
///|
fn OiDescriptorRefAsNonNullMovePreconditionFact::proves_ref_as_non_null_move_allowed(
self : OiDescriptorRefAsNonNullMovePreconditionFact,
) -> Bool {
match self.kind {
DescriptorRefAsNonNullMoveAllowed => {
let nullable = self.nullability_fact.proves_cast_is_nullable()
let descriptor_control_safe = match self.descriptor_control_fact {
Some(fact) => fact.proves_descriptor_control_safe()
None => false
}
nullable && descriptor_control_safe
}
DescriptorRefAsNonNullMoveBlockedByNonNullableCast => false
DescriptorRefAsNonNullMoveBlockedByEscapingDescriptorControl => false
}
}
///|
priv enum OiDescriptorResultNarrowingRewriteKind {
DescriptorResultNarrowCastRoot
DescriptorResultNarrowRefAsNonNullRoot
}
///|
priv struct OiDescriptorResultNarrowingRewritePlanFact {
kind : OiDescriptorResultNarrowingRewriteKind
narrowing_fact : OiDescriptorResultNarrowingFact
}
///|
fn OiDescriptorResultNarrowingRewritePlanFact::from_cast_root(
narrowing_fact : OiDescriptorResultNarrowingFact,
) -> OiDescriptorResultNarrowingRewritePlanFact? {
let precondition_fact = OiDescriptorResultNarrowingPreconditionFact::from_narrowing(
narrowing_fact,
)
if !precondition_fact.proves_result_narrowing_allowed() {
return None
}
Some({ kind: DescriptorResultNarrowCastRoot, narrowing_fact, })
}
///|
fn OiDescriptorResultNarrowingRewritePlanFact::from_ref_as_non_null_root(
narrowing_fact : OiDescriptorResultNarrowingFact,
) -> OiDescriptorResultNarrowingRewritePlanFact? {
let precondition_fact = OiDescriptorRefAsNonNullMovePreconditionFact::from_narrowing(
narrowing_fact,
)
if !precondition_fact.proves_ref_as_non_null_move_allowed() {
return None
}
Some({ kind: DescriptorResultNarrowRefAsNonNullRoot, narrowing_fact, })
}
///|
fn OiDescriptorResultNarrowingRewritePlanFact::apply(
self : OiDescriptorResultNarrowingRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
DescriptorResultNarrowCastRoot | DescriptorResultNarrowRefAsNonNullRoot =>
()
}
let cast_fact = self.narrowing_fact.cast_fact
let narrowed_result = self.narrowing_fact.narrowed_result
let narrowed_ref_type = @lib.RefType::new(
narrowed_result.nullable(),
narrowed_result.target(),
exact=narrowed_result.exact(),
)
let narrowed_instr = @lib.Instruction::ref_cast_desc_eq_type(
narrowed_ref_type,
)
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(narrowed_instr),
)
let narrowed = @ir.hot_build_node(
func,
@ir.hot_node_get(func, cast_fact.node_id).op,
@ir.hot_type_intern_value(func, @lib.ValType::ref_type(narrowed_ref_type)),
imm1=payload_id,
children=[cast_fact.operand_id, cast_fact.descriptor_id],
)
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, narrowed))
@ir.hot_delete_node(func, narrowed)
pass_mark_mutated(ctx, func)
true
}
///|
priv enum OiDescriptorOperandSimplificationKind {
DescriptorOperandDirectizeOnly
DescriptorOperandStripRefAsNonNull
DescriptorOperandMoveRefGetDescNullCheck
}
///|
priv enum OiDescriptorCallRefDirectizationChildSlot {
DescriptorCallRefOperandChild
DescriptorCallRefDescriptorChild
}
///|
priv struct OiDescriptorCallRefChildDirectizationFact {
slot : OiDescriptorCallRefDirectizationChildSlot
call_ref_id : Int32
}
///|
priv struct OiDescriptorCallRefDirectizationFact {
first_child_fact : OiDescriptorCallRefChildDirectizationFact
second_child_fact : OiDescriptorCallRefChildDirectizationFact?
}
///|
fn OiDescriptorCallRefDirectizationFact::from_cast_fact(
func : @ir.HotFunc,
cast_fact : OiDescriptorCastFact,
) -> OiDescriptorCallRefDirectizationFact? {
// Binaryen v130 reaches descriptor-cast operands after child visitors have had
// a chance to run `visitCallRef`; Starshine sometimes needs to request that
// same child directization while the descriptor-cast plan is active. Keep the
// descriptor-cast child slots as ordered payload facts, not as a result-kind
// enum with parallel optional call_ref ids. Operand directization still runs
// before descriptor directization when both children qualify.
let operand_child_fact = match
@ir.hot_node_get(func, cast_fact.operand_id).op {
@ir.HotOp::CallRef =>
Some({
slot: DescriptorCallRefOperandChild,
call_ref_id: cast_fact.operand_id,
})
_ => None
}
let descriptor_child_fact = match
@ir.hot_node_get(func, cast_fact.descriptor_id).op {
@ir.HotOp::CallRef =>
Some({
slot: DescriptorCallRefDescriptorChild,
call_ref_id: cast_fact.descriptor_id,
})
_ => None
}
match (operand_child_fact, descriptor_child_fact) {
(Some(operand_fact), Some(descriptor_fact)) =>
Some({
first_child_fact: operand_fact,
second_child_fact: Some(descriptor_fact),
})
(Some(operand_fact), None) =>
Some({ first_child_fact: operand_fact, second_child_fact: None, })
(None, Some(descriptor_fact)) =>
Some({ first_child_fact: descriptor_fact, second_child_fact: None, })
(None, None) => None
}
}
///|
fn OiDescriptorCallRefChildDirectizationFact::directize(
self : OiDescriptorCallRefChildDirectizationFact,
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
) -> Bool {
match self.slot {
DescriptorCallRefOperandChild | DescriptorCallRefDescriptorChild =>
optimize_instructions_try_directize_ref_func_call_ref(
ctx,
func,
dead,
self.call_ref_id,
)
}
}
///|
fn OiDescriptorCallRefDirectizationFact::directize_children(
self : OiDescriptorCallRefDirectizationFact,
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
) -> Bool {
let first_changed = self.first_child_fact.directize(ctx, func, dead)
let second_changed = match self.second_child_fact {
Some(child_fact) => child_fact.directize(ctx, func, dead)
None => false
}
first_changed || second_changed
}
///|
priv struct OiDescriptorOperandSimplificationFact {
cast_fact : OiDescriptorCastFact
directization_fact : OiDescriptorCallRefDirectizationFact?
kind : OiDescriptorOperandSimplificationKind
}
///|
fn OiDescriptorOperandSimplificationFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorOperandSimplificationFact? {
let cast_fact = match OiDescriptorCastFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let directization_fact = OiDescriptorCallRefDirectizationFact::from_cast_fact(
func, cast_fact,
)
let has_directization_fact = match directization_fact {
Some(_) => true
None => false
}
let kind = match @ir.hot_node_exact_instr(func, cast_fact.descriptor_id) {
Some(@lib.Instruction::RefAsNonNull) =>
if @ir.hot_child_count(func, cast_fact.descriptor_id) == 1 {
DescriptorOperandStripRefAsNonNull
} else if has_directization_fact {
DescriptorOperandDirectizeOnly
} else {
return None
}
Some(@lib.Instruction::RefGetDesc(_)) =>
DescriptorOperandMoveRefGetDescNullCheck
_ =>
if has_directization_fact {
DescriptorOperandDirectizeOnly
} else {
return None
}
}
Some({ cast_fact, directization_fact, kind, })
}
///|
priv struct OiDescriptorOperandSimplificationRewritePlanFact {
simplification_fact : OiDescriptorOperandSimplificationFact
}
///|
fn OiDescriptorOperandSimplificationRewritePlanFact::from_fact(
simplification_fact : OiDescriptorOperandSimplificationFact,
) -> OiDescriptorOperandSimplificationRewritePlanFact {
// Binaryen v130 `visitRefCast` first lets descriptor-cast operands expose
// simpler fallthrough/direct-call facts, then applies descriptor-operand
// cleanup such as stripping a descriptor-side `ref.as_non_null` or moving the
// nullable child through `ref.get_desc`. Keep that represented Starshine
// sequence as one plan fact instead of a visitor-local cascade over call_ref,
// descriptor wrapper, and ref.get_desc child shapes.
{ simplification_fact, }
}
///|
fn OiDescriptorOperandSimplificationRewritePlanFact::apply(
self : OiDescriptorOperandSimplificationRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let fact = self.simplification_fact
let cast_fact = fact.cast_fact
let directized_children = match fact.directization_fact {
Some(directization_fact) =>
directization_fact.directize_children(ctx, func, dead)
None => false
}
if optimize_instructions_try_skip_ref_cast_desc_eq_exact_success_traps_never_happen(
ctx, func, node_id,
) ||
optimize_instructions_try_skip_ref_cast_desc_eq_traps_never_happen(
ctx, func, dead, node_id,
) {
return true
}
match fact.kind {
DescriptorOperandDirectizeOnly => directized_children
DescriptorOperandStripRefAsNonNull => {
@ir.hot_child_set(
func,
node_id,
1,
@ir.hot_child_get(func, cast_fact.descriptor_id, 0),
)
pass_mark_mutated(ctx, func)
true
}
DescriptorOperandMoveRefGetDescNullCheck =>
optimize_instructions_try_move_ref_get_desc_child_null_check(
ctx,
func,
cast_fact.descriptor_id,
) ||
directized_children
}
}
///|
priv struct OiRefGetDescExplicitNullableChildFact {
child_id : Int32
}
///|
priv struct OiRefGetDescFact {
child_id : Int32
explicit_nullable_child_fact : OiRefGetDescExplicitNullableChildFact?
}
///|
fn OiRefGetDescFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefGetDescFact? {
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefGetDesc(_)) => ()
_ => return None
}
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
let child_id = @ir.hot_child_get(func, node_id, 0)
let explicit_nullable_child_fact : OiRefGetDescExplicitNullableChildFact? = match
@ir.hot_node_exact_instr(func, child_id) {
Some(@lib.Instruction::RefAsNonNull) if @ir.hot_child_count(func, child_id) ==
1 => Some({ child_id: @ir.hot_child_get(func, child_id, 0), })
_ => None
}
Some({ child_id, explicit_nullable_child_fact, })
}
///|
fn OiRefGetDescFact::explicit_nullable_child(self : OiRefGetDescFact) -> Int32? {
match self.explicit_nullable_child_fact {
Some(fact) => Some(fact.child_id)
None => None
}
}
///|
priv enum OiRefGetDescKnownNullChildKind {
RefGetDescKnownNullDirectTrap
RefGetDescKnownNullPreserveChild
}
///|
priv struct OiRefGetDescKnownNullPreserveChildFact {
child_id : Int32
}
///|
priv struct OiRefGetDescKnownNullChildFact {
kind : OiRefGetDescKnownNullChildKind
get_desc_fact : OiRefGetDescFact
preserve_child_fact : OiRefGetDescKnownNullPreserveChildFact?
}
///|
fn OiRefGetDescKnownNullChildFact::from_get_desc(
func : @ir.HotFunc,
get_desc_fact : OiRefGetDescFact,
) -> OiRefGetDescKnownNullChildFact? {
// Binaryen v130 treats standalone `ref.get_desc(ref.as_non_null(child))`
// with a provably null child as a null trap, while preserving child effects
// that must remain before the trap. Keep that as a fact rather than letting
// the visitor choose among nullable-child shape scanners directly.
let nullable_child = match get_desc_fact.explicit_nullable_child() {
Some(child) => child
None => return None
}
match @ir.hot_node_get(func, nullable_child).op {
@ir.HotOp::RefNull =>
if OiStandaloneRefGetDescLeafFact::from_node(func, nullable_child).proves_known_null_ref_null_leaf() {
Some({
kind: RefGetDescKnownNullDirectTrap,
get_desc_fact,
preserve_child_fact: None,
})
} else {
None
}
@ir.HotOp::LocalTee =>
if OiStandaloneRefGetDescLocalTeeFact::from_node(func, nullable_child).proves_known_null_child() {
Some({
kind: RefGetDescKnownNullPreserveChild,
get_desc_fact,
preserve_child_fact: Some({ child_id: nullable_child, }),
})
} else {
None
}
@ir.HotOp::Block =>
if OiStandaloneRefGetDescKnownNullBlockFact::from_block(
func, nullable_child,
).proves_known_null_child() {
Some({
kind: RefGetDescKnownNullPreserveChild,
get_desc_fact,
preserve_child_fact: Some({ child_id: nullable_child, }),
})
} else {
None
}
_ => None
}
}
///|
fn OiRefGetDescKnownNullChildFact::apply(
self : OiRefGetDescKnownNullChildFact,
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
) -> Bool {
ignore(self.get_desc_fact)
match self.kind {
RefGetDescKnownNullDirectTrap =>
ignore(
optimize_instructions_replace_with_unreachable(ctx, func, descriptor_id),
)
RefGetDescKnownNullPreserveChild =>
match self.preserve_child_fact {
Some(preserve_child) =>
ignore(
optimize_instructions_replace_with_drop_then_unreachable(
ctx,
func,
descriptor_id,
preserve_child.child_id,
),
)
None => ()
}
}
true
}
///|
priv enum OiRefGetDescNullCheckMoveKind {
RefGetDescStandaloneNullCheckMove
RefGetDescDescriptorOperandNullCheckMove
RefGetDescTrapsNeverHappenNullCheckMove
}
///|
priv enum OiRefGetDescNullableChildMoveKind {
RefGetDescNullableLeafMove
RefGetDescNullableLocalTeeMove
RefGetDescNullableCallMove
RefGetDescNullableSelectMove
RefGetDescNullableIfMove
RefGetDescNullableBlockMove
}
///|
priv enum OiRefGetDescDescriptorOperandMoveKind {
RefGetDescDescriptorOperandLocalLeaf
RefGetDescDescriptorOperandBranchFreeControl
RefGetDescDescriptorOperandBlocked
}
///|
priv struct OiRefGetDescDescriptorOperandMoveFact {
kind : OiRefGetDescDescriptorOperandMoveKind
}
///|
fn OiRefGetDescDescriptorOperandMoveFact::from_child(
func : @ir.HotFunc,
child_id : Int32,
) -> OiRefGetDescDescriptorOperandMoveFact {
// Descriptor operands are stricter than standalone ref.get_desc rewrites: only
// direct local leaves and represented branch-free control wrappers can move
// their null check into the descriptor operand slot. Keep that mode-specific
// decision as a source-backed fact rather than parallel `can_*` fields on the
// broader nullable-child shape record.
let child = @ir.hot_node_get(func, child_id)
let kind = match child.op {
@ir.HotOp::LocalGet => RefGetDescDescriptorOperandLocalLeaf
@ir.HotOp::Block | @ir.HotOp::Select =>
if !OiDescriptorBranchControlFact::from_node(func, child_id, child_id).proves_has_escaping_branch_control() {
RefGetDescDescriptorOperandBranchFreeControl
} else {
RefGetDescDescriptorOperandBlocked
}
@ir.HotOp::If =>
if OiDescriptorNullableIfControlMoveFact::from_if(func, child_id).proves_nullable_if_control_move_allowed() {
RefGetDescDescriptorOperandBranchFreeControl
} else {
RefGetDescDescriptorOperandBlocked
}
_ => RefGetDescDescriptorOperandBlocked
}
{ kind, }
}
///|
fn OiRefGetDescDescriptorOperandMoveFact::proves_descriptor_operand_move_allowed(
self : OiRefGetDescDescriptorOperandMoveFact,
) -> Bool {
match self.kind {
RefGetDescDescriptorOperandLocalLeaf
| RefGetDescDescriptorOperandBranchFreeControl => true
RefGetDescDescriptorOperandBlocked => false
}
}
///|
priv struct OiRefGetDescNullableChildMoveFact {
child_id : Int32
child_kind : OiRefGetDescNullableChildMoveKind
descriptor_operand_move : OiRefGetDescDescriptorOperandMoveFact
}
///|
fn OiRefGetDescNullableChildMoveFact::from_child(
func : @ir.HotFunc,
child_id : Int32,
) -> OiRefGetDescNullableChildMoveFact? {
// Binaryen v130 commonly strips `ref.as_non_null` under `ref.get_desc` when
// the nullable child is already the value that should be described. Keep the
// represented Starshine child-shape boundaries in a fact instead of letting
// each ref.get_desc caller branch on local/block/select/if/call shapes.
let child = @ir.hot_node_get(func, child_id)
let child_kind = match child.op {
@ir.HotOp::LocalGet | @ir.HotOp::GlobalGet =>
if OiStandaloneRefGetDescLeafFact::from_node(func, child_id).proves_nullable_ref_leaf_move_allowed() {
RefGetDescNullableLeafMove
} else {
return None
}
@ir.HotOp::LocalTee =>
if OiStandaloneRefGetDescLocalTeeFact::from_node(func, child_id).proves_nullable_ref_leaf_move_allowed() {
RefGetDescNullableLocalTeeMove
} else {
return None
}
@ir.HotOp::Call | @ir.HotOp::CallIndirect =>
if OiStandaloneRefGetDescCallMoveFact::from_call(func, child_id).proves_nullable_ref_call_move_allowed() {
RefGetDescNullableCallMove
} else {
return None
}
@ir.HotOp::Select =>
if OiResultShapeFact::from_node(func, child_id).proves_single_nullable_ref_result() &&
!OiDescriptorBranchControlFact::from_node(func, child_id, child_id).proves_has_escaping_branch_control() {
RefGetDescNullableSelectMove
} else {
return None
}
@ir.HotOp::If =>
if OiResultShapeFact::from_node(func, child_id).proves_single_nullable_ref_result() &&
OiDescriptorNullableIfControlMoveFact::from_if(func, child_id).proves_nullable_if_control_move_allowed() {
RefGetDescNullableIfMove
} else {
return None
}
@ir.HotOp::Block =>
if OiStandaloneRefGetDescBlockMoveFact::from_block(func, child_id).proves_nullable_block_move_allowed() {
RefGetDescNullableBlockMove
} else {
return None
}
_ => return None
}
Some({
child_id,
child_kind,
descriptor_operand_move: OiRefGetDescDescriptorOperandMoveFact::from_child(
func, child_id,
),
})
}
///|
fn OiRefGetDescNullableChildMoveFact::proves_move_allowed_for(
self : OiRefGetDescNullableChildMoveFact,
kind : OiRefGetDescNullCheckMoveKind,
) -> Bool {
ignore(self.child_id)
let supported_shape = match self.child_kind {
RefGetDescNullableLeafMove
| RefGetDescNullableLocalTeeMove
| RefGetDescNullableCallMove
| RefGetDescNullableSelectMove
| RefGetDescNullableIfMove
| RefGetDescNullableBlockMove => true
}
if !supported_shape {
return false
}
match kind {
RefGetDescTrapsNeverHappenNullCheckMove => true
RefGetDescStandaloneNullCheckMove => true
RefGetDescDescriptorOperandNullCheckMove =>
self.descriptor_operand_move.proves_descriptor_operand_move_allowed()
}
}
///|
priv struct OiRefGetDescNullCheckMoveFact {
kind : OiRefGetDescNullCheckMoveKind
get_desc_fact : OiRefGetDescFact
nullable_child_id : Int32
}
///|
fn OiRefGetDescNullCheckMoveFact::from_get_desc(
func : @ir.HotFunc,
get_desc_fact : OiRefGetDescFact,
kind : OiRefGetDescNullCheckMoveKind,
) -> OiRefGetDescNullCheckMoveFact? {
// Binaryen v130 commonly moves or removes the explicit `ref.as_non_null`
// child check under `ref.get_desc` when the nullable producer can be kept as
// the descriptor operand. Represent the mode-specific safety proof here so
// ref.get_desc callers request a plan instead of branching on raw child ops.
let nullable_child = match get_desc_fact.explicit_nullable_child() {
Some(child) => child
None => return None
}
let allowed = match kind {
RefGetDescTrapsNeverHappenNullCheckMove => true
_ =>
match
OiRefGetDescNullableChildMoveFact::from_child(func, nullable_child) {
Some(child_fact) => child_fact.proves_move_allowed_for(kind)
None => false
}
}
if !allowed {
return None
}
Some({ kind, get_desc_fact, nullable_child_id: nullable_child, })
}
///|
fn OiRefGetDescNullCheckMoveFact::apply(
self : OiRefGetDescNullCheckMoveFact,
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
) -> Bool {
ignore(self.kind)
ignore(self.get_desc_fact)
@ir.hot_child_set(func, descriptor_id, 0, self.nullable_child_id)
pass_mark_mutated(ctx, func)
true
}
///|
fn OiRefGetDescFact::child_is_unreachable(
self : OiRefGetDescFact,
func : @ir.HotFunc,
) -> Bool {
match @ir.hot_node_get(func, self.child_id).op {
@ir.HotOp::Unreachable => true
_ => false
}
}
///|
priv struct OptimizeInstructionsScratch {
cross_seen_stamps : Array[Int32]
loop_input_nodes : @bitset.BitSet
if_condition_nodes : @bitset.BitSet
local_facts : OptimizeInstructionsLocalFacts
mut loop_input_nodes_ready : Bool
mut if_condition_nodes_ready : Bool
mut next_cross_seen_stamp : Int32
}
///|
fn OptimizeInstructionsScratch::new(
node_count : Int32,
local_count : Int32,
) -> OptimizeInstructionsScratch {
{
cross_seen_stamps: Array::make(node_count, -1),
loop_input_nodes: @bitset.BitSet::new(node_count),
if_condition_nodes: @bitset.BitSet::new(node_count),
local_facts: OptimizeInstructionsLocalFacts::new(local_count, node_count),
loop_input_nodes_ready: false,
if_condition_nodes_ready: false,
next_cross_seen_stamp: 0,
}
}
///|
fn optimize_instructions_alloc_cross_seen_stamp(
scratch : OptimizeInstructionsScratch,
) -> Int32 {
let stamp = scratch.next_cross_seen_stamp
scratch.next_cross_seen_stamp = stamp + 1
stamp
}
///|
fn optimize_instructions_mark_loop_input_nodes(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
) -> Unit {
if scratch.loop_input_nodes_ready {
return
}
for node_id = 0; node_id < func.nodes.length(); node_id = node_id + 1 {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
continue
}
let node = func.nodes[node_id]
match node.op {
@ir.HotOp::Loop => ()
_ => continue
}
let input_count = @ir.hot_label_branch_arity(
func,
@ir.hot_control_node_label(func, node_id),
)
for slot = 0; slot < input_count; slot = slot + 1 {
let child_id = func.children[node.child_base + slot]
if child_id >= 0 && child_id < scratch.loop_input_nodes.length() {
scratch.loop_input_nodes[child_id] = true
}
}
}
scratch.loop_input_nodes_ready = true
}
///|
fn optimize_instructions_mark_if_condition_nodes(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
) -> Unit {
if scratch.if_condition_nodes_ready {
return
}
for node_id = 0; node_id < func.nodes.length(); node_id = node_id + 1 {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
continue
}
let node = func.nodes[node_id]
match node.op {
@ir.HotOp::If if node.child_count > 0 => {
let cond_id = func.children[node.child_base]
if cond_id >= 0 && cond_id < scratch.if_condition_nodes.length() {
scratch.if_condition_nodes[cond_id] = true
}
}
_ => ()
}
}
scratch.if_condition_nodes_ready = true
}
///|
fn optimize_instructions_sign_ext_bits_for_exact_instr(
instr : @lib.Instruction,
) -> Int32 {
match instr {
@lib.Instruction::I32Load8S(_) | @lib.Instruction::I64Load8S(_) => 8
@lib.Instruction::I32Load16S(_) | @lib.Instruction::I64Load16S(_) => 16
@lib.Instruction::I64Load32S(_) => 32
@lib.Instruction::I32Extend8S | @lib.Instruction::I64Extend8S => 8
@lib.Instruction::I32Extend16S | @lib.Instruction::I64Extend16S => 16
@lib.Instruction::I64Extend32S => 32
_ => 0
}
}
///|
fn optimize_instructions_i32_sign_ext_bits_for_const(raw : Int32) -> Int32 {
if raw >= -128 && raw <= 127 {
8
} else if raw >= -32768 && raw <= 32767 {
16
} else {
32
}
}
///|
fn optimize_instructions_i64_sign_ext_bits_for_const(raw : Int64) -> Int32 {
if raw >= -128L && raw <= 127L {
8
} else if raw >= -32768L && raw <= 32767L {
16
} else if raw >= -2147483648L && raw <= 2147483647L {
32
} else {
64
}
}
///|
priv struct OiSignExtValueFact {
bits : Int32
}
///|
fn OiSignExtValueFact::from_bits(bits : Int32) -> OiSignExtValueFact {
{ bits, }
}
///|
fn OiSignExtValueFact::from_node_with_local_facts(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> OiSignExtValueFact {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return OiSignExtValueFact::from_bits(0)
}
let node = @ir.hot_node_get(func, node_id)
let bits = match node.op {
@ir.HotOp::Const =>
match @ir.hot_const_get(func, node.imm0) {
@ir.HotConstPayload::I32Const(@lib.I32(raw)) =>
optimize_instructions_i32_sign_ext_bits_for_const(raw)
@ir.HotConstPayload::I64Const(@lib.I64(raw)) =>
optimize_instructions_i64_sign_ext_bits_for_const(raw)
_ => 0
}
@ir.HotOp::LocalGet =>
match local_facts {
Some(facts) => facts.sign_ext_bits_for_get(node.imm0)
None => 0
}
@ir.HotOp::Load | @ir.HotOp::Unary =>
optimize_instructions_sign_ext_bits_for_exact_instr(
func.instr_payloads[node.imm0].instr,
)
_ => 0
}
OiSignExtValueFact::from_bits(bits)
}
///|
fn OiSignExtValueFact::from_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiSignExtValueFact {
OiSignExtValueFact::from_node_with_local_facts(
func,
dead,
Some(scratch.local_facts),
node_id,
)
}
///|
fn OiSignExtValueFact::from_assigned_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiSignExtValueFact {
// Binaryen's LocalScanner computes the value fact for an assignment from the
// assigned expression itself, not from the local fact being built. Keep that
// no-local-get boundary explicit so self-referential local.set/tee shapes do
// not smuggle stale scan state into the whole-function summary. Its direct
// optimize-instructions output also keeps sign extensions after literal
// assignments, so literals do not seed this producer-only local fact.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Const => OiSignExtValueFact::from_bits(0)
_ =>
OiSignExtValueFact::from_node_with_local_facts(func, dead, None, node_id)
}
}
///|
fn optimize_instructions_sign_ext_bits_for_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Int32 {
OiSignExtValueFact::from_value(func, dead, scratch, node_id).bits
}
///|
fn optimize_instructions_collect_local_assignment_fact_region(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
seen : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
region : @ir.HotRegionRef,
) -> Unit {
for i = 0; i < @ir.hot_region_root_count(func, region); i = i + 1 {
optimize_instructions_collect_local_assignment_fact_expr(
ctx,
func,
dead,
seen,
scratch,
@ir.hot_region_get(func, region, i),
)
}
}
///|
fn optimize_instructions_collect_local_assignment_fact_expr(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
seen : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Unit {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) || seen[node_id] {
return
}
seen[node_id] = true
match ctx.perf {
Some(perf) => perf_count_traversal_visit(perf)
None => ()
}
let node = @ir.hot_node_get(func, node_id)
for slot = 0; slot < node.child_count; slot = slot + 1 {
optimize_instructions_collect_local_assignment_fact_expr(
ctx,
func,
dead,
seen,
scratch,
func.children[node.child_base + slot],
)
}
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
optimize_instructions_collect_local_assignment_fact_region(
ctx,
func,
dead,
seen,
scratch,
@ir.hot_region_body_ref(func, node_id),
)
@ir.HotOp::If => {
optimize_instructions_collect_local_assignment_fact_region(
ctx,
func,
dead,
seen,
scratch,
@ir.hot_region_then_ref(func, node_id),
)
match @ir.hot_region_else_ref(func, node_id) {
Some(else_region) =>
optimize_instructions_collect_local_assignment_fact_region(
ctx, func, dead, seen, scratch, else_region,
)
None => ()
}
}
_ => {
let assignment_fact = OiLocalAssignmentFact::from_node(
func, dead, node_id,
)
match assignment_fact.sign_ext_integer_assignment() {
Some(payload) =>
scratch.local_facts.merge_sign_ext_bits_for_local(
payload.local_id,
OiSignExtValueFact::from_assigned_value(
func,
dead,
payload.value_id,
).bits,
)
None => ()
}
match assignment_fact.i32_assignment() {
Some(payload) =>
scratch.local_facts.merge_i32_unsigned_max_for_local(
payload.local_id,
OiUnsignedMaxValueFact::from_assigned_i32_value(
func,
dead,
scratch,
payload.value_id,
).i32_max,
)
None =>
match assignment_fact.i64_assignment() {
Some(payload) =>
scratch.local_facts.merge_i64_unsigned_max_for_local(
payload.local_id,
OiUnsignedMaxValueFact::from_assigned_i64_value(
func,
dead,
scratch,
payload.value_id,
).i64_max,
)
None => ()
}
}
}
}
}
///|
fn optimize_instructions_collect_local_facts(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
) -> Unit {
if scratch.local_facts.sign_ext_ready &&
scratch.local_facts.unsigned_max_ready {
return
}
for local_id = @ir.hot_local_param_count(func)
local_id < @ir.hot_local_count(func)
local_id = local_id + 1 {
match @ir.hot_local_type(func, local_id) {
Some(val_type) if val_type == @lib.ValType::i32() =>
scratch.local_facts.seed_i32_unsigned_max_local(local_id)
Some(val_type) if val_type == @lib.ValType::i64() =>
scratch.local_facts.seed_i64_unsigned_max_local(local_id)
_ => ()
}
}
let seen = @bitset.BitSet::new(@ir.hot_node_count(func))
for slot = 0; slot < func.roots.length(); slot = slot + 1 {
optimize_instructions_collect_local_assignment_fact_expr(
ctx,
func,
dead,
seen,
scratch,
func.roots[slot],
)
}
scratch.local_facts.finalize_sign_ext_bits()
// Assignment collection recursively memoizes producer facts with local.get
// summaries disabled. Clear that query-mode cache before enabling finalized
// local summaries so rewrite-time consumers can derive stronger local-aware
// facts without inheriting conservative assignment-time misses.
for node_id = 0
node_id < scratch.local_facts.i32_max_ready.length()
node_id = node_id + 1 {
scratch.local_facts.i32_max_ready[node_id] = false
scratch.local_facts.i64_max_ready[node_id] = false
}
scratch.local_facts.sign_ext_ready = true
scratch.local_facts.unsigned_max_ready = true
}
///|
priv struct OiUnsignedMaxValueFact {
i32_max : Int32?
i64_max : Int64?
}
///|
fn OiUnsignedMaxValueFact::i32_bits_for_max(max_value : Int32) -> Int32 {
let mut bits = 0
let mut value = max_value
while value > 0 {
value = value >> 1
bits = bits + 1
}
bits
}
///|
fn OiUnsignedMaxValueFact::i64_bits_for_max(max_value : Int64) -> Int32 {
let mut bits = 0
let mut value = max_value
while value > 0L {
value = value >> 1
bits = bits + 1
}
bits
}
///|
fn OiUnsignedMaxValueFact::i32_max_for_bits(max_bits : Int32) -> Int32? {
if max_bits < 0 || max_bits > 31 {
None
} else if max_bits == 0 {
Some(0)
} else {
Some((1 << max_bits) - 1)
}
}
///|
fn OiUnsignedMaxValueFact::i64_max_for_bits(max_bits : Int32) -> Int64? {
if max_bits < 0 || max_bits > 63 {
None
} else if max_bits == 0 {
Some(0L)
} else {
Some((1L << max_bits) - 1L)
}
}
///|
fn OiUnsignedMaxValueFact::i32_max_bits_or_unknown(max_value : Int32?) -> Int32 {
match max_value {
Some(value) => OiUnsignedMaxValueFact::i32_bits_for_max(value)
None => 32
}
}
///|
fn OiUnsignedMaxValueFact::i64_max_bits_or_unknown(max_value : Int64?) -> Int32 {
match max_value {
Some(value) => OiUnsignedMaxValueFact::i64_bits_for_max(value)
None => 64
}
}
///|
fn OiUnsignedMaxValueFact::i32_max_for_bitwise_and(
lhs_max : Int32?,
rhs_max : Int32?,
) -> Int32? {
match (lhs_max, rhs_max) {
(Some(lhs), Some(rhs)) => Some(if lhs < rhs { lhs } else { rhs })
(Some(lhs), None) => Some(lhs)
(None, Some(rhs)) => Some(rhs)
(None, None) => None
}
}
///|
fn OiUnsignedMaxValueFact::i64_max_for_bitwise_and(
lhs_max : Int64?,
rhs_max : Int64?,
) -> Int64? {
match (lhs_max, rhs_max) {
(Some(lhs), Some(rhs)) => Some(if lhs < rhs { lhs } else { rhs })
(Some(lhs), None) => Some(lhs)
(None, Some(rhs)) => Some(rhs)
(None, None) => None
}
}
///|
fn OiUnsignedMaxValueFact::i32_max_for_bitwise_union(
lhs_max : Int32?,
rhs_max : Int32?,
) -> Int32? {
let lhs_bits = OiUnsignedMaxValueFact::i32_max_bits_or_unknown(lhs_max)
let rhs_bits = OiUnsignedMaxValueFact::i32_max_bits_or_unknown(rhs_max)
let max_bits = if lhs_bits > rhs_bits { lhs_bits } else { rhs_bits }
OiUnsignedMaxValueFact::i32_max_for_bits(max_bits)
}
///|
fn OiUnsignedMaxValueFact::i64_max_for_bitwise_union(
lhs_max : Int64?,
rhs_max : Int64?,
) -> Int64? {
let lhs_bits = OiUnsignedMaxValueFact::i64_max_bits_or_unknown(lhs_max)
let rhs_bits = OiUnsignedMaxValueFact::i64_max_bits_or_unknown(rhs_max)
let max_bits = if lhs_bits > rhs_bits { lhs_bits } else { rhs_bits }
OiUnsignedMaxValueFact::i64_max_for_bits(max_bits)
}
///|
priv struct OiI32UnsignedLoadMaxBitsFact {
value_id : Int32
max_value : Int32
}
///|
priv struct OiI64UnsignedLoadMaxBitsFact {
value_id : Int32
max_value : Int64
}
///|
fn OiI32UnsignedLoadMaxBitsFact::from_instr(
instr : @lib.Instruction,
value_id : Int32,
) -> OiI32UnsignedLoadMaxBitsFact? {
// Binaryen v130 `Bits::getMaxBits` bounds unsigned narrow loads by the number
// of loaded bits. Keep width-specific leaf producer facts explicit so load
// knowledge does not remain as an unsigned-max query-local opcode whitelist.
match instr {
@lib.Instruction::I32Load8U(_) => Some({ value_id, max_value: 255, })
@lib.Instruction::I32Load16U(_) => Some({ value_id, max_value: 65535, })
_ => None
}
}
///|
fn OiI64UnsignedLoadMaxBitsFact::from_instr(
instr : @lib.Instruction,
value_id : Int32,
) -> OiI64UnsignedLoadMaxBitsFact? {
// Same Binaryen v130 unsigned-load maxBits leaf facts for i64 loads.
match instr {
@lib.Instruction::I64Load8U(_) => Some({ value_id, max_value: 255L, })
@lib.Instruction::I64Load16U(_) => Some({ value_id, max_value: 65535L, })
@lib.Instruction::I64Load32U(_) =>
Some({ value_id, max_value: 4294967295L, })
_ => None
}
}
///|
priv struct OiI32UnsignedConstMaxValueFact {
value_id : Int32
max_value : Int32
}
///|
priv struct OiI64UnsignedConstMaxValueFact {
value_id : Int32
max_value : Int64
}
///|
fn OiI32UnsignedConstMaxValueFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiI32UnsignedConstMaxValueFact? {
// Binaryen v130 `Bits::getMaxBits` treats nonnegative integer constants as
// exact unsigned maxima. Negative constants are not bounded here because their
// high bit is set in the corresponding wasm integer width.
match @ir.hot_const_get(func, @ir.hot_node_get(func, node_id).imm0) {
@ir.HotConstPayload::I32Const(@lib.I32(raw)) if raw >= 0 =>
Some({ value_id: node_id, max_value: raw, })
_ => None
}
}
///|
fn OiI64UnsignedConstMaxValueFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiI64UnsignedConstMaxValueFact? {
// Same exact unsigned maximum fact for nonnegative i64 constants.
match @ir.hot_const_get(func, @ir.hot_node_get(func, node_id).imm0) {
@ir.HotConstPayload::I64Const(@lib.I64(raw)) if raw >= 0L =>
Some({ value_id: node_id, max_value: raw, })
_ => None
}
}
///|
priv struct OiI32LocalUnsignedMaxValueFact {
local_id : Int32
max_value : Int32
}
///|
priv struct OiI64LocalUnsignedMaxValueFact {
local_id : Int32
max_value : Int64
}
///|
fn OiI32LocalUnsignedMaxValueFact::from_get(
local_facts : OptimizeInstructionsLocalFacts?,
local_id : Int32,
) -> OiI32LocalUnsignedMaxValueFact? {
// Binaryen's `LocalScanner`-style fact store feeds local.get maxBits queries.
// Keep width-specific get-side consumption explicit and distinct from leaf
// constants, loads, and recursive producer rules.
match local_facts {
Some(facts) =>
match facts.i32_unsigned_max_for_get(local_id) {
Some(max_value) => Some({ local_id, max_value, })
None => None
}
None => None
}
}
///|
fn OiI64LocalUnsignedMaxValueFact::from_get(
local_facts : OptimizeInstructionsLocalFacts?,
local_id : Int32,
) -> OiI64LocalUnsignedMaxValueFact? {
// Same fact-store consumption for i64 local.get maxBits queries.
match local_facts {
Some(facts) =>
match facts.i64_unsigned_max_for_get(local_id) {
Some(max_value) => Some({ local_id, max_value, })
None => None
}
None => None
}
}
///|
priv struct OiI32ConvertMaxBitsFact {
operand_id : Int32
max_value : Int32
}
///|
priv struct OiI64ConvertMaxBitsFact {
operand_id : Int32
max_value : Int64
}
///|
fn OiI32ConvertMaxBitsFact::from_convert(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiI32ConvertMaxBitsFact? {
// Binaryen v130 `Bits::getMaxBits` keeps the bounded low-lane fact for
// `i32.wrap_i64(x)` when the wrapped i64 is proven below the i32 sign bit.
match instr {
@lib.Instruction::I32WrapI64 =>
match
OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, operand_id,
) {
Some(max_value) if max_value <= 2147483647L =>
Some({ operand_id, max_value: max_value.to_int(), })
_ => None
}
_ => None
}
}
///|
fn OiI64ConvertMaxBitsFact::from_convert(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiI64ConvertMaxBitsFact? {
// Binaryen v130 i32-to-i64 producer facts preserve known unsigned i32 maxima;
// unsigned extension has the full 32-bit fallback when the operand is not
// otherwise bounded. Other represented i32-to-i64 shells keep the existing
// conservative Starshine fact only when the i32 operand max is known.
match instr {
@lib.Instruction::I64ExtendI32U => {
let max_value = match
OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, operand_id,
) {
Some(value) => value.to_int64()
None => 4294967295L
}
Some({ operand_id, max_value, })
}
_ =>
match
OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, operand_id,
) {
Some(value) => Some({ operand_id, max_value: value.to_int64(), })
None => None
}
}
}
///|
priv struct OiI32BitwiseMaxBitsFact {
max_value : Int32?
}
///|
priv struct OiI64BitwiseMaxBitsFact {
max_value : Int64?
}
///|
fn OiI32BitwiseMaxBitsFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32BitwiseMaxBitsFact? {
// Binaryen v130 `Bits::getMaxBits` treats integer `and` as the minimum of
// operand max-bit counts and `or` / `xor` as the maximum. Keep those bitwise
// producer rules in an explicit width-specific fact instead of embedding an
// opcode whitelist inside the unsigned-max recursive query.
let lhs_max = OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, lhs_id,
)
let rhs_max = OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, rhs_id,
)
match instr {
@lib.Instruction::I32And =>
Some({
max_value: OiUnsignedMaxValueFact::i32_max_for_bitwise_and(
lhs_max, rhs_max,
),
})
@lib.Instruction::I32Or | @lib.Instruction::I32Xor =>
Some({
max_value: OiUnsignedMaxValueFact::i32_max_for_bitwise_union(
lhs_max, rhs_max,
),
})
_ => None
}
}
///|
fn OiI64BitwiseMaxBitsFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64BitwiseMaxBitsFact? {
// Same Binaryen v130 bitwise maxBits fact for i64 producers.
let lhs_max = OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, lhs_id,
)
let rhs_max = OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, rhs_id,
)
match instr {
@lib.Instruction::I64And =>
Some({
max_value: OiUnsignedMaxValueFact::i64_max_for_bitwise_and(
lhs_max, rhs_max,
),
})
@lib.Instruction::I64Or | @lib.Instruction::I64Xor =>
Some({
max_value: OiUnsignedMaxValueFact::i64_max_for_bitwise_union(
lhs_max, rhs_max,
),
})
_ => None
}
}
///|
priv struct OiI32EffectiveShiftAmountFact {
amount : Int32
}
///|
priv struct OiI64EffectiveShiftAmountFact {
amount : Int32
}
///|
fn OiI32EffectiveShiftAmountFact::from_const_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
rhs_id : Int32,
) -> OiI32EffectiveShiftAmountFact? {
// Binaryen v130 `Bits::getMaxBits` calls `Bits::getEffectiveShifts` for
// integer shift constants, matching WebAssembly's low-5-bit i32 shift amount
// even when the literal is negative or out of range. Keep the masked i32
// amount as an explicit width-specific fact so shl/shr_s/shr_u maxBits
// producers share one source rule without a generic width field.
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => Some({ amount: raw & 31, })
None => None
}
}
///|
fn OiI64EffectiveShiftAmountFact::from_const_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
rhs_id : Int32,
) -> OiI64EffectiveShiftAmountFact? {
// Same Binaryen v130 effective-shift fact for i64 shifts, using the low 6 bits
// of the i64 constant operand and keeping the i64 lane explicit in the type.
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) => Some({ amount: (raw & 63L).to_int(), })
None => None
}
}
///|
priv struct OiI32LogicalShiftMaxBitsFact {
max_value : Int32?
}
///|
priv struct OiI64LogicalShiftMaxBitsFact {
max_value : Int64?
}
///|
fn OiI32LogicalShiftMaxBitsFact::from_shr_u(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32LogicalShiftMaxBitsFact? {
// Binaryen v130 `Bits::getMaxBits` treats constant logical right shifts as
// max-bit reducers using the masked WebAssembly shift amount. Keep the `shr_u`
// producer in a named width-specific fact so it shares the i32 effective
// shift-amount fact with the arithmetic shl/shr_s fact instead of living as a
// recursive-query branch.
match instr {
@lib.Instruction::I32ShrU =>
match
OiI32EffectiveShiftAmountFact::from_const_operand(func, dead, rhs_id) {
Some(fact) if fact.amount > 0 => {
let max_value = match
OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, lhs_id,
) {
Some(value) => Some(value >> fact.amount)
None if fact.amount == 1 => Some(2147483647)
None => Some((1 << (32 - fact.amount)) - 1)
}
Some({ max_value, })
}
_ => None
}
_ => None
}
}
///|
fn OiI64LogicalShiftMaxBitsFact::from_shr_u(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64LogicalShiftMaxBitsFact? {
// Same Binaryen v130 logical-right-shift maxBits fact for i64 producers.
match instr {
@lib.Instruction::I64ShrU =>
match
OiI64EffectiveShiftAmountFact::from_const_operand(func, dead, rhs_id) {
Some(fact) if fact.amount > 0 => {
let max_value = match
OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, lhs_id,
) {
Some(value) => Some(value >> fact.amount)
None if fact.amount == 1 => Some(9223372036854775807L)
None => Some((1L << (64 - fact.amount)) - 1L)
}
Some({ max_value, })
}
_ => None
}
_ => None
}
}
///|
priv enum OiMaxBitsBlockPrefixKind {
MaxBitsBlockPrefixPure
MaxBitsBlockPrefixLocalSet
MaxBitsBlockPrefixStore
MaxBitsBlockPrefixAtomicStore
MaxBitsBlockPrefixCall
MaxBitsBlockPrefixCallIndirect
MaxBitsBlockPrefixCallRef
MaxBitsBlockPrefixDropCall
MaxBitsBlockPrefixDropCallIndirect
MaxBitsBlockPrefixDropCallRef
MaxBitsBlockPrefixGlobalSet
MaxBitsBlockPrefixTableSet
MaxBitsBlockPrefixTableFill
MaxBitsBlockPrefixTableCopy
MaxBitsBlockPrefixTableInit
MaxBitsBlockPrefixDropAtomicLoad
MaxBitsBlockPrefixDropStructAtomicGet
MaxBitsBlockPrefixDropStructAtomicRmwLowering
MaxBitsBlockPrefixDropStructAtomicCmpxchgLowering
MaxBitsBlockPrefixDropAtomicRmw
MaxBitsBlockPrefixDropArrayAtomicRmw
MaxBitsBlockPrefixDropArrayAtomicCmpxchg
MaxBitsBlockPrefixDropAtomicCmpxchg
MaxBitsBlockPrefixDropAtomicWait
MaxBitsBlockPrefixDropAtomicNotify
MaxBitsBlockPrefixDropTableGrow
MaxBitsBlockPrefixDropMemoryGrow
MaxBitsBlockPrefixDataDrop
MaxBitsBlockPrefixElemDrop
MaxBitsBlockPrefixMemoryInit
MaxBitsBlockPrefixMemoryCopy
MaxBitsBlockPrefixMemoryFill
}
///|
priv struct OiMaxBitsBlockPrefixFact {
kind : OiMaxBitsBlockPrefixKind
root_id : Int32
}
///|
priv struct OiDroppedStructAtomicRmwLoweringFact {
block_id : Int32
}
///|
priv struct OiDroppedStructAtomicCmpxchgLoweringFact {
block_id : Int32
}
///|
fn OiDroppedStructAtomicRmwLoweringFact::from_drop_child(
func : @ir.HotFunc,
block_id : Int32,
) -> OiDroppedStructAtomicRmwLoweringFact? {
// `visitStructRMW` can lower aggregate atomic RMWs to a result block before
// the block-tail maxBits fold runs. Binaryen v130 still folds the surrounding
// impossible compare while preserving the lowered get/set block. Keep this
// prefix fact tied to the emitted straight-line lowering skeleton instead of
// admitting arbitrary dropped effectful blocks.
let node = @ir.hot_node_get(func, block_id)
match node.op {
@ir.HotOp::Block => ()
_ => return None
}
let body_ref = @ir.hot_region_body_ref(func, block_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count != 5 {
return None
}
for slot = 0; slot < 3; slot = slot + 1 {
let root_id = @ir.hot_region_get(func, body_ref, slot)
match @ir.hot_node_get(func, root_id).op {
@ir.HotOp::LocalSet => ()
_ => return None
}
}
let set_id = @ir.hot_region_get(func, body_ref, 3)
match @ir.hot_node_get(func, set_id).op {
@ir.HotOp::Heap => ()
_ => return None
}
match @ir.hot_node_exact_instr(func, set_id) {
Some(@lib.Instruction::StructSet(_, _)) => ()
_ => return None
}
let old_value_id = @ir.hot_region_get(func, body_ref, 4)
match @ir.hot_node_get(func, old_value_id).op {
@ir.HotOp::LocalGet => ()
_ => return None
}
Some({ block_id, })
}
///|
fn OiDroppedStructAtomicRmwLoweringFact::proves_preserved_prefix(
self : Self,
) -> Bool {
ignore(self.block_id)
true
}
///|
fn OiDroppedStructAtomicCmpxchgLoweringFact::from_drop_child(
func : @ir.HotFunc,
block_id : Int32,
) -> OiDroppedStructAtomicCmpxchgLoweringFact? {
// `visitStructRMW` can lower aggregate cmpxchg to a result block before the
// block-tail maxBits fold runs. Binaryen v130 still folds the surrounding
// impossible compare while preserving the lowered get/conditional-set block.
// Keep this prefix fact tied to the emitted localize-read-write-old-value
// skeleton instead of admitting arbitrary dropped effectful blocks.
let node = @ir.hot_node_get(func, block_id)
match node.op {
@ir.HotOp::Block => ()
_ => return None
}
let body_ref = @ir.hot_region_body_ref(func, block_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count != 5 {
return None
}
for slot = 0; slot < 3; slot = slot + 1 {
let root_id = @ir.hot_region_get(func, body_ref, slot)
match @ir.hot_node_get(func, root_id).op {
@ir.HotOp::LocalSet => ()
_ => return None
}
}
let if_id = @ir.hot_region_get(func, body_ref, 3)
if !(@ir.hot_node_get(func, if_id).op is @ir.HotOp::If) ||
@ir.hot_region_else_ref(func, if_id) is Some(_) {
return None
}
let then_ref = @ir.hot_region_then_ref(func, if_id)
if @ir.hot_region_root_count(func, then_ref) != 1 {
return None
}
let set_id = @ir.hot_region_get(func, then_ref, 0)
if !(@ir.hot_node_exact_instr(func, set_id) is Some(@lib.StructSet(_, _))) {
return None
}
for child in @ir.hot_child_span(func, set_id) {
if !(@ir.hot_node_get(func, child).op is @ir.HotOp::LocalGet) {
return None
}
}
let condition = @ir.hot_child_get(func, if_id, 0)
match @ir.hot_node_exact_instr(func, condition) {
Some(@lib.I32Eq) | Some(@lib.I64Eq) => ()
_ => return None
}
let tee = @ir.hot_child_get(func, condition, 0)
if !(@ir.hot_node_get(func, tee).op is @ir.HotOp::LocalTee) ||
!(@ir.hot_node_get(func, @ir.hot_child_get(func, condition, 1)).op
is @ir.HotOp::LocalGet) {
return None
}
let read = @ir.hot_child_get(func, tee, 0)
if !(@ir.hot_node_exact_instr(func, read) is Some(@lib.StructGet(_, _))) ||
!(@ir.hot_node_get(func, @ir.hot_child_get(func, read, 0)).op
is @ir.HotOp::LocalGet) {
return None
}
let old_value_id = @ir.hot_region_get(func, body_ref, 4)
match @ir.hot_node_get(func, old_value_id).op {
@ir.HotOp::LocalGet => ()
_ => return None
}
Some({ block_id, })
}
///|
fn OiDroppedStructAtomicCmpxchgLoweringFact::proves_preserved_prefix(
self : Self,
) -> Bool {
ignore(self.block_id)
true
}
///|
fn OiMaxBitsBlockPrefixFact::from_root(
func : @ir.HotFunc,
dead : @bitset.BitSet,
root_id : Int32,
) -> OiMaxBitsBlockPrefixFact? {
// Binaryen v130 `Bits::getMaxBits` can use the value fallthrough of a block
// even when earlier straight-line roots have effects; the later replacement
// preserves those effects by dropping the whole block. Keep the source-backed
// effectful prefix outcomes intentionally narrow to straight-line assignment,
// memory/table store, atomic store, passive-data/table-init/drop, drop-wrapped
// atomic load/RMW/cmpxchg/wait/notify, aggregate atomic get/lowered RMW and
// cmpxchg, drop-wrapped array RMW, direct, indirect, and reference calls,
// table.grow and memory.grow, and global write roots so branch, control, and
// generalized localization remain out of this fact.
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, root_id) {
return Some({ kind: MaxBitsBlockPrefixPure, root_id, })
}
match @ir.hot_node_get(func, root_id).op {
@ir.HotOp::LocalSet if @ir.hot_child_count(func, root_id) == 1 =>
Some({ kind: MaxBitsBlockPrefixLocalSet, root_id, })
@ir.HotOp::Store if @ir.hot_child_count(func, root_id) == 2 =>
Some({ kind: MaxBitsBlockPrefixStore, root_id, })
@ir.HotOp::Atomic if @ir.hot_child_count(func, root_id) == 2 =>
match @ir.hot_node_exact_instr(func, root_id) {
Some(
@lib.Instruction::I32AtomicStore(_, _)
| @lib.Instruction::I64AtomicStore(_, _)
| @lib.Instruction::I32AtomicStore8(_, _)
| @lib.Instruction::I32AtomicStore16(_, _)
| @lib.Instruction::I64AtomicStore8(_, _)
| @lib.Instruction::I64AtomicStore16(_, _)
| @lib.Instruction::I64AtomicStore32(_, _)
) => Some({ kind: MaxBitsBlockPrefixAtomicStore, root_id, })
_ => None
}
@ir.HotOp::Call => Some({ kind: MaxBitsBlockPrefixCall, root_id, })
@ir.HotOp::CallIndirect =>
Some({ kind: MaxBitsBlockPrefixCallIndirect, root_id, })
@ir.HotOp::CallRef => Some({ kind: MaxBitsBlockPrefixCallRef, root_id, })
@ir.HotOp::GlobalSet if @ir.hot_child_count(func, root_id) == 1 =>
Some({ kind: MaxBitsBlockPrefixGlobalSet, root_id, })
@ir.HotOp::TableSet if @ir.hot_child_count(func, root_id) == 2 =>
Some({ kind: MaxBitsBlockPrefixTableSet, root_id, })
@ir.HotOp::TableFill if @ir.hot_child_count(func, root_id) == 3 =>
Some({ kind: MaxBitsBlockPrefixTableFill, root_id, })
@ir.HotOp::TableCopy if @ir.hot_child_count(func, root_id) == 3 =>
Some({ kind: MaxBitsBlockPrefixTableCopy, root_id, })
@ir.HotOp::TableInit if @ir.hot_child_count(func, root_id) == 3 =>
Some({ kind: MaxBitsBlockPrefixTableInit, root_id, })
@ir.HotOp::Drop if @ir.hot_child_count(func, root_id) == 1 => {
let child_id = @ir.hot_child_get(func, root_id, 0)
match @ir.hot_node_get(func, child_id).op {
@ir.HotOp::Atomic if @ir.hot_child_count(func, child_id) == 1 =>
match @ir.hot_node_exact_instr(func, child_id) {
Some(
@lib.Instruction::I32AtomicLoad(_, _)
| @lib.Instruction::I64AtomicLoad(_, _)
| @lib.Instruction::I32AtomicLoad8U(_, _)
| @lib.Instruction::I32AtomicLoad16U(_, _)
| @lib.Instruction::I64AtomicLoad8U(_, _)
| @lib.Instruction::I64AtomicLoad16U(_, _)
| @lib.Instruction::I64AtomicLoad32U(_, _)
) => Some({ kind: MaxBitsBlockPrefixDropAtomicLoad, root_id, })
Some(
@lib.Instruction::StructAtomicGet(_, _, _)
| @lib.Instruction::StructAtomicGetS(_, _, _)
| @lib.Instruction::StructAtomicGetU(_, _, _)
) => Some({ kind: MaxBitsBlockPrefixDropStructAtomicGet, root_id, })
_ => None
}
@ir.HotOp::Atomic if @ir.hot_child_count(func, child_id) == 2 =>
match @ir.hot_node_exact_instr(func, child_id) {
Some(@lib.Instruction::AtomicRmw(_, _, _)) =>
Some({ kind: MaxBitsBlockPrefixDropAtomicRmw, root_id, })
Some(@lib.Instruction::MemoryAtomicNotify(_)) =>
Some({ kind: MaxBitsBlockPrefixDropAtomicNotify, root_id, })
_ => None
}
@ir.HotOp::Atomic if @ir.hot_child_count(func, child_id) == 3 =>
match @ir.hot_node_exact_instr(func, child_id) {
Some(@lib.Instruction::AtomicCmpxchg(_, _, _)) =>
Some({ kind: MaxBitsBlockPrefixDropAtomicCmpxchg, root_id, })
Some(@lib.Instruction::ArrayAtomicRmw(_, _, _)) =>
Some({ kind: MaxBitsBlockPrefixDropArrayAtomicRmw, root_id, })
Some(
@lib.Instruction::MemoryAtomicWait32(_)
| @lib.Instruction::MemoryAtomicWait64(_)
) => Some({ kind: MaxBitsBlockPrefixDropAtomicWait, root_id, })
_ => None
}
@ir.HotOp::Atomic if @ir.hot_child_count(func, child_id) == 4 =>
match @ir.hot_node_exact_instr(func, child_id) {
Some(@lib.Instruction::ArrayAtomicCmpxchg(_)) =>
Some({ kind: MaxBitsBlockPrefixDropArrayAtomicCmpxchg, root_id, })
_ => None
}
@ir.HotOp::Block =>
match
OiDroppedStructAtomicRmwLoweringFact::from_drop_child(
func, child_id,
) {
Some(fact) if fact.proves_preserved_prefix() =>
Some({
kind: MaxBitsBlockPrefixDropStructAtomicRmwLowering,
root_id,
})
_ =>
match
OiDroppedStructAtomicCmpxchgLoweringFact::from_drop_child(
func, child_id,
) {
Some(fact) if fact.proves_preserved_prefix() =>
Some({
kind: MaxBitsBlockPrefixDropStructAtomicCmpxchgLowering,
root_id,
})
_ => None
}
}
@ir.HotOp::Call => Some({ kind: MaxBitsBlockPrefixDropCall, root_id, })
@ir.HotOp::CallIndirect =>
Some({ kind: MaxBitsBlockPrefixDropCallIndirect, root_id, })
@ir.HotOp::CallRef =>
Some({ kind: MaxBitsBlockPrefixDropCallRef, root_id, })
@ir.HotOp::TableGrow if @ir.hot_child_count(func, child_id) == 2 =>
Some({ kind: MaxBitsBlockPrefixDropTableGrow, root_id, })
@ir.HotOp::MemoryGrow if @ir.hot_child_count(func, child_id) == 1 =>
Some({ kind: MaxBitsBlockPrefixDropMemoryGrow, root_id, })
_ => None
}
}
@ir.HotOp::DataDrop => Some({ kind: MaxBitsBlockPrefixDataDrop, root_id, })
@ir.HotOp::ElemDrop => Some({ kind: MaxBitsBlockPrefixElemDrop, root_id, })
@ir.HotOp::MemoryInit if @ir.hot_child_count(func, root_id) == 3 =>
Some({ kind: MaxBitsBlockPrefixMemoryInit, root_id, })
@ir.HotOp::MemoryCopy if @ir.hot_child_count(func, root_id) == 3 =>
Some({ kind: MaxBitsBlockPrefixMemoryCopy, root_id, })
@ir.HotOp::MemoryFill if @ir.hot_child_count(func, root_id) == 3 =>
Some({ kind: MaxBitsBlockPrefixMemoryFill, root_id, })
_ => None
}
}
///|
fn OiMaxBitsBlockPrefixFact::proves_preservable_prefix(
self : OiMaxBitsBlockPrefixFact,
) -> Bool {
ignore(self.root_id)
match self.kind {
MaxBitsBlockPrefixPure
| MaxBitsBlockPrefixLocalSet
| MaxBitsBlockPrefixStore
| MaxBitsBlockPrefixAtomicStore
| MaxBitsBlockPrefixCall
| MaxBitsBlockPrefixCallIndirect
| MaxBitsBlockPrefixCallRef
| MaxBitsBlockPrefixDropCall
| MaxBitsBlockPrefixDropCallIndirect
| MaxBitsBlockPrefixDropCallRef
| MaxBitsBlockPrefixGlobalSet
| MaxBitsBlockPrefixTableSet
| MaxBitsBlockPrefixTableFill
| MaxBitsBlockPrefixTableCopy
| MaxBitsBlockPrefixTableInit
| MaxBitsBlockPrefixDropAtomicLoad
| MaxBitsBlockPrefixDropStructAtomicGet
| MaxBitsBlockPrefixDropStructAtomicRmwLowering
| MaxBitsBlockPrefixDropStructAtomicCmpxchgLowering
| MaxBitsBlockPrefixDropAtomicRmw
| MaxBitsBlockPrefixDropArrayAtomicRmw
| MaxBitsBlockPrefixDropArrayAtomicCmpxchg
| MaxBitsBlockPrefixDropAtomicCmpxchg
| MaxBitsBlockPrefixDropAtomicWait
| MaxBitsBlockPrefixDropAtomicNotify
| MaxBitsBlockPrefixDropTableGrow
| MaxBitsBlockPrefixDropMemoryGrow
| MaxBitsBlockPrefixDataDrop
| MaxBitsBlockPrefixElemDrop
| MaxBitsBlockPrefixMemoryInit
| MaxBitsBlockPrefixMemoryCopy
| MaxBitsBlockPrefixMemoryFill => true
}
}
///|
priv struct OiMaxBitsFallthroughFact {
value_id : Int32
}
///|
fn OiMaxBitsFallthroughFact::from_single_value_block(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
local_facts : OptimizeInstructionsLocalFacts?,
) -> OiMaxBitsFallthroughFact? {
// Starshine's represented block fallthrough mirrors the local Binaryen
// `Properties::getFallthrough` maxBits path used by the local scanner. Record
// the fallthrough child as a fact before the unsigned-max query recurses. For
// region-backed blocks, only pure prefixes and the first source-backed
// straight-line effectful prefix are admitted so folded constants do not erase
// effects or branch/control behavior.
let node = @ir.hot_node_get(func, node_id)
// The tail is not the only result if any operand can branch to this block.
// The shared inventory includes nested operands and exception destinations.
let label_used = match local_facts {
Some(facts) => {
if facts.label_used_revision != func.revision {
facts.label_used = pass_compute_label_used(func)
facts.label_used_revision = func.revision
}
facts.label_used
}
None => pass_compute_label_used(func)
}
if pass_label_is_used(@ir.hot_control_node_label(func, node_id), label_used) {
return None
}
match node.op {
@ir.HotOp::Block if node.child_count == 1 =>
Some({ value_id: func.children[node.child_base], })
@ir.HotOp::Block => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count == 0 {
return None
}
for slot = 0; slot < root_count - 1; slot = slot + 1 {
let root_id = @ir.hot_region_get(func, body_ref, slot)
match OiMaxBitsBlockPrefixFact::from_root(func, dead, root_id) {
Some(fact) if fact.proves_preservable_prefix() => ()
_ => return None
}
}
Some({ value_id: @ir.hot_region_get(func, body_ref, root_count - 1), })
}
_ => None
}
}
///|
fn OiMaxBitsFallthroughFact::from_local_tee(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMaxBitsFallthroughFact? {
// Binaryen's fallthrough/maxBits path can inspect the value written by a
// `local.tee` while later replacement still preserves the tee as a dropped
// effect. Keep that as a separate fact from erasable single-value blocks.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalTee if node.child_count == 1 =>
Some({ value_id: func.children[node.child_base], })
_ => None
}
}
///|
priv enum OiAssignedMaxBitsFallthroughKind {
AssignedMaxBitsNoFallthrough
AssignedMaxBitsLoopTail
AssignedMaxBitsIfOneSidedArmTail
}
///|
priv struct OiAssignedMaxBitsFallthroughFact {
kind : OiAssignedMaxBitsFallthroughKind
value_id : Int32
}
///|
fn OiAssignedMaxBitsFallthroughFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiAssignedMaxBitsFallthroughFact {
// Binaryen v130 LocalScanner asks `Properties::getFallthrough` about
// local-set/tee values before feeding `Bits::getMaxBits`. Keep assignment-only
// loop and one-sided-if fallthrough separate from the direct compare producer
// path because the oracle keeps direct control maxBits compares spelled as
// compares.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Loop => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
match optimize_instructions_region_tail_id(func, body_ref) {
Some(tail_id) => { kind: AssignedMaxBitsLoopTail, value_id: tail_id, }
None => { kind: AssignedMaxBitsNoFallthrough, value_id: node_id, }
}
}
@ir.HotOp::If => {
let else_ref = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None =>
return { kind: AssignedMaxBitsNoFallthrough, value_id: node_id, }
}
let then_ref = @ir.hot_region_then_ref(func, node_id)
let one_sided = OiIfOneSidedFallthroughRegionFact::from_regions(
func, then_ref, else_ref,
)
let fallthrough_region = match one_sided.proven_fallthrough_region() {
Some(region) => region
None =>
return { kind: AssignedMaxBitsNoFallthrough, value_id: node_id, }
}
match optimize_instructions_region_tail_id(func, fallthrough_region) {
Some(tail_id) =>
{ kind: AssignedMaxBitsIfOneSidedArmTail, value_id: tail_id, }
None => { kind: AssignedMaxBitsNoFallthrough, value_id: node_id, }
}
}
_ => { kind: AssignedMaxBitsNoFallthrough, value_id: node_id, }
}
}
///|
fn OiAssignedMaxBitsFallthroughFact::selected_value(
self : OiAssignedMaxBitsFallthroughFact,
) -> Int32 {
match self.kind {
AssignedMaxBitsLoopTail | AssignedMaxBitsIfOneSidedArmTail => self.value_id
AssignedMaxBitsNoFallthrough => self.value_id
}
}
///|
priv enum OiCombinedShiftZeroBitsKind {
CombinedShiftZeroI32
CombinedShiftZeroI64
}
///|
priv struct OiCombinedShiftZeroBitsFact {
kind : OiCombinedShiftZeroBitsKind
}
///|
fn OiCombinedShiftZeroBitsFact::from_i32_shr_u(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
amount : Int32,
) -> OiCombinedShiftZeroBitsFact? {
// When Starshine combines nested logical right shifts before the general
// zero-bits replacement gets a second look, preserve the same Binaryen
// `Bits::getMaxBits` fact: if all known lhs bits are shifted out and the lhs
// is pure, the combined shift is a typed zero.
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id) {
return None
}
match OiUnsignedMaxValueFact::i32_max_for_node(func, dead, None, lhs_id) {
Some(max_value) if OiUnsignedMaxValueFact::i32_bits_for_max(max_value) <=
amount => Some({ kind: CombinedShiftZeroI32, })
_ => None
}
}
///|
fn OiCombinedShiftZeroBitsFact::from_i64_shr_u(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
amount : Int32,
) -> OiCombinedShiftZeroBitsFact? {
// Same combined-shift zero-bit fact for i64 logical right shifts.
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id) {
return None
}
match OiUnsignedMaxValueFact::i64_max_for_node(func, dead, None, lhs_id) {
Some(max_value) if OiUnsignedMaxValueFact::i64_bits_for_max(max_value) <=
amount => Some({ kind: CombinedShiftZeroI64, })
_ => None
}
}
///|
fn OiCombinedShiftZeroBitsFact::apply(
self : OiCombinedShiftZeroBitsFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.kind {
CombinedShiftZeroI32 =>
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
CombinedShiftZeroI64 =>
optimize_instructions_replace_with_const_i64(
ctx,
func,
node_id,
@lib.I64(0L),
)
}
}
///|
priv struct OiI32ArithmeticMaxBitsFact {
value_id : Int32
lhs_id : Int32
rhs_id : Int32
max_bits : Int32
}
///|
priv struct OiI64ArithmeticMaxBitsFact {
value_id : Int32
lhs_id : Int32
rhs_id : Int32
max_bits : Int32
}
///|
fn optimize_instructions_max_bits_cap(
width_bits : Int32,
max_bits : Int32,
) -> Int32 {
if max_bits > width_bits {
width_bits
} else {
max_bits
}
}
///|
priv struct OiI32AddMaxBitsFact {
max_bits : Int32
}
///|
priv struct OiI64AddMaxBitsFact {
max_bits : Int32
}
///|
fn OiI32AddMaxBitsFact::from_bits(
left_bits : Int32,
right_bits : Int32,
) -> OiI32AddMaxBitsFact {
// Binaryen v130 add maxBits is one more than the widest operand, capped to the
// i32 lane width.
let operand_bits = if left_bits > right_bits { left_bits } else { right_bits }
{ max_bits: optimize_instructions_max_bits_cap(32, operand_bits + 1), }
}
///|
fn OiI64AddMaxBitsFact::from_bits(
left_bits : Int32,
right_bits : Int32,
) -> OiI64AddMaxBitsFact {
// Same Binaryen v130 add formula for the i64 lane.
let operand_bits = if left_bits > right_bits { left_bits } else { right_bits }
{ max_bits: optimize_instructions_max_bits_cap(64, operand_bits + 1), }
}
///|
priv struct OiI32MulMaxBitsFact {
max_bits : Int32
}
///|
priv struct OiI64MulMaxBitsFact {
max_bits : Int32
}
///|
fn OiI32MulMaxBitsFact::from_bits(
left_bits : Int32,
right_bits : Int32,
) -> OiI32MulMaxBitsFact {
// Binaryen v130 multiply maxBits sums operand max-bit counts, capped to the
// i32 lane width.
{ max_bits: optimize_instructions_max_bits_cap(32, left_bits + right_bits), }
}
///|
fn OiI64MulMaxBitsFact::from_bits(
left_bits : Int32,
right_bits : Int32,
) -> OiI64MulMaxBitsFact {
// Same Binaryen v130 multiply formula for the i64 lane.
{ max_bits: optimize_instructions_max_bits_cap(64, left_bits + right_bits), }
}
///|
priv struct OiI32DivMaxBitsFact {
max_bits : Int32
}
///|
priv struct OiI64DivMaxBitsFact {
max_bits : Int32
}
///|
fn OiI32DivMaxBitsFact::from_bits(
left_bits : Int32,
right_min_bits : Int32,
) -> OiI32DivMaxBitsFact? {
// Divisor *minimum* bits bound the quotient. A maximum-bit count cannot
// exclude divisor 1; zero denotes a known trapping divisor.
if right_min_bits == 0 {
None
} else if left_bits < right_min_bits {
Some({ max_bits: left_bits, })
} else {
let quotient_bits = left_bits - right_min_bits + 1
Some({
max_bits: if quotient_bits < left_bits {
quotient_bits
} else {
left_bits
},
})
}
}
///|
fn OiI64DivMaxBitsFact::from_bits(
left_bits : Int32,
right_min_bits : Int32,
) -> OiI64DivMaxBitsFact? {
// Divisor *minimum* bits bound the quotient. A maximum-bit count cannot
// exclude divisor 1; zero denotes a known trapping divisor.
if right_min_bits == 0 {
None
} else if left_bits < right_min_bits {
Some({ max_bits: left_bits, })
} else {
let quotient_bits = left_bits - right_min_bits + 1
Some({
max_bits: if quotient_bits < left_bits {
quotient_bits
} else {
left_bits
},
})
}
}
///|
priv struct OiI32RemMaxBitsFact {
max_bits : Int32
}
///|
priv struct OiI64RemMaxBitsFact {
max_bits : Int32
}
///|
fn OiI32RemMaxBitsFact::from_bits(
left_bits : Int32,
right_bits : Int32,
) -> OiI32RemMaxBitsFact? {
// Binaryen v130 bounds remainder by the smaller operand max-bit count. A zero
// max-bit divisor is a trap-preservation boundary for now, matching div above.
if right_bits == 0 {
None
} else {
Some({
max_bits: if left_bits < right_bits {
left_bits
} else {
right_bits
},
})
}
}
///|
fn OiI64RemMaxBitsFact::from_bits(
left_bits : Int32,
right_bits : Int32,
) -> OiI64RemMaxBitsFact? {
// Same Binaryen v130 remainder formula and zero-divisor trap boundary for the
// i64 lane.
if right_bits == 0 {
None
} else {
Some({
max_bits: if left_bits < right_bits {
left_bits
} else {
right_bits
},
})
}
}
///|
priv struct OiI32ArithmeticShiftMaxBitsFact {
amount : Int32
max_bits : Int32
}
///|
priv struct OiI64ArithmeticShiftMaxBitsFact {
amount : Int32
max_bits : Int32
}
///|
fn OiI32ArithmeticShiftMaxBitsFact::from_shl(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_bits : Int32,
rhs_id : Int32,
) -> OiI32ArithmeticShiftMaxBitsFact? {
match OiI32EffectiveShiftAmountFact::from_const_operand(func, dead, rhs_id) {
Some(fact) =>
Some({
amount: fact.amount,
max_bits: optimize_instructions_max_bits_cap(32, lhs_bits + fact.amount),
})
None => None
}
}
///|
fn OiI32ArithmeticShiftMaxBitsFact::from_shr_s(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_bits : Int32,
rhs_id : Int32,
) -> OiI32ArithmeticShiftMaxBitsFact? {
match OiI32EffectiveShiftAmountFact::from_const_operand(func, dead, rhs_id) {
Some(fact) if lhs_bits < 32 => {
let shifts = if fact.amount < lhs_bits { fact.amount } else { lhs_bits }
Some({ amount: fact.amount, max_bits: lhs_bits - shifts, })
}
_ => None
}
}
///|
fn OiI64ArithmeticShiftMaxBitsFact::from_shl(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_bits : Int32,
rhs_id : Int32,
) -> OiI64ArithmeticShiftMaxBitsFact? {
match OiI64EffectiveShiftAmountFact::from_const_operand(func, dead, rhs_id) {
Some(fact) =>
Some({
amount: fact.amount,
max_bits: optimize_instructions_max_bits_cap(64, lhs_bits + fact.amount),
})
None => None
}
}
///|
fn OiI64ArithmeticShiftMaxBitsFact::from_shr_s(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_bits : Int32,
rhs_id : Int32,
) -> OiI64ArithmeticShiftMaxBitsFact? {
match OiI64EffectiveShiftAmountFact::from_const_operand(func, dead, rhs_id) {
Some(fact) if lhs_bits < 64 => {
let shifts = if fact.amount < lhs_bits { fact.amount } else { lhs_bits }
Some({ amount: fact.amount, max_bits: lhs_bits - shifts, })
}
_ => None
}
}
///|
fn OiI32ArithmeticMaxBitsFact::from_max_bits(
value_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
max_bits : Int32,
) -> OiI32ArithmeticMaxBitsFact {
{ value_id, lhs_id, rhs_id, max_bits, }
}
///|
fn OiI64ArithmeticMaxBitsFact::from_max_bits(
value_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
max_bits : Int32,
) -> OiI64ArithmeticMaxBitsFact {
{ value_id, lhs_id, rhs_id, max_bits, }
}
///|
fn OiI32ArithmeticMaxBitsFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32ArithmeticMaxBitsFact? {
// Binaryen v130 `Bits::getMaxBits` derives i32 arithmetic max-bit counts
// from operand max bits for add, multiply, constant left shift, arithmetic
// right shift, division, and remainder. Keep the width-specific wrapper fact
// explicit so downstream zero-bit, signed-to-unsigned, and wrap/extend
// consumers do not grow their own operator whitelists.
let lhs_max = OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, lhs_id,
)
let lhs_bits = OiUnsignedMaxValueFact::i32_max_bits_or_unknown(lhs_max)
let max_bits = match instr {
@lib.Instruction::I32Add => {
let rhs_bits = OiUnsignedMaxValueFact::i32_max_bits_or_unknown(
OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, rhs_id,
),
)
let fact = OiI32AddMaxBitsFact::from_bits(lhs_bits, rhs_bits)
Some(fact.max_bits)
}
@lib.Instruction::I32Mul => {
let rhs_bits = OiUnsignedMaxValueFact::i32_max_bits_or_unknown(
OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, rhs_id,
),
)
let fact = OiI32MulMaxBitsFact::from_bits(lhs_bits, rhs_bits)
Some(fact.max_bits)
}
@lib.Instruction::I32DivS | @lib.Instruction::I32DivU => {
let rhs_max = OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, rhs_id,
)
let rhs_bits = OiUnsignedMaxValueFact::i32_max_bits_or_unknown(rhs_max)
// Signed negative quotients have high bits set. Both operands must be
// nonnegative before an unsigned magnitude bound is useful.
if instr is @lib.Instruction::I32DivS &&
(lhs_bits == 32 || rhs_bits == 32) {
return None
}
let rhs_min_bits = match
optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(value)) =>
OiUnsignedMaxValueFact::i32_max_bits_or_unknown(Some(value))
None => 1
}
match OiI32DivMaxBitsFact::from_bits(lhs_bits, rhs_min_bits) {
Some(fact) => Some(fact.max_bits)
None => None
}
}
@lib.Instruction::I32RemS | @lib.Instruction::I32RemU => {
// Signed remainder inherits the dividend sign, not an unsigned bound
// from the divisor's magnitude.
if instr is @lib.Instruction::I32RemS && lhs_bits == 32 {
return None
}
let rhs_bits = OiUnsignedMaxValueFact::i32_max_bits_or_unknown(
OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, rhs_id,
),
)
match OiI32RemMaxBitsFact::from_bits(lhs_bits, rhs_bits) {
Some(fact) => Some(fact.max_bits)
None => None
}
}
@lib.Instruction::I32Shl =>
match
OiI32ArithmeticShiftMaxBitsFact::from_shl(func, dead, lhs_bits, rhs_id) {
Some(fact) => {
ignore(fact.amount)
Some(fact.max_bits)
}
None => None
}
@lib.Instruction::I32ShrS =>
match
OiI32ArithmeticShiftMaxBitsFact::from_shr_s(
func, dead, lhs_bits, rhs_id,
) {
Some(fact) => {
ignore(fact.amount)
Some(fact.max_bits)
}
None => None
}
_ => None
}
match max_bits {
Some(bits) =>
Some(
OiI32ArithmeticMaxBitsFact::from_max_bits(node_id, lhs_id, rhs_id, bits),
)
None => None
}
}
///|
fn OiI64ArithmeticMaxBitsFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64ArithmeticMaxBitsFact? {
// Same Binaryen v130 arithmetic `Bits::getMaxBits` family as the i32 fact,
// specialized for i64 widths and i64-typed shift operands in Starshine HOT.
let lhs_max = OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, lhs_id,
)
let lhs_bits = OiUnsignedMaxValueFact::i64_max_bits_or_unknown(lhs_max)
let max_bits = match instr {
@lib.Instruction::I64Add => {
let rhs_bits = OiUnsignedMaxValueFact::i64_max_bits_or_unknown(
OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, rhs_id,
),
)
let fact = OiI64AddMaxBitsFact::from_bits(lhs_bits, rhs_bits)
Some(fact.max_bits)
}
@lib.Instruction::I64Mul => {
let rhs_bits = OiUnsignedMaxValueFact::i64_max_bits_or_unknown(
OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, rhs_id,
),
)
let fact = OiI64MulMaxBitsFact::from_bits(lhs_bits, rhs_bits)
Some(fact.max_bits)
}
@lib.Instruction::I64DivS | @lib.Instruction::I64DivU => {
let rhs_max = OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, rhs_id,
)
let rhs_bits = OiUnsignedMaxValueFact::i64_max_bits_or_unknown(rhs_max)
// Signed negative quotients have high bits set. Both operands must be
// nonnegative before an unsigned magnitude bound is useful.
if instr is @lib.Instruction::I64DivS &&
(lhs_bits == 64 || rhs_bits == 64) {
return None
}
let rhs_min_bits = match
optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(value)) =>
OiUnsignedMaxValueFact::i64_max_bits_or_unknown(Some(value))
None => 1
}
match OiI64DivMaxBitsFact::from_bits(lhs_bits, rhs_min_bits) {
Some(fact) => Some(fact.max_bits)
None => None
}
}
@lib.Instruction::I64RemS | @lib.Instruction::I64RemU => {
if instr is @lib.Instruction::I64RemS && lhs_bits == 64 {
return None
}
let rhs_bits = OiUnsignedMaxValueFact::i64_max_bits_or_unknown(
OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, rhs_id,
),
)
match OiI64RemMaxBitsFact::from_bits(lhs_bits, rhs_bits) {
Some(fact) => Some(fact.max_bits)
None => None
}
}
@lib.Instruction::I64Shl =>
match
OiI64ArithmeticShiftMaxBitsFact::from_shl(func, dead, lhs_bits, rhs_id) {
Some(fact) => {
ignore(fact.amount)
Some(fact.max_bits)
}
None => None
}
@lib.Instruction::I64ShrS =>
match
OiI64ArithmeticShiftMaxBitsFact::from_shr_s(
func, dead, lhs_bits, rhs_id,
) {
Some(fact) => {
ignore(fact.amount)
Some(fact.max_bits)
}
None => None
}
_ => None
}
match max_bits {
Some(bits) =>
Some(
OiI64ArithmeticMaxBitsFact::from_max_bits(node_id, lhs_id, rhs_id, bits),
)
None => None
}
}
///|
fn OiUnsignedMaxValueFact::from_i32_max(
max_value : Int32?,
) -> OiUnsignedMaxValueFact {
{ i32_max: max_value, i64_max: None, }
}
///|
fn OiUnsignedMaxValueFact::from_i64_max(
max_value : Int64?,
) -> OiUnsignedMaxValueFact {
{ i32_max: None, i64_max: max_value, }
}
///|
fn OiUnsignedMaxValueFact::from_i32_node_with_local_facts(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> OiUnsignedMaxValueFact {
let max_value = OiUnsignedMaxValueFact::i32_max_for_node(
func, dead, local_facts, node_id,
)
OiUnsignedMaxValueFact::from_i32_max(max_value)
}
///|
fn OiUnsignedMaxValueFact::from_i64_node_with_local_facts(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> OiUnsignedMaxValueFact {
let max_value = OiUnsignedMaxValueFact::i64_max_for_node(
func, dead, local_facts, node_id,
)
OiUnsignedMaxValueFact::from_i64_max(max_value)
}
///|
fn OiUnsignedMaxValueFact::from_i32_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiUnsignedMaxValueFact {
OiUnsignedMaxValueFact::from_i32_node_with_local_facts(
func,
dead,
Some(scratch.local_facts),
node_id,
)
}
///|
fn OiUnsignedMaxValueFact::from_i64_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiUnsignedMaxValueFact {
OiUnsignedMaxValueFact::from_i64_node_with_local_facts(
func,
dead,
Some(scratch.local_facts),
node_id,
)
}
///|
fn OiUnsignedMaxValueFact::assigned_i32_max_for_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
facts : OptimizeInstructionsLocalFacts,
node_id : Int32,
) -> Int32? {
if node_id < 0 || node_id >= facts.assigned_i32_max_by_node.length() {
return None
}
if facts.assigned_i32_max_ready[node_id] {
return facts.assigned_i32_max_by_node[node_id]
}
facts.assigned_i32_max_ready[node_id] = true
let max_value = match
OiI32MaxBitsProducerFact::from_node(func, dead, Some(facts), node_id) {
Some(fact) => {
ignore(fact.value_id)
fact.max_value
}
None => None
}
facts.assigned_i32_max_by_node[node_id] = max_value
max_value
}
///|
fn OiUnsignedMaxValueFact::assigned_i64_max_for_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
facts : OptimizeInstructionsLocalFacts,
node_id : Int32,
) -> Int64? {
if node_id < 0 || node_id >= facts.assigned_i64_max_by_node.length() {
return None
}
if facts.assigned_i64_max_ready[node_id] {
return facts.assigned_i64_max_by_node[node_id]
}
facts.assigned_i64_max_ready[node_id] = true
let max_value = match
OiI64MaxBitsProducerFact::from_node(func, dead, Some(facts), node_id) {
Some(fact) => {
ignore(fact.value_id)
fact.max_value
}
None => None
}
facts.assigned_i64_max_by_node[node_id] = max_value
max_value
}
///|
fn OiUnsignedMaxValueFact::from_assigned_i32_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiUnsignedMaxValueFact {
// Assignment facts seed the LocalScanner-like summary. They deliberately do
// not recurse through LocalGet summaries, matching the conservative previous
// scanner behavior for local-carried and self-referential values. Keep a
// separate node cache for this local-free query mode: long local.tee chains
// share expression prefixes, while the ordinary cache may consume finalized
// local facts and therefore cannot safely be reused during collection.
let fallthrough = OiAssignedMaxBitsFallthroughFact::from_node(func, node_id)
OiUnsignedMaxValueFact::from_i32_max(
OiUnsignedMaxValueFact::assigned_i32_max_for_node(
func,
dead,
scratch.local_facts,
fallthrough.selected_value(),
),
)
}
///|
fn OiUnsignedMaxValueFact::from_assigned_i64_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiUnsignedMaxValueFact {
// Same separate local-free cache for i64 assignment values.
let fallthrough = OiAssignedMaxBitsFallthroughFact::from_node(func, node_id)
OiUnsignedMaxValueFact::from_i64_max(
OiUnsignedMaxValueFact::assigned_i64_max_for_node(
func,
dead,
scratch.local_facts,
fallthrough.selected_value(),
),
)
}
///|
priv struct OiBooleanI32MaxBitsFact {
value_id : Int32
}
///|
priv struct OiDroppedChildrenReplacementFact {
drop_ids : Array[Int32]
}
///|
priv enum OiBooleanMaxBitsBinaryRewriteKind {
BooleanMaxBitsForwardLeft
BooleanMaxBitsEqzLeftI32
BooleanMaxBitsConstI32One
BooleanMaxBitsWrapLeftI64
BooleanMaxBitsEqzLeftI64ExtendU
BooleanMaxBitsConstI64One
}
///|
priv struct OiBooleanMaxBitsBinaryRewriteFact {
kind : OiBooleanMaxBitsBinaryRewriteKind
left_id : Int32
dropped_children : OiDroppedChildrenReplacementFact
}
///|
fn OiBooleanI32MaxBitsFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBooleanI32MaxBitsFact? {
// Binaryen v130 `Bits::getMaxBits` treats integer comparisons and `eqz`
// results as i32 booleans, so they require at most one bit. Keep this as an
// explicit fact instead of scattering compare/eqz whitelists across maxBits
// consumers.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Compare => Some({ value_id: node_id, })
@ir.HotOp::Unary =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Eqz) | Some(@lib.Instruction::I64Eqz) =>
Some({ value_id: node_id, })
_ => None
}
_ => None
}
}
///|
fn OiDroppedChildrenReplacementFact::empty() -> OiDroppedChildrenReplacementFact {
{ drop_ids: [], }
}
///|
fn OiDroppedChildrenReplacementFact::from_child_ids(
ctx : HotPassContext,
func : @ir.HotFunc,
child_ids : Array[Int32],
) -> OiDroppedChildrenReplacementFact {
// Binaryen v130 rewrites such as `replaceZeroBitsWithZero` and maxBits
// `bool | 1` delegate erased operands to `getDroppedChildrenAndAppend`.
// Represent the bounded HOT subset explicitly: each immediate child whose
// subtree has unremovable effects or traps is sequenced as `drop(child)` in
// original child order before the synthesized constant, while pure children
// are erased.
let effects = pass_require_effects(ctx, func)
let drop_ids : Array[Int32] = []
for child_id in child_ids {
if !OiNodeEffectPurityFact::from_node_masks(effects.node_masks, child_id).proves_pure() {
drop_ids.push(child_id)
}
}
{ drop_ids, }
}
///|
fn OiDroppedChildrenReplacementFact::from_node_children(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiDroppedChildrenReplacementFact {
let node = @ir.hot_node_get(func, node_id)
let child_ids : Array[Int32] = []
for slot = 0; slot < node.child_count; slot = slot + 1 {
child_ids.push(@ir.hot_child_get(func, node_id, slot))
}
OiDroppedChildrenReplacementFact::from_child_ids(ctx, func, child_ids)
}
///|
fn OiDroppedChildrenReplacementFact::needs_drops(
self : OiDroppedChildrenReplacementFact,
) -> Bool {
self.drop_ids.length() > 0
}
///|
fn OiDroppedChildrenReplacementFact::replace_with_const_i32(
self : OiDroppedChildrenReplacementFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
value : @lib.I32,
) -> Unit {
if !self.needs_drops() {
optimize_instructions_replace_with_const_i32(ctx, func, node_id, value)
return
}
let body : Array[Int32] = []
for drop_id in self.drop_ids {
body.push(@ir.hot_build_drop(func, drop_id))
}
body.push(@ir.hot_build_const_i32(func, value))
optimize_instructions_replace_with_block_body(
ctx,
func,
node_id,
@lib.BlockType::val_type(@lib.ValType::i32()),
body,
)
}
///|
fn OiDroppedChildrenReplacementFact::replace_with_const_i64(
self : OiDroppedChildrenReplacementFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
value : @lib.I64,
) -> Unit {
if !self.needs_drops() {
optimize_instructions_replace_with_const_i64(ctx, func, node_id, value)
return
}
let body : Array[Int32] = []
for drop_id in self.drop_ids {
body.push(@ir.hot_build_drop(func, drop_id))
}
body.push(@ir.hot_build_const_i64(func, value))
optimize_instructions_replace_with_block_body(
ctx,
func,
node_id,
@lib.BlockType::val_type(@lib.ValType::i64()),
body,
)
}
///|
priv enum OiDroppedChildrenNontrappingRootKind {
DroppedChildrenI32AndRoot
DroppedChildrenI64AndRoot
DroppedChildrenI32OrRoot
DroppedChildrenI64OrRoot
DroppedChildrenI32XorRoot
DroppedChildrenI64XorRoot
DroppedChildrenI32AddRoot
DroppedChildrenI64AddRoot
DroppedChildrenI32ShlRoot
DroppedChildrenI64ShlRoot
DroppedChildrenI32ShrURoot
DroppedChildrenI64ShrURoot
DroppedChildrenI32ShrSRoot
DroppedChildrenI64ShrSRoot
DroppedChildrenI32EqzRoot
DroppedChildrenI64EqzRoot
DroppedChildrenI32WrapI64Root
DroppedChildrenI64ExtendI32SRoot
DroppedChildrenI64ExtendI32URoot
}
///|
priv struct OiDroppedChildrenNontrappingRootFact {
kind : OiDroppedChildrenNontrappingRootKind
}
///|
fn OiDroppedChildrenNontrappingRootFact::from_instr(
instr : @lib.Instruction,
) -> OiDroppedChildrenNontrappingRootFact? {
// Keep the non-pure zero-bit rewrite bounded to source-probed numeric roots
// whose Binaryen v130 `replaceZeroBitsWithZero` rewrite delegates preserved
// effects to `getDroppedChildrenAndAppend`. Trapping div/rem, checked
// conversions, and effectful multiply-by-zero (kept by Binaryen) remain
// explicit miss cases instead of being hidden behind a generic nontrapping
// opcode helper.
let kind = match instr {
@lib.Instruction::I32And => DroppedChildrenI32AndRoot
@lib.Instruction::I64And => DroppedChildrenI64AndRoot
@lib.Instruction::I32Or => DroppedChildrenI32OrRoot
@lib.Instruction::I64Or => DroppedChildrenI64OrRoot
@lib.Instruction::I32Xor => DroppedChildrenI32XorRoot
@lib.Instruction::I64Xor => DroppedChildrenI64XorRoot
@lib.Instruction::I32Add => DroppedChildrenI32AddRoot
@lib.Instruction::I64Add => DroppedChildrenI64AddRoot
@lib.Instruction::I32Shl => DroppedChildrenI32ShlRoot
@lib.Instruction::I64Shl => DroppedChildrenI64ShlRoot
@lib.Instruction::I32ShrU => DroppedChildrenI32ShrURoot
@lib.Instruction::I64ShrU => DroppedChildrenI64ShrURoot
@lib.Instruction::I32ShrS => DroppedChildrenI32ShrSRoot
@lib.Instruction::I64ShrS => DroppedChildrenI64ShrSRoot
@lib.Instruction::I32Eqz => DroppedChildrenI32EqzRoot
@lib.Instruction::I64Eqz => DroppedChildrenI64EqzRoot
@lib.Instruction::I32WrapI64 => DroppedChildrenI32WrapI64Root
@lib.Instruction::I64ExtendI32S => DroppedChildrenI64ExtendI32SRoot
@lib.Instruction::I64ExtendI32U => DroppedChildrenI64ExtendI32URoot
_ => return None
}
Some({ kind, })
}
///|
fn OiDroppedChildrenNontrappingRootFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDroppedChildrenNontrappingRootFact? {
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) => OiDroppedChildrenNontrappingRootFact::from_instr(instr)
None => None
}
}
///|
fn OiDroppedChildrenNontrappingRootFact::proves_nontrapping_root(
self : OiDroppedChildrenNontrappingRootFact,
) -> Bool {
match self.kind {
DroppedChildrenI32AndRoot
| DroppedChildrenI64AndRoot
| DroppedChildrenI32OrRoot
| DroppedChildrenI64OrRoot
| DroppedChildrenI32XorRoot
| DroppedChildrenI64XorRoot
| DroppedChildrenI32AddRoot
| DroppedChildrenI64AddRoot
| DroppedChildrenI32ShlRoot
| DroppedChildrenI64ShlRoot
| DroppedChildrenI32ShrURoot
| DroppedChildrenI64ShrURoot
| DroppedChildrenI32ShrSRoot
| DroppedChildrenI64ShrSRoot
| DroppedChildrenI32EqzRoot
| DroppedChildrenI64EqzRoot
| DroppedChildrenI32WrapI64Root
| DroppedChildrenI64ExtendI32SRoot
| DroppedChildrenI64ExtendI32URoot => true
}
}
///|
fn OiBooleanMaxBitsBinaryRewriteFact::from_i32_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiBooleanMaxBitsBinaryRewriteFact? {
// Binaryen v130 `visitBinary` uses `Bits::getMaxBits(left) == 1` to simplify
// boolean-valued integer expressions in `== 1`, `!= 0`, `& 1`, `!= 1`,
// `^ 1`, and `| 1` shells. Keep that as a maxBits consumer fact so compare,
// eqz, and future one-bit producers do not reappear as visitor whitelists.
match
OiUnsignedMaxValueFact::i32_max_for_node(func, dead, local_facts, lhs_id) {
Some(max_value) if OiUnsignedMaxValueFact::i32_bits_for_max(max_value) == 1 =>
()
_ => return None
}
let dropped_children = OiDroppedChildrenReplacementFact::empty()
match (instr, optimize_instructions_i32_const(func, dead, rhs_id)) {
(@lib.Instruction::I32Eq, Some(@lib.I32(1)))
| (@lib.Instruction::I32Ne, Some(@lib.I32(0)))
| (@lib.Instruction::I32And, Some(@lib.I32(1))) =>
Some({
kind: BooleanMaxBitsForwardLeft,
left_id: lhs_id,
dropped_children,
})
(@lib.Instruction::I32Ne, Some(@lib.I32(1)))
| (@lib.Instruction::I32Xor, Some(@lib.I32(1))) =>
Some({
kind: BooleanMaxBitsEqzLeftI32,
left_id: lhs_id,
dropped_children,
})
(@lib.Instruction::I32Or, Some(@lib.I32(1))) =>
Some({
kind: BooleanMaxBitsConstI32One,
left_id: lhs_id,
dropped_children: OiDroppedChildrenReplacementFact::from_child_ids(
ctx,
func,
[lhs_id],
),
})
_ => None
}
}
///|
fn OiBooleanMaxBitsBinaryRewriteFact::from_i64_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiBooleanMaxBitsBinaryRewriteFact? {
// Same Binaryen v130 maxBits consumer family for i64 boolean values. Compare
// shells return i32 via `i32.wrap_i64(left)`, while `xor 1` inverts the i64
// boolean and extends the i32 predicate result back to i64.
match
OiUnsignedMaxValueFact::i64_max_for_node(func, dead, local_facts, lhs_id) {
Some(max_value) if OiUnsignedMaxValueFact::i64_bits_for_max(max_value) == 1 =>
()
_ => return None
}
let dropped_children = OiDroppedChildrenReplacementFact::empty()
match (instr, optimize_instructions_i64_const(func, dead, rhs_id)) {
(@lib.Instruction::I64Eq, Some(@lib.I64(1L)))
| (@lib.Instruction::I64Ne, Some(@lib.I64(0L))) =>
Some({
kind: BooleanMaxBitsWrapLeftI64,
left_id: lhs_id,
dropped_children,
})
(@lib.Instruction::I64And, Some(@lib.I64(1L))) =>
Some({
kind: BooleanMaxBitsForwardLeft,
left_id: lhs_id,
dropped_children,
})
(@lib.Instruction::I64Ne, Some(@lib.I64(1L))) =>
Some({
kind: BooleanMaxBitsEqzLeftI32,
left_id: lhs_id,
dropped_children,
})
(@lib.Instruction::I64Xor, Some(@lib.I64(1L))) =>
Some({
kind: BooleanMaxBitsEqzLeftI64ExtendU,
left_id: lhs_id,
dropped_children,
})
(@lib.Instruction::I64Or, Some(@lib.I64(1L))) =>
Some({
kind: BooleanMaxBitsConstI64One,
left_id: lhs_id,
dropped_children: OiDroppedChildrenReplacementFact::from_child_ids(
ctx,
func,
[lhs_id],
),
})
_ => None
}
}
///|
fn OiBooleanMaxBitsBinaryRewriteFact::apply(
self : OiBooleanMaxBitsBinaryRewriteFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.kind {
BooleanMaxBitsForwardLeft =>
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.left_id,
)
BooleanMaxBitsEqzLeftI32 =>
match @ir.hot_node_exact_instr(func, self.left_id) {
Some(@lib.Instruction::I32Eqz) if @ir.hot_child_count(
func,
self.left_id,
) ==
1 =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
@ir.hot_child_get(func, self.left_id, 0),
@ir.hot_build_const_i32(func, @lib.I32(0)),
)
Some(@lib.Instruction::I64Eqz) if @ir.hot_child_count(
func,
self.left_id,
) ==
1 =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
@ir.hot_child_get(func, self.left_id, 0),
@ir.hot_build_const_i64(func, @lib.I64(0L)),
)
_ =>
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
self.left_id,
)
}
BooleanMaxBitsConstI32One =>
self.dropped_children.replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(1),
)
BooleanMaxBitsWrapLeftI64 =>
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_wrap_i64(),
self.left_id,
)
BooleanMaxBitsEqzLeftI64ExtendU => {
let eqz_id = @ir.hot_build_unary(func, @lib.ValType::i32(), self.left_id)
@ir.hot_node_exact_instr_set(func, eqz_id, @lib.Instruction::i64_eqz())
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_extend_i32u(),
eqz_id,
)
}
BooleanMaxBitsConstI64One =>
self.dropped_children.replace_with_const_i64(
ctx,
func,
node_id,
@lib.I64(1L),
)
}
}
///|
priv struct OiI32BinaryMaxBitsProducerFact {
value_id : Int32
lhs_id : Int32
rhs_id : Int32
max_value : Int32?
}
///|
priv struct OiI64BinaryMaxBitsProducerFact {
value_id : Int32
lhs_id : Int32
rhs_id : Int32
max_value : Int64?
}
///|
fn OiI32BinaryMaxBitsProducerFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32BinaryMaxBitsProducerFact? {
// Binaryen v130 `Bits::getMaxBits` binary-producer dispatch first tries
// bitwise facts, then logical-right-shift reducers, then arithmetic formulas.
// Keep that source order in one i32 fact so the width-specific producer
// dispatcher does not grow its own nested opcode list.
let max_value = match
OiI32BitwiseMaxBitsFact::from_binary(
func, dead, local_facts, instr, lhs_id, rhs_id,
) {
Some(fact) => fact.max_value
None =>
match
OiI32LogicalShiftMaxBitsFact::from_shr_u(
func, dead, local_facts, instr, lhs_id, rhs_id,
) {
Some(fact) => fact.max_value
None =>
match
OiI32ArithmeticMaxBitsFact::from_binary(
func, dead, local_facts, node_id, instr, lhs_id, rhs_id,
) {
Some(fact) => {
ignore(fact.value_id)
ignore(fact.lhs_id)
ignore(fact.rhs_id)
OiUnsignedMaxValueFact::i32_max_for_bits(fact.max_bits)
}
None => return None
}
}
}
Some({ value_id: node_id, lhs_id, rhs_id, max_value, })
}
///|
fn OiI64BinaryMaxBitsProducerFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64BinaryMaxBitsProducerFact? {
// Same Binaryen v130 binary-producer dispatch for i64 maxBits queries.
let max_value = match
OiI64BitwiseMaxBitsFact::from_binary(
func, dead, local_facts, instr, lhs_id, rhs_id,
) {
Some(fact) => fact.max_value
None =>
match
OiI64LogicalShiftMaxBitsFact::from_shr_u(
func, dead, local_facts, instr, lhs_id, rhs_id,
) {
Some(fact) => fact.max_value
None =>
match
OiI64ArithmeticMaxBitsFact::from_binary(
func, dead, local_facts, node_id, instr, lhs_id, rhs_id,
) {
Some(fact) => {
ignore(fact.value_id)
ignore(fact.lhs_id)
ignore(fact.rhs_id)
OiUnsignedMaxValueFact::i64_max_for_bits(fact.max_bits)
}
None => return None
}
}
}
Some({ value_id: node_id, lhs_id, rhs_id, max_value, })
}
///|
priv struct OiI32MaxBitsProducerFact {
value_id : Int32
max_value : Int32?
}
///|
priv struct OiI64MaxBitsProducerFact {
value_id : Int32
max_value : Int64?
}
///|
fn OiI32MaxBitsProducerFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> OiI32MaxBitsProducerFact? {
// Binaryen v130 `Bits::getMaxBits` asks a single producer classifier for the
// current expression before consumers use the resulting unsigned maximum.
// Keep the i32 dispatch itself fact-owned so the query method remains a thin
// accessor over source-backed leaf, local, boolean, conversion, bitwise,
// shift/arithmetic, and fallthrough producer facts.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
match OiI32UnsignedLoadMaxBitsFact::from_instr(instr, node_id) {
Some(fact) => {
ignore(fact.value_id)
return Some({ value_id: node_id, max_value: Some(fact.max_value), })
}
None => ()
}
None => ()
}
match OiBooleanI32MaxBitsFact::from_node(func, dead, node_id) {
Some(fact) => {
ignore(fact.value_id)
return Some({ value_id: node_id, max_value: Some(1), })
}
None => ()
}
let max_value = match node.op {
@ir.HotOp::Const =>
match OiI32UnsignedConstMaxValueFact::from_node(func, node_id) {
Some(fact) => {
ignore(fact.value_id)
Some(fact.max_value)
}
None => return None
}
@ir.HotOp::LocalGet =>
match OiI32LocalUnsignedMaxValueFact::from_get(local_facts, node.imm0) {
Some(fact) => {
ignore(fact.local_id)
Some(fact.max_value)
}
None => return None
}
@ir.HotOp::Unary | @ir.HotOp::Convert => {
let operand_id = func.children[node.child_base]
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
match
OiI32ConvertMaxBitsFact::from_convert(
func, dead, local_facts, instr, operand_id,
) {
Some(fact) => {
ignore(fact.operand_id)
Some(fact.max_value)
}
None => return None
}
None => return None
}
}
@ir.HotOp::Binary => {
let lhs_id = func.children[node.child_base]
let rhs_id = func.children[node.child_base + 1]
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
match
OiI32BinaryMaxBitsProducerFact::from_binary(
func, dead, local_facts, node_id, instr, lhs_id, rhs_id,
) {
Some(fact) => {
ignore(fact.value_id)
ignore(fact.lhs_id)
ignore(fact.rhs_id)
fact.max_value
}
None => return None
}
_ => return None
}
}
@ir.HotOp::Block =>
match
OiMaxBitsFallthroughFact::from_single_value_block(
func, dead, node_id, local_facts,
) {
Some(fact) =>
OiUnsignedMaxValueFact::i32_max_for_node(
func,
dead,
local_facts,
fact.value_id,
)
None => return None
}
@ir.HotOp::LocalTee =>
match OiMaxBitsFallthroughFact::from_local_tee(func, node_id) {
Some(fact) =>
OiUnsignedMaxValueFact::i32_max_for_node(
func,
dead,
local_facts,
fact.value_id,
)
None => return None
}
_ => return None
}
Some({ value_id: node_id, max_value, })
}
///|
fn OiI64MaxBitsProducerFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> OiI64MaxBitsProducerFact? {
// Same fact-owned producer dispatch for i64 maxBits queries. I64 does not use
// the i32 boolean-result fact directly; i64 one-bit values come from ordinary
// i64 producers such as zero/sign-extension, bitwise, shifts, and arithmetic.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
match OiI64UnsignedLoadMaxBitsFact::from_instr(instr, node_id) {
Some(fact) => {
ignore(fact.value_id)
return Some({ value_id: node_id, max_value: Some(fact.max_value), })
}
None => ()
}
None => ()
}
let max_value = match node.op {
@ir.HotOp::Const =>
match OiI64UnsignedConstMaxValueFact::from_node(func, node_id) {
Some(fact) => {
ignore(fact.value_id)
Some(fact.max_value)
}
None => return None
}
@ir.HotOp::LocalGet =>
match OiI64LocalUnsignedMaxValueFact::from_get(local_facts, node.imm0) {
Some(fact) => {
ignore(fact.local_id)
Some(fact.max_value)
}
None => return None
}
@ir.HotOp::Unary | @ir.HotOp::Convert => {
let operand_id = func.children[node.child_base]
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
match
OiI64ConvertMaxBitsFact::from_convert(
func, dead, local_facts, instr, operand_id,
) {
Some(fact) => {
ignore(fact.operand_id)
Some(fact.max_value)
}
None => return None
}
None => return None
}
}
@ir.HotOp::Binary => {
let lhs_id = func.children[node.child_base]
let rhs_id = func.children[node.child_base + 1]
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
match
OiI64BinaryMaxBitsProducerFact::from_binary(
func, dead, local_facts, node_id, instr, lhs_id, rhs_id,
) {
Some(fact) => {
ignore(fact.value_id)
ignore(fact.lhs_id)
ignore(fact.rhs_id)
fact.max_value
}
None => return None
}
_ => return None
}
}
@ir.HotOp::Block =>
match
OiMaxBitsFallthroughFact::from_single_value_block(
func, dead, node_id, local_facts,
) {
Some(fact) =>
OiUnsignedMaxValueFact::i64_max_for_node(
func,
dead,
local_facts,
fact.value_id,
)
None => return None
}
@ir.HotOp::LocalTee =>
match OiMaxBitsFallthroughFact::from_local_tee(func, node_id) {
Some(fact) =>
OiUnsignedMaxValueFact::i64_max_for_node(
func,
dead,
local_facts,
fact.value_id,
)
None => return None
}
_ => return None
}
Some({ value_id: node_id, max_value, })
}
///|
fn OiUnsignedMaxValueFact::i32_max_for_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> Int32? {
match local_facts {
Some(facts) if node_id >= 0 && node_id < facts.i32_max_by_node.length() => {
if facts.i32_max_ready[node_id] {
return facts.i32_max_by_node[node_id]
}
// Mark before recursive fallthrough queries so malformed or cyclic HOT
// graphs fail closed instead of recursing. OI rewrites preserve values,
// so this revision-local upper bound remains sound after child rewrites;
// an unknown pre-rewrite result intentionally stays unknown.
facts.i32_max_ready[node_id] = true
let max_value = match
OiI32MaxBitsProducerFact::from_node(func, dead, local_facts, node_id) {
Some(fact) => {
ignore(fact.value_id)
fact.max_value
}
None => None
}
facts.i32_max_by_node[node_id] = max_value
max_value
}
_ =>
match
OiI32MaxBitsProducerFact::from_node(func, dead, local_facts, node_id) {
Some(fact) => {
ignore(fact.value_id)
fact.max_value
}
None => None
}
}
}
///|
fn OiUnsignedMaxValueFact::i64_max_for_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> Int64? {
match local_facts {
Some(facts) if node_id >= 0 && node_id < facts.i64_max_by_node.length() => {
if facts.i64_max_ready[node_id] {
return facts.i64_max_by_node[node_id]
}
facts.i64_max_ready[node_id] = true
let max_value = match
OiI64MaxBitsProducerFact::from_node(func, dead, local_facts, node_id) {
Some(fact) => {
ignore(fact.value_id)
fact.max_value
}
None => None
}
facts.i64_max_by_node[node_id] = max_value
max_value
}
_ =>
match
OiI64MaxBitsProducerFact::from_node(func, dead, local_facts, node_id) {
Some(fact) => {
ignore(fact.value_id)
fact.max_value
}
None => None
}
}
}
///|
fn optimize_instructions_i32_unsigned_max_fact_for_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Int32? {
OiUnsignedMaxValueFact::from_i32_value(func, dead, scratch, node_id).i32_max
}
///|
priv struct OiEqzWrapI64MaxBitsFact {
value_id : Int32
}
///|
fn OiEqzWrapI64MaxBitsFact::from_eqz_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiEqzWrapI64MaxBitsFact? {
// Binaryen v130 rewrites `i32.eqz(i32.wrap_i64(x))` to `i64.eqz(x)` when
// `Bits::getMaxBits(x) <= 32`. Reuse the shared unsigned-max fact instead of
// adding a local wrapper scanner so all source-backed maxBits producers feed
// this consumer.
match instr {
@lib.Instruction::I32Eqz => ()
_ => return None
}
match @ir.hot_node_exact_instr(func, operand_id) {
Some(@lib.Instruction::I32WrapI64) if @ir.hot_child_count(func, operand_id) ==
1 => ()
_ => return None
}
let value_id = @ir.hot_child_get(func, operand_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, value_id) {
return None
}
match
OiUnsignedMaxValueFact::from_i64_value(func, dead, scratch, value_id).i64_max {
Some(max_value) if max_value <= 4294967295L => Some({ value_id, })
_ => None
}
}
///|
fn optimize_instructions_i64_unsigned_max_fact_for_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Int64? {
OiUnsignedMaxValueFact::from_i64_value(func, dead, scratch, node_id).i64_max
}
///|
priv struct OiExtendWrapI64MaxBitsFact {
value_id : Int32
}
///|
priv struct OiSignedExtendWrapI64FallbackFact {
value_id : Int32
}
///|
priv enum OiZeroBitsReplacementKind {
ZeroBitsI32
ZeroBitsI64
}
///|
priv struct OiZeroBitsReplacementFact {
kind : OiZeroBitsReplacementKind
dropped_children : OiDroppedChildrenReplacementFact
}
///|
fn OiZeroBitsReplacementFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiZeroBitsReplacementFact? {
// Binaryen v130 `replaceZeroBitsWithZero` replaces non-constant integer
// expressions when `Bits::getMaxBits(curr) == 0`, delegating effect
// preservation to `getDroppedChildrenAndAppend`. Starshine keeps that
// decision explicit as dropped-child facts and bounds the non-pure path to
// non-trapping numeric roots so operator traps are not erased.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Const => return None
_ => ()
}
let dropped_children = if OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, node_id,
) {
OiDroppedChildrenReplacementFact::empty()
} else {
match OiDroppedChildrenNontrappingRootFact::from_node(func, node_id) {
Some(root_fact) if root_fact.proves_nontrapping_root() =>
OiDroppedChildrenReplacementFact::from_node_children(ctx, func, node_id)
_ => return None
}
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[value_type] if value_type == @lib.ValType::i32() =>
match
OiUnsignedMaxValueFact::from_i32_value(func, dead, scratch, node_id).i32_max {
Some(0) => Some({ kind: ZeroBitsI32, dropped_children, })
_ => None
}
[value_type] if value_type == @lib.ValType::i64() =>
match
OiUnsignedMaxValueFact::from_i64_value(func, dead, scratch, node_id).i64_max {
Some(0L) => Some({ kind: ZeroBitsI64, dropped_children, })
_ => None
}
_ => None
}
}
///|
fn OiZeroBitsReplacementFact::apply(
self : OiZeroBitsReplacementFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.kind {
ZeroBitsI32 =>
self.dropped_children.replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
ZeroBitsI64 =>
self.dropped_children.replace_with_const_i64(
ctx,
func,
node_id,
@lib.I64(0L),
)
}
}
///|
fn OiExtendWrapI64MaxBitsFact::from_extend_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiExtendWrapI64MaxBitsFact? {
// Binaryen v130 rewrites `i64.extend_i32_s(i32.wrap_i64(x))` to `x` when
// `Bits::getMaxBits(x) <= 31`, and the unsigned spelling when maxBits <= 32.
// Use the shared unsigned-max fact so all source-backed maxBits producers feed
// the extend-wrap consumer instead of adding another wrapper-specific scanner.
let max_value_limit = match instr {
@lib.Instruction::I64ExtendI32S => 2147483647L
@lib.Instruction::I64ExtendI32U => 4294967295L
_ => return None
}
match @ir.hot_node_exact_instr(func, operand_id) {
Some(@lib.Instruction::I32WrapI64) if @ir.hot_child_count(func, operand_id) ==
1 => ()
_ => return None
}
let value_id = @ir.hot_child_get(func, operand_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, value_id) {
return None
}
match
OiUnsignedMaxValueFact::from_i64_value(func, dead, scratch, value_id).i64_max {
Some(max_value) if max_value <= max_value_limit => Some({ value_id, })
_ => None
}
}
///|
fn OiSignedExtendWrapI64FallbackFact::from_extend_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiSignedExtendWrapI64FallbackFact? {
// Binaryen v130 `visitUnary` applies the maxBits removal above first, then
// rewrites the remaining signed low-32-bit lane as
// `i64.extend32_s(x)`. Keep the fallback as an explicit fact sibling so the
// wrap/extend family stays source-shaped instead of a local opcode scanner.
match instr {
@lib.Instruction::I64ExtendI32S => ()
_ => return None
}
match @ir.hot_node_exact_instr(func, operand_id) {
Some(@lib.Instruction::I32WrapI64) if @ir.hot_child_count(func, operand_id) ==
1 => ()
_ => return None
}
let value_id = @ir.hot_child_get(func, operand_id, 0)
if OiLiveNodeFact::proves_node_live(func, dead, value_id) {
Some({ value_id, })
} else {
None
}
}
///|
fn OiSignedExtendWrapI64FallbackFact::apply(
self : OiSignedExtendWrapI64FallbackFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_extend32s(),
self.value_id,
)
}
///|
priv struct OiLiveNodeFact {
live : Bool
}
///|
fn OiLiveNodeFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiLiveNodeFact {
// Starshine keeps a pass-local dead bitset while mutating HOT. Route every
// OI liveness guard through a fact so rewrite families share one boundary
// for dead scratch ids, old HOT node slots, and freshly built live nodes.
let live = if node_id < 0 {
false
} else if node_id < dead.length() {
!dead[node_id]
} else {
@ir.hot_node_is_live(func, node_id)
}
{ live, }
}
///|
fn OiLiveNodeFact::proves_live(self : OiLiveNodeFact) -> Bool {
self.live
}
///|
fn OiLiveNodeFact::proves_node_live(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
OiLiveNodeFact::from_node(func, dead, node_id).proves_live()
}
///|
fn optimize_instructions_replace_with_const_i32(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
value : @lib.I32,
) -> Unit {
let temp = @ir.hot_build_const_i32(func, value)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_const_i64(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
value : @lib.I64,
) -> Unit {
let temp = @ir.hot_build_const_i64(func, value)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
test "OI zero bits replacement retains the original expression evaluation position" {
let input = pass_test_module_from_wat(
"(module (import \"env\" \"log\" (func $log (param i32))) (func $se (param i32) (result i32) (call $log (local.get 0)) (i32.const 0)) (func (export \"f\") (result i32) (local i32) i32.const 1 call $se i32.const 0 i32.and i32.const 5 i32.const 0 i32.const 2 call $se i32.const 0 select local.set 0 i32.add))",
)
let func = @ir.hot_lift_func(input, @lib.FuncIdx::new(2U))
let mut zero = -1
for id in 0..<@ir.hot_node_count(func) {
if @ir.hot_node_exact_instr(func, id) == Some(@lib.Instruction::i32_and()) {
zero = id
}
}
assert_true(zero >= 0)
let old_order = @ir.hot_node_get(func, zero).order
let ctx = HotPassContext::new(
optimize_instructions_descriptor(),
@ir.HotAnalysisCache::new(),
HotPipelineVerifyPolicy::after_each_pass(),
false,
fn(_) { () },
)
let fact = OiZeroBitsReplacementFact::from_node(
ctx,
func,
@bitset.BitSet::new(@ir.hot_node_count(func)),
OptimizeInstructionsScratch::new(@ir.hot_node_count(func), 1),
zero,
).unwrap()
assert_true(fact.dropped_children.needs_drops())
fact.apply(ctx, func, zero)
let replacement = @ir.hot_node_get(func, zero)
assert_true(replacement.op is @ir.HotOp::Block)
assert_eq(replacement.order, old_order)
let region = @ir.hot_region_body_ref(func, zero)
let roots = [
@ir.hot_region_get(func, region, 0),
@ir.hot_region_get(func, region, 1),
]
assert_true(@ir.hot_node_op(func, roots[0]) is @ir.HotOp::Drop)
assert_eq(
@ir.hot_const_get(func, @ir.hot_node_get(func, roots[1]).imm0),
@ir.HotConstPayload::i32_const(@lib.I32(0)),
)
@ir.hot_verify_core(func).unwrap()
@ir.hot_verify_control(func).unwrap()
}
///|
fn optimize_instructions_replace_with_block_body(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
block_type : @lib.BlockType,
body : Array[Int32],
) -> Unit {
let temp = @ir.hot_build_block(func, block_type, body)
// This block retains the expression's effects at its original evaluation
// position. A fresh block order hides its body from carried-value queries.
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node_preserving_position(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_unreachable(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
let temp = @ir.hot_build_unreachable(func)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_existing_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
let replacement_id = @ir.hot_child_get(func, node_id, 0)
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, replacement_id))
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_node_id(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
replacement_id : Int32,
) -> Unit {
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, replacement_id))
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_exact_unary(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
instr : @lib.Instruction,
operand_id : Int32,
) -> Unit {
let result_ty = match instr {
@lib.Instruction::I64Eqz => @lib.ValType::i32()
@lib.Instruction::I64Extend8S
| @lib.Instruction::I64Extend16S
| @lib.Instruction::I64Extend32S => @lib.ValType::i64()
_ => @lib.ValType::i32()
}
let temp = @ir.hot_build_unary(func, result_ty, operand_id)
@ir.hot_node_exact_instr_set(func, temp, instr)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_exact_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Unit {
let temp = @ir.hot_build_compare(func, lhs_id, rhs_id)
@ir.hot_node_exact_instr_set(func, temp, instr)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
priv struct OiI32ValueFact {
value_type : @lib.ValType?
}
///|
fn OiI32ValueFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiI32ValueFact {
// Binaryen condition cleanup only erases compare-to-zero shells when the
// exposed operand is an i32 condition value. Keep that result-type proof as
// data instead of a freestanding helper predicate.
let value_type = match
@ir.hot_type_get(func, @ir.hot_node_get(func, node_id).ty) {
@ir.HotTypeInfo::Value(val_type) => Some(val_type)
_ => None
}
{ value_type, }
}
///|
fn OiI32ValueFact::proves_i32_value(self : OiI32ValueFact) -> Bool {
self.value_type == Some(@lib.ValType::i32())
}
///|
fn OiI32ValueFact::proves_node_i32_value(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
OiI32ValueFact::from_node(func, node_id).proves_i32_value()
}
///|
fn optimize_instructions_rewrite_exact_with_children(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
instr : @lib.Instruction,
new_children : Array[Int32],
) -> Unit {
ignore(@ir.hot_replace_child_span(func, node_id, new_children))
@ir.hot_node_exact_instr_set(func, node_id, instr)
pass_mark_mutated(ctx, func)
}
///|
priv struct OiCallRefFallthroughFunctionIndexFact {
direct_func : Int32
}
///|
priv struct OiCallRefFallthroughFunctionFact {
index_fact : OiCallRefFallthroughFunctionIndexFact
ref_func_heap : @lib.HeapType?
}
///|
priv struct OiCallRefFallthroughDirectFuncFact {
function_fact : OiCallRefFallthroughFunctionFact
}
///|
priv enum OiCallRefFallthroughWrapperKind {
CallRefFallthroughLocalTeeWrapper
CallRefFallthroughRefAsNonNullWrapper
CallRefFallthroughRefCastWrapper
}
///|
priv struct OiCallRefFallthroughWrapperChildFact {
kind : OiCallRefFallthroughWrapperKind
wrapper_id : Int32
child_id : Int32
}
///|
priv struct OiCallRefWrappedFallthroughTargetFact {
wrapper_child_fact : OiCallRefFallthroughWrapperChildFact
child_fact : OiCallRefFallthroughTargetFact
}
///|
priv enum OiCallRefBlockTailKind {
CallRefBlockFinalRoot
}
///|
priv struct OiCallRefBlockTailFact {
kind : OiCallRefBlockTailKind
block_id : Int32
tail_id : Int32
}
///|
priv struct OiCallRefBlockFallthroughTargetFact {
block_tail_fact : OiCallRefBlockTailFact
tail_fact : OiCallRefFallthroughTargetFact
}
///|
priv struct OiCallRefFallthroughTargetFact {
direct_fact : OiCallRefFallthroughDirectFuncFact?
}
///|
fn OiCallRefFallthroughTargetFact::unknown() -> OiCallRefFallthroughTargetFact {
{ direct_fact: None, }
}
///|
fn OiCallRefFallthroughDirectFuncFact::from_fallthrough(
fact : OiCallRefFallthroughTargetFact,
) -> OiCallRefFallthroughDirectFuncFact? {
// Binaryen v130 `visitCallRef` only consumes a `Properties::getFallthrough`
// target when that fallthrough is a concrete `ref.func`. Keep the direct
// fallthrough proof separate from the broader recursive target-descent record
// so loop/if/br_on wrappers and the call_ref target classifier consume an
// explicit nested direct-ref.func fact rather than sentinel-style payload
// fields on the traversal fact.
fact.direct_fact
}
///|
fn OiCallRefFallthroughFunctionIndexFact::proves_function_index(
self : OiCallRefFallthroughFunctionIndexFact,
) -> Bool {
self.direct_func >= 0
}
///|
fn OiCallRefFallthroughFunctionIndexFact::function_index(
self : OiCallRefFallthroughFunctionIndexFact,
) -> Int32 {
self.direct_func
}
///|
fn OiCallRefFallthroughFunctionFact::proves_ref_func_payload(
self : OiCallRefFallthroughFunctionFact,
) -> Bool {
// Binaryen v130 `visitCallRef` fallthrough directization consumes a concrete
// `ref.func` selected by `Properties::getFallthrough`. Keep that selected
// function index as its own proof fact so the optional heap compatibility
// payload does not double as a sentinel for directizability.
ignore(self.ref_func_heap)
self.index_fact.proves_function_index()
}
///|
fn OiCallRefFallthroughFunctionFact::function_index(
self : OiCallRefFallthroughFunctionFact,
) -> Int32 {
self.index_fact.function_index()
}
///|
fn OiCallRefFallthroughDirectFuncFact::proves_direct_ref_func_fallthrough(
self : OiCallRefFallthroughDirectFuncFact,
) -> Bool {
self.function_fact.proves_ref_func_payload()
}
///|
priv enum OiCallRefIfFallthroughArmChoiceKind {
CallRefIfThenStopsElseFallsThrough
CallRefIfElseStopsThenFallsThrough
}
///|
priv struct OiCallRefIfFallthroughArmTailFact {
fallthrough_arm_id : Int32
}
///|
priv struct OiCallRefIfFallthroughArmChoiceFact {
kind : OiCallRefIfFallthroughArmChoiceKind
arm_tail_fact : OiCallRefIfFallthroughArmTailFact
}
///|
priv struct OiCallRefIfFallthroughTargetFact {
arm_choice_fact : OiCallRefIfFallthroughArmChoiceFact
arm_fact : OiCallRefFallthroughTargetFact
}
///|
priv enum OiCallRefLoopBodyTailKind {
CallRefLoopBodyFinalRoot
}
///|
priv struct OiCallRefLoopBodyTailFact {
kind : OiCallRefLoopBodyTailKind
body_id : Int32
}
///|
priv struct OiCallRefLoopFallthroughTargetFact {
body_tail_fact : OiCallRefLoopBodyTailFact
body_fact : OiCallRefFallthroughTargetFact
}
///|
priv enum OiCallRefBrOnFallthroughTargetKind {
CallRefBrOnNullFallthroughRef
CallRefBrOnNonNullFallthroughRef
CallRefBrOnCastFallthroughRef
CallRefBrOnCastFailFallthroughRef
}
///|
priv enum OiCallRefBrOnRefChildKind {
CallRefBrOnRefFinalChild
}
///|
priv struct OiCallRefBrOnRefChildFact {
kind : OiCallRefBrOnRefChildKind
ref_id : Int32
ref_fact : OiCallRefFallthroughTargetFact
}
///|
priv struct OiCallRefBrOnBranchTargetFact {
branch_target_id : Int32
}
///|
priv struct OiCallRefBrOnFallthroughTargetFact {
kind : OiCallRefBrOnFallthroughTargetKind
branch_target_fact : OiCallRefBrOnBranchTargetFact
ref_child_fact : OiCallRefBrOnRefChildFact
}
///|
fn OiCallRefFallthroughTargetFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
depth~ : Int32 = 0,
) -> OiCallRefFallthroughTargetFact {
if depth > 8 || !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return OiCallRefFallthroughTargetFact::unknown()
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::RefFunc => {
let ref_func_heap = match
optimize_instructions_node_ref_result_type(func, node_id) {
Some(ref_type) => Some(ref_type.get_heap_type())
None => None
}
{
direct_fact: Some({
function_fact: {
index_fact: { direct_func: node.imm0, },
ref_func_heap,
},
}),
}
}
@ir.HotOp::LocalTee | @ir.HotOp::Heap | @ir.HotOp::RefCast =>
match
OiCallRefWrappedFallthroughTargetFact::from_wrapper_target(
func, dead, node_id, depth,
) {
Some(fact) if fact.proves_ref_func_wrapped_fallthrough() =>
fact.child_fact
_ => OiCallRefFallthroughTargetFact::unknown()
}
@ir.HotOp::Block =>
match
OiCallRefBlockFallthroughTargetFact::from_block_target(
func, dead, node_id, depth,
) {
Some(fact) if fact.proves_ref_func_block_fallthrough() => fact.tail_fact
_ => OiCallRefFallthroughTargetFact::unknown()
}
@ir.HotOp::Loop =>
match
OiCallRefLoopFallthroughTargetFact::from_loop_target(
func, dead, node_id, depth,
) {
Some(fact) if fact.proves_ref_func_body_fallthrough() => fact.body_fact
_ => OiCallRefFallthroughTargetFact::unknown()
}
@ir.HotOp::If =>
match
OiCallRefIfFallthroughTargetFact::from_if_target(
func, dead, node_id, depth,
) {
Some(fact) if fact.proves_one_sided_ref_func_fallthrough() =>
fact.arm_fact
_ => OiCallRefFallthroughTargetFact::unknown()
}
@ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail =>
match
OiCallRefBrOnFallthroughTargetFact::from_br_on_target(
func, dead, node_id, depth,
) {
Some(fact) if fact.proves_ref_func_ref_fallthrough() =>
fact.ref_child_fact.ref_fact
_ => OiCallRefFallthroughTargetFact::unknown()
}
_ => OiCallRefFallthroughTargetFact::unknown()
}
}
///|
fn OiCallRefWrappedFallthroughTargetFact::from_wrapper_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
depth : Int32,
) -> OiCallRefWrappedFallthroughTargetFact? {
// Binaryen v130 `Properties::getImmediateFallthrough` can peel simple target
// wrappers such as `local.tee`, `ref.as_non_null`, and `ref.cast` before
// `visitCallRef` checks whether the selected fallthrough is a direct
// `ref.func`. Carry the wrapper kind and selected child as a fact so wrapper
// descent is not encoded as a small opcode whitelist in the generic traversal.
let node = @ir.hot_node_get(func, node_id)
if node.child_count != 1 {
return None
}
let kind = match node.op {
@ir.HotOp::LocalTee => CallRefFallthroughLocalTeeWrapper
@ir.HotOp::Heap =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull) =>
CallRefFallthroughRefAsNonNullWrapper
_ => return None
}
@ir.HotOp::RefCast => CallRefFallthroughRefCastWrapper
_ => return None
}
let child_id = @ir.hot_child_get(func, node_id, 0)
let child_fact = OiCallRefFallthroughTargetFact::from_node(
func,
dead,
child_id,
depth=depth + 1,
)
let wrapper_child_fact = { kind, wrapper_id: node_id, child_id, }
Some({ wrapper_child_fact, child_fact, })
}
///|
fn OiCallRefFallthroughWrapperChildFact::proves_selected_wrapper_child(
self : OiCallRefFallthroughWrapperChildFact,
) -> Bool {
match self.kind {
CallRefFallthroughLocalTeeWrapper
| CallRefFallthroughRefAsNonNullWrapper
| CallRefFallthroughRefCastWrapper =>
self.wrapper_id >= 0 && self.child_id >= 0
}
}
///|
fn OiCallRefWrappedFallthroughTargetFact::proves_ref_func_wrapped_fallthrough(
self : OiCallRefWrappedFallthroughTargetFact,
) -> Bool {
let direct_child_fact = match
OiCallRefFallthroughDirectFuncFact::from_fallthrough(self.child_fact) {
Some(fact) => fact
None => return false
}
self.wrapper_child_fact.proves_selected_wrapper_child() &&
direct_child_fact.proves_direct_ref_func_fallthrough()
}
///|
fn OiCallRefBlockFallthroughTargetFact::from_block_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
depth : Int32,
) -> OiCallRefBlockFallthroughTargetFact? {
// Binaryen v130 `Properties::getImmediateFallthrough` descends from a block
// target to the block body's final root before `visitCallRef` asks whether the
// selected fallthrough is a `ref.func`. Carry that block-tail proof separately
// from the recursive target fact so block descent does not stay as an inline
// root-count scanner in the generic call_ref fallthrough traversal.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Block => ()
_ => return None
}
let body_ref = @ir.hot_region_body_ref(func, node_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count < 1 {
return None
}
let tail_id = @ir.hot_region_get(func, body_ref, root_count - 1)
if !OiLiveNodeFact::proves_node_live(func, dead, tail_id) {
return None
}
let tail_fact = OiCallRefFallthroughTargetFact::from_node(
func,
dead,
tail_id,
depth=depth + 1,
)
let block_tail_fact = {
kind: CallRefBlockFinalRoot,
block_id: node_id,
tail_id,
}
Some({ block_tail_fact, tail_fact, })
}
///|
fn OiCallRefBlockTailFact::proves_live_final_root(
self : OiCallRefBlockTailFact,
) -> Bool {
match self.kind {
CallRefBlockFinalRoot => self.block_id >= 0 && self.tail_id >= 0
}
}
///|
fn OiCallRefBlockFallthroughTargetFact::proves_ref_func_block_fallthrough(
self : OiCallRefBlockFallthroughTargetFact,
) -> Bool {
let direct_tail_fact = match
OiCallRefFallthroughDirectFuncFact::from_fallthrough(self.tail_fact) {
Some(fact) => fact
None => return false
}
self.block_tail_fact.proves_live_final_root() &&
direct_tail_fact.proves_direct_ref_func_fallthrough()
}
///|
fn OiCallRefBrOnFallthroughTargetFact::from_br_on_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
depth : Int32,
) -> OiCallRefBrOnFallthroughTargetFact? {
// Binaryen v130 `Properties::getImmediateFallthroughPtr` returns `BrOn::ref`
// for every `br_on_*` target before `visitCallRef` checks for a `ref.func`.
// Starshine's represented subset keeps the original branch target intact in
// the later dropped evaluation, so this fact only proves that the explicit
// reference child falls through to a direct function. It does not claim broad
// branch retargeting, branch-value localization, EH, or multivalue repair.
let node = @ir.hot_node_get(func, node_id)
let kind = match node.op {
@ir.HotOp::BrOnNull => CallRefBrOnNullFallthroughRef
@ir.HotOp::BrOnNonNull => CallRefBrOnNonNullFallthroughRef
@ir.HotOp::BrOnCast => CallRefBrOnCastFallthroughRef
@ir.HotOp::BrOnCastFail => CallRefBrOnCastFailFallthroughRef
_ => return None
}
let ref_child_fact = match
OiCallRefBrOnRefChildFact::from_br_on_target(func, dead, node_id, depth) {
Some(fact) => fact
None => return None
}
let branch_target_fact = { branch_target_id: node.imm0, }
Some({ kind, branch_target_fact, ref_child_fact, })
}
///|
fn OiCallRefBrOnBranchTargetFact::proves_valid_branch_target(
self : OiCallRefBrOnBranchTargetFact,
) -> Bool {
self.branch_target_id >= 0
}
///|
fn OiCallRefBrOnRefChildFact::from_br_on_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
depth : Int32,
) -> OiCallRefBrOnRefChildFact? {
// Binaryen v130 `Properties::getImmediateFallthroughPtr` exposes the final
// reference child of `br_on_*` as the fallthrough value. Carry that selected
// child plus its recursive direct-function proof as a nested payload fact so
// branch-target validation and ref-child fallthrough proof do not remain as
// parallel booleans on the broader branch fact.
let node = @ir.hot_node_get(func, node_id)
if node.child_count < 1 {
return None
}
let ref_slot = node.child_count - 1
let ref_id = @ir.hot_child_get(func, node_id, ref_slot)
if !OiLiveNodeFact::proves_node_live(func, dead, ref_id) {
return None
}
let ref_fact = OiCallRefFallthroughTargetFact::from_node(
func,
dead,
ref_id,
depth=depth + 1,
)
Some({ kind: CallRefBrOnRefFinalChild, ref_id, ref_fact, })
}
///|
fn OiCallRefBrOnRefChildFact::proves_final_ref_func_fallthrough(
self : OiCallRefBrOnRefChildFact,
) -> Bool {
let direct_ref_fact = match
OiCallRefFallthroughDirectFuncFact::from_fallthrough(self.ref_fact) {
Some(fact) => fact
None => return false
}
match self.kind {
CallRefBrOnRefFinalChild =>
self.ref_id >= 0 && direct_ref_fact.proves_direct_ref_func_fallthrough()
}
}
///|
fn OiCallRefBrOnFallthroughTargetFact::proves_ref_func_ref_fallthrough(
self : OiCallRefBrOnFallthroughTargetFact,
) -> Bool {
let supported_br_on_kind = match self.kind {
CallRefBrOnNullFallthroughRef
| CallRefBrOnNonNullFallthroughRef
| CallRefBrOnCastFallthroughRef
| CallRefBrOnCastFailFallthroughRef => true
}
supported_br_on_kind &&
self.branch_target_fact.proves_valid_branch_target() &&
self.ref_child_fact.proves_final_ref_func_fallthrough()
}
///|
fn OiCallRefLoopFallthroughTargetFact::from_loop_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
depth : Int32,
) -> OiCallRefLoopFallthroughTargetFact? {
// Binaryen v130 `Properties::getImmediateFallthrough` descends from a loop
// target to its body before `visitCallRef` asks whether the final fallthrough
// is a `ref.func`. The represented safe subset keeps the whole loop target in
// the later dropped evaluation and proves the explicit live tail root of the
// loop body; branch/EH loop exits remain reopening criteria for a
// ChildLocalizer/effect-aware loop fact.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Loop => ()
_ => return None
}
let body_ref = @ir.hot_region_body_ref(func, node_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count < 1 {
return None
}
let body_id = @ir.hot_region_get(func, body_ref, root_count - 1)
if !OiLiveNodeFact::proves_node_live(func, dead, body_id) {
return None
}
let body_fact = OiCallRefFallthroughTargetFact::from_node(
func,
dead,
body_id,
depth=depth + 1,
)
let body_tail_fact = { kind: CallRefLoopBodyFinalRoot, body_id, }
Some({ body_tail_fact, body_fact, })
}
///|
fn OiCallRefLoopBodyTailFact::proves_live_final_root(
self : OiCallRefLoopBodyTailFact,
) -> Bool {
match self.kind {
CallRefLoopBodyFinalRoot => self.body_id >= 0
}
}
///|
fn OiCallRefLoopFallthroughTargetFact::proves_ref_func_body_fallthrough(
self : OiCallRefLoopFallthroughTargetFact,
) -> Bool {
let direct_body_fact = match
OiCallRefFallthroughDirectFuncFact::from_fallthrough(self.body_fact) {
Some(fact) => fact
None => return false
}
self.body_tail_fact.proves_live_final_root() &&
direct_body_fact.proves_direct_ref_func_fallthrough()
}
///|
fn OiCallRefIfFallthroughArmChoiceFact::from_regions(
func : @ir.HotFunc,
dead : @bitset.BitSet,
then_ref : @ir.HotRegionRef,
else_ref : @ir.HotRegionRef,
) -> OiCallRefIfFallthroughArmChoiceFact? {
// Binaryen v130 `Properties::getImmediateFallthrough` descends through an
// `if` only when exactly one arm is unreachable. Carry that as the explicit
// chosen-arm fact instead of storing parallel then/else stop booleans on the
// broader call_ref target record.
let one_sided_fact = OiIfOneSidedFallthroughRegionFact::from_regions(
func, then_ref, else_ref,
)
let kind = match one_sided_fact.kind {
IfThenStopsElseFallsThroughRegion => CallRefIfThenStopsElseFallsThrough
IfElseStopsThenFallsThroughRegion => CallRefIfElseStopsThenFallsThrough
IfNoOneSidedFallthroughRegion => return None
}
let fallthrough_region = match one_sided_fact.proven_fallthrough_region() {
Some(region_ref) => region_ref
None => return None
}
let fallthrough_arm_id = match
optimize_instructions_region_tail_id(func, fallthrough_region) {
Some(tail_id) if OiLiveNodeFact::proves_node_live(func, dead, tail_id) =>
tail_id
_ => return None
}
let arm_tail_fact = { fallthrough_arm_id, }
Some({ kind, arm_tail_fact, })
}
///|
fn OiCallRefIfFallthroughArmTailFact::proves_live_fallthrough_arm(
self : OiCallRefIfFallthroughArmTailFact,
) -> Bool {
self.fallthrough_arm_id >= 0
}
///|
fn OiCallRefIfFallthroughArmChoiceFact::proves_one_sided_fallthrough_arm(
self : OiCallRefIfFallthroughArmChoiceFact,
) -> Bool {
let supported_one_sided_choice = match self.kind {
CallRefIfThenStopsElseFallsThrough | CallRefIfElseStopsThenFallsThrough =>
true
}
supported_one_sided_choice && self.arm_tail_fact.proves_live_fallthrough_arm()
}
///|
fn OiCallRefIfFallthroughTargetFact::from_if_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
depth : Int32,
) -> OiCallRefIfFallthroughTargetFact? {
// Binaryen v130 `Properties::getImmediateFallthrough` descends through an
// `if` only when exactly one arm is unreachable. `visitCallRef` still drops
// the original target before the direct call, so this fact only proves which
// arm supplies the target's fallthrough `ref.func`; it does not claim general
// two-arm control localization or branch/EH/multivalue preservation.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::If => ()
_ => return None
}
let else_ref = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return None
}
let then_ref = @ir.hot_region_then_ref(func, node_id)
let arm_choice_fact = match
OiCallRefIfFallthroughArmChoiceFact::from_regions(
func, dead, then_ref, else_ref,
) {
Some(fact) if fact.proves_one_sided_fallthrough_arm() => fact
_ => return None
}
let arm_fact = OiCallRefFallthroughTargetFact::from_node(
func,
dead,
arm_choice_fact.arm_tail_fact.fallthrough_arm_id,
depth=depth + 1,
)
Some({ arm_choice_fact, arm_fact, })
}
///|
fn OiCallRefIfFallthroughTargetFact::proves_one_sided_ref_func_fallthrough(
self : OiCallRefIfFallthroughTargetFact,
) -> Bool {
let direct_arm_fact = match
OiCallRefFallthroughDirectFuncFact::from_fallthrough(self.arm_fact) {
Some(fact) => fact
None => return false
}
self.arm_choice_fact.proves_one_sided_fallthrough_arm() &&
direct_arm_fact.proves_direct_ref_func_fallthrough()
}
///|
priv struct OiCallRefFallthroughHeapMatchFact {
target_heap : @lib.HeapType
fallthrough_heap : @lib.HeapType
}
///|
priv struct OiCallRefFallthroughTypeFact {
heap_match_fact : OiCallRefFallthroughHeapMatchFact?
}
///|
fn OiCallRefFallthroughTypeFact::from_target(
func : @ir.HotFunc,
target_id : Int32,
fallthrough : OiCallRefFallthroughTargetFact,
) -> OiCallRefFallthroughTypeFact {
// Binaryen v130 `visitCallRef` accepts `getFallthrough(curr->target)` only
// when `curr->target->type.getHeapType()` equals the fallthrough `ref.func`
// heap. A cast can expose a `ref.func` fallthrough that would trap at runtime
// because its static function type differs from the call_ref target type, so
// keep that compatibility proof separate from the wrapper-descent fact.
let target_heap = match
optimize_instructions_node_ref_result_type(func, target_id) {
Some(ref_type) => Some(ref_type.get_heap_type())
None => None
}
let fallthrough_heap = match fallthrough.direct_fact {
Some(direct_fact) => direct_fact.function_fact.ref_func_heap
None => None
}
let heap_match_fact = match (target_heap, fallthrough_heap) {
(Some(target), Some(fallthrough)) if target == fallthrough =>
Some({ target_heap: target, fallthrough_heap: fallthrough, })
_ => None
}
{ heap_match_fact, }
}
///|
fn OiCallRefFallthroughHeapMatchFact::proves_target_heap_matches_ref_func_heap(
self : OiCallRefFallthroughHeapMatchFact,
) -> Bool {
self.target_heap == self.fallthrough_heap
}
///|
fn OiCallRefFallthroughTypeFact::proves_target_heap_matches_ref_func_heap(
self : OiCallRefFallthroughTypeFact,
) -> Bool {
match self.heap_match_fact {
Some(fact) => fact.proves_target_heap_matches_ref_func_heap()
None => false
}
}
///|
priv enum OiCallRefTargetSkipNonNullOrderKind {
CallRefSkipNonNullAlreadyNonNullChild
CallRefSkipNonNullFinalTargetSlot
}
///|
priv struct OiCallRefTargetSkipNonNullOrderFact {
kind : OiCallRefTargetSkipNonNullOrderKind
child_static_ref_type : @lib.RefType?
}
///|
priv struct OiCallRefTargetSkippedNonNullCastFact {
replacement_target_id : Int32
order_fact : OiCallRefTargetSkipNonNullOrderFact
}
///|
priv struct OiCallRefTargetSkipNonNullFact {
skipped_fact : OiCallRefTargetSkippedNonNullCastFact
}
///|
fn OiCallRefTargetSkipNonNullFact::from_call_ref_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
call_ref_id : Int32,
) -> OiCallRefTargetSkipNonNullFact? {
// Binaryen v130 `visitCallRef` runs `skipNonNullCast(curr->target, curr)`
// before target directization. `skipNonNullCast` may remove a nullable
// `ref.as_non_null` when doing so does not cross later sibling effects. For
// call_ref / return_call_ref the target is the final child, so this fact can
// prove that nullable-child skip without turning target classifiers into
// wrapper scanners. Direct, table, fallthrough, select, and future
// descriptor-producer facts all consume the same effective target.
let call_ref = @ir.hot_node_get(func, call_ref_id)
match call_ref.op {
@ir.HotOp::CallRef | @ir.HotOp::ReturnCallRef => ()
_ => return None
}
if call_ref.child_count < 1 {
return None
}
let target_id = @ir.hot_child_get(func, call_ref_id, call_ref.child_count - 1)
if !OiLiveNodeFact::proves_node_live(func, dead, target_id) {
return None
}
let target = @ir.hot_node_get(func, target_id)
match target.op {
@ir.HotOp::Heap if target.child_count == 1 =>
match @ir.hot_node_exact_instr(func, target_id) {
Some(@lib.Instruction::RefAsNonNull) => {
let child_id = @ir.hot_child_get(func, target_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, child_id) {
return None
}
let order_fact = OiCallRefTargetSkipNonNullOrderFact::from_final_target_child(
optimize_instructions_node_ref_result_type(func, child_id),
)
Some({
skipped_fact: { replacement_target_id: child_id, order_fact, },
})
}
_ => None
}
_ => None
}
}
///|
fn OiCallRefTargetSkipNonNullOrderFact::from_final_target_child(
child_static_ref_type : @lib.RefType?,
) -> OiCallRefTargetSkipNonNullOrderFact {
// Binaryen v130 `skipNonNullCast(input, parent)` accepts a nullable
// `ref.as_non_null` child when either the child is already non-null or the
// wrapper is the source-order-final call_ref target operand. Keep that
// order-preservation proof as a selected fact so call_ref target cleanup no
// longer carries a direct boolean that merely says "this is the final child".
match child_static_ref_type {
Some(ref_type) if ref_type.is_non_nullable() =>
{ kind: CallRefSkipNonNullAlreadyNonNullChild, child_static_ref_type, }
_ => { kind: CallRefSkipNonNullFinalTargetSlot, child_static_ref_type, }
}
}
///|
fn OiCallRefTargetSkipNonNullOrderFact::proves_order_preserving_skip(
self : OiCallRefTargetSkipNonNullOrderFact,
) -> Bool {
match self.kind {
CallRefSkipNonNullAlreadyNonNullChild =>
match self.child_static_ref_type {
Some(ref_type) => ref_type.is_non_nullable()
None => false
}
CallRefSkipNonNullFinalTargetSlot => true
}
}
///|
fn OiCallRefTargetSkippedNonNullCastFact::proves_order_preserving_skip(
self : OiCallRefTargetSkippedNonNullCastFact,
) -> Bool {
self.order_fact.proves_order_preserving_skip()
}
///|
fn OiCallRefTargetSkipNonNullFact::effective_target_id(
self : OiCallRefTargetSkipNonNullFact,
) -> Int32? {
if self.skipped_fact.proves_order_preserving_skip() {
Some(self.skipped_fact.replacement_target_id)
} else {
None
}
}
///|
priv enum OiCallRefKnownNullTargetEffectKind {
CallRefKnownNullTargetEffectsElidable
CallRefKnownNullTargetEffectsPreserveByDrop
}
///|
priv struct OiCallRefKnownNullTargetPreserveEffectsFact {
target_effect_drop_id : Int32
}
///|
priv struct OiCallRefKnownNullTargetEffectFact {
kind : OiCallRefKnownNullTargetEffectKind
preserve_fact : OiCallRefKnownNullTargetPreserveEffectsFact?
}
///|
priv struct OiCallRefKnownNullTargetFact {
target_id : Int32
fallthrough_null_id : Int32
target_effect_fact : OiCallRefKnownNullTargetEffectFact
}
///|
priv enum OiCallRefKnownNullBlockTailKind {
CallRefKnownNullBlockFinalRoot
}
///|
priv struct OiCallRefKnownNullBlockTailFact {
kind : OiCallRefKnownNullBlockTailKind
block_id : Int32
tail_id : Int32
root_count : Int32
}
///|
priv struct OiCallRefKnownNullBlockFallthroughFact {
block_tail_fact : OiCallRefKnownNullBlockTailFact
}
///|
fn OiCallRefKnownNullTargetEffectFact::elidable() -> OiCallRefKnownNullTargetEffectFact {
{ kind: CallRefKnownNullTargetEffectsElidable, preserve_fact: None, }
}
///|
fn OiCallRefKnownNullTargetEffectFact::preserve_by_drop(
target_effect_drop_id : Int32,
) -> OiCallRefKnownNullTargetEffectFact? {
if target_effect_drop_id < 0 {
return None
}
Some({
kind: CallRefKnownNullTargetEffectsPreserveByDrop,
preserve_fact: Some({ target_effect_drop_id, }),
})
}
///|
fn OiCallRefKnownNullTargetEffectFact::proves_preservable(
self : OiCallRefKnownNullTargetEffectFact,
) -> Bool {
match self.kind {
CallRefKnownNullTargetEffectsElidable => true
CallRefKnownNullTargetEffectsPreserveByDrop =>
match self.preserve_fact {
Some(fact) => fact.target_effect_drop_id >= 0
None => false
}
}
}
///|
fn OiCallRefKnownNullTargetEffectFact::preserved_drop_id(
self : OiCallRefKnownNullTargetEffectFact,
) -> Int32? {
match self.kind {
CallRefKnownNullTargetEffectsElidable => None
CallRefKnownNullTargetEffectsPreserveByDrop =>
match self.preserve_fact {
Some(fact) if fact.target_effect_drop_id >= 0 =>
Some(fact.target_effect_drop_id)
_ => None
}
}
}
///|
fn OiCallRefKnownNullTargetFact::from_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
target_id : Int32,
) -> OiCallRefKnownNullTargetFact? {
// Binaryen v130 `trapOnNull` checks `getFallthrough(ref)->type.isNull()`
// before replacing the parent with `getDroppedChildrenAndAppend(...,
// unreachable)`. Keep that as target-slot evidence rather than folding every
// wrapper into the call_ref directization classifier. The safe Starshine
// subset here is a direct known-null leaf, a single-root block whose only
// fallthrough root is that known-null leaf, or a branch-free multi-root block
// whose fallthrough root is known-null and whose target effects can be
// preserved by dropping the whole target block after the call_ref operands.
// Broader control/EH fallthrough targets still need ChildLocalizer-style
// ordered-effect support before this fact may preserve target-side effects.
if target_id < 0 || !OiLiveNodeFact::proves_node_live(func, dead, target_id) {
return None
}
let null_trap_fact = OiNullTrapFact::for_operand(
func,
target_id,
caller_scope_traps=true,
)
match OiNullTrapRewriteFact::from_trap_fact(null_trap_fact).kind {
NullTrapReplaceParentWithUnreachable =>
return Some({
target_id,
fallthrough_null_id: target_id,
target_effect_fact: OiCallRefKnownNullTargetEffectFact::elidable(),
})
_ => ()
}
let target = @ir.hot_node_get(func, target_id)
match target.op {
@ir.HotOp::Block => {
let block_fallthrough_fact = match
OiCallRefKnownNullBlockFallthroughFact::from_block_target(
func, dead, target_id,
) {
Some(fact) if fact.proves_known_null_fallthrough() => fact
_ => return None
}
let fallthrough_null_id = block_fallthrough_fact.fallthrough_null_id()
if block_fallthrough_fact.block_tail_fact.root_count == 1 {
return Some({
target_id,
fallthrough_null_id,
target_effect_fact: OiCallRefKnownNullTargetEffectFact::elidable(),
})
}
let branch_fact = OiDescriptorBranchControlFact::from_node(
func, target_id, target_id,
)
if branch_fact.proves_has_branch_control() {
return None
}
let target_results = @ir.hot_type_results(
func,
@ir.hot_node_type(func, target_id),
)
if target_results.length() != 1 {
return None
}
if OiDescriptorInternalControlEffectFreeFact::from_node(
func, dead, target_id, target_id,
).proves_effect_free_allowing_internal_control() {
Some({
target_id,
fallthrough_null_id,
target_effect_fact: OiCallRefKnownNullTargetEffectFact::elidable(),
})
} else {
match OiCallRefKnownNullTargetEffectFact::preserve_by_drop(target_id) {
Some(target_effect_fact) =>
Some({ target_id, fallthrough_null_id, target_effect_fact, })
None => None
}
}
}
_ => None
}
}
///|
fn OiCallRefKnownNullBlockFallthroughFact::from_block_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
target_id : Int32,
) -> OiCallRefKnownNullBlockFallthroughFact? {
// Binaryen v130 `trapOnNull(curr, curr->target)` can see a known-null
// fallthrough through a block target when the block body's final root traps as
// null. Keep that block-tail/null proof separate from the later target-effect
// preservation decision so known-null call_ref planning does not carry an
// inline block root scanner.
match @ir.hot_node_get(func, target_id).op {
@ir.HotOp::Block => ()
_ => return None
}
let body_ref = @ir.hot_region_body_ref(func, target_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count < 1 {
return None
}
let tail_id = @ir.hot_region_get(func, body_ref, root_count - 1)
if !OiLiveNodeFact::proves_node_live(func, dead, tail_id) {
return None
}
let tail_trap_fact = OiNullTrapFact::for_operand(
func,
tail_id,
caller_scope_traps=true,
)
match OiNullTrapRewriteFact::from_trap_fact(tail_trap_fact).kind {
NullTrapReplaceParentWithUnreachable => ()
_ => return None
}
let block_tail_fact = {
kind: CallRefKnownNullBlockFinalRoot,
block_id: target_id,
tail_id,
root_count,
}
Some({ block_tail_fact, })
}
///|
fn OiCallRefKnownNullBlockTailFact::proves_live_final_root(
self : OiCallRefKnownNullBlockTailFact,
) -> Bool {
match self.kind {
CallRefKnownNullBlockFinalRoot =>
self.block_id >= 0 && self.tail_id >= 0 && self.root_count >= 1
}
}
///|
fn OiCallRefKnownNullBlockFallthroughFact::proves_known_null_fallthrough(
self : OiCallRefKnownNullBlockFallthroughFact,
) -> Bool {
self.block_tail_fact.proves_live_final_root()
}
///|
fn OiCallRefKnownNullBlockFallthroughFact::fallthrough_null_id(
self : OiCallRefKnownNullBlockFallthroughFact,
) -> Int32 {
self.block_tail_fact.tail_id
}
///|
fn OiCallRefKnownNullTargetFact::proves_elidable_known_null_fallthrough(
self : OiCallRefKnownNullTargetFact,
) -> Bool {
ignore(self.target_id)
self.fallthrough_null_id >= 0 && self.target_effect_fact.proves_preservable()
}
///|
priv struct OiCallRefNullTrapPlanFact {
target_fact : OiCallRefKnownNullTargetFact
child_layout_fact : OiCallRefChildLayoutFact
}
///|
fn OiCallRefNullTrapPlanFact::from_child_layout(
func : @ir.HotFunc,
dead : @bitset.BitSet,
child_layout_fact : OiCallRefChildLayoutFact,
) -> OiCallRefNullTrapPlanFact? {
// Binaryen v130 `visitCallRef` runs `skipNonNullCast(curr->target, curr)` and
// then `trapOnNull(curr, curr->target)` before any direct/table/fallthrough
// target classification. Consume the shared child-layout/effective-target
// fact here so the null-trap path does not re-split call_ref operands or
// duplicate the target-slot `skipNonNullCast` proof.
if !child_layout_fact.target_slot_fact.proves_effective_target() {
return None
}
let target_fact = match
OiCallRefKnownNullTargetFact::from_target(
func,
dead,
child_layout_fact.target_slot_fact.effective_target_id,
) {
Some(fact) if fact.proves_elidable_known_null_fallthrough() => fact
_ => return None
}
Some({ target_fact, child_layout_fact, })
}
///|
fn OiCallRefNullTrapPlanFact::apply(
self : OiCallRefNullTrapPlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let body : Array[Int32] = []
if !optimize_instructions_append_ordered_effects(
ctx,
func,
self.child_layout_fact.argument_list_fact.args(),
body,
) {
return false
}
match self.target_fact.target_effect_fact.preserved_drop_id() {
Some(target_effect_drop_id) =>
body.push(@ir.hot_build_drop(func, target_effect_drop_id))
None => ()
}
if body.length() == 0 {
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
return true
}
body.push(@ir.hot_build_unreachable(func))
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[] => @lib.BlockType::void_()
[val_type] => @lib.BlockType::val_type(val_type)
_ => return false
}
let replacement = @ir.hot_build_block(func, block_type, body)
let replacement_node = @ir.hot_node_get(func, replacement)
@ir.hot_replace_node(func, node_id, replacement_node)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, replacement)
pass_mark_mutated(ctx, func)
true
}
///|
priv struct OiCallRefSelectDirectArmsFact {
true_fact : OiCallRefSelectArmDirectFact
false_fact : OiCallRefSelectArmDirectFact
}
///|
priv struct OiCallRefSelectResidueArmFact {
arm_id : Int32
}
///|
priv struct OiCallRefSelectTargetFact {
direct_arms_fact : OiCallRefSelectDirectArmsFact
condition_fact : OiCallRefSelectConditionFact
residue_arm_fact : OiCallRefSelectResidueArmFact
}
///|
priv struct OiCallRefDirectFunctionIndexFact {
direct_func : Int32
}
///|
priv struct OiCallRefDirectFunctionFact {
index_fact : OiCallRefDirectFunctionIndexFact
ref_func_heap : @lib.HeapType?
}
///|
priv struct OiCallRefDirectTargetFact {
target_id : Int32
direct_func_fact : OiCallRefDirectFunctionFact
}
///|
priv struct OiCallRefTableImmediateFact {
table_idx : Int32
}
///|
priv struct OiCallRefTableIndexOperandFact {
table_index_id : Int32
index_live_fact : OiLiveNodeFact
}
///|
priv struct OiCallRefTableIndexFact {
table_fact : OiCallRefTableImmediateFact
index_operand_fact : OiCallRefTableIndexOperandFact
}
///|
priv struct OiCallRefTableTargetFact {
target_id : Int32
table_index_fact : OiCallRefTableIndexFact
}
///|
priv struct OiCallRefFallthroughDirectTargetFact {
target_id : Int32
function_fact : OiCallRefFallthroughFunctionFact
fallthrough_fact : OiCallRefFallthroughTargetFact
type_fact : OiCallRefFallthroughTypeFact
}
///|
priv struct OiCallRefTargetClassificationFact {
target_id : Int32
classified_target_fact : OiCallRefTargetFact
}
///|
priv enum OiCallRefTargetKind {
CallRefUnknownTarget
CallRefDirectTarget
CallRefTableTarget
CallRefFallthroughDirectTarget
CallRefSelectTarget
}
///|
priv struct OiCallRefTargetFact {
kind : OiCallRefTargetKind
select_fact : OiCallRefSelectTargetFact?
direct_fact : OiCallRefDirectTargetFact?
table_fact : OiCallRefTableTargetFact?
fallthrough_direct_fact : OiCallRefFallthroughDirectTargetFact?
}
///|
fn OiCallRefTargetFact::unknown() -> OiCallRefTargetFact {
{
kind: CallRefUnknownTarget,
select_fact: None,
direct_fact: None,
table_fact: None,
fallthrough_direct_fact: None,
}
}
///|
fn OiCallRefTargetFact::from_direct_fact(
fact : OiCallRefDirectTargetFact,
) -> OiCallRefTargetFact {
{
kind: CallRefDirectTarget,
select_fact: None,
direct_fact: Some(fact),
table_fact: None,
fallthrough_direct_fact: None,
}
}
///|
fn OiCallRefTargetFact::from_table_fact(
fact : OiCallRefTableTargetFact,
) -> OiCallRefTargetFact {
{
kind: CallRefTableTarget,
select_fact: None,
direct_fact: None,
table_fact: Some(fact),
fallthrough_direct_fact: None,
}
}
///|
fn OiCallRefTargetFact::from_fallthrough_direct_fact(
fact : OiCallRefFallthroughDirectTargetFact,
) -> OiCallRefTargetFact {
{
kind: CallRefFallthroughDirectTarget,
select_fact: None,
direct_fact: None,
table_fact: None,
fallthrough_direct_fact: Some(fact),
}
}
///|
fn OiCallRefTargetFact::from_select_fact(
fact : OiCallRefSelectTargetFact,
) -> OiCallRefTargetFact {
{
kind: CallRefSelectTarget,
select_fact: Some(fact),
direct_fact: None,
table_fact: None,
fallthrough_direct_fact: None,
}
}
///|
fn OiCallRefTargetFact::direct_target(
self : OiCallRefTargetFact,
) -> OiCallRefDirectTargetFact? {
match self.kind {
CallRefDirectTarget => self.direct_fact
_ => None
}
}
///|
fn OiCallRefTargetFact::table_target(
self : OiCallRefTargetFact,
) -> OiCallRefTableTargetFact? {
match self.kind {
CallRefTableTarget => self.table_fact
_ => None
}
}
///|
fn OiCallRefTargetFact::fallthrough_direct_target(
self : OiCallRefTargetFact,
) -> OiCallRefFallthroughDirectTargetFact? {
match self.kind {
CallRefFallthroughDirectTarget => self.fallthrough_direct_fact
_ => None
}
}
///|
fn OiCallRefTargetFact::select_target(
self : OiCallRefTargetFact,
) -> OiCallRefSelectTargetFact? {
match self.kind {
CallRefSelectTarget => self.select_fact
_ => None
}
}
///|
fn OiCallRefDirectTargetFact::from_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
target_id : Int32,
) -> OiCallRefDirectTargetFact? {
// Binaryen v130 `visitCallRef` handles immediate `RefFunc` targets before
// the fallthrough and select paths. Keep the direct target classifier as its
// own source-shaped fact so the generic call_ref plan consumes a proven
// target rather than doing an opcode-local direct-call scan.
if target_id < 0 || !OiLiveNodeFact::proves_node_live(func, dead, target_id) {
return None
}
let target = @ir.hot_node_get(func, target_id)
match target.op {
@ir.HotOp::RefFunc => {
let direct_func_fact : OiCallRefDirectFunctionFact = {
index_fact: { direct_func: target.imm0, },
ref_func_heap: match
optimize_instructions_node_ref_result_type(func, target_id) {
Some(ref_type) => Some(ref_type.get_heap_type())
None => None
},
}
Some({ target_id, direct_func_fact, })
}
_ => None
}
}
///|
fn OiCallRefDirectFunctionIndexFact::proves_function_index(
self : OiCallRefDirectFunctionIndexFact,
) -> Bool {
self.direct_func >= 0
}
///|
fn OiCallRefDirectFunctionIndexFact::function_index(
self : OiCallRefDirectFunctionIndexFact,
) -> Int32 {
self.direct_func
}
///|
fn OiCallRefDirectFunctionFact::proves_direct_ref_func(
self : OiCallRefDirectFunctionFact,
) -> Bool {
// Binaryen v130 `visitCallRef` direct-target lowering needs a concrete
// immediate `ref.func` index. Keep that nonnegative index proof separate from
// the optional heap payload used by sibling fallthrough/select compatibility
// facts so the direct target fact does not expose raw sentinel fields.
ignore(self.ref_func_heap)
self.index_fact.proves_function_index()
}
///|
fn OiCallRefDirectFunctionFact::function_index(
self : OiCallRefDirectFunctionFact,
) -> Int32 {
self.index_fact.function_index()
}
///|
fn OiCallRefDirectTargetFact::proves_direct_ref_func(
self : OiCallRefDirectTargetFact,
) -> Bool {
self.target_id >= 0 && self.direct_func_fact.proves_direct_ref_func()
}
///|
fn OiCallRefTableTargetFact::from_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
target_id : Int32,
) -> OiCallRefTableTargetFact? {
// Binaryen v130 `visitCallRef` handles immediate `TableGet` targets as the
// table/index fact that lowers `call_ref` to `call_indirect`. Keep that fact
// separate from direct, fallthrough, and select target facts so table lowering
// is not an ad hoc branch inside the generic target classifier.
if target_id < 0 || !OiLiveNodeFact::proves_node_live(func, dead, target_id) {
return None
}
let target = @ir.hot_node_get(func, target_id)
match target.op {
@ir.HotOp::TableGet if target.child_count == 1 => {
let table_index_id = @ir.hot_child_get(func, target_id, 0)
let table_index_fact : OiCallRefTableIndexFact = {
table_fact: { table_idx: target.imm0, },
index_operand_fact: {
table_index_id,
index_live_fact: OiLiveNodeFact::from_node(func, dead, table_index_id),
},
}
if table_index_fact.proves_live_table_index() {
Some({ target_id, table_index_fact, })
} else {
None
}
}
_ => None
}
}
///|
fn OiCallRefTableImmediateFact::table_index(
self : OiCallRefTableImmediateFact,
) -> Int32 {
self.table_idx
}
///|
fn OiCallRefTableIndexOperandFact::proves_live_table_index_operand(
self : OiCallRefTableIndexOperandFact,
) -> Bool {
ignore(self.table_index_id)
self.index_live_fact.proves_live()
}
///|
fn OiCallRefTableIndexOperandFact::index_operand_id(
self : OiCallRefTableIndexOperandFact,
) -> Int32 {
self.table_index_id
}
///|
fn OiCallRefTableIndexFact::proves_live_table_index(
self : OiCallRefTableIndexFact,
) -> Bool {
ignore(self.table_fact)
self.index_operand_fact.proves_live_table_index_operand()
}
///|
fn OiCallRefTableIndexFact::table_index(
self : OiCallRefTableIndexFact,
) -> Int32 {
self.table_fact.table_index()
}
///|
fn OiCallRefTableIndexFact::index_operand_id(
self : OiCallRefTableIndexFact,
) -> Int32 {
self.index_operand_fact.index_operand_id()
}
///|
fn OiCallRefTableTargetFact::proves_table_get_target(
self : OiCallRefTableTargetFact,
) -> Bool {
ignore(self.target_id)
self.table_index_fact.proves_live_table_index()
}
///|
fn OiCallRefFallthroughDirectTargetFact::from_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
target_id : Int32,
) -> OiCallRefFallthroughDirectTargetFact? {
// Binaryen v130 `visitCallRef` asks `Properties::getFallthrough` for a
// `ref.func` after immediate direct/table targets fail, then checks that the
// original call_ref target heap still matches the fallthrough function heap.
// Keep that directizable-fallthrough proof as a source-shaped fact consumed
// by the generic target plan, rather than branching on block/wrapper opcodes
// in the target classifier.
if target_id < 0 || !OiLiveNodeFact::proves_node_live(func, dead, target_id) {
return None
}
let fallthrough_fact = OiCallRefFallthroughTargetFact::from_node(
func, dead, target_id,
)
let type_fact = OiCallRefFallthroughTypeFact::from_target(
func, target_id, fallthrough_fact,
)
let direct_fallthrough_fact = match
OiCallRefFallthroughDirectFuncFact::from_fallthrough(fallthrough_fact) {
Some(fact) => fact
None => return None
}
if direct_fallthrough_fact.proves_direct_ref_func_fallthrough() &&
type_fact.proves_target_heap_matches_ref_func_heap() {
Some({
target_id,
function_fact: direct_fallthrough_fact.function_fact,
fallthrough_fact,
type_fact,
})
} else {
None
}
}
///|
fn OiCallRefFallthroughDirectTargetFact::proves_fallthrough_ref_func(
self : OiCallRefFallthroughDirectTargetFact,
) -> Bool {
let direct_fallthrough_fact = match
OiCallRefFallthroughDirectFuncFact::from_fallthrough(self.fallthrough_fact) {
Some(fact) => fact
None => return false
}
self.target_id >= 0 &&
self.function_fact.proves_ref_func_payload() &&
direct_fallthrough_fact.proves_direct_ref_func_fallthrough() &&
self.type_fact.proves_target_heap_matches_ref_func_heap()
}
///|
fn OiCallRefTargetClassificationFact::unknown(
target_id : Int32,
) -> OiCallRefTargetClassificationFact {
{ target_id, classified_target_fact: OiCallRefTargetFact::unknown(), }
}
///|
fn OiCallRefTargetClassificationFact::from_target(
func : @ir.HotFunc,
dead : @bitset.BitSet,
callee_ref_id : Int32,
) -> OiCallRefTargetClassificationFact {
// Binaryen v130 `visitCallRef` classifies targets in a fixed source order:
// direct `ref.func`, immediate `table.get`, `Properties::getFallthrough`
// direct `ref.func`, and finally the select/direct-call utility. Keep the
// ordered classification as a fact so directization consumes one target
// record instead of rebuilding the visitor's opcode cascade inline.
if !OiLiveNodeFact::proves_node_live(func, dead, callee_ref_id) {
return OiCallRefTargetClassificationFact::unknown(callee_ref_id)
}
let callee_ref = @ir.hot_node_get(func, callee_ref_id)
match OiCallRefDirectTargetFact::from_target(func, dead, callee_ref_id) {
Some(fact) if fact.proves_direct_ref_func() =>
return {
target_id: callee_ref_id,
classified_target_fact: OiCallRefTargetFact::from_direct_fact(fact),
}
_ => ()
}
match OiCallRefTableTargetFact::from_target(func, dead, callee_ref_id) {
Some(fact) if fact.proves_table_get_target() =>
return {
target_id: callee_ref_id,
classified_target_fact: OiCallRefTargetFact::from_table_fact(fact),
}
_ => ()
}
match
OiCallRefFallthroughDirectTargetFact::from_target(func, dead, callee_ref_id) {
Some(fact) if fact.proves_fallthrough_ref_func() =>
return {
target_id: callee_ref_id,
classified_target_fact: OiCallRefTargetFact::from_fallthrough_direct_fact(
fact,
),
}
_ => ()
}
match callee_ref.op {
@ir.HotOp::Select if callee_ref.child_count == 3 =>
match OiCallRefSelectTargetFact::from_select(func, dead, callee_ref_id) {
Some(fact) =>
{
target_id: callee_ref_id,
classified_target_fact: OiCallRefTargetFact::from_select_fact(fact),
}
None => OiCallRefTargetClassificationFact::unknown(callee_ref_id)
}
_ => OiCallRefTargetClassificationFact::unknown(callee_ref_id)
}
}
///|
fn OiCallRefTargetClassificationFact::target_fact(
self : OiCallRefTargetClassificationFact,
) -> OiCallRefTargetFact {
ignore(self.target_id)
self.classified_target_fact
}
///|
priv struct OiCallRefSelectArmFunctionIndexFact {
direct_func : Int32
}
///|
priv struct OiCallRefSelectArmFunctionFact {
index_fact : OiCallRefSelectArmFunctionIndexFact
}
///|
priv struct OiCallRefSelectArmDirectFact {
function_fact : OiCallRefSelectArmFunctionFact
ref_func_heap : @lib.HeapType?
elided_non_null_wrapper_depth : Int32
elided_cast_wrapper_depth : Int32
}
///|
priv struct OiCallRefSelectArmFact {
direct_fact : OiCallRefSelectArmDirectFact?
}
///|
fn OiCallRefSelectArmFact::unknown() -> OiCallRefSelectArmFact {
{ direct_fact: None, }
}
///|
fn OiCallRefSelectArmFunctionIndexFact::from_ref_func(
direct_func : Int32,
) -> OiCallRefSelectArmFunctionIndexFact? {
if direct_func < 0 {
return None
}
Some({ direct_func, })
}
///|
fn OiCallRefSelectArmFunctionIndexFact::proves_function_index(
self : OiCallRefSelectArmFunctionIndexFact,
) -> Bool {
self.direct_func >= 0
}
///|
fn OiCallRefSelectArmFunctionIndexFact::function_index(
self : OiCallRefSelectArmFunctionIndexFact,
) -> Int32 {
self.direct_func
}
///|
fn OiCallRefSelectArmFunctionFact::from_ref_func(
direct_func : Int32,
) -> OiCallRefSelectArmFunctionFact? {
let index_fact = match
OiCallRefSelectArmFunctionIndexFact::from_ref_func(direct_func) {
Some(fact) => fact
None => return None
}
Some({ index_fact, })
}
///|
fn OiCallRefSelectArmFunctionFact::proves_function_index(
self : OiCallRefSelectArmFunctionFact,
) -> Bool {
self.index_fact.proves_function_index()
}
///|
fn OiCallRefSelectArmFunctionFact::function_index(
self : OiCallRefSelectArmFunctionFact,
) -> Int32 {
self.index_fact.function_index()
}
///|
fn OiCallRefSelectArmFact::direct(
direct_func : Int32,
ref_func_heap : @lib.HeapType?,
elided_non_null_wrapper_depth~ : Int32 = 0,
elided_cast_wrapper_depth~ : Int32 = 0,
) -> OiCallRefSelectArmFact {
let function_fact = match
OiCallRefSelectArmFunctionFact::from_ref_func(direct_func) {
Some(fact) => fact
None => return OiCallRefSelectArmFact::unknown()
}
{
direct_fact: Some({
function_fact,
ref_func_heap,
elided_non_null_wrapper_depth,
elided_cast_wrapper_depth,
}),
}
}
///|
fn OiCallRefSelectArmDirectFact::function_index(
self : OiCallRefSelectArmDirectFact,
) -> Int32 {
self.function_fact.function_index()
}
///|
fn OiCallRefSelectArmDirectFact::from_arm_fact(
fact : OiCallRefSelectArmFact,
) -> OiCallRefSelectArmDirectFact? {
// Binaryen v130 `CallUtils::convertToDirectCalls` select classification needs
// both target arms to be known direct functions before it chooses either the
// same-target residue path or the two-direct-call `if` path. Keep the function
// index proof nested under the arm-direct fact instead of leaving direct-arm
// payloads as open sentinel fields on the arm traversal record.
match fact.direct_fact {
Some(direct_fact) if direct_fact.function_fact.proves_function_index() =>
Some(direct_fact)
_ => None
}
}
///|
priv struct OiCallRefSelectArmCastHeapMatchFact {
cast_heap : @lib.HeapType
ref_func_heap : @lib.HeapType
}
///|
priv struct OiCallRefSelectArmCastFact {
child_direct_fact : OiCallRefSelectArmDirectFact
heap_match_fact : OiCallRefSelectArmCastHeapMatchFact?
}
///|
fn OiCallRefSelectArmCastFact::from_cast_arm(
func : @ir.HotFunc,
dead : @bitset.BitSet,
arm_id : Int32,
depth : Int32,
) -> OiCallRefSelectArmCastFact? {
let arm = @ir.hot_node_get(func, arm_id)
match arm.op {
@ir.HotOp::RefCast if arm.child_count == 1 => ()
_ => return None
}
let cast_heap = match @ir.hot_node_exact_instr(func, arm_id) {
Some(@lib.Instruction::RefCast(_, _, heap)) => heap
_ => return None
}
let child_fact = OiCallRefSelectArmFact::from_target_arm(
func,
dead,
@ir.hot_child_get(func, arm_id, 0),
depth=depth + 1,
)
let child_direct_fact = match
OiCallRefSelectArmDirectFact::from_arm_fact(child_fact) {
Some(fact) => fact
None => return None
}
let heap_match_fact = match child_direct_fact.ref_func_heap {
Some(ref_func_heap) if ref_func_heap == cast_heap =>
Some({ cast_heap, ref_func_heap, })
_ => None
}
Some({ child_direct_fact, heap_match_fact, })
}
///|
fn OiCallRefSelectArmCastHeapMatchFact::proves_ref_func_heap_matches_cast_heap(
self : OiCallRefSelectArmCastHeapMatchFact,
) -> Bool {
self.ref_func_heap == self.cast_heap
}
///|
fn OiCallRefSelectArmCastFact::proves_known_success(
self : OiCallRefSelectArmCastFact,
) -> Bool {
match self.heap_match_fact {
Some(fact) => fact.proves_ref_func_heap_matches_cast_heap()
None => false
}
}
///|
fn OiCallRefSelectArmCastFact::matched_ref_func_heap(
self : OiCallRefSelectArmCastFact,
) -> @lib.HeapType? {
match self.heap_match_fact {
Some(fact) if fact.proves_ref_func_heap_matches_cast_heap() =>
Some(fact.ref_func_heap)
_ => None
}
}
///|
fn OiCallRefSelectArmFact::from_target_arm(
func : @ir.HotFunc,
dead : @bitset.BitSet,
arm_id : Int32,
depth~ : Int32 = 0,
) -> OiCallRefSelectArmFact {
if depth > 4 || !OiLiveNodeFact::proves_node_live(func, dead, arm_id) {
return OiCallRefSelectArmFact::unknown()
}
let arm = @ir.hot_node_get(func, arm_id)
match arm.op {
// Binaryen v130 `OptimizeInstructions::visitCallRef` passes
// `CallUtils::convertToDirectCalls` a classifier whose known target is a
// direct `RefFunc` after child OI visitors have already removed no-op
// `ref.as_non_null` wrappers around direct `ref.func` producers. Keep
// effectful fallthrough arms such as
// `local.tee(ref.func)` out of this fact until a source-backed
// ChildLocalizer fact can preserve arm-side effects.
@ir.HotOp::RefFunc => {
let ref_func_heap = match
optimize_instructions_node_ref_result_type(func, arm_id) {
Some(ref_type) => Some(ref_type.get_heap_type())
None => None
}
OiCallRefSelectArmFact::direct(arm.imm0, ref_func_heap)
}
@ir.HotOp::Heap if arm.child_count == 1 =>
match @ir.hot_node_exact_instr(func, arm_id) {
Some(@lib.Instruction::RefAsNonNull) => {
let child_id = @ir.hot_child_get(func, arm_id, 0)
let child_fact = OiCallRefSelectArmFact::from_target_arm(
func,
dead,
child_id,
depth=depth + 1,
)
match OiCallRefSelectArmDirectFact::from_arm_fact(child_fact) {
Some(direct_fact) =>
OiCallRefSelectArmFact::direct(
direct_fact.function_index(),
direct_fact.ref_func_heap,
elided_non_null_wrapper_depth=direct_fact.elided_non_null_wrapper_depth +
1,
elided_cast_wrapper_depth=direct_fact.elided_cast_wrapper_depth,
)
None => OiCallRefSelectArmFact::unknown()
}
}
_ => OiCallRefSelectArmFact::unknown()
}
@ir.HotOp::RefCast =>
match
OiCallRefSelectArmCastFact::from_cast_arm(func, dead, arm_id, depth) {
Some(cast_fact) if cast_fact.proves_known_success() =>
OiCallRefSelectArmFact::direct(
cast_fact.child_direct_fact.function_index(),
cast_fact.matched_ref_func_heap(),
elided_non_null_wrapper_depth=cast_fact.child_direct_fact.elided_non_null_wrapper_depth,
elided_cast_wrapper_depth=cast_fact.child_direct_fact.elided_cast_wrapper_depth +
1,
)
_ => OiCallRefSelectArmFact::unknown()
}
_ => OiCallRefSelectArmFact::unknown()
}
}
///|
priv enum OiCallRefSelectConditionEffectKind {
CallRefSelectConditionEffectsElidable
CallRefSelectConditionPreserveByDrop
}
///|
priv struct OiCallRefSelectConditionPreserveEffectsFact {
condition_drop_id : Int32
}
///|
priv struct OiCallRefSelectConditionEffectFact {
kind : OiCallRefSelectConditionEffectKind
preserve_fact : OiCallRefSelectConditionPreserveEffectsFact?
}
///|
priv struct OiCallRefSelectConditionTypeFact {
condition_type : @lib.ValType
}
///|
priv struct OiCallRefSelectConditionFact {
condition_id : Int32
type_fact : OiCallRefSelectConditionTypeFact
effect_fact : OiCallRefSelectConditionEffectFact
}
///|
fn OiCallRefSelectConditionEffectFact::elidable() -> OiCallRefSelectConditionEffectFact {
{ kind: CallRefSelectConditionEffectsElidable, preserve_fact: None, }
}
///|
fn OiCallRefSelectConditionEffectFact::preserve_by_drop(
condition_drop_id : Int32,
) -> OiCallRefSelectConditionEffectFact? {
if condition_drop_id < 0 {
return None
}
Some({
kind: CallRefSelectConditionPreserveByDrop,
preserve_fact: Some({ condition_drop_id, }),
})
}
///|
fn OiCallRefSelectConditionEffectFact::preserved_drop_id(
self : OiCallRefSelectConditionEffectFact,
) -> Int32? {
match self.kind {
CallRefSelectConditionEffectsElidable => None
CallRefSelectConditionPreserveByDrop =>
match self.preserve_fact {
Some(fact) if fact.condition_drop_id >= 0 =>
Some(fact.condition_drop_id)
_ => None
}
}
}
///|
fn OiCallRefSelectConditionTypeFact::from_condition(
func : @ir.HotFunc,
condition_id : Int32,
) -> OiCallRefSelectConditionTypeFact? {
// Binaryen v130 `CallUtils::convertToDirectCalls` emits an `if` over the
// select condition, so Starshine's represented select-target directization
// requires a single i32 condition before deciding whether condition effects
// can be erased or must be preserved by an explicit drop.
match @ir.hot_type_results(func, @ir.hot_node_type(func, condition_id)) {
[condition_type] if condition_type == @lib.ValType::i32() =>
Some({ condition_type, })
_ => None
}
}
///|
fn OiCallRefSelectConditionTypeFact::proves_i32_condition(
self : OiCallRefSelectConditionTypeFact,
) -> Bool {
self.condition_type == @lib.ValType::i32()
}
///|
fn OiCallRefSelectConditionFact::from_select_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
condition_id : Int32,
) -> OiCallRefSelectConditionFact? {
if !OiLiveNodeFact::proves_node_live(func, dead, condition_id) {
return None
}
let type_fact = match
OiCallRefSelectConditionTypeFact::from_condition(func, condition_id) {
Some(fact) if fact.proves_i32_condition() => fact
_ => return None
}
let effect_fact = if OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, condition_id,
) {
OiCallRefSelectConditionEffectFact::elidable()
} else {
match OiCallRefSelectConditionEffectFact::preserve_by_drop(condition_id) {
Some(fact) => fact
None => return None
}
}
Some({ condition_id, type_fact, effect_fact, })
}
///|
fn OiCallRefSelectConditionFact::proves_i32_condition(
self : OiCallRefSelectConditionFact,
) -> Bool {
self.condition_id >= 0 && self.type_fact.proves_i32_condition()
}
///|
fn OiCallRefSelectDirectArmsFact::from_arm_facts(
true_fact : OiCallRefSelectArmFact,
false_fact : OiCallRefSelectArmFact,
) -> OiCallRefSelectDirectArmsFact? {
// Binaryen v130 `CallUtils::convertToDirectCalls` only enters the select
// target directization paths after both arms have been classified as known
// direct function targets. Store those proven arm facts directly so the
// surrounding select target no longer carries `-1` sentinel function fields.
let true_direct_fact = match
OiCallRefSelectArmDirectFact::from_arm_fact(true_fact) {
Some(fact) => fact
None => return None
}
let false_direct_fact = match
OiCallRefSelectArmDirectFact::from_arm_fact(false_fact) {
Some(fact) => fact
None => return None
}
Some({ true_fact: true_direct_fact, false_fact: false_direct_fact, })
}
///|
fn OiCallRefSelectResidueArmFact::from_arm_id(
arm_id : Int32,
) -> OiCallRefSelectResidueArmFact? {
if arm_id < 0 {
return None
}
Some({ arm_id, })
}
///|
fn OiCallRefSelectTargetFact::from_direct_arms(
condition_fact : OiCallRefSelectConditionFact,
true_fact : OiCallRefSelectArmFact,
false_fact : OiCallRefSelectArmFact,
residue_arm_id : Int32,
) -> OiCallRefSelectTargetFact? {
// Binaryen v130 `CallUtils::convertToDirectCalls` first classifies both
// select arms as known direct targets, then either emits a one-call residue
// for identical functions or an if-over-direct-calls for different functions.
// Keep that select-specific target state together instead of spreading the
// arm ids, same-target proof, condition effect proof, and same-target residue
// arm through sentinel fields on the generic call_ref target fact.
let direct_arms_fact = match
OiCallRefSelectDirectArmsFact::from_arm_facts(true_fact, false_fact) {
Some(fact) => fact
None => return None
}
let residue_arm_fact = match
OiCallRefSelectResidueArmFact::from_arm_id(residue_arm_id) {
Some(fact) => fact
None => return None
}
if !condition_fact.proves_i32_condition() {
return None
}
Some({ direct_arms_fact, condition_fact, residue_arm_fact, })
}
///|
fn OiCallRefSelectTargetFact::from_select(
func : @ir.HotFunc,
dead : @bitset.BitSet,
callee_ref_id : Int32,
) -> OiCallRefSelectTargetFact? {
// Binaryen v130 delegates select target directization to
// `CallUtils::convertToDirectCalls` after direct/table/fallthrough targets
// have failed. This select fact owns the three child slots and the arm/condition
// classification before the outer target-classification fact records the
// select as one ordered target family.
let true_ref_id = @ir.hot_child_get(func, callee_ref_id, 0)
let false_ref_id = @ir.hot_child_get(func, callee_ref_id, 1)
let condition_id = @ir.hot_child_get(func, callee_ref_id, 2)
let condition_fact = match
OiCallRefSelectConditionFact::from_select_condition(
func, dead, condition_id,
) {
Some(fact) => fact
None => return None
}
let true_fact = OiCallRefSelectArmFact::from_target_arm(
func, dead, true_ref_id,
)
let false_fact = OiCallRefSelectArmFact::from_target_arm(
func, dead, false_ref_id,
)
OiCallRefSelectTargetFact::from_direct_arms(
condition_fact, true_fact, false_fact, true_ref_id,
)
}
///|
priv struct OiCallRefDirectFuncPlanFact {
direct_func : Int32
}
///|
priv struct OiCallRefTableTargetPlanFact {
table_idx : Int32
table_index_id : Int32
}
///|
priv struct OiCallRefFallthroughFuncPlanFact {
direct_func : Int32
}
///|
fn OiCallRefDirectFuncPlanFact::from_target_fact(
target_fact : OiCallRefTargetFact,
) -> OiCallRefDirectFuncPlanFact? {
// Binaryen v130 `visitCallRef` direct-target planning should consume the
// explicit immediate `ref.func` proof, not sentinel fields on the broader
// target record.
match target_fact.direct_target() {
Some(fact) if fact.proves_direct_ref_func() =>
Some({ direct_func: fact.direct_func_fact.function_index(), })
_ => None
}
}
///|
fn OiCallRefTableTargetPlanFact::from_target_fact(
target_fact : OiCallRefTargetFact,
) -> OiCallRefTableTargetPlanFact? {
// Binaryen v130 lowers only a proven immediate `table.get` callee to
// call_indirect. Keep the table/index payload behind that proof fact instead
// of asking sentinel-style questions of `OiCallRefTargetFact`.
match target_fact.table_target() {
Some(fact) if fact.proves_table_get_target() =>
Some({
table_idx: fact.table_index_fact.table_index(),
table_index_id: fact.table_index_fact.index_operand_id(),
})
_ => None
}
}
///|
fn OiCallRefFallthroughFuncPlanFact::from_target_fact(
target_fact : OiCallRefTargetFact,
) -> OiCallRefFallthroughFuncPlanFact? {
// Binaryen v130's fallthrough directization requires the fallthrough-target
// proof plus heap compatibility. Route the direct function payload through a
// plan fact so fallthrough calls do not depend on generic sentinel fields.
match target_fact.fallthrough_direct_target() {
Some(fact) if fact.proves_fallthrough_ref_func() =>
Some({ direct_func: fact.function_fact.function_index(), })
_ => None
}
}
///|
priv struct OiCallRefSelectSameTargetFact {
direct_func : Int32
residue_arm_id : Int32
condition_fact : OiCallRefSelectConditionFact
}
///|
priv struct OiCallRefSelectDirectCallsFact {
true_func : Int32
false_func : Int32
condition_fact : OiCallRefSelectConditionFact
}
///|
fn OiCallRefSelectSameTargetFact::from_select_target(
select_fact : OiCallRefSelectTargetFact,
) -> OiCallRefSelectSameTargetFact? {
// Binaryen v130 `CallUtils::convertToDirectCalls` takes the identical-arm
// select path only after both target arms are known direct functions and their
// function indices match. Keep that same-target proof as a dedicated fact so
// the directization plan consumes the direct-arm records instead of asking
// sentinel-style questions of the broader select-target record.
let true_direct_fact = select_fact.direct_arms_fact.true_fact
let false_direct_fact = select_fact.direct_arms_fact.false_fact
let true_func = true_direct_fact.function_index()
let false_func = false_direct_fact.function_index()
if true_func != false_func {
return None
}
Some({
direct_func: true_func,
residue_arm_id: select_fact.residue_arm_fact.arm_id,
condition_fact: select_fact.condition_fact,
})
}
///|
fn OiCallRefSelectDirectCallsFact::from_select_target(
select_fact : OiCallRefSelectTargetFact,
) -> OiCallRefSelectDirectCallsFact? {
// Binaryen v130 falls back from the identical-arm path to an `if` that calls
// each proven direct target. This fact owns the two function indices and the
// typed i32 condition proof by reading the direct-arm proof record instead of
// leaving those as open sentinel fields.
let true_direct_fact = select_fact.direct_arms_fact.true_fact
let false_direct_fact = select_fact.direct_arms_fact.false_fact
Some({
true_func: true_direct_fact.function_index(),
false_func: false_direct_fact.function_index(),
condition_fact: select_fact.condition_fact,
})
}
///|
priv struct OiCallRefOperandTypeFact {
arg_id : Int32
arg_type : @lib.ValType
}
///|
priv struct OiCallRefOperandFact {
args : Array[Int32]
type_facts : Array[OiCallRefOperandTypeFact]
}
///|
priv struct OiCallRefLocalizedOperandFact {
prefix : Array[Int32]
local_ids : Array[Int32]
}
///|
priv struct OiCallRefFallthroughOperandInterpositionFact {
args : Array[Int32]
type_facts : Array[OiCallRefOperandTypeFact]
callee_ref_id : Int32
}
///|
priv struct OiCallRefSelectResidueOperandInterpositionFact {
args : Array[Int32]
type_facts : Array[OiCallRefOperandTypeFact]
residue_fact : OiCallRefSelectTargetResidueFact
}
///|
fn OiCallRefOperandFact::from_args(
func : @ir.HotFunc,
args : Array[Int32],
) -> OiCallRefOperandFact? {
let fact_args : Array[Int32] = []
let type_facts : Array[OiCallRefOperandTypeFact] = []
for arg in args {
let arg_node = @ir.hot_node_get(func, arg)
let arg_results = @ir.hot_type_results(func, arg_node.ty)
if arg_results.length() != 1 {
return None
}
fact_args.push(arg)
type_facts.push({ arg_id: arg, arg_type: arg_results[0], })
}
Some({ args: fact_args, type_facts, })
}
///|
fn OiCallRefOperandTypeFact::proves_typed_operand(
self : OiCallRefOperandTypeFact,
arg_id : Int32,
) -> Bool {
self.arg_id == arg_id
}
///|
fn OiCallRefOperandTypeFact::operand_type(
self : OiCallRefOperandTypeFact,
arg_id : Int32,
) -> @lib.ValType? {
if self.proves_typed_operand(arg_id) {
Some(self.arg_type)
} else {
None
}
}
///|
fn OiCallRefOperandFact::operand_type_at(
self : OiCallRefOperandFact,
index : Int32,
) -> @lib.ValType? {
if index < 0 ||
index >= self.args.length() ||
index >= self.type_facts.length() {
return None
}
self.type_facts[index].operand_type(self.args[index])
}
///|
fn OiCallRefOperandFact::localize_all(
self : OiCallRefOperandFact,
func : @ir.HotFunc,
) -> OiCallRefLocalizedOperandFact {
let prefix : Array[Int32] = []
let local_ids : Array[Int32] = []
for i = 0; i < self.args.length(); i = i + 1 {
let arg_type = match self.operand_type_at(i) {
Some(ty) => ty
None => self.type_facts[i].arg_type
}
let local_id = @ir.hot_append_body_local(func, arg_type)
prefix.push(@ir.hot_build_local_set(func, local_id, self.args[i]))
local_ids.push(local_id)
}
{ prefix, local_ids, }
}
///|
fn OiCallRefLocalizedOperandFact::local_gets(
self : OiCallRefLocalizedOperandFact,
func : @ir.HotFunc,
) -> Array[Int32] {
let gets : Array[Int32] = []
for local_id in self.local_ids {
gets.push(@ir.hot_build_local_get(func, local_id))
}
gets
}
///|
fn OiCallRefFallthroughOperandInterpositionFact::from_operand_fact(
operand_fact : OiCallRefOperandFact,
callee_ref_id : Int32,
) -> OiCallRefFallthroughOperandInterpositionFact? {
// Binaryen v130 fallthrough-target directization preserves call_ref operand
// order by interposing the original target evaluation after the last operand:
// `set(last); drop(target); get(last)`. Keep that last-operand proof as an
// explicit operand fact instead of open-coding it in the directization plan.
if callee_ref_id < 0 {
return None
}
Some({
args: operand_fact.args,
type_facts: operand_fact.type_facts,
callee_ref_id,
})
}
///|
fn OiCallRefFallthroughOperandInterpositionFact::build_args(
self : OiCallRefFallthroughOperandInterpositionFact,
func : @ir.HotFunc,
) -> Array[Int32]? {
let arg_count = self.args.length()
if arg_count == 0 {
return Some([])
}
let last_index = arg_count - 1
let operands : Array[Int32] = []
for i = 0; i < last_index; i = i + 1 {
operands.push(self.args[i])
}
let last_type = match
self.type_facts[last_index].operand_type(self.args[last_index]) {
Some(ty) => ty
None => return None
}
let temp_local = @ir.hot_append_body_local(func, last_type)
operands.push(
@ir.hot_build_block(func, @lib.BlockType::val_type(last_type), [
@ir.hot_build_local_set(func, temp_local, self.args[last_index]),
@ir.hot_build_drop(func, self.callee_ref_id),
@ir.hot_build_local_get(func, temp_local),
]),
)
Some(operands)
}
///|
fn OiCallRefOperandFact::args_with_fallthrough_target_after_last_operand(
self : OiCallRefOperandFact,
func : @ir.HotFunc,
callee_ref_id : Int32,
) -> Array[Int32]? {
match
OiCallRefFallthroughOperandInterpositionFact::from_operand_fact(
self, callee_ref_id,
) {
Some(fact) => fact.build_args(func)
None => None
}
}
///|
fn OiCallRefSelectResidueOperandInterpositionFact::from_operand_fact(
operand_fact : OiCallRefOperandFact,
residue_fact : OiCallRefSelectTargetResidueFact,
) -> OiCallRefSelectResidueOperandInterpositionFact? {
// Binaryen v130 collapses same-target select call_ref producers to a single
// direct call but keeps the effectful select target evaluation in the original
// target slot: after ordinary call_ref operands and before the direct call.
// `OiCallRefSelectTargetResidueFact` owns that target residue; this fact owns
// the matching last-operand interposition rule.
Some({
args: operand_fact.args,
type_facts: operand_fact.type_facts,
residue_fact,
})
}
///|
fn OiCallRefSelectResidueOperandInterpositionFact::build_args(
self : OiCallRefSelectResidueOperandInterpositionFact,
func : @ir.HotFunc,
) -> Array[Int32]? {
match self.residue_fact.preserved_condition_drop_id() {
Some(_) => ()
None => return Some(self.args.copy())
}
let arg_count = self.args.length()
if arg_count == 0 {
return Some([])
}
let last_index = arg_count - 1
let operands : Array[Int32] = []
for i = 0; i < last_index; i = i + 1 {
operands.push(self.args[i])
}
let target_evaluation = match
self.residue_fact.build_dropped_target_evaluation(func) {
Some(evaluation) => evaluation
None => return None
}
let last_type = match
self.type_facts[last_index].operand_type(self.args[last_index]) {
Some(ty) => ty
None => return None
}
let temp_local = @ir.hot_append_body_local(func, last_type)
operands.push(
@ir.hot_build_block(func, @lib.BlockType::val_type(last_type), [
@ir.hot_build_local_set(func, temp_local, self.args[last_index]),
target_evaluation,
@ir.hot_build_local_get(func, temp_local),
]),
)
Some(operands)
}
///|
fn OiCallRefOperandFact::args_with_select_condition_after_last_operand(
self : OiCallRefOperandFact,
func : @ir.HotFunc,
residue_fact : OiCallRefSelectTargetResidueFact,
) -> Array[Int32]? {
match
OiCallRefSelectResidueOperandInterpositionFact::from_operand_fact(
self, residue_fact,
) {
Some(fact) => fact.build_args(func)
None => None
}
}
///|
priv struct OiCallRefSelectTargetResidueTypeFact {
target_type : @lib.ValType
}
///|
priv struct OiCallRefSelectTargetResidueFact {
target_arm_id : Int32
type_fact : OiCallRefSelectTargetResidueTypeFact
condition_fact : OiCallRefSelectConditionFact
}
///|
fn OiCallRefSelectTargetResidueFact::from_same_target(
func : @ir.HotFunc,
callee_ref_id : Int32,
target_fact : OiCallRefTargetFact,
) -> OiCallRefSelectTargetResidueFact? {
let same_target_fact = match target_fact.select_target() {
Some(select_fact) =>
match OiCallRefSelectSameTargetFact::from_select_target(select_fact) {
Some(fact) => fact
None => return None
}
None => return None
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, callee_ref_id)) {
[target_type] =>
Some({
target_arm_id: same_target_fact.residue_arm_id,
type_fact: { target_type, },
condition_fact: same_target_fact.condition_fact,
})
_ => None
}
}
///|
fn OiCallRefSelectTargetResidueTypeFact::result_type(
self : OiCallRefSelectTargetResidueTypeFact,
) -> @lib.ValType {
self.target_type
}
///|
fn OiCallRefSelectTargetResidueFact::preserved_condition_drop_id(
self : OiCallRefSelectTargetResidueFact,
) -> Int32? {
self.condition_fact.effect_fact.preserved_drop_id()
}
///|
fn OiCallRefSelectTargetResidueFact::build_dropped_target_evaluation(
self : OiCallRefSelectTargetResidueFact,
func : @ir.HotFunc,
) -> Int32? {
// This mirrors the Binaryen v130 `CallUtils::convertToDirectCalls` residue for
// identical select arms: an effectful i32 condition is dropped, then a
// fact-proven target arm is materialized as the block result and the whole
// target evaluation is dropped before the direct call.
let condition_drop_id = match self.preserved_condition_drop_id() {
Some(id) => id
None => return None
}
Some(
@ir.hot_build_drop(
func,
@ir.hot_build_block(
func,
@lib.BlockType::val_type(self.type_fact.result_type()),
[@ir.hot_build_drop(func, condition_drop_id), self.target_arm_id],
),
),
)
}
///|
priv struct OiCallRefResultArityFact {
result_count : Int32
}
///|
priv struct OiCallRefResultBlockTypeFact {
arity_fact : OiCallRefResultArityFact
block_type : @lib.BlockType?
}
///|
fn OiCallRefResultBlockTypeFact::from_call_ref(
func : @ir.HotFunc,
node : @ir.HotNode,
) -> OiCallRefResultBlockTypeFact {
// Binaryen v130 `visitCallRef` rebuilds directized `call_ref` replacements
// with `curr->type`. Starshine's HOT builder needs the equivalent block
// wrapper type only for fallthrough/select residue cases that must sequence a
// dropped target before the direct call. Keep the single-result/multivalue
// boundary as a fact instead of a generic helper hidden in replacement code.
let results = @ir.hot_type_results(func, node.ty)
let arity_fact : OiCallRefResultArityFact = {
result_count: results.length(),
}
let block_type = match arity_fact.result_count {
0 => Some(@lib.BlockType::void_())
1 => Some(@lib.BlockType::val_type(results[0]))
_ => None
}
{ arity_fact, block_type, }
}
///|
fn OiCallRefResultArityFact::proves_single_or_void_result(
self : OiCallRefResultArityFact,
) -> Bool {
self.result_count <= 1
}
///|
fn OiCallRefResultBlockTypeFact::proves_representable_block_type(
self : OiCallRefResultBlockTypeFact,
) -> Bool {
self.arity_fact.proves_single_or_void_result()
}
///|
fn OiCallRefResultBlockTypeFact::representable_block_type(
self : OiCallRefResultBlockTypeFact,
) -> @lib.BlockType? {
match self.block_type {
Some(block_type) =>
if self.proves_representable_block_type() {
Some(block_type)
} else {
None
}
None => None
}
}
///|
priv enum OiCallRefDirectizePlanKind {
CallRefDirectFunc
CallRefTableTarget
CallRefFallthroughFunc
CallRefSelectSameDirectFunc
CallRefSelectDirectFuncs
}
///|
priv struct OiCallRefDirectizePlanFact {
kind : OiCallRefDirectizePlanKind
target_fact : OiCallRefTargetFact
operand_fact : OiCallRefOperandFact?
}
///|
fn OiCallRefDirectizePlanFact::from_target(
func : @ir.HotFunc,
target_fact : OiCallRefTargetFact,
args : Array[Int32],
) -> OiCallRefDirectizePlanFact? {
// Binaryen v130 `visitCallRef` routes direct targets, typed-table targets,
// fallthrough `ref.func` producers, and two-direct-arm selects through
// `CallUtils::convertToDirectCalls`. Keep that routing as explicit plan
// facts so operand localization and call/return-call emission do not live in
// an ad hoc branch cascade inside the pass visitor.
match OiCallRefDirectFuncPlanFact::from_target_fact(target_fact) {
Some(_) =>
return Some({ kind: CallRefDirectFunc, target_fact, operand_fact: None, })
None => ()
}
match OiCallRefTableTargetPlanFact::from_target_fact(target_fact) {
Some(_) =>
return Some({ kind: CallRefTableTarget, target_fact, operand_fact: None, })
None => ()
}
match OiCallRefFallthroughFuncPlanFact::from_target_fact(target_fact) {
Some(_) => {
let operand_fact = match OiCallRefOperandFact::from_args(func, args) {
Some(fact) => fact
None => return None
}
return Some({
kind: CallRefFallthroughFunc,
target_fact,
operand_fact: Some(operand_fact),
})
}
None => ()
}
match target_fact.select_target() {
Some(select_fact) =>
match OiCallRefSelectSameTargetFact::from_select_target(select_fact) {
Some(_) => {
let operand_fact = match OiCallRefOperandFact::from_args(func, args) {
Some(fact) => fact
None => return None
}
return Some({
kind: CallRefSelectSameDirectFunc,
target_fact,
operand_fact: Some(operand_fact),
})
}
None => ()
}
None => ()
}
match target_fact.select_target() {
Some(select_fact) =>
match OiCallRefSelectDirectCallsFact::from_select_target(select_fact) {
Some(_) => {
let operand_fact = match OiCallRefOperandFact::from_args(func, args) {
Some(fact) => fact
None => return None
}
return Some({
kind: CallRefSelectDirectFuncs,
target_fact,
operand_fact: Some(operand_fact),
})
}
None => ()
}
None => ()
}
None
}
///|
fn OiCallRefDirectizePlanFact::operand_fact(
self : OiCallRefDirectizePlanFact,
) -> OiCallRefOperandFact? {
self.operand_fact
}
///|
fn OiCallRefDirectizePlanFact::build_replacement(
self : OiCallRefDirectizePlanFact,
func : @ir.HotFunc,
node : @ir.HotNode,
callee_ref_id : Int32,
args : Array[Int32],
) -> Int32? {
match self.kind {
CallRefDirectFunc => self.build_direct_func_replacement(func, node, args)
CallRefTableTarget => self.build_table_target_replacement(func, node, args)
CallRefFallthroughFunc => {
let operand_fact = match self.operand_fact() {
Some(fact) => fact
None => return None
}
self.build_fallthrough_func_replacement(
func, node, callee_ref_id, operand_fact,
)
}
CallRefSelectSameDirectFunc => {
let operand_fact = match self.operand_fact() {
Some(fact) => fact
None => return None
}
self.build_select_same_direct_func_replacement(
func, node, callee_ref_id, operand_fact,
)
}
CallRefSelectDirectFuncs => {
let operand_fact = match self.operand_fact() {
Some(fact) => fact
None => return None
}
self.build_select_direct_funcs_replacement(func, node, operand_fact)
}
}
}
///|
fn OiCallRefDirectizePlanFact::build_fallthrough_func_replacement(
self : OiCallRefDirectizePlanFact,
func : @ir.HotFunc,
node : @ir.HotNode,
callee_ref_id : Int32,
operand_fact : OiCallRefOperandFact,
) -> Int32? {
let fallthrough_plan = match
OiCallRefFallthroughFuncPlanFact::from_target_fact(self.target_fact) {
Some(fact) => fact
None => return None
}
match node.op {
@ir.HotOp::CallRef => {
let result_type_fact = OiCallRefResultBlockTypeFact::from_call_ref(
func, node,
)
let block_type = match result_type_fact.representable_block_type() {
Some(block_type) => block_type
None => return None
}
let results = @ir.hot_type_results(func, node.ty)
match
operand_fact.args_with_fallthrough_target_after_last_operand(
func, callee_ref_id,
) {
Some([]) =>
Some(
@ir.hot_build_block(func, block_type, [
@ir.hot_build_drop(func, callee_ref_id),
@ir.hot_build_call(func, fallthrough_plan.direct_func, results, []),
]),
)
Some(operands) =>
Some(
@ir.hot_build_call(
func,
fallthrough_plan.direct_func,
results,
operands,
),
)
None => None
}
}
@ir.HotOp::ReturnCallRef =>
match
operand_fact.args_with_fallthrough_target_after_last_operand(
func, callee_ref_id,
) {
Some([]) =>
Some(
@ir.hot_build_block(func, @lib.BlockType::void_(), [
@ir.hot_build_drop(func, callee_ref_id),
@ir.hot_build_return_call(func, fallthrough_plan.direct_func, []),
]),
)
Some(operands) =>
Some(
@ir.hot_build_return_call(
func,
fallthrough_plan.direct_func,
operands,
),
)
None => None
}
_ => None
}
}
///|
fn OiCallRefDirectizePlanFact::build_direct_func_replacement(
self : OiCallRefDirectizePlanFact,
func : @ir.HotFunc,
node : @ir.HotNode,
args : Array[Int32],
) -> Int32? {
let direct_plan = match
OiCallRefDirectFuncPlanFact::from_target_fact(self.target_fact) {
Some(fact) => fact
None => return None
}
match node.op {
@ir.HotOp::CallRef =>
Some(
@ir.hot_build_call(
func,
direct_plan.direct_func,
@ir.hot_type_results(func, node.ty),
args,
),
)
@ir.HotOp::ReturnCallRef =>
Some(@ir.hot_build_return_call(func, direct_plan.direct_func, args))
_ => None
}
}
///|
fn OiCallRefDirectizePlanFact::build_table_target_replacement(
self : OiCallRefDirectizePlanFact,
func : @ir.HotFunc,
node : @ir.HotNode,
args : Array[Int32],
) -> Int32? {
let table_plan = match
OiCallRefTableTargetPlanFact::from_target_fact(self.target_fact) {
Some(fact) => fact
None => return None
}
let sig = @ir.hot_call_sig_get(func, node.imm0)
match node.op {
@ir.HotOp::CallRef =>
Some(
@ir.hot_build_call_indirect(
func,
sig.type_idx,
@lib.TableIdx::new(table_plan.table_idx.reinterpret_as_uint()),
@ir.hot_type_results(func, node.ty),
args,
table_plan.table_index_id,
),
)
@ir.HotOp::ReturnCallRef =>
Some(
@ir.hot_build_return_call_indirect(
func,
sig.type_idx,
@lib.TableIdx::new(table_plan.table_idx.reinterpret_as_uint()),
args,
table_plan.table_index_id,
),
)
_ => None
}
}
///|
fn OiCallRefDirectizePlanFact::build_select_same_direct_func_replacement(
self : OiCallRefDirectizePlanFact,
func : @ir.HotFunc,
node : @ir.HotNode,
callee_ref_id : Int32,
operand_fact : OiCallRefOperandFact,
) -> Int32? {
let same_target_fact = match self.target_fact.select_target() {
Some(select_fact) =>
match OiCallRefSelectSameTargetFact::from_select_target(select_fact) {
Some(fact) => fact
None => return None
}
None => return None
}
let residue_fact = match
OiCallRefSelectTargetResidueFact::from_same_target(
func,
callee_ref_id,
self.target_fact,
) {
Some(fact) => fact
None => return None
}
let results = @ir.hot_type_results(func, node.ty)
let operands = match
operand_fact.args_with_select_condition_after_last_operand(
func, residue_fact,
) {
Some(operands) => operands
None => return None
}
match node.op {
@ir.HotOp::CallRef => {
let direct_call = @ir.hot_build_call(
func,
same_target_fact.direct_func,
results,
operands,
)
match residue_fact.build_dropped_target_evaluation(func) {
Some(target_evaluation) if operand_fact.args.length() == 0 => {
let result_type_fact = OiCallRefResultBlockTypeFact::from_call_ref(
func, node,
)
let block_type = match result_type_fact.representable_block_type() {
Some(block_type) => block_type
None => return None
}
Some(
@ir.hot_build_block(func, block_type, [
target_evaluation, direct_call,
]),
)
}
_ => Some(direct_call)
}
}
@ir.HotOp::ReturnCallRef => {
let direct_call = @ir.hot_build_return_call(
func,
same_target_fact.direct_func,
operands,
)
match residue_fact.build_dropped_target_evaluation(func) {
Some(target_evaluation) if operand_fact.args.length() == 0 =>
Some(
@ir.hot_build_block(func, @lib.BlockType::void_(), [
target_evaluation, direct_call,
]),
)
_ => Some(direct_call)
}
}
_ => None
}
}
///|
fn OiCallRefDirectizePlanFact::build_select_direct_funcs_replacement(
self : OiCallRefDirectizePlanFact,
func : @ir.HotFunc,
node : @ir.HotNode,
operand_fact : OiCallRefOperandFact,
) -> Int32? {
let direct_calls_fact = match self.target_fact.select_target() {
Some(select_fact) =>
match OiCallRefSelectDirectCallsFact::from_select_target(select_fact) {
Some(fact) => fact
None => return None
}
None => return None
}
let block_type = match node.op {
@ir.HotOp::CallRef => {
let result_type_fact = OiCallRefResultBlockTypeFact::from_call_ref(
func, node,
)
match result_type_fact.representable_block_type() {
Some(block_type) => block_type
None => return None
}
}
@ir.HotOp::ReturnCallRef => @lib.BlockType::void_()
_ => return None
}
let localized_operands = operand_fact.localize_all(func)
let replacement_if = match node.op {
@ir.HotOp::CallRef =>
@ir.hot_build_if(
func,
block_type,
direct_calls_fact.condition_fact.condition_id,
[
@ir.hot_build_call(
func,
direct_calls_fact.true_func,
@ir.hot_type_results(func, node.ty),
localized_operands.local_gets(func),
),
],
else_body=Some([
@ir.hot_build_call(
func,
direct_calls_fact.false_func,
@ir.hot_type_results(func, node.ty),
localized_operands.local_gets(func),
),
]),
)
@ir.HotOp::ReturnCallRef =>
@ir.hot_build_if(
func,
@lib.BlockType::void_(),
direct_calls_fact.condition_fact.condition_id,
[
@ir.hot_build_return_call(
func,
direct_calls_fact.true_func,
localized_operands.local_gets(func),
),
],
else_body=Some([
@ir.hot_build_return_call(
func,
direct_calls_fact.false_func,
localized_operands.local_gets(func),
),
]),
)
_ => return None
}
if localized_operands.prefix.length() == 0 {
Some(replacement_if)
} else {
let body = localized_operands.prefix.copy()
body.push(replacement_if)
Some(@ir.hot_build_block(func, block_type, body))
}
}
///|
priv struct OiCallRefTargetSlotFact {
source_target_id : Int32
effective_target_id : Int32
}
///|
priv struct OiCallRefArgumentListFact {
args : Array[Int32]
}
///|
priv struct OiCallRefChildLayoutFact {
target_slot_fact : OiCallRefTargetSlotFact
argument_list_fact : OiCallRefArgumentListFact
}
///|
priv struct OiCallRefInvocationFact {
child_layout_fact : OiCallRefChildLayoutFact
target_classification_fact : OiCallRefTargetClassificationFact
}
///|
priv struct OiCallRefDirectizationFact {
invocation_fact : OiCallRefInvocationFact
plan_fact : OiCallRefDirectizePlanFact
}
///|
priv enum OiCallRefRewriteKind {
CallRefNullTrapRewrite
CallRefDirectizationRewrite
}
///|
priv struct OiCallRefRewriteFact {
kind : OiCallRefRewriteKind
null_trap_plan : OiCallRefNullTrapPlanFact?
directization_fact : OiCallRefDirectizationFact?
}
///|
fn OiCallRefChildLayoutFact::from_call_ref(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
node : @ir.HotNode,
) -> OiCallRefChildLayoutFact? {
// Binaryen v130 `visitCallRef` treats the final child as the callee reference
// and all earlier children as call operands before applying target-slot
// `skipNonNullCast`. Keep that child-slot layout and effective-target
// projection separate from later target classification so null-trap and
// directization plans share the same source-shaped call_ref record.
match node.op {
@ir.HotOp::CallRef | @ir.HotOp::ReturnCallRef => ()
_ => return None
}
if node.child_count < 1 {
return None
}
let callee_ref_id = @ir.hot_child_get(func, node_id, node.child_count - 1)
if !OiLiveNodeFact::proves_node_live(func, dead, callee_ref_id) {
return None
}
let effective_callee_ref_id = match
OiCallRefTargetSkipNonNullFact::from_call_ref_target(func, dead, node_id) {
Some(fact) =>
match fact.effective_target_id() {
Some(target_id) => target_id
None => return None
}
None => callee_ref_id
}
let args : Array[Int32] = []
for slot = 0; slot < node.child_count - 1; slot = slot + 1 {
args.push(@ir.hot_child_get(func, node_id, slot))
}
let target_slot_fact : OiCallRefTargetSlotFact = {
source_target_id: callee_ref_id,
effective_target_id: effective_callee_ref_id,
}
let argument_list_fact : OiCallRefArgumentListFact = { args, }
Some({ target_slot_fact, argument_list_fact, })
}
///|
fn OiCallRefTargetSlotFact::proves_effective_target(
self : OiCallRefTargetSlotFact,
) -> Bool {
self.source_target_id >= 0 && self.effective_target_id >= 0
}
///|
fn OiCallRefArgumentListFact::args(
self : OiCallRefArgumentListFact,
) -> Array[Int32] {
self.args
}
///|
fn OiCallRefInvocationFact::from_child_layout(
func : @ir.HotFunc,
dead : @bitset.BitSet,
child_layout_fact : OiCallRefChildLayoutFact,
) -> OiCallRefInvocationFact {
// Binaryen v130 classifies direct/table/fallthrough/select targets only after
// the target-slot null-trap opportunity has been considered. Keep the ordered
// target classification as a fact layered on top of the shared call_ref child
// layout so null-trap plans can stop before this broader target proof.
let target_classification_fact = OiCallRefTargetClassificationFact::from_target(
func,
dead,
child_layout_fact.target_slot_fact.effective_target_id,
)
{ child_layout_fact, target_classification_fact, }
}
///|
fn OiCallRefInvocationFact::target_fact(
self : OiCallRefInvocationFact,
) -> OiCallRefTargetFact {
self.target_classification_fact.target_fact()
}
///|
fn OiCallRefDirectizationFact::from_invocation(
func : @ir.HotFunc,
invocation_fact : OiCallRefInvocationFact,
) -> OiCallRefDirectizationFact? {
let plan_fact = match
OiCallRefDirectizePlanFact::from_target(
func,
invocation_fact.target_fact(),
invocation_fact.child_layout_fact.argument_list_fact.args(),
) {
Some(fact) => fact
None => return None
}
Some({ invocation_fact, plan_fact, })
}
///|
fn OiCallRefDirectizationFact::apply(
self : OiCallRefDirectizationFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if !self.invocation_fact.child_layout_fact.target_slot_fact.proves_effective_target() {
return false
}
let node = @ir.hot_node_get(func, node_id)
let temp = match
self.plan_fact.build_replacement(
func,
node,
self.invocation_fact.child_layout_fact.target_slot_fact.effective_target_id,
self.invocation_fact.child_layout_fact.argument_list_fact.args(),
) {
Some(replacement) => replacement
None => return false
}
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
match replacement.op {
@ir.HotOp::If
| @ir.HotOp::Block
| @ir.HotOp::Loop
| @ir.HotOp::Try
| @ir.HotOp::TryTable =>
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
_ => ()
}
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn OiCallRefRewriteFact::from_call_ref(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiCallRefRewriteFact? {
// Binaryen v130 `visitCallRef` runs target `skipNonNullCast`, target
// `trapOnNull`, direct `ref.func`, immediate `table.get`, fallthrough
// `ref.func`, and `CallUtils::convertToDirectCalls` in one visitor. Keep that
// order as a fact-owned rewrite record so callers request a call_ref rewrite
// without rebuilding the visitor's proof cascade themselves.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::CallRef | @ir.HotOp::ReturnCallRef => ()
_ => return None
}
let child_layout_fact = match
OiCallRefChildLayoutFact::from_call_ref(func, dead, node_id, node) {
Some(fact) => fact
None => return None
}
match
OiCallRefNullTrapPlanFact::from_child_layout(func, dead, child_layout_fact) {
Some(fact) =>
return Some({
kind: CallRefNullTrapRewrite,
null_trap_plan: Some(fact),
directization_fact: None,
})
None => ()
}
let invocation_fact = OiCallRefInvocationFact::from_child_layout(
func, dead, child_layout_fact,
)
match OiCallRefDirectizationFact::from_invocation(func, invocation_fact) {
Some(fact) =>
Some({
kind: CallRefDirectizationRewrite,
null_trap_plan: None,
directization_fact: Some(fact),
})
None => None
}
}
///|
fn OiCallRefRewriteFact::apply(
self : OiCallRefRewriteFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
CallRefNullTrapRewrite =>
match self.null_trap_plan {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
CallRefDirectizationRewrite =>
match self.directization_fact {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
}
///|
fn optimize_instructions_try_directize_ref_func_call_ref(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match OiCallRefRewriteFact::from_call_ref(func, dead, node_id) {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_sink_same_global_set_if(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return false
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => ()
_ => return false
}
if @ir.hot_type_results(func, node.ty).length() != 0 {
return false
}
let then_region = @ir.hot_region_then_ref(func, node_id)
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return false
}
if @ir.hot_region_root_count(func, then_region) != 1 ||
@ir.hot_region_root_count(func, else_region) != 1 {
return false
}
let then_set_id = @ir.hot_region_get(func, then_region, 0)
let else_set_id = @ir.hot_region_get(func, else_region, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, then_set_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, else_set_id) {
return false
}
let then_set = @ir.hot_node_get(func, then_set_id)
let else_set = @ir.hot_node_get(func, else_set_id)
let global_id = match (then_set.op, else_set.op) {
(@ir.HotOp::GlobalSet, @ir.HotOp::GlobalSet) if then_set.imm0 ==
else_set.imm0 => then_set.imm0
_ => return false
}
if then_set.child_count != 1 || else_set.child_count != 1 {
return false
}
let then_value = @ir.hot_child_get(func, then_set_id, 0)
let else_value = @ir.hot_child_get(func, else_set_id, 0)
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, then_value) ||
!OiSideEffectFreeExpressionFact::proves_node_free(func, dead, else_value) {
return false
}
let then_results = @ir.hot_type_results(
func,
@ir.hot_node_get(func, then_value).ty,
)
let else_results = @ir.hot_type_results(
func,
@ir.hot_node_get(func, else_value).ty,
)
if then_results.length() != 1 ||
else_results.length() != 1 ||
then_results[0] != else_results[0] {
return false
}
let condition = @ir.hot_child_get(func, node_id, 0)
let value_if = @ir.hot_build_if(
func,
@lib.BlockType::val_type(then_results[0]),
condition,
[then_value],
else_body=Some([else_value]),
)
let replacement_set = @ir.hot_build_global_set(func, global_id, value_if)
@ir.hot_control_demote_if_to_block(func, node_id, [replacement_set])
pass_mark_mutated(ctx, func)
true
}
///|
priv struct OiSameLocalSetIfFact {
reusable_set_id : Int32
then_value_id : Int32
else_value_id : Int32
value_type : @lib.ValType
use_site : @ir.HotUseSite
use_region : @ir.HotRegionRef?
}
///|
fn OiSameLocalSetIfFact::from_if(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiSameLocalSetIfFact? {
// Binaryen v130's parsed OI shape sinks matching local writes from both arms
// into one write around a value-producing `if`. Keep the same-local target,
// exact result type, and arm purity proofs together so this remains a general
// state-write rule rather than a generated-profile shape admission.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => ()
_ => return None
}
if @ir.hot_type_results(func, node.ty).length() != 0 {
return None
}
let then_region = @ir.hot_region_then_ref(func, node_id)
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return None
}
if @ir.hot_region_root_count(func, then_region) != 1 ||
@ir.hot_region_root_count(func, else_region) != 1 {
return None
}
let then_set_id = @ir.hot_region_get(func, then_region, 0)
let else_set_id = @ir.hot_region_get(func, else_region, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, then_set_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, else_set_id) {
return None
}
let then_set = @ir.hot_node_get(func, then_set_id)
let else_set = @ir.hot_node_get(func, else_set_id)
let local_id = match (then_set.op, else_set.op) {
(@ir.HotOp::LocalSet, @ir.HotOp::LocalSet) if then_set.imm0 == else_set.imm0 =>
then_set.imm0
_ => return None
}
if then_set.child_count != 1 || else_set.child_count != 1 {
return None
}
let then_value_id = @ir.hot_child_get(func, then_set_id, 0)
let else_value_id = @ir.hot_child_get(func, else_set_id, 0)
if !OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, then_value_id,
) ||
!OiSideEffectFreeExpressionFact::proves_node_free(func, dead, else_value_id) {
return None
}
let value_type = match
(
@ir.hot_type_results(func, @ir.hot_node_type(func, then_value_id)),
@ir.hot_type_results(func, @ir.hot_node_type(func, else_value_id)),
) {
([then_type], [else_type]) if then_type == else_type => then_type
_ => return None
}
match @ir.hot_local_type(func, local_id) {
Some(local_type) if local_type == value_type => ()
_ => return None
}
let use_def = pass_require_use_def(ctx, func)
if @ir.node_use_count(use_def, node_id) != 1 {
return None
}
let use_sites = @ir.node_use_sites_borrowed(use_def, node_id)
if use_sites.length() != 1 {
return None
}
let use_site = use_sites[0]
let use_region = match use_site.kind {
@ir.HotUseSlotKind::ChildUseSlot => None
@ir.HotUseSlotKind::RootUseSlot if use_site.user == -1 => None
@ir.HotUseSlotKind::RootUseSlot => {
let holder = @ir.hot_node_get(func, use_site.user)
match holder.op {
@ir.HotOp::Block | @ir.HotOp::Loop => {
let region = @ir.hot_region_body_ref(func, use_site.user)
if use_site.slot < 0 ||
use_site.slot >= @ir.hot_region_root_count(func, region) ||
@ir.hot_region_get(func, region, use_site.slot) != node_id {
return None
}
Some(region)
}
_ => return None
}
}
}
Some({
reusable_set_id: then_set_id,
then_value_id,
else_value_id,
value_type,
use_site,
use_region,
})
}
///|
fn OiSameLocalSetIfFact::apply(
self : OiSameLocalSetIfFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
@ir.hot_region_replace_body(func, @ir.hot_region_then_ref(func, node_id), [
self.then_value_id,
])
match @ir.hot_region_else_ref(func, node_id) {
Some(region) =>
@ir.hot_region_replace_body(func, region, [self.else_value_id])
None => return false
}
@ir.hot_control_result_type_set(
func,
node_id,
@ir.hot_type_intern_block_result(
func,
@lib.BlockType::val_type(self.value_type),
),
)
@ir.hot_child_set(func, self.reusable_set_id, 0, node_id)
let replacement_set = self.reusable_set_id
match self.use_site.kind {
@ir.HotUseSlotKind::ChildUseSlot =>
@ir.hot_child_set(
func,
self.use_site.user,
self.use_site.slot,
replacement_set,
)
@ir.HotUseSlotKind::RootUseSlot if self.use_site.user == -1 =>
@ir.hot_root_set(func, self.use_site.slot, replacement_set)
@ir.HotUseSlotKind::RootUseSlot =>
match self.use_region {
Some(region) =>
ignore(
@ir.hot_region_set(
func,
region,
self.use_site.slot,
replacement_set,
),
)
None => return false
}
}
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_sink_same_local_set_if(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match OiSameLocalSetIfFact::from_if(ctx, func, dead, node_id) {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_sink_same_local_set_if_then_revisit_condition(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
if !optimize_instructions_try_sink_same_local_set_if(ctx, func, dead, node_id) {
return false
}
// Sinking keeps this exact If node alive but changes its arms and result
// type. Reconsider the new value If immediately, including a constant
// condition exposed by the already visited children.
if !optimize_instructions_try_fold_constant_if_condition(
ctx, func, dead, node_id,
) {
ignore(
optimize_instructions_try_optimize_if_condition(ctx, func, dead, node_id),
)
}
true
}
///|
fn optimize_instructions_try_fold_constant_if_condition(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => ()
_ => return false
}
let cond_id = @ir.hot_child_get(func, node_id, 0)
let take_then = match optimize_instructions_i32_const(func, dead, cond_id) {
Some(@lib.I32(0)) => false
Some(@lib.I32(_)) => true
None => return false
}
let chosen_region = if take_then {
@ir.hot_region_then_ref(func, node_id)
} else {
match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => {
@ir.hot_control_demote_if_to_block(func, node_id, [])
pass_mark_mutated(ctx, func)
return true
}
}
}
if !precompute_constant_if_chosen_region_can_inline(func, chosen_region) {
return false
}
let root_count = @ir.hot_region_root_count(func, chosen_region)
let body : Array[Int32] = []
for i = 0; i < root_count; i = i + 1 {
body.push(@ir.hot_region_get(func, chosen_region, i))
}
@ir.hot_control_demote_if_to_block(func, node_id, body)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_i32_const(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> @lib.I32? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
match node.op {
@ir.HotOp::Const => ()
_ => return None
}
match func.consts[node.imm0] {
@ir.HotConstPayload::I32Const(v) => Some(v)
_ => None
}
}
///|
fn optimize_instructions_i64_const(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> @lib.I64? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
match node.op {
@ir.HotOp::Const => ()
_ => return None
}
match func.consts[node.imm0] {
@ir.HotConstPayload::I64Const(v) => Some(v)
_ => None
}
}
///|
fn optimize_instructions_f32_const(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> @lib.F32? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
match node.op {
@ir.HotOp::Const => ()
_ => return None
}
match func.consts[node.imm0] {
@ir.HotConstPayload::F32Const(v) => Some(v)
_ => None
}
}
///|
fn optimize_instructions_f64_const(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> @lib.F64? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
match node.op {
@ir.HotOp::Const => ()
_ => return None
}
match func.consts[node.imm0] {
@ir.HotConstPayload::F64Const(v) => Some(v)
_ => None
}
}
///|
fn optimize_instructions_shift_mask_bits(instr : @lib.Instruction) -> Int32? {
match instr {
@lib.Instruction::I32Shl
| @lib.Instruction::I32ShrS
| @lib.Instruction::I32ShrU
| @lib.Instruction::I32Rotl
| @lib.Instruction::I32Rotr => Some(31)
@lib.Instruction::I64Shl
| @lib.Instruction::I64ShrS
| @lib.Instruction::I64ShrU
| @lib.Instruction::I64Rotl
| @lib.Instruction::I64Rotr => Some(63)
_ => None
}
}
///|
fn optimize_instructions_shift_rhs_i32_amount(
func : @ir.HotFunc,
dead : @bitset.BitSet,
rhs_id : Int32,
mask_bits : Int32,
) -> Int32? {
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => return Some(raw & mask_bits)
None => ()
}
if !OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return None
}
let rhs = @ir.hot_node_get(func, rhs_id)
match rhs.op {
@ir.HotOp::Binary =>
match @ir.hot_node_exact_instr(func, rhs_id) {
Some(@lib.Instruction::I32And) =>
match
optimize_instructions_i32_const(
func,
dead,
func.children[rhs.child_base + 1],
) {
Some(@lib.I32(raw)) if (raw & mask_bits) == mask_bits =>
match
optimize_instructions_i32_const(
func,
dead,
func.children[rhs.child_base],
) {
Some(@lib.I32(amount)) => Some(amount & mask_bits)
_ => None
}
_ => None
}
_ => None
}
_ => None
}
}
///|
fn optimize_instructions_shift_rhs_i64_amount(
func : @ir.HotFunc,
dead : @bitset.BitSet,
rhs_id : Int32,
mask_bits : Int32,
) -> Int32? {
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) => return Some((raw & mask_bits.to_int64()).to_int())
None => ()
}
if !OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return None
}
let rhs = @ir.hot_node_get(func, rhs_id)
match rhs.op {
@ir.HotOp::Binary =>
match @ir.hot_node_exact_instr(func, rhs_id) {
Some(@lib.Instruction::I64And) =>
match
optimize_instructions_i64_const(
func,
dead,
func.children[rhs.child_base + 1],
) {
Some(@lib.I64(raw)) if (raw & mask_bits.to_int64()) ==
mask_bits.to_int64() =>
match
optimize_instructions_i64_const(
func,
dead,
func.children[rhs.child_base],
) {
Some(@lib.I64(amount)) =>
Some((amount & mask_bits.to_int64()).to_int())
_ => None
}
_ => None
}
_ => None
}
_ => None
}
}
///|
priv enum OiI32NonNegativeIntegerKind {
I32NonNegativeConst
I32NegativeConst
I32MissingConst
I32UnsignedMaxBelowSignBit
I32UnsignedMaxMayReachSignBit
I32UnsignedMaxMissing
}
///|
priv struct OiI32NonNegativeIntegerFact {
kind : OiI32NonNegativeIntegerKind
}
///|
priv enum OiI64NonNegativeIntegerKind {
I64NonNegativeConst
I64NegativeConst
I64MissingConst
I64UnsignedMaxBelowSignBit
I64UnsignedMaxMayReachSignBit
I64UnsignedMaxMissing
}
///|
priv struct OiI64NonNegativeIntegerFact {
kind : OiI64NonNegativeIntegerKind
}
///|
fn OiI32NonNegativeIntegerFact::from_const(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiI32NonNegativeIntegerFact {
let kind = match optimize_instructions_i32_const(func, dead, node_id) {
Some(@lib.I32(raw)) =>
if raw >= 0 {
I32NonNegativeConst
} else {
I32NegativeConst
}
None => I32MissingConst
}
{ kind, }
}
///|
fn OiI64NonNegativeIntegerFact::from_const(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiI64NonNegativeIntegerFact {
let kind = match optimize_instructions_i64_const(func, dead, node_id) {
Some(@lib.I64(raw)) =>
if raw >= 0L {
I64NonNegativeConst
} else {
I64NegativeConst
}
None => I64MissingConst
}
{ kind, }
}
///|
fn OiI32NonNegativeIntegerFact::from_unsigned_max(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiI32NonNegativeIntegerFact {
// Binaryen v130 uses `Bits::getMaxBits(left, this) <= signBit - 1` before
// rewriting signed compare/div/rem/shr operations to unsigned spellings. The
// Starshine equivalent is the shared unsigned-max fact: if the fact proves the
// value cannot reach the sign bit, signed and unsigned operations observe the
// same nonnegative domain. This owns both direct nonnegative constants and
// LocalScanner-shaped local/mask/load/shift summaries.
let kind = match
OiUnsignedMaxValueFact::from_i32_value(func, dead, scratch, node_id).i32_max {
Some(value) =>
if value <= 2147483647 {
I32UnsignedMaxBelowSignBit
} else {
I32UnsignedMaxMayReachSignBit
}
None => I32UnsignedMaxMissing
}
{ kind, }
}
///|
fn OiI64NonNegativeIntegerFact::from_unsigned_max(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> OiI64NonNegativeIntegerFact {
let kind = match
OiUnsignedMaxValueFact::from_i64_value(func, dead, scratch, node_id).i64_max {
Some(value) =>
if value <= 9223372036854775807L {
I64UnsignedMaxBelowSignBit
} else {
I64UnsignedMaxMayReachSignBit
}
None => I64UnsignedMaxMissing
}
{ kind, }
}
///|
fn OiI32NonNegativeIntegerFact::proves_non_negative(
self : OiI32NonNegativeIntegerFact,
) -> Bool {
match self.kind {
I32NonNegativeConst | I32UnsignedMaxBelowSignBit => true
I32NegativeConst
| I32MissingConst
| I32UnsignedMaxMayReachSignBit
| I32UnsignedMaxMissing => false
}
}
///|
fn OiI64NonNegativeIntegerFact::proves_non_negative(
self : OiI64NonNegativeIntegerFact,
) -> Bool {
match self.kind {
I64NonNegativeConst | I64UnsignedMaxBelowSignBit => true
I64NegativeConst
| I64MissingConst
| I64UnsignedMaxMayReachSignBit
| I64UnsignedMaxMissing => false
}
}
///|
priv struct OiI32SignedToUnsignedOpcodeFact {
unsigned_instr : @lib.Instruction?
}
///|
priv struct OiI64SignedToUnsignedOpcodeFact {
unsigned_instr : @lib.Instruction?
}
///|
fn OiI32SignedToUnsignedOpcodeFact::from_instr(
instr : @lib.Instruction,
) -> OiI32SignedToUnsignedOpcodeFact {
// Mirrors Binaryen v130 `makeUnsignedBinaryOp`: only signed integer
// comparisons, div/rem, and arithmetic right shift have an unsigned spelling
// that can replace the original when operand facts prove the nonnegative
// domain. Keep this table as a fact instead of a naked opcode whitelist.
match instr {
@lib.Instruction::I32LtS =>
{ unsigned_instr: Some(@lib.Instruction::i32_lt_u()), }
@lib.Instruction::I32GtS =>
{ unsigned_instr: Some(@lib.Instruction::i32_gt_u()), }
@lib.Instruction::I32LeS =>
{ unsigned_instr: Some(@lib.Instruction::i32_le_u()), }
@lib.Instruction::I32GeS =>
{ unsigned_instr: Some(@lib.Instruction::i32_ge_u()), }
@lib.Instruction::I32DivS =>
{ unsigned_instr: Some(@lib.Instruction::i32_div_u()), }
@lib.Instruction::I32RemS =>
{ unsigned_instr: Some(@lib.Instruction::i32_rem_u()), }
@lib.Instruction::I32ShrS =>
{ unsigned_instr: Some(@lib.Instruction::i32_shr_u()), }
_ => { unsigned_instr: None, }
}
}
///|
fn OiI64SignedToUnsignedOpcodeFact::from_instr(
instr : @lib.Instruction,
) -> OiI64SignedToUnsignedOpcodeFact {
match instr {
@lib.Instruction::I64LtS =>
{ unsigned_instr: Some(@lib.Instruction::i64_lt_u()), }
@lib.Instruction::I64GtS =>
{ unsigned_instr: Some(@lib.Instruction::i64_gt_u()), }
@lib.Instruction::I64LeS =>
{ unsigned_instr: Some(@lib.Instruction::i64_le_u()), }
@lib.Instruction::I64GeS =>
{ unsigned_instr: Some(@lib.Instruction::i64_ge_u()), }
@lib.Instruction::I64DivS =>
{ unsigned_instr: Some(@lib.Instruction::i64_div_u()), }
@lib.Instruction::I64RemS =>
{ unsigned_instr: Some(@lib.Instruction::i64_rem_u()), }
@lib.Instruction::I64ShrS =>
{ unsigned_instr: Some(@lib.Instruction::i64_shr_u()), }
_ => { unsigned_instr: None, }
}
}
///|
fn OiI32SignedToUnsignedOpcodeFact::replacement(
self : OiI32SignedToUnsignedOpcodeFact,
) -> @lib.Instruction? {
self.unsigned_instr
}
///|
fn OiI64SignedToUnsignedOpcodeFact::replacement(
self : OiI64SignedToUnsignedOpcodeFact,
) -> @lib.Instruction? {
self.unsigned_instr
}
///|
priv struct OiI32SignedConstUnsignedCanonicalizationFact {
opcode_fact : OiI32SignedToUnsignedOpcodeFact
lhs_fact : OiI32NonNegativeIntegerFact
rhs_fact : OiI32NonNegativeIntegerFact
}
///|
priv struct OiI64SignedConstUnsignedCanonicalizationFact {
opcode_fact : OiI64SignedToUnsignedOpcodeFact
lhs_fact : OiI64NonNegativeIntegerFact
rhs_fact : OiI64NonNegativeIntegerFact
}
///|
priv enum OiSignedConstUnsignedCanonicalizationFact {
I32SignedConstUnsignedCanonicalization(
OiI32SignedConstUnsignedCanonicalizationFact
)
I64SignedConstUnsignedCanonicalization(
OiI64SignedConstUnsignedCanonicalizationFact
)
}
///|
fn OiI32SignedConstUnsignedCanonicalizationFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32SignedConstUnsignedCanonicalizationFact? {
// Binaryen v130 runs this source step only when the right operand is a
// nonnegative constant and the left operand has no sign bit according to
// `Bits::getMaxBits`. Preserve that asymmetry: a nonconstant nonnegative RHS
// is not enough, and an arbitrary nonnegative constant LHS does not bypass the
// shared left unsigned-max fact.
let opcode_fact = OiI32SignedToUnsignedOpcodeFact::from_instr(instr)
match opcode_fact.replacement() {
Some(_) => ()
None => return None
}
let rhs_fact = OiI32NonNegativeIntegerFact::from_const(func, dead, rhs_id)
if !rhs_fact.proves_non_negative() {
return None
}
let lhs_fact = OiI32NonNegativeIntegerFact::from_unsigned_max(
func, dead, scratch, lhs_id,
)
if !lhs_fact.proves_non_negative() {
return None
}
Some({ opcode_fact, lhs_fact, rhs_fact, })
}
///|
fn OiI64SignedConstUnsignedCanonicalizationFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64SignedConstUnsignedCanonicalizationFact? {
let opcode_fact = OiI64SignedToUnsignedOpcodeFact::from_instr(instr)
match opcode_fact.replacement() {
Some(_) => ()
None => return None
}
let rhs_fact = OiI64NonNegativeIntegerFact::from_const(func, dead, rhs_id)
if !rhs_fact.proves_non_negative() {
return None
}
let lhs_fact = OiI64NonNegativeIntegerFact::from_unsigned_max(
func, dead, scratch, lhs_id,
)
if !lhs_fact.proves_non_negative() {
return None
}
Some({ opcode_fact, lhs_fact, rhs_fact, })
}
///|
fn OiSignedConstUnsignedCanonicalizationFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiSignedConstUnsignedCanonicalizationFact? {
match
OiI32SignedConstUnsignedCanonicalizationFact::from_binary(
func, dead, scratch, instr, lhs_id, rhs_id,
) {
Some(fact) => Some(I32SignedConstUnsignedCanonicalization(fact))
None =>
match
OiI64SignedConstUnsignedCanonicalizationFact::from_binary(
func, dead, scratch, instr, lhs_id, rhs_id,
) {
Some(fact) => Some(I64SignedConstUnsignedCanonicalization(fact))
None => None
}
}
}
///|
fn OiI32SignedConstUnsignedCanonicalizationFact::replacement(
self : OiI32SignedConstUnsignedCanonicalizationFact,
) -> @lib.Instruction? {
ignore(self.lhs_fact)
ignore(self.rhs_fact)
self.opcode_fact.replacement()
}
///|
fn OiI64SignedConstUnsignedCanonicalizationFact::replacement(
self : OiI64SignedConstUnsignedCanonicalizationFact,
) -> @lib.Instruction? {
ignore(self.lhs_fact)
ignore(self.rhs_fact)
self.opcode_fact.replacement()
}
///|
fn OiSignedConstUnsignedCanonicalizationFact::replacement(
self : OiSignedConstUnsignedCanonicalizationFact,
) -> @lib.Instruction? {
match self {
I32SignedConstUnsignedCanonicalization(fact) => fact.replacement()
I64SignedConstUnsignedCanonicalization(fact) => fact.replacement()
}
}
///|
fn optimize_instructions_try_canonicalize_signed_const_unsigned(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
let fact = match
OiSignedConstUnsignedCanonicalizationFact::from_binary(
func, dead, scratch, instr, lhs_id, rhs_id,
) {
Some(fact) => fact
None => return false
}
let unsigned_instr = match fact.replacement() {
Some(instr) => instr
None => return false
}
@ir.hot_node_exact_instr_set(func, node_id, unsigned_instr)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_prefer_negative_leb_i32(raw : Int32) -> Bool {
match raw {
0x40 | 0x2000 | 0x100000 | 0x8000000 => true
_ => false
}
}
///|
fn optimize_instructions_prefer_negative_leb_i64(raw : Int64) -> Bool {
match raw {
0x40L
| 0x2000L
| 0x100000L
| 0x8000000L
| 0x400000000L
| 0x20000000000L
| 0x1000000000000L
| 0x80000000000000L
| 0x4000000000000000L => true
_ => false
}
}
///|
fn optimize_instructions_i32_power_of_two_shift(raw : Int32) -> Int32? {
// The sign bit is the largest unsigned power of two, despite its signed
// representation. Other negative bit patterns remain excluded.
if raw == -2147483648 {
return Some(31)
}
if raw <= 0 || (raw & (raw - 1)) != 0 {
return None
}
let mut shift = 0
let mut value = raw
while value > 1 {
value = value >> 1
shift = shift + 1
}
Some(shift)
}
///|
fn optimize_instructions_i64_power_of_two_shift(raw : Int64) -> Int64? {
if raw == -9223372036854775808L {
return Some(63L)
}
if raw <= 0L || (raw & (raw - 1L)) != 0L {
return None
}
let mut shift = 0L
let mut value = raw
while value > 1L {
value = value >> 1
shift = shift + 1L
}
Some(shift)
}
///|
priv struct OiI32LowOnesMaskFact {
low_ones : Bool
}
///|
fn OiI32LowOnesMaskFact::from_raw(raw : Int32) -> OiI32LowOnesMaskFact {
{ low_ones: raw >= 0 && (raw & (raw + 1)) == 0, }
}
///|
fn OiI32LowOnesMaskFact::proves_low_ones(self : OiI32LowOnesMaskFact) -> Bool {
self.low_ones
}
///|
priv enum OiCommutativeInstructionKind {
NonCommutativeInstruction
CommutativeI32Add
CommutativeI64Add
CommutativeI32Mul
CommutativeI64Mul
CommutativeI32And
CommutativeI64And
CommutativeI32Or
CommutativeI64Or
CommutativeI32Xor
CommutativeI64Xor
CommutativeI32Eq
CommutativeI64Eq
CommutativeI32Ne
CommutativeI64Ne
}
///|
priv struct OiCommutativeInstructionFact {
kind : OiCommutativeInstructionKind
}
///|
fn OiCommutativeInstructionFact::from_instr(
instr : @lib.Instruction,
) -> OiCommutativeInstructionFact {
// Binaryen's commutative canonicalization is driven by the operator fact,
// then gated separately by operand facts/effects. Keep the represented OI-G
// integer equality/arithmetic subset here rather than at each canonicalizer
// call site.
let kind = match instr {
@lib.Instruction::I32Add => CommutativeI32Add
@lib.Instruction::I64Add => CommutativeI64Add
@lib.Instruction::I32Mul => CommutativeI32Mul
@lib.Instruction::I64Mul => CommutativeI64Mul
@lib.Instruction::I32And => CommutativeI32And
@lib.Instruction::I64And => CommutativeI64And
@lib.Instruction::I32Or => CommutativeI32Or
@lib.Instruction::I64Or => CommutativeI64Or
@lib.Instruction::I32Xor => CommutativeI32Xor
@lib.Instruction::I64Xor => CommutativeI64Xor
@lib.Instruction::I32Eq => CommutativeI32Eq
@lib.Instruction::I64Eq => CommutativeI64Eq
@lib.Instruction::I32Ne => CommutativeI32Ne
@lib.Instruction::I64Ne => CommutativeI64Ne
_ => NonCommutativeInstruction
}
{ kind, }
}
///|
fn OiCommutativeInstructionFact::proves_commutative(
self : OiCommutativeInstructionFact,
) -> Bool {
match self.kind {
NonCommutativeInstruction => false
CommutativeI32Add
| CommutativeI64Add
| CommutativeI32Mul
| CommutativeI64Mul
| CommutativeI32And
| CommutativeI64And
| CommutativeI32Or
| CommutativeI64Or
| CommutativeI32Xor
| CommutativeI64Xor
| CommutativeI32Eq
| CommutativeI64Eq
| CommutativeI32Ne
| CommutativeI64Ne => true
}
}
///|
fn OiCommutativeInstructionFact::proves_instr_commutative(
instr : @lib.Instruction,
) -> Bool {
OiCommutativeInstructionFact::from_instr(instr).proves_commutative()
}
///|
priv struct OiLocalGetLeafFact {
local_id : Int32?
}
///|
fn OiLocalGetLeafFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiLocalGetLeafFact {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return { local_id: None, }
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
{
local_id: match node.op {
@ir.HotOp::LocalGet => Some(node.imm0)
_ => None
},
}
}
///|
priv enum OiConstlikeLeafKind {
ConstlikeLeafNoProof
ConstlikeConstLeaf
ConstlikeRefNullLeaf
ConstlikeRefFuncLeaf
}
///|
priv struct OiConstlikeLeafFact {
kind : OiConstlikeLeafKind
}
///|
fn OiConstlikeLeafFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiConstlikeLeafFact {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return { kind: ConstlikeLeafNoProof, }
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
let kind = match node.op {
@ir.HotOp::Const => ConstlikeConstLeaf
@ir.HotOp::RefNull => ConstlikeRefNullLeaf
@ir.HotOp::RefFunc => ConstlikeRefFuncLeaf
_ => ConstlikeLeafNoProof
}
{ kind, }
}
///|
fn OiConstlikeLeafFact::proves_constlike(self : OiConstlikeLeafFact) -> Bool {
match self.kind {
ConstlikeLeafNoProof => false
ConstlikeConstLeaf => true
ConstlikeRefNullLeaf => true
ConstlikeRefFuncLeaf => true
}
}
///|
priv enum OiSharedCrossSafeLeafKind {
SharedCrossSafeLeafNoProof
SharedCrossSafeLocalGetLeaf
SharedCrossSafeConstLeaf
SharedCrossSafeRefNullLeaf
SharedCrossSafeRefFuncLeaf
}
///|
priv struct OiSharedCrossSafeLeafFact {
kind : OiSharedCrossSafeLeafKind
}
///|
fn OiSharedCrossSafeLeafFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiSharedCrossSafeLeafFact {
// Binaryen v130 local reordering and select cleanup only allow multiply used
// children to cross a local.get when the child is a direct immutable leaf.
// Keep Starshine's represented leaf set explicit so local-get crossing,
// relational canonicalization, and identical-select cleanup share facts
// instead of each owning an opcode whitelist.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return { kind: SharedCrossSafeLeafNoProof, }
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
let kind = match node.op {
@ir.HotOp::LocalGet => SharedCrossSafeLocalGetLeaf
@ir.HotOp::Const => SharedCrossSafeConstLeaf
@ir.HotOp::RefNull => SharedCrossSafeRefNullLeaf
@ir.HotOp::RefFunc => SharedCrossSafeRefFuncLeaf
_ => SharedCrossSafeLeafNoProof
}
{ kind, }
}
///|
fn OiSharedCrossSafeLeafFact::proves_safe(
self : OiSharedCrossSafeLeafFact,
) -> Bool {
match self.kind {
SharedCrossSafeLeafNoProof => false
SharedCrossSafeLocalGetLeaf => true
SharedCrossSafeConstLeaf => true
SharedCrossSafeRefNullLeaf => true
SharedCrossSafeRefFuncLeaf => true
}
}
///|
priv enum OiLocalGetCrossingChildTraversalKind {
OiLocalGetCrossingChildTraversalSkipped
OiLocalGetCrossingAllChildrenClear
OiLocalGetCrossingBlockedChild
}
///|
priv struct OiLocalGetCrossingChildTraversalFact {
kind : OiLocalGetCrossingChildTraversalKind
}
///|
fn OiLocalGetCrossingChildTraversalFact::skipped() -> OiLocalGetCrossingChildTraversalFact {
{ kind: OiLocalGetCrossingChildTraversalSkipped, }
}
///|
fn OiLocalGetCrossingChildTraversalFact::from_children(
func : @ir.HotFunc,
dead : @bitset.BitSet,
use_def : @ir.HotUseDef,
node : @ir.HotNode,
local_id : Int32,
loop_input_nodes : @bitset.BitSet,
seen_stamps : Array[Int32],
seen_stamp : Int32,
) -> OiLocalGetCrossingChildTraversalFact {
for slot = 0; slot < node.child_count; slot = slot + 1 {
let child_is_local_tee_payload = match node.op {
@ir.HotOp::LocalTee => slot == 0
_ => false
}
if !OiLocalGetCrossingFact::from_node(
func,
dead,
use_def,
func.children[node.child_base + slot],
local_id,
child_is_local_tee_payload,
loop_input_nodes,
seen_stamps,
seen_stamp,
).proves_crossing_allowed() {
return { kind: OiLocalGetCrossingBlockedChild, }
}
}
{ kind: OiLocalGetCrossingAllChildrenClear, }
}
///|
fn OiLocalGetCrossingChildTraversalFact::proves_children_clear(
self : OiLocalGetCrossingChildTraversalFact,
) -> Bool {
match self.kind {
OiLocalGetCrossingChildTraversalSkipped => true
OiLocalGetCrossingAllChildrenClear => true
OiLocalGetCrossingBlockedChild => false
}
}
///|
priv struct OiLocalGetCrossingFact {
live : Bool
repeated_visit : Bool
loop_input_boundary : Bool
unsupported_control : Bool
shared_use_hazard : Bool
same_local_write : Bool
child_traversal : OiLocalGetCrossingChildTraversalFact
}
///|
fn OiLocalGetCrossingFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
use_def : @ir.HotUseDef,
node_id : Int32,
local_id : Int32,
under_local_tee_payload : Bool,
loop_input_nodes : @bitset.BitSet,
seen_stamps : Array[Int32],
seen_stamp : Int32,
) -> OiLocalGetCrossingFact {
// Binaryen OI's operand canonicalization may move a subtree across a
// local.get only when the intervening subtree cannot write that local or hide
// unsupported control/EH. Preserve the previous Starshine subset as a
// fact-driven traversal: live/revisit boundaries, loop-input blockers,
// accepted control roots, shared-use leaf safety, same-local writes, and child
// traversal all become named evidence facts rather than a standalone scanner.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return {
live: false,
repeated_visit: false,
loop_input_boundary: false,
unsupported_control: false,
shared_use_hazard: false,
same_local_write: false,
child_traversal: OiLocalGetCrossingChildTraversalFact::skipped(),
}
}
if node_id < seen_stamps.length() && seen_stamps[node_id] == seen_stamp {
return {
live: true,
repeated_visit: true,
loop_input_boundary: false,
unsupported_control: false,
shared_use_hazard: false,
same_local_write: false,
child_traversal: OiLocalGetCrossingChildTraversalFact::skipped(),
}
}
if node_id < seen_stamps.length() {
seen_stamps[node_id] = seen_stamp
}
if node_id < loop_input_nodes.length() && loop_input_nodes[node_id] {
return {
live: true,
repeated_visit: false,
loop_input_boundary: true,
unsupported_control: false,
shared_use_hazard: false,
same_local_write: false,
child_traversal: OiLocalGetCrossingChildTraversalFact::skipped(),
}
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
let unsupported_control = match node.op {
@ir.HotOp::If => false
@ir.HotOp::Block => {
let label_id = @ir.hot_control_node_label(func, node_id)
match @ir.hot_label_kind(func, label_id) {
@ir.HotLabelKind::RegionLabel => false
_ => true
}
}
@ir.HotOp::Loop
| @ir.HotOp::Try
| @ir.HotOp::TryTable
| @ir.HotOp::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::Return
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate
| @ir.HotOp::Catch
| @ir.HotOp::CatchAll => true
_ => false
}
let shared_use_hazard = @ir.node_use_count(use_def, node_id) > 1 &&
(
!OiSharedCrossSafeLeafFact::from_node(func, dead, node_id).proves_safe() ||
under_local_tee_payload
)
let same_local_write = match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee if node.imm0 == local_id => true
_ => false
}
let child_traversal = if unsupported_control ||
shared_use_hazard ||
same_local_write {
OiLocalGetCrossingChildTraversalFact::skipped()
} else {
OiLocalGetCrossingChildTraversalFact::from_children(
func, dead, use_def, node, local_id, loop_input_nodes, seen_stamps, seen_stamp,
)
}
{
live: true,
repeated_visit: false,
loop_input_boundary: false,
unsupported_control,
shared_use_hazard,
same_local_write,
child_traversal,
}
}
///|
fn OiLocalGetCrossingFact::proves_crossing_allowed(
self : OiLocalGetCrossingFact,
) -> Bool {
ignore(self.live)
self.repeated_visit ||
(
!self.loop_input_boundary &&
!self.unsupported_control &&
!self.shared_use_hazard &&
!self.same_local_write &&
self.child_traversal.proves_children_clear()
)
}
///|
priv enum OiCommutativeOperandClassKind {
OiCommutativeOperandUnsupported
OiCommutativeOperandCallLike
OiCommutativeOperandLocalGet
OiCommutativeOperandLocalTee
OiCommutativeOperandGlobalGet
OiCommutativeOperandLoad
OiCommutativeOperandLeafLiteral
OiCommutativeOperandUnaryLike
OiCommutativeOperandBinary
OiCommutativeOperandCompare
}
///|
priv struct OiCommutativeOperandClassFact {
kind : OiCommutativeOperandClassKind
local_index : Int32?
binaryen_compare_order : Int32?
}
///|
fn optimize_instructions_binaryen_v132_scalar_compare_order(
instr : @lib.Instruction,
) -> Int32? {
// Binaryen's BinaryOp enum supplies the final canonicalization tie-break
// when two commutative operands have the same expression node kind. Keep
// the supported scalar comparison subset explicit because Starshine's
// instruction declaration order differs for gt/le.
match instr {
@lib.I32Eq => Some(0)
@lib.I32Ne => Some(1)
@lib.I32LtS => Some(2)
@lib.I32LtU => Some(3)
@lib.I32LeS => Some(4)
@lib.I32LeU => Some(5)
@lib.I32GtS => Some(6)
@lib.I32GtU => Some(7)
@lib.I32GeS => Some(8)
@lib.I32GeU => Some(9)
@lib.I64Eq => Some(10)
@lib.I64Ne => Some(11)
@lib.I64LtS => Some(12)
@lib.I64LtU => Some(13)
@lib.I64LeS => Some(14)
@lib.I64LeU => Some(15)
@lib.I64GtS => Some(16)
@lib.I64GtU => Some(17)
@lib.I64GeS => Some(18)
@lib.I64GeU => Some(19)
@lib.F32Eq => Some(20)
@lib.F32Ne => Some(21)
@lib.F32Lt => Some(22)
@lib.F32Le => Some(23)
@lib.F32Gt => Some(24)
@lib.F32Ge => Some(25)
@lib.F64Eq => Some(26)
@lib.F64Ne => Some(27)
@lib.F64Lt => Some(28)
@lib.F64Le => Some(29)
@lib.F64Gt => Some(30)
@lib.F64Ge => Some(31)
_ => None
}
}
///|
fn OiCommutativeOperandClassFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiCommutativeOperandClassFact {
// Binaryen OI canonicalizes commutative operands into a stable source order
// before applying rewrites. Keep Starshine's represented HOT subset as an
// explicit operand-class fact instead of a nullable rank table with a call
// sentinel. The later swap fact consumes only these named classes plus the
// local.get and scalar-comparison tie-break payloads.
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
match node.op {
@ir.HotOp::Call | @ir.HotOp::CallIndirect | @ir.HotOp::CallRef =>
{
kind: OiCommutativeOperandCallLike,
local_index: None,
binaryen_compare_order: None,
}
@ir.HotOp::LocalGet =>
{
kind: OiCommutativeOperandLocalGet,
local_index: Some(node.imm0),
binaryen_compare_order: None,
}
@ir.HotOp::LocalTee =>
{
kind: OiCommutativeOperandLocalTee,
local_index: None,
binaryen_compare_order: None,
}
@ir.HotOp::GlobalGet =>
{
kind: OiCommutativeOperandGlobalGet,
local_index: None,
binaryen_compare_order: None,
}
@ir.HotOp::Load =>
{
kind: OiCommutativeOperandLoad,
local_index: None,
binaryen_compare_order: None,
}
@ir.HotOp::Const | @ir.HotOp::RefNull | @ir.HotOp::RefFunc =>
{
kind: OiCommutativeOperandLeafLiteral,
local_index: None,
binaryen_compare_order: None,
}
@ir.HotOp::Unary | @ir.HotOp::Convert =>
{
kind: OiCommutativeOperandUnaryLike,
local_index: None,
binaryen_compare_order: None,
}
@ir.HotOp::Binary =>
{
kind: OiCommutativeOperandBinary,
local_index: None,
binaryen_compare_order: None,
}
@ir.HotOp::Compare =>
{
kind: OiCommutativeOperandCompare,
local_index: None,
binaryen_compare_order: match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
optimize_instructions_binaryen_v132_scalar_compare_order(instr)
None => None
},
}
_ =>
{
kind: OiCommutativeOperandUnsupported,
local_index: None,
binaryen_compare_order: None,
}
}
}
///|
fn OiCommutativeOperandClassFact::order_index(
self : OiCommutativeOperandClassFact,
) -> Int32? {
match self.kind {
OiCommutativeOperandUnsupported => None
OiCommutativeOperandCallLike => Some(0)
OiCommutativeOperandLocalGet => Some(1)
OiCommutativeOperandLocalTee => Some(2)
OiCommutativeOperandGlobalGet => Some(3)
OiCommutativeOperandLoad => Some(4)
OiCommutativeOperandUnaryLike => Some(5)
OiCommutativeOperandBinary => Some(6)
OiCommutativeOperandCompare => Some(7)
OiCommutativeOperandLeafLiteral => Some(8)
}
}
///|
priv enum OiCommutativeOperandSwapKind {
OiCommutativeOperandSwapUnsupported
OiCommutativeOperandAlreadyCanonical
OiCommutativeOperandSwapByClassOrder
OiCommutativeOperandSwapByLocalGetIndex
OiCommutativeOperandSwapByCompareOrder
}
///|
priv struct OiCommutativeOperandSwapFact {
kind : OiCommutativeOperandSwapKind
}
///|
fn OiCommutativeOperandSwapFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiCommutativeOperandSwapFact {
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return { kind: OiCommutativeOperandSwapUnsupported, }
}
let lhs_fact = OiCommutativeOperandClassFact::from_node(func, lhs_id)
let rhs_fact = OiCommutativeOperandClassFact::from_node(func, rhs_id)
match (lhs_fact.order_index(), rhs_fact.order_index()) {
(Some(lhs_order), Some(rhs_order)) =>
if lhs_order > rhs_order {
{ kind: OiCommutativeOperandSwapByClassOrder, }
} else if lhs_order < rhs_order {
{ kind: OiCommutativeOperandAlreadyCanonical, }
} else {
match (lhs_fact.local_index, rhs_fact.local_index) {
(Some(lhs_local), Some(rhs_local)) if lhs_local > rhs_local =>
{ kind: OiCommutativeOperandSwapByLocalGetIndex, }
_ =>
match
(lhs_fact.binaryen_compare_order, rhs_fact.binaryen_compare_order) {
(Some(lhs_compare), Some(rhs_compare)) if lhs_compare >
rhs_compare => { kind: OiCommutativeOperandSwapByCompareOrder, }
_ => { kind: OiCommutativeOperandAlreadyCanonical, }
}
}
}
_ => { kind: OiCommutativeOperandSwapUnsupported, }
}
}
///|
fn OiCommutativeOperandSwapFact::proves_should_swap(
self : OiCommutativeOperandSwapFact,
) -> Bool {
match self.kind {
OiCommutativeOperandSwapByClassOrder => true
OiCommutativeOperandSwapByLocalGetIndex => true
OiCommutativeOperandSwapByCompareOrder => true
OiCommutativeOperandSwapUnsupported
| OiCommutativeOperandAlreadyCanonical => false
}
}
///|
fn optimize_instructions_root_use_region_ref(
func : @ir.HotFunc,
node_id : Int32,
holder_id : Int32,
slot : Int32,
) -> @ir.HotRegionRef? {
let holder = @ir.hot_node_get(func, holder_id)
for info in @ir.hot_control_region_slot_info(holder.op) {
let region_ref = match info.role {
@ir.HotControlRegionRole::BodyRegion =>
Some(@ir.hot_region_body_ref(func, holder_id))
@ir.HotControlRegionRole::ThenRegion =>
Some(@ir.hot_region_then_ref(func, holder_id))
@ir.HotControlRegionRole::ElseRegion =>
@ir.hot_region_else_ref(func, holder_id)
@ir.HotControlRegionRole::CatchRegion =>
@ir.hot_region_catch_ref(func, holder_id)
@ir.HotControlRegionRole::CatchListRegion =>
Some(@ir.hot_region_catch_list_ref(func, holder_id))
}
match region_ref {
Some(region_ref) =>
if slot < @ir.hot_region_root_count(func, region_ref) &&
@ir.hot_region_get(func, region_ref, slot) == node_id {
return Some(region_ref)
}
None => ()
}
}
None
}
///|
fn optimize_instructions_subtree_writes_local_after_read(
func : @ir.HotFunc,
node_id : Int32,
local_a : Int32,
read_a : Int32,
local_b : Int32,
read_b : Int32,
seen : @bitset.BitSet,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
if node_id >= seen.length() {
seen.grow(node_id + 1)
}
if seen[node_id] {
return false
}
seen[node_id] = true
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
if (node.imm0 == local_a && node_id > read_a) ||
(node.imm0 == local_b && node_id > read_b) {
return true
}
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if optimize_instructions_subtree_writes_local_after_read(
func,
@ir.hot_child_get(func, node_id, slot),
local_a,
read_a,
local_b,
read_b,
seen,
) {
return true
}
}
for info in @ir.hot_control_region_slot_info(node.op) {
let region_ref = match info.role {
@ir.HotControlRegionRole::BodyRegion =>
Some(@ir.hot_region_body_ref(func, node_id))
@ir.HotControlRegionRole::ThenRegion =>
Some(@ir.hot_region_then_ref(func, node_id))
@ir.HotControlRegionRole::ElseRegion =>
@ir.hot_region_else_ref(func, node_id)
@ir.HotControlRegionRole::CatchRegion =>
@ir.hot_region_catch_ref(func, node_id)
@ir.HotControlRegionRole::CatchListRegion =>
Some(@ir.hot_region_catch_list_ref(func, node_id))
}
match region_ref {
Some(region_ref) =>
for idx = 0
idx < @ir.hot_region_root_count(func, region_ref)
idx = idx + 1 {
if optimize_instructions_subtree_writes_local_after_read(
func,
@ir.hot_region_get(func, region_ref, idx),
local_a,
read_a,
local_b,
read_b,
seen,
) {
return true
}
}
None => ()
}
}
false
}
///|
fn optimize_instructions_local_swap_crosses_preceding_write(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
let lhs = @ir.hot_node_get(func, lhs_id)
let rhs = @ir.hot_node_get(func, rhs_id)
match (lhs.op, rhs.op) {
(@ir.HotOp::LocalGet, @ir.HotOp::LocalGet) => ()
_ => return false
}
let use_def = pass_require_use_def(ctx, func)
let mut current = node_id
for hops = 0; hops < 64; hops = hops + 1 {
ignore(hops)
if @ir.node_use_count(use_def, current) != 1 {
return false
}
let sites = @ir.node_use_sites_borrowed(use_def, current)
if sites.length() != 1 {
return false
}
let site = sites[0]
match site.kind {
@ir.HotUseSlotKind::ChildUseSlot => {
if site.user < 0 {
return false
}
current = site.user
}
@ir.HotUseSlotKind::RootUseSlot => {
let region_ref = if site.user == -1 {
@ir.HotRegionRef::root()
} else {
match
optimize_instructions_root_use_region_ref(
func,
current,
site.user,
site.slot,
) {
Some(region_ref) => region_ref
None => return false
}
}
let seen = @bitset.BitSet::new(@ir.hot_node_count(func))
for idx = 0; idx < site.slot; idx = idx + 1 {
if optimize_instructions_subtree_writes_local_after_read(
func,
@ir.hot_region_get(func, region_ref, idx),
lhs.imm0,
lhs_id,
rhs.imm0,
rhs_id,
seen,
) {
return true
}
}
return false
}
}
}
false
}
///|
fn optimize_instructions_try_canonicalize_commutative(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
scratch : OptimizeInstructionsScratch,
) -> Bool {
// The old O4z guard disabled this after address-like operand reordering bugs.
// Re-enable only when both the local kind-rank order wants a swap and the
// shared Binaryen-style effect/use-def proof says the operand subtrees can
// exchange positions without changing observable behavior.
ignore(scratch)
if !OiCommutativeOperandSwapFact::from_operands(func, dead, lhs_id, rhs_id).proves_should_swap() {
return false
}
if optimize_instructions_local_swap_crosses_preceding_write(
ctx, func, node_id, lhs_id, rhs_id,
) ||
!OiLeadingOperandSwapFact::from_operands(ctx, func, lhs_id, rhs_id).proves_swappable() {
return false
}
optimize_instructions_rewrite_exact_with_children(ctx, func, node_id, instr, [
rhs_id, lhs_id,
])
true
}
///|
priv enum OiTriviallyPureOperandKind {
TriviallyPureOperandNoProof
TriviallyPureLocalGetLeaf
TriviallyPureGlobalGetLeaf
TriviallyPureConstLeaf
TriviallyPureRefNullLeaf
TriviallyPureRefFuncLeaf
}
///|
priv struct OiTriviallyPureOperandFact {
kind : OiTriviallyPureOperandKind
}
///|
fn OiTriviallyPureOperandFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiTriviallyPureOperandFact {
// Binaryen OI frequently asks whether an operand can be erased without
// preserving effects. Keep the represented direct leaf set as a selected fact
// kind instead of parallel booleans or a helper-local opcode whitelist so
// descriptor, array, select, compare, and memory facts share the same
// source-backed predicate.
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
let kind = match node.op {
@ir.HotOp::LocalGet => TriviallyPureLocalGetLeaf
@ir.HotOp::GlobalGet => TriviallyPureGlobalGetLeaf
@ir.HotOp::Const => TriviallyPureConstLeaf
@ir.HotOp::RefNull => TriviallyPureRefNullLeaf
@ir.HotOp::RefFunc => TriviallyPureRefFuncLeaf
_ => TriviallyPureOperandNoProof
}
{ kind, }
}
///|
fn OiTriviallyPureOperandFact::proves_pure(
self : OiTriviallyPureOperandFact,
) -> Bool {
match self.kind {
TriviallyPureOperandNoProof => false
TriviallyPureLocalGetLeaf
| TriviallyPureGlobalGetLeaf
| TriviallyPureConstLeaf
| TriviallyPureRefNullLeaf
| TriviallyPureRefFuncLeaf => true
}
}
///|
fn OiTriviallyPureOperandFact::proves_node_pure(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
OiTriviallyPureOperandFact::from_node(func, node_id).proves_pure()
}
///|
fn OiTriviallyPureOperandFact::proves_live_node_pure(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
OiLiveNodeFact::proves_node_live(func, dead, node_id) &&
OiTriviallyPureOperandFact::proves_node_pure(func, node_id)
}
///|
priv enum OiSideEffectFreeInstructionKind {
SideEffectFreeInstructionNoProof
SideEffectFreeV128ConstLeafInstruction
SideEffectFreeSimdSplatPayloadInstruction
}
///|
priv struct OiSideEffectFreeInstructionFact {
kind : OiSideEffectFreeInstructionKind
}
///|
fn OiSideEffectFreeInstructionFact::from_instruction(
instr : @lib.Instruction,
) -> OiSideEffectFreeInstructionFact {
match instr {
@lib.Instruction::V128Const(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) =>
{ kind: SideEffectFreeV128ConstLeafInstruction, }
_ =>
if instr == @lib.Instruction::i8x16_splat() ||
instr == @lib.Instruction::i16x8_splat() ||
instr == @lib.Instruction::i32x4_splat() ||
instr == @lib.Instruction::i64x2_splat() ||
instr == @lib.Instruction::f32x4_splat() ||
instr == @lib.Instruction::f64x2_splat() {
{ kind: SideEffectFreeSimdSplatPayloadInstruction, }
} else {
{ kind: SideEffectFreeInstructionNoProof, }
}
}
}
///|
priv enum OiSideEffectFreeExpressionKind {
SideEffectFreeExpressionNoProof
SideEffectFreeTrivialLeaf
SideEffectFreeExactRefNullLeaf
SideEffectFreeExactV128ConstLeaf
SideEffectFreeRefI31Payload
SideEffectFreeSimdSplatPayload
SideEffectFreeBranchlessScalarShell
SideEffectFreeDropPayload
SideEffectFreeSingleValueBlock
SideEffectFreeBlockRegion
}
///|
priv struct OiSideEffectFreeExpressionFact {
kind : OiSideEffectFreeExpressionKind
trivial_leaf : OiTriviallyPureOperandFact
children_effect_free : Bool
}
///|
fn OiSideEffectFreeExpressionFact::empty() -> OiSideEffectFreeExpressionFact {
{
kind: SideEffectFreeExpressionNoProof,
trivial_leaf: { kind: TriviallyPureOperandNoProof, },
children_effect_free: false,
}
}
///|
fn OiSideEffectFreeExpressionFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiSideEffectFreeExpressionFact {
// Represented subset of Binaryen v130 `EffectAnalyzer` /
// `ExpressionAnalyzer` purity queries used by OI when it can erase an
// operand, collapse identical select arms, move a null check, or replace an
// expression with a constant while preserving effects. The fact preserves the
// previous Starshine-supported set exactly: direct pure leaves, exact
// `ref.null`, byte-constant SIMD leaves, `ref.i31` over side-effect-free
// payloads, direct SIMD splats over side-effect-free payloads, branchless
// scalar shells whose children are all side-effect-free, pure dropped payloads,
// the single-value block fallthrough shape Binaryen's maxBits path can erase
// after folding, and pure block regions with no effectful roots.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return OiSideEffectFreeExpressionFact::empty()
}
let node = if node_id < func.nodes.length() {
func.nodes[node_id]
} else {
@ir.hot_node_get(func, node_id)
}
let trivial_leaf = OiTriviallyPureOperandFact::from_node(func, node_id)
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.I32DivS | @lib.I32DivU | @lib.I32RemS | @lib.I32RemU) => {
if node.child_count != 2 {
return OiSideEffectFreeExpressionFact::empty()
}
let divisor = optimize_instructions_i32_const(
func,
dead,
@ir.hot_child_get(func, node_id, 1),
)
let signed_div = @ir.hot_node_exact_instr(func, node_id)
is Some(@lib.I32DivS)
match divisor {
Some(@lib.I32(value)) if value != 0 && (!signed_div || value != -1) =>
()
_ => return OiSideEffectFreeExpressionFact::empty()
}
}
Some(@lib.I64DivS | @lib.I64DivU | @lib.I64RemS | @lib.I64RemU) => {
if node.child_count != 2 {
return OiSideEffectFreeExpressionFact::empty()
}
let divisor = optimize_instructions_i64_const(
func,
dead,
@ir.hot_child_get(func, node_id, 1),
)
let signed_div = @ir.hot_node_exact_instr(func, node_id)
is Some(@lib.I64DivS)
match divisor {
Some(@lib.I64(value)) if value != 0L && (!signed_div || value != -1L) =>
()
_ => return OiSideEffectFreeExpressionFact::empty()
}
}
Some(
@lib.I32TruncF32S
| @lib.I32TruncF32U
| @lib.I32TruncF64S
| @lib.I32TruncF64U
| @lib.I64TruncF32S
| @lib.I64TruncF32U
| @lib.I64TruncF64S
| @lib.I64TruncF64U
) => return OiSideEffectFreeExpressionFact::empty()
Some(@lib.Instruction::RefNull(_)) =>
return {
kind: SideEffectFreeExactRefNullLeaf,
trivial_leaf,
children_effect_free: true,
}
Some(@lib.Instruction::RefI31) if node.child_count == 1 => {
let child_fact = OiSideEffectFreeExpressionFact::from_node(
func,
dead,
func.children[node.child_base],
)
return {
kind: SideEffectFreeRefI31Payload,
trivial_leaf,
children_effect_free: child_fact.proves_free(),
}
}
Some(instr) =>
match OiSideEffectFreeInstructionFact::from_instruction(instr).kind {
SideEffectFreeV128ConstLeafInstruction =>
return {
kind: SideEffectFreeExactV128ConstLeaf,
trivial_leaf,
children_effect_free: true,
}
SideEffectFreeSimdSplatPayloadInstruction if node.child_count == 1 => {
let child_fact = OiSideEffectFreeExpressionFact::from_node(
func,
dead,
func.children[node.child_base],
)
return {
kind: SideEffectFreeSimdSplatPayload,
trivial_leaf,
children_effect_free: child_fact.proves_free(),
}
}
_ => ()
}
_ => ()
}
if trivial_leaf.proves_pure() {
return {
kind: SideEffectFreeTrivialLeaf,
trivial_leaf,
children_effect_free: true,
}
}
match node.op {
@ir.HotOp::Drop if node.child_count == 1 => {
let child_fact = OiSideEffectFreeExpressionFact::from_node(
func,
dead,
func.children[node.child_base],
)
{
kind: SideEffectFreeDropPayload,
trivial_leaf,
children_effect_free: child_fact.proves_free(),
}
}
@ir.HotOp::Block if node.child_count == 1 => {
let child_fact = OiSideEffectFreeExpressionFact::from_node(
func,
dead,
func.children[node.child_base],
)
{
kind: SideEffectFreeSingleValueBlock,
trivial_leaf,
children_effect_free: child_fact.proves_free(),
}
}
@ir.HotOp::Block => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
let mut roots_effect_free = root_count > 0
for slot = 0; slot < root_count; slot = slot + 1 {
let root_id = @ir.hot_region_get(func, body_ref, slot)
let root_fact = OiSideEffectFreeExpressionFact::from_node(
func, dead, root_id,
)
if !root_fact.proves_free() {
roots_effect_free = false
}
}
{
kind: SideEffectFreeBlockRegion,
trivial_leaf,
children_effect_free: roots_effect_free,
}
}
@ir.HotOp::Unary
| @ir.HotOp::Binary
| @ir.HotOp::Compare
| @ir.HotOp::Convert
| @ir.HotOp::Select => {
// Erasure requires the scalar operation itself to be nontrapping, as
// well as all eagerly evaluated operands. Division and truncation can
// trap even when their operands are pure local reads or constants.
let mut children_effect_free = !@ir.hot_may_trap(func, node_id)
for slot = 0; slot < node.child_count; slot = slot + 1 {
let child_fact = OiSideEffectFreeExpressionFact::from_node(
func,
dead,
func.children[node.child_base + slot],
)
if !child_fact.proves_free() {
children_effect_free = false
}
}
{
kind: SideEffectFreeBranchlessScalarShell,
trivial_leaf,
children_effect_free,
}
}
_ =>
{
kind: SideEffectFreeExpressionNoProof,
trivial_leaf,
children_effect_free: false,
}
}
}
///|
fn OiSideEffectFreeExpressionFact::proves_free(
self : OiSideEffectFreeExpressionFact,
) -> Bool {
match self.kind {
SideEffectFreeExpressionNoProof => false
SideEffectFreeTrivialLeaf => self.trivial_leaf.proves_pure()
SideEffectFreeExactRefNullLeaf => true
SideEffectFreeExactV128ConstLeaf => true
SideEffectFreeRefI31Payload => self.children_effect_free
SideEffectFreeSimdSplatPayload => self.children_effect_free
SideEffectFreeBranchlessScalarShell => self.children_effect_free
SideEffectFreeDropPayload => self.children_effect_free
SideEffectFreeSingleValueBlock => self.children_effect_free
SideEffectFreeBlockRegion => self.children_effect_free
}
}
///|
fn OiSideEffectFreeExpressionFact::proves_node_free(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
OiSideEffectFreeExpressionFact::from_node(func, dead, node_id).proves_free()
}
///|
fn optimize_instructions_negate_boolean_expr(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Int32? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Eqz) if @ir.hot_child_count(func, node_id) == 1 =>
Some(@ir.hot_child_get(func, node_id, 0))
_ =>
match optimize_instructions_i32_const(func, dead, node_id) {
Some(@lib.I32(raw)) if raw == 0 =>
Some(@ir.hot_build_const_i32(func, @lib.I32(1)))
Some(@lib.I32(raw)) if raw == 1 =>
Some(@ir.hot_build_const_i32(func, @lib.I32(0)))
_ => None
}
}
}
///|
fn optimize_instructions_negate_boolean_expr_recursive(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Int32? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => {
let then_region = @ir.hot_region_then_ref(func, node_id)
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return None
}
if @ir.hot_region_root_count(func, then_region) != 1 ||
@ir.hot_region_root_count(func, else_region) != 1 {
return None
}
let neg_then = match
optimize_instructions_negate_boolean_expr_recursive(
func,
dead,
@ir.hot_region_get(func, then_region, 0),
) {
Some(node) => node
None => return None
}
let neg_else = match
optimize_instructions_negate_boolean_expr_recursive(
func,
dead,
@ir.hot_region_get(func, else_region, 0),
) {
Some(node) => node
None => return None
}
@ir.hot_region_replace_body(func, then_region, [neg_then])
@ir.hot_region_replace_body(func, else_region, [neg_else])
return Some(node_id)
}
_ => ()
}
optimize_instructions_negate_boolean_expr(func, dead, node_id)
}
///|
priv enum OiBooleanConstantLeafKind {
BooleanConstantLeafNoProof
BooleanConstantFalseLeaf
BooleanConstantTrueLeaf
}
///|
priv struct OiBooleanConstantLeafFact {
kind : OiBooleanConstantLeafKind
}
///|
fn OiBooleanConstantLeafFact::empty() -> OiBooleanConstantLeafFact {
{ kind: BooleanConstantLeafNoProof, }
}
///|
fn OiBooleanConstantLeafFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBooleanConstantLeafFact {
// Binaryen's OI `optimizeBoolean` recursive boolean context accepts exact
// i32 `0` / `1` leaves. Keep the literal proof separate from recursive `if`
// payloads so the represented tree fact is source-shaped instead of a flat
// scanner result.
let kind = match optimize_instructions_i32_const(func, dead, node_id) {
Some(@lib.I32(0)) => BooleanConstantFalseLeaf
Some(@lib.I32(1)) => BooleanConstantTrueLeaf
_ => BooleanConstantLeafNoProof
}
{ kind, }
}
///|
fn OiBooleanConstantLeafFact::proves_boolean_leaf(
self : OiBooleanConstantLeafFact,
) -> Bool {
match self.kind {
BooleanConstantLeafNoProof => false
BooleanConstantFalseLeaf => true
BooleanConstantTrueLeaf => true
}
}
///|
priv enum OiBooleanConstantIfPayloadKind {
BooleanConstantIfPayloadNoProof
BooleanConstantIfMissingElse
BooleanConstantIfMultiResultArms
BooleanConstantIfNonBooleanArm
BooleanConstantIfBooleanArms
}
///|
priv struct OiBooleanConstantIfPayloadFact {
kind : OiBooleanConstantIfPayloadKind
}
///|
fn OiBooleanConstantIfPayloadFact::empty() -> OiBooleanConstantIfPayloadFact {
{ kind: BooleanConstantIfPayloadNoProof, }
}
///|
fn OiBooleanConstantIfPayloadFact::proves_boolean_if_tree(
self : OiBooleanConstantIfPayloadFact,
) -> Bool {
match self.kind {
BooleanConstantIfPayloadNoProof => false
BooleanConstantIfMissingElse => false
BooleanConstantIfMultiResultArms => false
BooleanConstantIfNonBooleanArm => false
BooleanConstantIfBooleanArms => true
}
}
///|
priv struct OiBooleanConstantTreeFact {
live : Bool
leaf_fact : OiBooleanConstantLeafFact
if_payload : OiBooleanConstantIfPayloadFact
}
///|
fn OiBooleanConstantTreeFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBooleanConstantTreeFact {
// Binaryen's OI `optimizeBoolean` recursively treats i32-valued `if` arms as
// boolean contexts, and Starshine's represented nested-boolean cleanup only
// needs exact `0` / `1` leaves plus single-result `if` trees. Keep the leaf
// and `if` branch facts explicit instead of reintroducing a helper-local
// recursive scanner.
let leaf_fact = OiBooleanConstantLeafFact::from_node(func, dead, node_id)
if leaf_fact.proves_boolean_leaf() {
return {
live: true,
leaf_fact,
if_payload: OiBooleanConstantIfPayloadFact::empty(),
}
}
let empty_leaf = OiBooleanConstantLeafFact::empty()
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return {
live: false,
leaf_fact: empty_leaf,
if_payload: OiBooleanConstantIfPayloadFact::empty(),
}
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::If => ()
_ =>
return {
live: true,
leaf_fact: empty_leaf,
if_payload: OiBooleanConstantIfPayloadFact::empty(),
}
}
let then_region = @ir.hot_region_then_ref(func, node_id)
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None =>
return {
live: true,
leaf_fact: empty_leaf,
if_payload: { kind: BooleanConstantIfMissingElse, },
}
}
let single_result_arms = @ir.hot_region_root_count(func, then_region) == 1 &&
@ir.hot_region_root_count(func, else_region) == 1
if !single_result_arms {
return {
live: true,
leaf_fact: empty_leaf,
if_payload: { kind: BooleanConstantIfMultiResultArms, },
}
}
let then_fact = OiBooleanConstantTreeFact::from_node(
func,
dead,
@ir.hot_region_get(func, then_region, 0),
)
let else_fact = OiBooleanConstantTreeFact::from_node(
func,
dead,
@ir.hot_region_get(func, else_region, 0),
)
let kind = if then_fact.proves_boolean_tree() &&
else_fact.proves_boolean_tree() {
BooleanConstantIfBooleanArms
} else {
BooleanConstantIfNonBooleanArm
}
{ live: true, leaf_fact: empty_leaf, if_payload: { kind, }, }
}
///|
fn OiBooleanConstantTreeFact::proves_boolean_tree(
self : OiBooleanConstantTreeFact,
) -> Bool {
self.live &&
(
self.leaf_fact.proves_boolean_leaf() ||
self.if_payload.proves_boolean_if_tree()
)
}
///|
fn OiBooleanConstantTreeFact::proves_node_boolean_tree(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
OiBooleanConstantTreeFact::from_node(func, dead, node_id).proves_boolean_tree()
}
///|
priv enum OiBooleanTreeLeafKind {
BooleanTreeLeafNoProof
BooleanTreeEqzLeaf
BooleanTreeFalseLeaf
BooleanTreeTrueLeaf
}
///|
priv struct OiBooleanTreeLeafFact {
kind : OiBooleanTreeLeafKind
}
///|
fn OiBooleanTreeLeafFact::empty() -> OiBooleanTreeLeafFact {
{ kind: BooleanTreeLeafNoProof, }
}
///|
fn OiBooleanTreeLeafFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBooleanTreeLeafFact {
// Source-backed leaf facts for Binaryen's nested boolean-condition rewrites:
// direct `eqz` plus exact boolean constants. The unshared recursive `if`
// proof is recorded separately below.
let kind = match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Eqz) if @ir.hot_child_count(func, node_id) == 1 =>
BooleanTreeEqzLeaf
_ =>
match optimize_instructions_i32_const(func, dead, node_id) {
Some(@lib.I32(0)) => BooleanTreeFalseLeaf
Some(@lib.I32(1)) => BooleanTreeTrueLeaf
_ => BooleanTreeLeafNoProof
}
}
{ kind, }
}
///|
fn OiBooleanTreeLeafFact::proves_allowed_leaf(
self : OiBooleanTreeLeafFact,
) -> Bool {
match self.kind {
BooleanTreeLeafNoProof => false
BooleanTreeEqzLeaf => true
BooleanTreeFalseLeaf => true
BooleanTreeTrueLeaf => true
}
}
///|
priv enum OiBooleanTreeIfPayloadKind {
BooleanTreeIfPayloadNoProof
BooleanTreeIfShared
BooleanTreeIfMissingElse
BooleanTreeIfMultiResultArms
BooleanTreeIfNonBooleanArm
BooleanTreeIfAllowedArms
}
///|
priv struct OiBooleanTreeIfPayloadFact {
kind : OiBooleanTreeIfPayloadKind
}
///|
fn OiBooleanTreeIfPayloadFact::empty() -> OiBooleanTreeIfPayloadFact {
{ kind: BooleanTreeIfPayloadNoProof, }
}
///|
fn OiBooleanTreeIfPayloadFact::proves_allowed_if_tree(
self : OiBooleanTreeIfPayloadFact,
) -> Bool {
match self.kind {
BooleanTreeIfPayloadNoProof => false
BooleanTreeIfShared => false
BooleanTreeIfMissingElse => false
BooleanTreeIfMultiResultArms => false
BooleanTreeIfNonBooleanArm => false
BooleanTreeIfAllowedArms => true
}
}
///|
priv struct OiBooleanTreeUnsharedFact {
live : Bool
leaf_fact : OiBooleanTreeLeafFact
if_payload : OiBooleanTreeIfPayloadFact
}
///|
fn OiBooleanTreeUnsharedFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
use_def : @ir.HotUseDef,
node_id : Int32,
) -> OiBooleanTreeUnsharedFact {
// Source-backed subset for Binaryen's nested boolean-condition rewrites:
// direct `eqz`, exact boolean constants, and recursively unshared one-result
// `if` arms. This records leaves, the sharing gate, and branch payloads as
// data instead of letting callers embed the old tree scanner.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return {
live: false,
leaf_fact: OiBooleanTreeLeafFact::empty(),
if_payload: OiBooleanTreeIfPayloadFact::empty(),
}
}
let leaf_fact = OiBooleanTreeLeafFact::from_node(func, dead, node_id)
if leaf_fact.proves_allowed_leaf() {
return {
live: true,
leaf_fact,
if_payload: OiBooleanTreeIfPayloadFact::empty(),
}
}
let empty_leaf = OiBooleanTreeLeafFact::empty()
if @ir.node_use_count(use_def, node_id) > 1 {
return {
live: true,
leaf_fact: empty_leaf,
if_payload: { kind: BooleanTreeIfShared, },
}
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::If => ()
_ =>
return {
live: true,
leaf_fact: empty_leaf,
if_payload: OiBooleanTreeIfPayloadFact::empty(),
}
}
let then_region = @ir.hot_region_then_ref(func, node_id)
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None =>
return {
live: true,
leaf_fact: empty_leaf,
if_payload: { kind: BooleanTreeIfMissingElse, },
}
}
let single_result_arms = @ir.hot_region_root_count(func, then_region) == 1 &&
@ir.hot_region_root_count(func, else_region) == 1
if !single_result_arms {
return {
live: true,
leaf_fact: empty_leaf,
if_payload: { kind: BooleanTreeIfMultiResultArms, },
}
}
let then_fact = OiBooleanTreeUnsharedFact::from_node(
func,
dead,
use_def,
@ir.hot_region_get(func, then_region, 0),
)
let else_fact = OiBooleanTreeUnsharedFact::from_node(
func,
dead,
use_def,
@ir.hot_region_get(func, else_region, 0),
)
let kind = if then_fact.proves_allowed_tree() &&
else_fact.proves_allowed_tree() {
BooleanTreeIfAllowedArms
} else {
BooleanTreeIfNonBooleanArm
}
{ live: true, leaf_fact: empty_leaf, if_payload: { kind, }, }
}
///|
fn OiBooleanTreeUnsharedFact::proves_allowed_tree(
self : OiBooleanTreeUnsharedFact,
) -> Bool {
self.live &&
(
self.leaf_fact.proves_allowed_leaf() ||
self.if_payload.proves_allowed_if_tree()
)
}
///|
fn optimize_instructions_boolean_tree_if_nodes_are_unshared(
func : @ir.HotFunc,
dead : @bitset.BitSet,
use_def : @ir.HotUseDef,
node_id : Int32,
) -> Bool {
OiBooleanTreeUnsharedFact::from_node(func, dead, use_def, node_id).proves_allowed_tree()
}
///|
priv struct OiDirectBranchRootFact {
node_id : Int32
label_id : Int32
value_count : Int32
}
///|
fn OiDirectBranchRootFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDirectBranchRootFact? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Br =>
Some({ node_id, label_id: node.imm0, value_count: node.child_count, })
_ => None
}
}
///|
priv enum OiBranchPairRelationKind {
BranchPairMissingDirectBranch
BranchPairDifferentLabel
BranchPairDifferentArity
BranchPairDifferentOperands
BranchPairSameDirectBranch
}
///|
priv struct OiBranchShapeFact {
lhs_branch : OiDirectBranchRootFact?
rhs_branch : OiDirectBranchRootFact?
relation : OiBranchPairRelationKind
}
///|
fn OiBranchShapeFact::from_pair(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiBranchShapeFact {
// The represented duplicate-branch collapse mirrors Binaryen's ability to
// fold identical branch arms after condition cleanup, but HOT only rewrites
// direct `br` roots with byte-identical target label and value operands. Keep
// the two direct-branch roots and the selected pair relation as explicit
// facts so future `br_if`/fallthrough work can extend the relation instead of
// adding ad hoc branch scanners or parallel shell booleans.
let lhs_branch = OiDirectBranchRootFact::from_node(func, dead, lhs_id)
let rhs_branch = OiDirectBranchRootFact::from_node(func, dead, rhs_id)
let relation = match (lhs_branch, rhs_branch) {
(Some(lhs), Some(rhs)) =>
if lhs.label_id != rhs.label_id {
BranchPairDifferentLabel
} else if lhs.value_count != rhs.value_count {
BranchPairDifferentArity
} else {
let mut same_operands = true
for slot = 0; slot < lhs.value_count; slot = slot + 1 {
if @ir.hot_child_get(func, lhs.node_id, slot) !=
@ir.hot_child_get(func, rhs.node_id, slot) {
same_operands = false
}
}
if same_operands {
BranchPairSameDirectBranch
} else {
BranchPairDifferentOperands
}
}
_ => BranchPairMissingDirectBranch
}
{ lhs_branch, rhs_branch, relation, }
}
///|
fn OiBranchShapeFact::proves_same_direct_branch(
self : OiBranchShapeFact,
) -> Bool {
ignore(self.lhs_branch)
ignore(self.rhs_branch)
match self.relation {
BranchPairMissingDirectBranch => false
BranchPairDifferentLabel => false
BranchPairDifferentArity => false
BranchPairDifferentOperands => false
BranchPairSameDirectBranch => true
}
}
///|
fn optimize_instructions_same_branch_shape(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
OiBranchShapeFact::from_pair(func, dead, lhs_id, rhs_id).proves_same_direct_branch()
}
///|
fn optimize_instructions_try_collapse_duplicate_then_branch(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let then_region = @ir.hot_region_then_ref(func, node_id)
let then_root_count = @ir.hot_region_root_count(func, then_region)
if then_root_count != 1 && then_root_count != 2 {
return false
}
let inner_if_id = @ir.hot_region_get(func, then_region, 0)
if then_root_count == 2 {
let tail_id = @ir.hot_region_get(func, then_region, 1)
match @ir.hot_node_get(func, tail_id).op {
@ir.HotOp::Unreachable => ()
_ => return false
}
}
match @ir.hot_node_get(func, inner_if_id).op {
@ir.HotOp::If => ()
_ => return false
}
let inner_else = match @ir.hot_region_else_ref(func, inner_if_id) {
Some(region) => region
None => return false
}
let inner_then = @ir.hot_region_then_ref(func, inner_if_id)
if @ir.hot_region_root_count(func, inner_then) != 1 ||
@ir.hot_region_root_count(func, inner_else) != 1 {
return false
}
let inner_cond_id = @ir.hot_child_get(func, inner_if_id, 0)
if !OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, inner_cond_id,
) {
return false
}
let then_root = @ir.hot_region_get(func, inner_then, 0)
let else_root = @ir.hot_region_get(func, inner_else, 0)
if !optimize_instructions_same_branch_shape(func, dead, then_root, else_root) {
return false
}
let branch = @ir.hot_node_get(func, then_root)
let replacement = @ir.hot_build_br(
func,
branch.imm0,
values=@ir.hot_child_span(func, then_root),
)
@ir.hot_region_replace_body(func, then_region, [replacement])
pass_mark_mutated(ctx, func)
true
}
///|
priv struct OiDirectZeroSentinelFact {
node_id : Int32
}
///|
fn OiDirectZeroSentinelFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDirectZeroSentinelFact? {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Const =>
if @ir.hot_const_get(func, node.imm0) ==
@ir.HotConstPayload::i32_const(@lib.I32(0)) {
Some({ node_id, })
} else {
None
}
_ => None
}
}
///|
fn OiDirectZeroSentinelFact::proves_direct_zero_sentinel(
self : OiDirectZeroSentinelFact,
) -> Bool {
ignore(self.node_id)
true
}
///|
priv struct OiDropZeroSentinelFact {
child_id : Int32
}
///|
fn OiDropZeroSentinelFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDropZeroSentinelFact? {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Drop =>
if @ir.hot_child_count(func, node_id) == 1 {
let child_id = @ir.hot_child_get(func, node_id, 0)
if OiZeroSentinelFact::from_node(func, child_id).proves_zero_sentinel() {
Some({ child_id, })
} else {
None
}
} else {
None
}
_ => None
}
}
///|
fn OiDropZeroSentinelFact::proves_drop_zero_sentinel(
self : OiDropZeroSentinelFact,
) -> Bool {
ignore(self.child_id)
true
}
///|
priv struct OiZeroSentinelFact {
direct_fact : OiDirectZeroSentinelFact?
drop_fact : OiDropZeroSentinelFact?
}
///|
fn OiZeroSentinelFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiZeroSentinelFact {
// Starshine's dead-suffix cleanup uses explicit zero sentinels to avoid
// deleting value carriers that still satisfy a surrounding drop or result
// shape. Record the direct `i32.const 0` proof and nested single-child `drop`
// proof as sibling facts so the cleanup does not own a recursive opcode
// scanner or parallel shell booleans.
{
direct_fact: OiDirectZeroSentinelFact::from_node(func, node_id),
drop_fact: OiDropZeroSentinelFact::from_node(func, node_id),
}
}
///|
fn OiZeroSentinelFact::proves_zero_sentinel(self : OiZeroSentinelFact) -> Bool {
match self.direct_fact {
Some(fact) => fact.proves_direct_zero_sentinel()
None =>
match self.drop_fact {
Some(fact) => fact.proves_drop_zero_sentinel()
None => false
}
}
}
///|
fn OiZeroSentinelFact::proves_drop_zero_sentinel(
self : OiZeroSentinelFact,
) -> Bool {
match self.drop_fact {
Some(fact) => fact.proves_drop_zero_sentinel()
None => false
}
}
///|
fn OiZeroSentinelFact::proves_node_zero_sentinel(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
OiZeroSentinelFact::from_node(func, node_id).proves_zero_sentinel()
}
///|
fn OiZeroSentinelFact::proves_node_drop_zero_sentinel(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
OiZeroSentinelFact::from_node(func, node_id).proves_drop_zero_sentinel()
}
///|
priv struct OiBranchTableDefaultTargetFact {
targets_label : Bool
}
///|
fn OiBranchTableDefaultTargetFact::from_table(
func : @ir.HotFunc,
table_id : Int32,
label_id : Int32,
) -> OiBranchTableDefaultTargetFact {
{
targets_label: @ir.hot_branch_table_default_target(func, table_id) ==
label_id,
}
}
///|
fn OiBranchTableDefaultTargetFact::proves_targets_label(
self : OiBranchTableDefaultTargetFact,
) -> Bool {
self.targets_label
}
///|
priv struct OiBranchTableExplicitTargetsFact {
all_targets_label : Bool
}
///|
fn OiBranchTableExplicitTargetsFact::from_table(
func : @ir.HotFunc,
table_id : Int32,
label_id : Int32,
) -> OiBranchTableExplicitTargetsFact {
let mut all_targets_label = true
for target in @ir.hot_branch_table_targets(func, table_id) {
if target != label_id {
all_targets_label = false
}
}
{ all_targets_label, }
}
///|
fn OiBranchTableExplicitTargetsFact::proves_all_targets_label(
self : OiBranchTableExplicitTargetsFact,
) -> Bool {
self.all_targets_label
}
///|
priv struct OiBranchTableAllTargetsFact {
default_target_fact : OiBranchTableDefaultTargetFact
explicit_targets_fact : OiBranchTableExplicitTargetsFact
}
///|
fn OiBranchTableAllTargetsFact::proves_all_targets_label(
self : OiBranchTableAllTargetsFact,
) -> Bool {
self.default_target_fact.proves_targets_label() &&
self.explicit_targets_fact.proves_all_targets_label()
}
///|
priv struct OiBranchTableTargetFact {
all_targets_fact : OiBranchTableAllTargetsFact?
}
///|
fn OiBranchTableTargetFact::from_table(
func : @ir.HotFunc,
table_id : Int32,
label_id : Int32,
) -> OiBranchTableTargetFact {
// `br_table` exits to a surrounding label only when both the default edge and
// every explicit table edge target that label. Keep those source-level edges
// as nested facts instead of parallel default/all booleans on the control
// cleanup fact.
let default_target_fact = OiBranchTableDefaultTargetFact::from_table(
func, table_id, label_id,
)
if !default_target_fact.proves_targets_label() {
return { all_targets_fact: None, }
}
let explicit_targets_fact = OiBranchTableExplicitTargetsFact::from_table(
func, table_id, label_id,
)
let all_targets_fact = if explicit_targets_fact.proves_all_targets_label() {
Some({ default_target_fact, explicit_targets_fact, })
} else {
None
}
{ all_targets_fact, }
}
///|
fn OiBranchTableTargetFact::proves_all_targets_label(
self : OiBranchTableTargetFact,
) -> Bool {
match self.all_targets_fact {
Some(fact) => fact.proves_all_targets_label()
None => false
}
}
///|
priv enum OiControlDirectExitKind {
ControlDirectMayFallThrough
ControlDirectNonbranchTerminal
ControlDirectBranchTerminal
ControlDirectBrToLabel
ControlDirectBranchTableTargetsLabel
}
///|
priv struct OiControlDirectExitFact {
kind : OiControlDirectExitKind
}
///|
priv enum OiControlExitRegionKind {
ControlExitRegionEmpty
ControlExitRegionTailSummary
ControlExitRegionNonbranchTerminalPrefix
ControlExitRegionNeverFallthroughPrefix
}
///|
priv struct OiControlExitRegionTailFact {
never_falls_through : Bool
never_falls_through_to_label : Bool
exits_to_label : Bool
exits_only_via_branches : Bool
branches_to_label : Bool
}
///|
priv struct OiControlExitRegionFact {
kind : OiControlExitRegionKind
tail_fact : OiControlExitRegionTailFact
}
///|
priv struct OiControlIfExitFact {
then_fact : OiControlExitRegionFact
else_fact : OiControlExitRegionFact
}
///|
priv struct OiControlBlockExitFact {
body_fact : OiControlExitRegionFact
}
///|
priv struct OiControlExitFact {
live : Bool
direct_fact : OiControlDirectExitFact
if_fact : OiControlIfExitFact?
block_fact : OiControlBlockExitFact?
}
///|
fn OiControlDirectExitFact::fallthrough() -> OiControlDirectExitFact {
{ kind: ControlDirectMayFallThrough, }
}
///|
fn OiControlDirectExitFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
label_id : Int32,
) -> OiControlDirectExitFact {
// Binaryen's OI control cleanup treats terminal roots, local-label branches,
// and all-target branch tables as separate source facts. Keep that direct
// node classification in one payload instead of parallel booleans on the
// recursive control-exit fact.
let node = @ir.hot_node_get(func, node_id)
let kind = match node.op {
@ir.HotOp::Unreachable
| @ir.HotOp::Return
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate => ControlDirectNonbranchTerminal
@ir.HotOp::Br if node.imm0 == label_id => ControlDirectBrToLabel
@ir.HotOp::Br => ControlDirectBranchTerminal
@ir.HotOp::BrTable if OiBranchTableTargetFact::from_table(
func,
node.imm0,
label_id,
).proves_all_targets_label() => ControlDirectBranchTableTargetsLabel
@ir.HotOp::BrTable => ControlDirectBranchTerminal
_ => ControlDirectMayFallThrough
}
{ kind, }
}
///|
fn OiControlDirectExitFact::proves_direct_terminal(
self : OiControlDirectExitFact,
) -> Bool {
match self.kind {
ControlDirectNonbranchTerminal
| ControlDirectBranchTerminal
| ControlDirectBrToLabel
| ControlDirectBranchTableTargetsLabel => true
ControlDirectMayFallThrough => false
}
}
///|
fn OiControlDirectExitFact::proves_never_falls_through(
self : OiControlDirectExitFact,
) -> Bool {
self.proves_direct_terminal()
}
///|
fn OiControlDirectExitFact::proves_never_falls_through_to_label(
self : OiControlDirectExitFact,
) -> Bool {
match self.kind {
ControlDirectNonbranchTerminal
| ControlDirectBrToLabel
| ControlDirectBranchTableTargetsLabel => true
ControlDirectMayFallThrough | ControlDirectBranchTerminal => false
}
}
///|
fn OiControlDirectExitFact::proves_exits_to_label(
self : OiControlDirectExitFact,
) -> Bool {
self.proves_never_falls_through_to_label()
}
///|
fn OiControlDirectExitFact::proves_exits_only_via_branches(
self : OiControlDirectExitFact,
) -> Bool {
match self.kind {
ControlDirectBranchTerminal
| ControlDirectBrToLabel
| ControlDirectBranchTableTargetsLabel => true
ControlDirectMayFallThrough | ControlDirectNonbranchTerminal => false
}
}
///|
fn OiControlDirectExitFact::proves_branches_to_label(
self : OiControlDirectExitFact,
) -> Bool {
match self.kind {
ControlDirectBrToLabel => true
_ => false
}
}
///|
fn OiControlDirectExitFact::proves_exits_via_nonbranch_terminal(
self : OiControlDirectExitFact,
) -> Bool {
match self.kind {
ControlDirectNonbranchTerminal => true
_ => false
}
}
///|
fn OiControlIfExitFact::proves_never_falls_through(
self : OiControlIfExitFact,
) -> Bool {
self.then_fact.proves_never_falls_through() &&
self.else_fact.proves_never_falls_through()
}
///|
fn OiControlIfExitFact::proves_never_falls_through_to_label(
self : OiControlIfExitFact,
) -> Bool {
self.then_fact.proves_never_falls_through_to_label() &&
self.else_fact.proves_never_falls_through_to_label()
}
///|
fn OiControlIfExitFact::proves_exits_to_label(
self : OiControlIfExitFact,
) -> Bool {
self.then_fact.proves_exits_to_label() &&
self.else_fact.proves_exits_to_label()
}
///|
fn OiControlIfExitFact::proves_exits_only_via_branches(
self : OiControlIfExitFact,
) -> Bool {
self.then_fact.proves_exits_only_via_branches() &&
self.else_fact.proves_exits_only_via_branches()
}
///|
fn OiControlIfExitFact::proves_branches_to_label(
self : OiControlIfExitFact,
) -> Bool {
self.then_fact.proves_branches_to_label() &&
self.else_fact.proves_branches_to_label()
}
///|
fn OiControlIfExitFact::proves_exits_via_nonbranch_terminal(
self : OiControlIfExitFact,
) -> Bool {
self.then_fact.proves_exits_via_nonbranch_terminal() &&
self.else_fact.proves_exits_via_nonbranch_terminal()
}
///|
fn OiControlBlockExitFact::proves_exits_via_nonbranch_terminal(
self : OiControlBlockExitFact,
) -> Bool {
self.body_fact.proves_exits_via_nonbranch_terminal()
}
///|
fn OiControlExitFact::empty(live : Bool) -> OiControlExitFact {
{
live,
direct_fact: OiControlDirectExitFact::fallthrough(),
if_fact: None,
block_fact: None,
}
}
///|
fn OiControlExitFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
label_id : Int32,
) -> OiControlExitFact {
// Binaryen's OI relies on control/fallthrough facts when deleting dead
// suffixes and preserving branch-carried values. Starshine's represented
// cleanup keeps a narrower HOT subset: direct nonbranch terminals, direct
// branch terminals, all-target `br_table`s, two-arm `if` summaries, and block
// bodies that exit through nonbranch terminals. Keep those source-order facts
// nested instead of as one parallel-boolean recursive scanner result.
let live = OiLiveNodeFact::proves_node_live(func, dead, node_id)
if !live {
return OiControlExitFact::empty(false)
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => {
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return OiControlExitFact::empty(true)
}
let then_fact = OiControlExitRegionFact::from_region(
func,
dead,
@ir.hot_region_then_ref(func, node_id),
label_id,
)
let else_fact = OiControlExitRegionFact::from_region(
func, dead, else_region, label_id,
)
return {
live: true,
direct_fact: OiControlDirectExitFact::fallthrough(),
if_fact: Some({ then_fact, else_fact, }),
block_fact: None,
}
}
@ir.HotOp::Block => {
let body_fact = OiControlExitRegionFact::from_region(
func,
dead,
@ir.hot_region_body_ref(func, node_id),
label_id,
)
return {
live: true,
direct_fact: OiControlDirectExitFact::fallthrough(),
if_fact: None,
block_fact: Some({ body_fact, }),
}
}
_ => ()
}
{
live: true,
direct_fact: OiControlDirectExitFact::from_node(func, node_id, label_id),
if_fact: None,
block_fact: None,
}
}
///|
fn OiControlExitFact::proves_direct_terminal(self : OiControlExitFact) -> Bool {
self.live && self.direct_fact.proves_direct_terminal()
}
///|
fn OiControlExitFact::proves_never_falls_through(
self : OiControlExitFact,
) -> Bool {
if !self.live {
return false
}
if self.direct_fact.proves_never_falls_through() {
return true
}
match self.if_fact {
Some(fact) => fact.proves_never_falls_through()
None => false
}
}
///|
fn OiControlExitFact::proves_never_falls_through_to_label(
self : OiControlExitFact,
) -> Bool {
if !self.live {
return false
}
if self.direct_fact.proves_never_falls_through_to_label() {
return true
}
match self.if_fact {
Some(fact) => fact.proves_never_falls_through_to_label()
None => false
}
}
///|
fn OiControlExitFact::proves_exits_to_label(self : OiControlExitFact) -> Bool {
if !self.live {
return false
}
if self.direct_fact.proves_exits_to_label() {
return true
}
match self.if_fact {
Some(fact) => fact.proves_exits_to_label()
None => false
}
}
///|
fn OiControlExitFact::proves_exits_only_via_branches(
self : OiControlExitFact,
) -> Bool {
if !self.live {
return false
}
if self.direct_fact.proves_exits_only_via_branches() {
return true
}
match self.if_fact {
Some(fact) => fact.proves_exits_only_via_branches()
None => false
}
}
///|
fn OiControlExitFact::proves_branches_to_label(
self : OiControlExitFact,
) -> Bool {
if !self.live {
return false
}
if self.direct_fact.proves_branches_to_label() {
return true
}
match self.if_fact {
Some(fact) => fact.proves_branches_to_label()
None => false
}
}
///|
fn OiControlExitFact::proves_exits_via_nonbranch_terminal(
self : OiControlExitFact,
) -> Bool {
if !self.live {
return false
}
if self.direct_fact.proves_exits_via_nonbranch_terminal() {
return true
}
match self.block_fact {
Some(fact) if fact.proves_exits_via_nonbranch_terminal() => true
_ =>
match self.if_fact {
Some(fact) => fact.proves_exits_via_nonbranch_terminal()
None => false
}
}
}
///|
fn OiControlExitRegionTailFact::from_tail(
tail_fact : OiControlExitFact,
) -> OiControlExitRegionTailFact {
{
never_falls_through: tail_fact.proves_never_falls_through(),
never_falls_through_to_label: tail_fact.proves_never_falls_through_to_label(),
exits_to_label: tail_fact.proves_exits_to_label(),
exits_only_via_branches: tail_fact.proves_exits_only_via_branches(),
branches_to_label: tail_fact.proves_branches_to_label(),
}
}
///|
fn OiControlExitRegionTailFact::empty() -> OiControlExitRegionTailFact {
{
never_falls_through: false,
never_falls_through_to_label: false,
exits_to_label: false,
exits_only_via_branches: false,
branches_to_label: false,
}
}
///|
fn OiControlExitRegionFact::from_region(
func : @ir.HotFunc,
dead : @bitset.BitSet,
region_ref : @ir.HotRegionRef,
label_id : Int32,
) -> OiControlExitRegionFact {
// Region summaries have two separate source-shaped decisions: the tail exit
// facts exposed to parents, and whether a live prefix root already exits via
// a nonbranch terminal or another no-fallthrough path. Keep that prefix kind
// explicit instead of parallel empty/nonbranch/never-fallthrough booleans.
let root_count = @ir.hot_region_root_count(func, region_ref)
if root_count == 0 {
return OiControlExitRegionFact::empty()
}
let tail_fact = OiControlExitRegionTailFact::from_tail(
OiControlExitFact::from_node(
func,
dead,
@ir.hot_region_get(func, region_ref, root_count - 1),
label_id,
),
)
for idx = 0; idx < root_count; idx = idx + 1 {
let root = @ir.hot_region_get(func, region_ref, idx)
if !OiLiveNodeFact::proves_node_live(func, dead, root) {
continue
}
let root_fact = OiControlExitFact::from_node(func, dead, root, label_id)
if root_fact.proves_exits_via_nonbranch_terminal() {
return { kind: ControlExitRegionNonbranchTerminalPrefix, tail_fact, }
}
if root_fact.proves_never_falls_through() {
return { kind: ControlExitRegionNeverFallthroughPrefix, tail_fact, }
}
}
{ kind: ControlExitRegionTailSummary, tail_fact, }
}
///|
fn OiControlExitRegionFact::empty() -> OiControlExitRegionFact {
{
kind: ControlExitRegionEmpty,
tail_fact: OiControlExitRegionTailFact::empty(),
}
}
///|
fn OiControlExitRegionFact::proves_tail_summary_available(
self : OiControlExitRegionFact,
) -> Bool {
match self.kind {
ControlExitRegionEmpty => false
ControlExitRegionTailSummary
| ControlExitRegionNonbranchTerminalPrefix
| ControlExitRegionNeverFallthroughPrefix => true
}
}
///|
fn OiControlExitRegionFact::proves_never_falls_through(
self : OiControlExitRegionFact,
) -> Bool {
self.proves_tail_summary_available() && self.tail_fact.never_falls_through
}
///|
fn OiControlExitRegionFact::proves_never_falls_through_to_label(
self : OiControlExitRegionFact,
) -> Bool {
self.proves_tail_summary_available() &&
self.tail_fact.never_falls_through_to_label
}
///|
fn OiControlExitRegionFact::proves_exits_to_label(
self : OiControlExitRegionFact,
) -> Bool {
self.proves_tail_summary_available() && self.tail_fact.exits_to_label
}
///|
fn OiControlExitRegionFact::proves_exits_only_via_branches(
self : OiControlExitRegionFact,
) -> Bool {
self.proves_tail_summary_available() && self.tail_fact.exits_only_via_branches
}
///|
fn OiControlExitRegionFact::proves_branches_to_label(
self : OiControlExitRegionFact,
) -> Bool {
self.proves_tail_summary_available() && self.tail_fact.branches_to_label
}
///|
fn OiControlExitRegionFact::proves_exits_via_nonbranch_terminal(
self : OiControlExitRegionFact,
) -> Bool {
match self.kind {
ControlExitRegionNonbranchTerminalPrefix => true
ControlExitRegionEmpty
| ControlExitRegionTailSummary
| ControlExitRegionNeverFallthroughPrefix => false
}
}
///|
priv enum OiRegionHolderDropFlowKind {
RegionHolderDropFlowRootRegion
RegionHolderDropFlowSharedHolder
RegionHolderDropFlowRootUseSentinel
RegionHolderDropFlowChildDrop
RegionHolderDropFlowChildNonDrop
RegionHolderDropFlowTraversalLimit
}
///|
priv struct OiRegionHolderDropFlowFact {
kind : OiRegionHolderDropFlowKind
}
///|
fn OiRegionHolderDropFlowFact::from_region(
ctx : HotPassContext,
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> OiRegionHolderDropFlowFact {
// Dead-suffix zero-sentinel preservation needs to know whether the current
// region holder reaches a surrounding `drop` through single-use root hops.
// Keep the local writeback/root-use sentinel and traversal-limit outcomes as
// an explicit fact so the cleanup no longer owns a raw use-def scanner.
if region_ref == @ir.HotRegionRef::root() {
return { kind: RegionHolderDropFlowRootRegion, }
}
let use_def = pass_require_use_def(ctx, func)
let mut current = region_ref.holder
for hops = 0; hops < 64; hops = hops + 1 {
if @ir.node_use_count(use_def, current) != 1 {
return { kind: RegionHolderDropFlowSharedHolder, }
}
let sites = @ir.node_use_sites_borrowed(use_def, current)
let site = sites[0]
if site.user == -1 {
return { kind: RegionHolderDropFlowRootUseSentinel, }
}
let user = @ir.hot_node_get(func, site.user)
match site.kind {
@ir.HotUseSlotKind::ChildUseSlot =>
match user.op {
@ir.HotOp::Drop => return { kind: RegionHolderDropFlowChildDrop, }
_ => return { kind: RegionHolderDropFlowChildNonDrop, }
}
@ir.HotUseSlotKind::RootUseSlot => current = site.user
}
}
{ kind: RegionHolderDropFlowTraversalLimit, }
}
///|
fn OiRegionHolderDropFlowFact::proves_flows_to_drop(
self : OiRegionHolderDropFlowFact,
) -> Bool {
match self.kind {
RegionHolderDropFlowChildDrop => true
RegionHolderDropFlowRootRegion
| RegionHolderDropFlowSharedHolder
| RegionHolderDropFlowRootUseSentinel
| RegionHolderDropFlowChildNonDrop
| RegionHolderDropFlowTraversalLimit => false
}
}
///|
fn optimize_instructions_try_collapse_dead_region_suffix(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
region_ref : @ir.HotRegionRef,
label_id : Int32,
) -> Bool {
let root_count = @ir.hot_region_root_count(func, region_ref)
if root_count < 2 {
return false
}
for idx = 0; idx < root_count - 1; idx = idx + 1 {
let root = @ir.hot_region_get(func, region_ref, idx)
let root_op = @ir.hot_node_get(func, root).op
let root_exit_fact = OiControlExitFact::from_node(
func, dead, root, label_id,
)
let branch_only_if_escape = match root_op {
@ir.HotOp::If => root_exit_fact.proves_exits_only_via_branches()
_ => false
}
let direct_terminal = root_exit_fact.proves_direct_terminal()
let tail_is_value_branch_to_label = if idx + 2 == root_count {
let tail_id = @ir.hot_region_get(func, region_ref, idx + 1)
let tail = @ir.hot_node_get(func, tail_id)
match tail.op {
@ir.HotOp::Br =>
tail.imm0 == label_id && @ir.hot_child_count(func, tail_id) > 0
_ => false
}
} else {
false
}
let never_falls_through = if tail_is_value_branch_to_label {
root_exit_fact.proves_never_falls_through_to_label()
} else {
root_exit_fact.proves_never_falls_through()
}
let escapes = if branch_only_if_escape {
false
} else {
match root_op {
@ir.HotOp::Br
| @ir.HotOp::BrTable
| @ir.HotOp::Unreachable
| @ir.HotOp::Return
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate => true
_ => root_exit_fact.proves_exits_to_label() || never_falls_through
}
}
if !escapes {
continue
}
if idx + 2 == root_count {
let tail_id = @ir.hot_region_get(func, region_ref, idx + 1)
let root_is_if = match root_op {
@ir.HotOp::If => true
_ => false
}
if root_is_if &&
OiZeroSentinelFact::proves_node_drop_zero_sentinel(func, tail_id) {
return false
}
let root_is_result_carrier = match root_op {
@ir.HotOp::If | @ir.HotOp::Block => true
_ => false
}
let keep_result_zero_sentinel = root_is_result_carrier &&
root_exit_fact.proves_exits_via_nonbranch_terminal()
if OiZeroSentinelFact::proves_node_zero_sentinel(func, tail_id) &&
(
OiRegionHolderDropFlowFact::from_region(ctx, func, region_ref).proves_flows_to_drop() ||
keep_result_zero_sentinel
) {
return false
}
}
if idx + 2 == root_count {
let tail_id = @ir.hot_region_get(func, region_ref, idx + 1)
let tail = @ir.hot_node_get(func, tail_id)
match tail.op {
@ir.HotOp::Unreachable => return false
@ir.HotOp::Br =>
if @ir.hot_child_count(func, tail_id) > 0 {
let root_is_if = match root_op {
@ir.HotOp::If => true
_ => false
}
if root_is_if &&
!OiControlExitFact::from_node(func, dead, root, tail.imm0).proves_exits_to_label() {
return false
}
}
_ => ()
}
}
let next : Array[Int32] = []
for keep = 0; keep <= idx; keep = keep + 1 {
next.push(@ir.hot_region_get(func, region_ref, keep))
}
let root_is_if = match root_op {
@ir.HotOp::If => true
_ => false
}
let needs_unreachable = if direct_terminal {
false
} else if root_is_if && branch_only_if_escape {
false
} else if region_ref == @ir.HotRegionRef::root() {
true
} else {
let holder = region_ref.holder
let holder_label = @ir.hot_control_node_label(func, holder)
match @ir.hot_label_kind(func, holder_label) {
@ir.HotLabelKind::RegionLabel => false
_ => true
}
}
if needs_unreachable {
next.push(@ir.hot_build_unreachable(func))
}
@ir.hot_region_replace_body(func, region_ref, next)
pass_mark_mutated(ctx, func)
return true
}
false
}
///|
fn optimize_instructions_reverse_relational_instr(
instr : @lib.Instruction,
) -> @lib.Instruction? {
match instr {
@lib.Instruction::I32LtS => Some(@lib.Instruction::i32_gt_s())
@lib.Instruction::I32LtU => Some(@lib.Instruction::i32_gt_u())
@lib.Instruction::I32LeS => Some(@lib.Instruction::i32_ge_s())
@lib.Instruction::I32LeU => Some(@lib.Instruction::i32_ge_u())
@lib.Instruction::I32GtS => Some(@lib.Instruction::i32_lt_s())
@lib.Instruction::I32GtU => Some(@lib.Instruction::i32_lt_u())
@lib.Instruction::I32GeS => Some(@lib.Instruction::i32_le_s())
@lib.Instruction::I32GeU => Some(@lib.Instruction::i32_le_u())
@lib.Instruction::I64LtS => Some(@lib.Instruction::i64_gt_s())
@lib.Instruction::I64LtU => Some(@lib.Instruction::i64_gt_u())
@lib.Instruction::I64LeS => Some(@lib.Instruction::i64_ge_s())
@lib.Instruction::I64LeU => Some(@lib.Instruction::i64_ge_u())
@lib.Instruction::I64GtS => Some(@lib.Instruction::i64_lt_s())
@lib.Instruction::I64GtU => Some(@lib.Instruction::i64_lt_u())
@lib.Instruction::I64GeS => Some(@lib.Instruction::i64_le_s())
@lib.Instruction::I64GeU => Some(@lib.Instruction::i64_le_u())
_ => None
}
}
///|
fn optimize_instructions_try_canonicalize_relational_const(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
if !OiConstlikeLeafFact::from_node(func, dead, lhs_id).proves_constlike() ||
OiConstlikeLeafFact::from_node(func, dead, rhs_id).proves_constlike() {
return false
}
match optimize_instructions_reverse_relational_instr(instr) {
Some(reversed) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
reversed,
[rhs_id, lhs_id],
)
true
}
None => false
}
}
///|
fn optimize_instructions_try_canonicalize_relational_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
scratch : OptimizeInstructionsScratch,
) -> Bool {
let reversed = match optimize_instructions_reverse_relational_instr(instr) {
Some(reversed) => reversed
None => return false
}
let lhs_canonicalized = match
OiLocalGetLeafFact::from_node(func, dead, lhs_id).local_id {
Some(local_id) => {
if OiConstlikeLeafFact::from_node(func, dead, rhs_id).proves_constlike() {
return false
}
let should_swap = match
OiLocalGetLeafFact::from_node(func, dead, rhs_id).local_id {
Some(rhs_local_id) => local_id > rhs_local_id
None => true
}
if !should_swap {
return false
}
let use_def = pass_require_use_def(ctx, func)
if @ir.node_use_count(use_def, rhs_id) > 1 {
return false
}
optimize_instructions_mark_loop_input_nodes(func, dead, scratch)
let seen_stamp = optimize_instructions_alloc_cross_seen_stamp(scratch)
let crossing_allowed = OiLocalGetCrossingFact::from_node(
func,
dead,
use_def,
rhs_id,
local_id,
false,
scratch.loop_input_nodes,
scratch.cross_seen_stamps,
seen_stamp,
).proves_crossing_allowed()
if !crossing_allowed {
return false
}
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
reversed,
[rhs_id, lhs_id],
)
true
}
None => false
}
if lhs_canonicalized {
return true
}
match OiLocalGetLeafFact::from_node(func, dead, rhs_id).local_id {
Some(local_id) => {
if OiConstlikeLeafFact::from_node(func, dead, lhs_id).proves_constlike() ||
!OiCommutativeOperandSwapFact::from_operands(func, dead, lhs_id, rhs_id).proves_should_swap() {
return false
}
let use_def = pass_require_use_def(ctx, func)
if @ir.node_use_count(use_def, lhs_id) > 1 {
return false
}
optimize_instructions_mark_loop_input_nodes(func, dead, scratch)
let seen_stamp = optimize_instructions_alloc_cross_seen_stamp(scratch)
let crossing_allowed = OiLocalGetCrossingFact::from_node(
func,
dead,
use_def,
lhs_id,
local_id,
false,
scratch.loop_input_nodes,
scratch.cross_seen_stamps,
seen_stamp,
).proves_crossing_allowed()
if !crossing_allowed {
return false
}
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
reversed,
[rhs_id, lhs_id],
)
true
}
None => false
}
}
///|
fn optimize_instructions_try_canonicalize_compare_const(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match (instr, optimize_instructions_i32_const(func, dead, rhs_id)) {
(@lib.Instruction::I32GtS, Some(@lib.I32(raw))) if raw == -1 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_ge_s(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(0))],
)
true
}
(@lib.Instruction::I32LeS, Some(@lib.I32(raw))) if raw == -1 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_lt_s(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(0))],
)
true
}
(@lib.Instruction::I32LtS, Some(@lib.I32(raw))) if raw == 1 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_le_s(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(0))],
)
true
}
(@lib.Instruction::I32GeS, Some(@lib.I32(raw))) if raw == 1 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_gt_s(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(0))],
)
true
}
(@lib.Instruction::I32LtU, Some(@lib.I32(raw))) if raw == 1 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_eq(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(0))],
)
true
}
(@lib.Instruction::I32GeU, Some(@lib.I32(raw))) if raw == 1 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(0))],
)
true
}
(@lib.Instruction::I32LtS, Some(@lib.I32(raw))) if raw == -2147483647 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_eq(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(-2147483648))],
)
true
}
(@lib.Instruction::I32GeS, Some(@lib.I32(raw))) if raw == -2147483647 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(-2147483648))],
)
true
}
(@lib.Instruction::I32GtS, Some(@lib.I32(raw))) if raw == -2147483648 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
[lhs_id, rhs_id],
)
true
}
(@lib.Instruction::I32LtS, Some(@lib.I32(raw))) if raw == 2147483647 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
[lhs_id, rhs_id],
)
true
}
(@lib.Instruction::I32GtS, Some(@lib.I32(raw))) if raw == 2147483646 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_eq(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(2147483647))],
)
true
}
(@lib.Instruction::I32LeS, Some(@lib.I32(raw))) if raw == 2147483646 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(2147483647))],
)
true
}
_ =>
match (instr, optimize_instructions_i64_const(func, dead, rhs_id)) {
(@lib.Instruction::I64GtS, Some(@lib.I64(raw))) if raw == -1L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_ge_s(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(0L))],
)
true
}
(@lib.Instruction::I64LeS, Some(@lib.I64(raw))) if raw == -1L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_lt_s(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(0L))],
)
true
}
(@lib.Instruction::I64LtS, Some(@lib.I64(raw))) if raw == 1L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_le_s(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(0L))],
)
true
}
(@lib.Instruction::I64GeS, Some(@lib.I64(raw))) if raw == 1L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_gt_s(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(0L))],
)
true
}
(@lib.Instruction::I64LtU, Some(@lib.I64(raw))) if raw == 1L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_eq(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(0L))],
)
true
}
(@lib.Instruction::I64GeU, Some(@lib.I64(raw))) if raw == 1L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(0L))],
)
true
}
(@lib.Instruction::I64LtS, Some(@lib.I64(raw))) if raw ==
-9223372036854775807L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_eq(),
[
lhs_id,
@ir.hot_build_const_i64(func, @lib.I64(-9223372036854775808L)),
],
)
true
}
(@lib.Instruction::I64GeS, Some(@lib.I64(raw))) if raw ==
-9223372036854775807L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
[
lhs_id,
@ir.hot_build_const_i64(func, @lib.I64(-9223372036854775808L)),
],
)
true
}
(@lib.Instruction::I64GtS, Some(@lib.I64(raw))) if raw ==
-9223372036854775808L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
[lhs_id, rhs_id],
)
true
}
(@lib.Instruction::I64LtS, Some(@lib.I64(raw))) if raw ==
9223372036854775807L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
[lhs_id, rhs_id],
)
true
}
(@lib.Instruction::I64GtS, Some(@lib.I64(raw))) if raw ==
9223372036854775806L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_eq(),
[
lhs_id,
@ir.hot_build_const_i64(func, @lib.I64(9223372036854775807L)),
],
)
true
}
(@lib.Instruction::I64LeS, Some(@lib.I64(raw))) if raw ==
9223372036854775806L => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
[
lhs_id,
@ir.hot_build_const_i64(func, @lib.I64(9223372036854775807L)),
],
)
true
}
_ => false
}
}
}
///|
fn optimize_instructions_try_optimize_if_condition(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If if node.child_count == 3 => ()
_ => return false
}
if optimize_instructions_try_collapse_duplicate_then_branch(
ctx, func, dead, node_id,
) {
return true
}
let cond_id = func.children[node.child_base]
let then_region = func.children[node.child_base + 1]
let else_region = func.children[node.child_base + 2]
let flip_if = fn(next_cond : Int32) -> Bool {
match optimize_instructions_i32_const(func, dead, next_cond) {
Some(@lib.I32(raw)) if raw == 0 => return false
_ => ()
}
match optimize_instructions_i64_const(func, dead, next_cond) {
Some(@lib.I64(raw)) if raw == 0L => return false
_ => ()
}
ignore(
@ir.hot_replace_child_span(func, node_id, [
next_cond, else_region, then_region,
]),
)
pass_mark_mutated(ctx, func)
true
}
let cond_instr = match @ir.hot_node_exact_instr(func, cond_id) {
Some(instr) => instr
None => {
if optimize_instructions_try_invert_nested_if_condition(
ctx, func, dead, node_id, cond_id, then_region, else_region,
) {
return true
}
return optimize_instructions_try_negate_nested_if_condition_to_eqz(
ctx, func, dead, node_id, cond_id, else_region,
)
}
}
let direct_rewrite = match cond_instr {
@lib.Instruction::I32Eq => {
if @ir.hot_child_count(func, cond_id) != 2 {
return false
}
let lhs_id = @ir.hot_child_get(func, cond_id, 0)
let rhs_id = @ir.hot_child_get(func, cond_id, 1)
let operand_id = match
(
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(Some(@lib.I32(0)), _) => Some(rhs_id)
(_, Some(@lib.I32(0))) => Some(lhs_id)
_ => None
}
match operand_id {
Some(operand_id) if OiI32ValueFact::proves_node_i32_value(
func, operand_id,
) =>
if @ir.hot_node_is_live(func, else_region) {
flip_if(operand_id)
} else {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
cond_id,
@lib.Instruction::i32_eqz(),
operand_id,
)
true
}
Some(_) => false
None => false
}
}
@lib.Instruction::I32Eqz => {
if !@ir.hot_node_is_live(func, else_region) ||
@ir.hot_child_count(func, cond_id) != 1 {
return false
}
let operand_id = @ir.hot_child_get(func, cond_id, 0)
if !OiI32ValueFact::proves_node_i32_value(func, operand_id) {
return false
}
flip_if(operand_id)
}
_ => false
}
if direct_rewrite {
return true
}
if optimize_instructions_try_invert_nested_if_condition(
ctx, func, dead, node_id, cond_id, then_region, else_region,
) {
return true
}
optimize_instructions_try_negate_nested_if_condition_to_eqz(
ctx, func, dead, node_id, cond_id, else_region,
)
}
///|
fn optimize_instructions_try_wrap_boolean_if_value_in_eqz(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
if @ir.hot_node_result_arity(func, node_id) != 1 {
return false
}
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return false
}
let then_region = @ir.hot_region_then_ref(func, node_id)
if @ir.hot_region_root_count(func, then_region) != 1 ||
@ir.hot_region_root_count(func, else_region) != 1 {
return false
}
let then_root = @ir.hot_region_get(func, then_region, 0)
let else_root = @ir.hot_region_get(func, else_region, 0)
let plain_boolean_const_arms = match
(
optimize_instructions_i32_const(func, dead, then_root),
optimize_instructions_i32_const(func, dead, else_root),
) {
(Some(@lib.I32(0)), Some(@lib.I32(1)))
| (Some(@lib.I32(1)), Some(@lib.I32(0))) => true
_ => false
}
if plain_boolean_const_arms {
return false
}
if OiBooleanConstantTreeFact::proves_node_boolean_tree(func, dead, then_root) &&
OiBooleanConstantTreeFact::proves_node_boolean_tree(func, dead, else_root) {
return false
}
let use_def = pass_require_use_def(ctx, func)
if @ir.node_use_count(use_def, node_id) != 1 {
return false
}
let use_sites = @ir.node_use_sites_borrowed(use_def, node_id)
if use_sites.length() != 1 {
return false
}
let use_site = use_sites[0]
if use_site.user == -1 {
return false
}
match use_site.kind {
@ir.HotUseSlotKind::ChildUseSlot => ()
@ir.HotUseSlotKind::RootUseSlot => return false
}
let use_user = @ir.hot_node_get(func, use_site.user)
match use_user.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => ()
_ => return false
}
let neg_then = match
optimize_instructions_negate_boolean_expr_recursive(func, dead, then_root) {
Some(node) => node
None => return false
}
let neg_else = match
optimize_instructions_negate_boolean_expr_recursive(func, dead, else_root) {
Some(node) => node
None => return false
}
@ir.hot_region_replace_body(func, then_region, [neg_then])
@ir.hot_region_replace_body(func, else_region, [neg_else])
let wrapped = @ir.hot_build_unary(func, @lib.ValType::i32(), node_id)
@ir.hot_node_exact_instr_set(func, wrapped, @lib.Instruction::i32_eqz())
@ir.hot_child_set(func, use_site.user, use_site.slot, wrapped)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_invert_nested_if_condition(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
cond_id : Int32,
then_region : Int32,
else_region : Int32,
) -> Bool {
if !@ir.hot_node_is_live(func, else_region) {
return false
}
let use_def = pass_require_use_def(ctx, func)
if @ir.node_use_count(use_def, cond_id) != 1 {
return false
}
match @ir.hot_node_get(func, cond_id).op {
@ir.HotOp::If => ()
_ => return false
}
let cond_then = @ir.hot_region_then_ref(func, cond_id)
let cond_else = match @ir.hot_region_else_ref(func, cond_id) {
Some(region) => region
None => return false
}
if @ir.hot_region_root_count(func, cond_then) != 1 ||
@ir.hot_region_root_count(func, cond_else) != 1 {
return false
}
let neg_then = match
optimize_instructions_negate_boolean_expr(
func,
dead,
@ir.hot_region_get(func, cond_then, 0),
) {
Some(node) => node
None => return false
}
let neg_else = match
optimize_instructions_negate_boolean_expr(
func,
dead,
@ir.hot_region_get(func, cond_else, 0),
) {
Some(node) => node
None => return false
}
@ir.hot_region_replace_body(func, cond_then, [neg_then])
@ir.hot_region_replace_body(func, cond_else, [neg_else])
ignore(
@ir.hot_replace_child_span(func, node_id, [
cond_id, else_region, then_region,
]),
)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_negate_nested_if_condition_to_eqz(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
cond_id : Int32,
else_region : Int32,
) -> Bool {
if @ir.hot_node_is_live(func, else_region) {
return false
}
let use_def = pass_require_use_def(ctx, func)
if !optimize_instructions_boolean_tree_if_nodes_are_unshared(
func, dead, use_def, cond_id,
) {
return false
}
match @ir.hot_node_get(func, cond_id).op {
@ir.HotOp::If => ()
_ => return false
}
let cond_then = @ir.hot_region_then_ref(func, cond_id)
let cond_else = match @ir.hot_region_else_ref(func, cond_id) {
Some(region) => region
None => return false
}
if @ir.hot_region_root_count(func, cond_then) != 1 ||
@ir.hot_region_root_count(func, cond_else) != 1 {
return false
}
let neg_then = match
optimize_instructions_negate_boolean_expr_recursive(
func,
dead,
@ir.hot_region_get(func, cond_then, 0),
) {
Some(node) => node
None => return false
}
let neg_else = match
optimize_instructions_negate_boolean_expr_recursive(
func,
dead,
@ir.hot_region_get(func, cond_else, 0),
) {
Some(node) => node
None => return false
}
@ir.hot_region_replace_body(func, cond_then, [neg_then])
@ir.hot_region_replace_body(func, cond_else, [neg_else])
let wrapped = @ir.hot_build_unary(func, @lib.ValType::i32(), cond_id)
@ir.hot_node_exact_instr_set(func, wrapped, @lib.Instruction::i32_eqz())
@ir.hot_child_set(func, node_id, 0, wrapped)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_const_memory_size_exact(
func : @ir.HotFunc,
node_id : Int32,
) -> Int32? {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Const =>
match @ir.hot_const_get(func, node.imm0) {
@ir.HotConstPayload::I32Const(@lib.I32(raw)) if raw >= 0 => Some(raw)
@ir.HotConstPayload::I64Const(@lib.I64(raw)) if raw >= 0L &&
raw <= 2147483647L => Some(raw.to_int())
_ => None
}
_ => None
}
}
///|
fn optimize_instructions_const_memory_size(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Int32? {
match optimize_instructions_i32_const(func, dead, node_id) {
Some(@lib.I32(raw)) if raw >= 0 => Some(raw)
_ =>
match optimize_instructions_i64_const(func, dead, node_id) {
Some(@lib.I64(raw)) if raw >= 0L && raw <= 2147483647L =>
Some(raw.to_int())
_ => optimize_instructions_const_memory_size_exact(func, node_id)
}
}
}
///|
fn optimize_instructions_i32_const_for_memory_fill(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> @lib.I32? {
match optimize_instructions_i32_const(func, dead, node_id) {
Some(value) => Some(value)
None => {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Const =>
match @ir.hot_const_get(func, node.imm0) {
@ir.HotConstPayload::I32Const(value) => Some(value)
_ => None
}
_ => None
}
}
}
}
///|
fn optimize_instructions_memarg_for_mem_idx(mem_idx : Int32) -> @lib.MemArg {
@lib.MemArg::new(
@lib.U32(0),
Some(@lib.MemIdx::new(mem_idx.reinterpret_as_uint())),
@lib.U64(0),
)
}
///|
priv struct OiStateWriteSetFact {
local_writes : Array[Int32]
global_writes : Array[Int32]
}
///|
priv struct OiLeftFallthroughInputFact {
fallthrough_id : Int32
after_value_effect_mask : UInt32
after_value_write_mask : UInt32
after_value_state_writes : OiStateWriteSetFact
unsupported_after_value_control : Bool
}
///|
priv struct OiRightFallthroughInputFact {
fallthrough_id : Int32
prefix_effect_mask : UInt32
prefix_write_mask : UInt32
prefix_state_writes : OiStateWriteSetFact
unsupported_prefix_control : Bool
peeled_local_tee : Bool
}
///|
priv struct OiInterveningEffectFact {
effect_mask : UInt32
write_mask : UInt32
state_writes : OiStateWriteSetFact
unsupported_control : Bool
}
///|
priv struct OiSubtreeReorderEffectFact {
effect_mask : UInt32
read_mask : UInt32
write_mask : UInt32
state_writes : OiStateWriteSetFact
carries_control : Bool
disrupts : Bool
has_side_effect : Bool
}
///|
priv struct OiLeadingOperandSwapFact {
left_in_bounds : Bool
right_in_bounds : Bool
left_effects : OiSubtreeReorderEffectFact
right_effects : OiSubtreeReorderEffectFact
}
///|
priv enum OiNodeEffectPurityKind {
NodeEffectPureMask
NodeEffectImpureMask
NodeEffectMissingMask
}
///|
priv struct OiNodeEffectPurityFact {
effect_mask : UInt32
kind : OiNodeEffectPurityKind
}
///|
priv enum OiConsecutiveInputEqualityProofKind {
ConsecutiveInputNoKnownEquality
ConsecutiveInputLocalTeeAndGet
ConsecutiveInputStoredScalarValue
ConsecutiveInputPureScalarTree
ConsecutiveInputStableStateRead
ConsecutiveInputStableStateScalarTree
ConsecutiveInputStableMemoryRead
ConsecutiveInputRightLocalTeeFallthrough
ConsecutiveInputIdempotentCall
}
///|
priv struct OiConsecutiveInputEqualityProofFact {
kind : OiConsecutiveInputEqualityProofKind
}
///|
priv enum OiConsecutiveInputInterferenceKind {
ConsecutiveInputEffectsIndependent
ConsecutiveInputEffectsOrderedBeforeRight
}
///|
priv struct OiConsecutiveInputInterferenceFact {
kind : OiConsecutiveInputInterferenceKind
}
///|
priv struct OiConsecutiveInputEqualityFact {
left_fallthrough_id : Int32
right_fallthrough_id : Int32
equality_proof : OiConsecutiveInputEqualityProofFact
interference_fact : OiConsecutiveInputInterferenceFact
}
///|
fn OiStateWriteSetFact::empty() -> OiStateWriteSetFact {
{ local_writes: [], global_writes: [], }
}
///|
fn OiStateWriteSetFact::add_local(
self : OiStateWriteSetFact,
local_idx : Int32,
) -> Unit {
for value in self.local_writes {
if value == local_idx {
return
}
}
self.local_writes.push(local_idx)
}
///|
fn OiStateWriteSetFact::add_global(
self : OiStateWriteSetFact,
global_idx : Int32,
) -> Unit {
for value in self.global_writes {
if value == global_idx {
return
}
}
self.global_writes.push(global_idx)
}
///|
fn OiStateWriteSetFact::append_from(
self : OiStateWriteSetFact,
other : OiStateWriteSetFact,
) -> Unit {
for local_idx in other.local_writes {
self.add_local(local_idx)
}
for global_idx in other.global_writes {
self.add_global(global_idx)
}
}
///|
fn OiStateWriteSetFact::writes_local(
self : OiStateWriteSetFact,
local_idx : Int32,
) -> Bool {
for value in self.local_writes {
if value == local_idx {
return true
}
}
false
}
///|
fn OiStateWriteSetFact::writes_global(
self : OiStateWriteSetFact,
global_idx : Int32,
module_ctx~ : @ir.HotModuleContext? = None,
) -> Bool {
for value in self.global_writes {
if !globals_are_provably_disjoint(module_ctx, value, global_idx) {
return true
}
}
false
}
///|
fn OiStateWriteSetFact::has_local_writes(self : OiStateWriteSetFact) -> Bool {
self.local_writes.length() != 0
}
///|
fn OiStateWriteSetFact::has_global_writes(self : OiStateWriteSetFact) -> Bool {
self.global_writes.length() != 0
}
///|
fn OiStateWriteSetFact::from_value_subtree(
func : @ir.HotFunc,
node_id : Int32,
) -> OiStateWriteSetFact {
// Binaryen's `EffectAnalyzer::invalidates` reasons about precise local/global
// locations even though coarse HOT effect bits conflate reads and writes.
// Keep the represented OI subset as a fact so fallthrough-prefix and
// after-value equality paths share the same source-backed write-set proof.
let fact = OiStateWriteSetFact::empty()
fn walk(id : Int32) -> Unit {
let node = @ir.hot_node_get(func, id)
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => fact.add_local(node.imm0)
@ir.HotOp::GlobalSet => fact.add_global(node.imm0)
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, id); slot = slot + 1 {
let child_id = @ir.hot_child_get(func, id, slot)
// Optional else/catch regions use HOT's absent-node sentinel. They have
// no writes to collect; all present children still require a live node.
if child_id != -1 {
walk(child_id)
}
}
}
walk(node_id)
fact
}
///|
fn OiNodeEffectPurityFact::from_mask(mask : UInt32) -> OiNodeEffectPurityFact {
// Shared OI view of Binaryen `EffectAnalyzer(...).hasUnremovableSideEffects()`
// negative queries: callers that only need the existing HOT effect-mask
// proof should record that as a fact instead of reading raw masks directly.
let kind = if @ir.effects_is_pure(mask) {
NodeEffectPureMask
} else {
NodeEffectImpureMask
}
{ effect_mask: mask, kind, }
}
///|
fn OiNodeEffectPurityFact::from_node_masks(
node_masks : Array[UInt32],
node_id : Int32,
) -> OiNodeEffectPurityFact {
if node_id < 0 || node_id >= node_masks.length() {
{ effect_mask: 0U, kind: NodeEffectMissingMask, }
} else {
OiNodeEffectPurityFact::from_mask(node_masks[node_id])
}
}
///|
fn OiNodeEffectPurityFact::proves_pure(self : OiNodeEffectPurityFact) -> Bool {
ignore(self.effect_mask)
match self.kind {
NodeEffectPureMask => true
NodeEffectImpureMask | NodeEffectMissingMask => false
}
}
///|
fn OiSubtreeReorderEffectFact::empty() -> OiSubtreeReorderEffectFact {
{
effect_mask: 0U,
read_mask: 0U,
write_mask: 0U,
state_writes: OiStateWriteSetFact::empty(),
carries_control: false,
disrupts: false,
has_side_effect: false,
}
}
///|
fn OiSubtreeReorderEffectFact::read_effect_mask() -> UInt32 {
@ir.EFFECT_MASK_LOCAL_STATE |
@ir.EFFECT_MASK_GLOBAL_STATE |
@ir.EFFECT_MASK_MEMORY_READ |
@ir.EFFECT_MASK_TABLE_READ
}
///|
fn OiSubtreeReorderEffectFact::write_mask_from_effects(
effect_mask : UInt32,
state_writes : OiStateWriteSetFact,
) -> UInt32 {
// Binaryen's `EffectAnalyzer::invalidates` separates reads from writes. HOT's
// coarse effect masks already identify memory/table writes and calls, while
// local/global writes need the precise `OiStateWriteSetFact` subtree walk.
let mut writes = 0U
if (effect_mask & (@ir.EFFECT_MASK_MEMORY_WRITE | @ir.EFFECT_MASK_CALL)) != 0U {
writes = writes | @ir.EFFECT_MASK_MEMORY_READ
}
if (effect_mask & (@ir.EFFECT_MASK_TABLE_WRITE | @ir.EFFECT_MASK_CALL)) != 0U {
writes = writes | @ir.EFFECT_MASK_TABLE_READ
}
if (effect_mask & @ir.EFFECT_MASK_CALL) != 0U ||
state_writes.has_global_writes() {
writes = writes | @ir.EFFECT_MASK_GLOBAL_STATE
}
if state_writes.has_local_writes() {
writes = writes | @ir.EFFECT_MASK_LOCAL_STATE
}
writes
}
///|
fn OiSubtreeReorderEffectFact::from_node(
func : @ir.HotFunc,
node_masks : Array[UInt32],
node_id : Int32,
) -> OiSubtreeReorderEffectFact {
// Source-backed subset of Binaryen v130 `EffectAnalyzer::orderedBefore` /
// `invalidates` used by OI operand reordering and consecutive-input equality:
// represent reorderable read regions, precise local/global writes, coarse
// memory/table/call writes, control blockers, and trap/throw side-effect
// hazards as fact fields instead of recomputing scanner-local booleans.
let effect_mask = node_masks[node_id]
let state_writes = OiStateWriteSetFact::from_value_subtree(func, node_id)
let write_mask = OiSubtreeReorderEffectFact::write_mask_from_effects(
effect_mask, state_writes,
)
{
effect_mask,
read_mask: effect_mask & OiSubtreeReorderEffectFact::read_effect_mask(),
write_mask,
state_writes,
carries_control: (effect_mask & @ir.EFFECT_MASK_CONTROL) != 0U,
disrupts: (effect_mask & (@ir.EFFECT_MASK_TRAP | @ir.EFFECT_MASK_THROW)) !=
0U,
has_side_effect: write_mask != 0U ||
(effect_mask & @ir.EFFECT_MASK_THROW) != 0U,
}
}
///|
fn OiSubtreeReorderEffectFact::can_swap_with(
self : OiSubtreeReorderEffectFact,
other : OiSubtreeReorderEffectFact,
) -> Bool {
if self.carries_control || other.carries_control {
return false
}
let region_conflict = ((self.read_mask | self.write_mask) & other.write_mask) !=
0U ||
((other.read_mask | other.write_mask) & self.write_mask) != 0U
if region_conflict {
return false
}
if (self.disrupts && other.has_side_effect) ||
(other.disrupts && self.has_side_effect) {
return false
}
true
}
///|
fn optimize_instructions_mask_without_local_state(mask : UInt32) -> UInt32 {
if (mask & @ir.EFFECT_MASK_LOCAL_STATE) != 0U {
mask ^ @ir.EFFECT_MASK_LOCAL_STATE
} else {
mask
}
}
///|
fn optimize_instructions_mask_without_global_state(mask : UInt32) -> UInt32 {
if (mask & @ir.EFFECT_MASK_GLOBAL_STATE) != 0U {
mask ^ @ir.EFFECT_MASK_GLOBAL_STATE
} else {
mask
}
}
///|
priv struct OiPureScalarEqualTreeShellFact {
branchless_scalar_shell : Bool
}
///|
fn OiPureScalarEqualTreeShellFact::from_op(
op : @ir.HotOp,
) -> OiPureScalarEqualTreeShellFact {
// Source-mapped subset of Binaryen v130 `ExpressionAnalyzer::equal` used by
// OI's consecutive-input equality facts: branchless scalar expression shells
// whose children are recursively deterministic under the enclosing ordered-
// effects check.
match op {
@ir.HotOp::Unary
| @ir.HotOp::Binary
| @ir.HotOp::Compare
| @ir.HotOp::Convert
| @ir.HotOp::Select => { branchless_scalar_shell: true, }
_ => { branchless_scalar_shell: false, }
}
}
///|
fn OiPureScalarEqualTreeShellFact::proves_shell(
self : OiPureScalarEqualTreeShellFact,
) -> Bool {
self.branchless_scalar_shell
}
///|
priv enum OiStableStateReadEqualityKind {
StableStateReadUnsupported
StableStateReadMemorySize
StableStateReadTableSize
}
///|
priv struct OiStableStateReadIndexRelationFact {
left_index : Int32
right_index : Int32
}
///|
fn OiStableStateReadIndexRelationFact::proves_same_index(
self : OiStableStateReadIndexRelationFact,
) -> Bool {
self.left_index == self.right_index
}
///|
priv struct OiStableStateReadEqualityFact {
kind : OiStableStateReadEqualityKind
index_relation : OiStableStateReadIndexRelationFact?
}
///|
fn OiStableStateReadEqualityFact::from_nodes(
left : @ir.HotNode,
right : @ir.HotNode,
) -> OiStableStateReadEqualityFact {
// Binaryen v130 `areConsecutiveInputsEqual` reaches
// `ExpressionAnalyzer::equal` after fallthrough peeling. Deterministic state
// reads with the same index compare equal there, but only after the enclosing
// equality fact has checked that intervening effects are not ordered before
// the right read. Keep the represented memory.size/table.size source kind and
// the same-index relation as explicit facts so state reads remain visible as
// effect-sensitive equality, not pure values or parallel booleans.
match (left.op, right.op) {
(@ir.HotOp::MemorySize, @ir.HotOp::MemorySize) =>
{
kind: StableStateReadMemorySize,
index_relation: Some({ left_index: left.imm0, right_index: right.imm0, }),
}
(@ir.HotOp::TableSize, @ir.HotOp::TableSize) =>
{
kind: StableStateReadTableSize,
index_relation: Some({ left_index: left.imm0, right_index: right.imm0, }),
}
_ => { kind: StableStateReadUnsupported, index_relation: None, }
}
}
///|
fn OiStableStateReadEqualityFact::proves_equal(
self : OiStableStateReadEqualityFact,
) -> Bool {
let same_index = match self.index_relation {
Some(fact) => fact.proves_same_index()
None => false
}
let matching_state_kind = match self.kind {
StableStateReadMemorySize => true
StableStateReadTableSize => true
StableStateReadUnsupported => false
}
same_index && matching_state_kind
}
///|
fn optimize_instructions_scalar_tree_metadata_equal(
func : @ir.HotFunc,
left_id : Int32,
right_id : Int32,
left : @ir.HotNode,
right : @ir.HotNode,
) -> Bool {
if left.op != right.op ||
!OiPureScalarEqualTreeShellFact::from_op(left.op).proves_shell() ||
left.ty != right.ty ||
left.flags != right.flags ||
left.imm1 != right.imm1 ||
left.child_count != right.child_count {
return false
}
match
(
@ir.hot_node_exact_instr(func, left_id),
@ir.hot_node_exact_instr(func, right_id),
) {
(Some(left_instr), Some(right_instr)) => left_instr == right_instr
_ => left.imm0 == right.imm0
}
}
///|
priv enum OiScalarTreeLeafEqualityKind {
ScalarTreeLeafLocalGet
ScalarTreeLeafGlobalGet
ScalarTreeLeafConst
ScalarTreeLeafReferenceLiteral
ScalarTreeLeafStableStateRead
}
///|
priv struct OiScalarTreeLeafEqualityFact {
kind : OiScalarTreeLeafEqualityKind
stable_state_fact : OiStableStateReadEqualityFact?
}
///|
fn OiScalarTreeLeafEqualityFact::from_nodes(
func : @ir.HotFunc,
left_id : Int32,
right_id : Int32,
left : @ir.HotNode,
right : @ir.HotNode,
include_stable_state_read : Bool,
) -> OiScalarTreeLeafEqualityFact? {
match (left.op, right.op) {
(@ir.HotOp::LocalGet, @ir.HotOp::LocalGet) if left.imm0 == right.imm0 =>
return Some({ kind: ScalarTreeLeafLocalGet, stable_state_fact: None, })
(@ir.HotOp::GlobalGet, @ir.HotOp::GlobalGet) if left.imm0 == right.imm0 =>
return Some({ kind: ScalarTreeLeafGlobalGet, stable_state_fact: None, })
(@ir.HotOp::Const, @ir.HotOp::Const) if pass_hot_const_bits_equal(
@ir.hot_const_get(func, left.imm0),
@ir.hot_const_get(func, right.imm0),
) => return Some({ kind: ScalarTreeLeafConst, stable_state_fact: None, })
_ => ()
}
match
(
@ir.hot_node_exact_instr(func, left_id),
@ir.hot_node_exact_instr(func, right_id),
) {
(
Some(@lib.Instruction::RefNull(left_heap)),
Some(@lib.Instruction::RefNull(right_heap)),
) if left_heap == right_heap =>
return Some({
kind: ScalarTreeLeafReferenceLiteral,
stable_state_fact: None,
})
(
Some(@lib.Instruction::RefFunc(left_func)),
Some(@lib.Instruction::RefFunc(right_func)),
) if left_func == right_func =>
return Some({
kind: ScalarTreeLeafReferenceLiteral,
stable_state_fact: None,
})
_ => ()
}
if include_stable_state_read {
let stable_state_fact = OiStableStateReadEqualityFact::from_nodes(
left, right,
)
if stable_state_fact.proves_equal() {
return Some({
kind: ScalarTreeLeafStableStateRead,
stable_state_fact: Some(stable_state_fact),
})
}
}
None
}
///|
fn OiScalarTreeLeafEqualityFact::proves_equal(
self : OiScalarTreeLeafEqualityFact,
) -> Bool {
match self.kind {
ScalarTreeLeafLocalGet
| ScalarTreeLeafGlobalGet
| ScalarTreeLeafConst
| ScalarTreeLeafReferenceLiteral => true
ScalarTreeLeafStableStateRead =>
match self.stable_state_fact {
Some(fact) => fact.proves_equal()
None => false
}
}
}
///|
fn OiScalarTreeLeafEqualityFact::includes_stable_state_read(
self : OiScalarTreeLeafEqualityFact,
) -> Bool {
match self.kind {
ScalarTreeLeafStableStateRead => self.proves_equal()
_ => false
}
}
///|
priv struct OiScalarTreeChildEqualityFact {
children_equal : Bool
includes_stable_state_read : Bool
}
///|
fn OiScalarTreeChildEqualityFact::proves_children_equal(
self : OiScalarTreeChildEqualityFact,
) -> Bool {
self.children_equal
}
///|
fn OiScalarTreeChildEqualityFact::proves_includes_stable_state_read(
self : OiScalarTreeChildEqualityFact,
) -> Bool {
self.includes_stable_state_read
}
///|
priv enum OiScalarTreeEqualityKind {
ScalarTreeEqualityUnsupported
ScalarTreeEqualityLeaf
ScalarTreeEqualityBranchlessShell
}
///|
priv struct OiScalarTreeEqualityFact {
kind : OiScalarTreeEqualityKind
leaf_fact : OiScalarTreeLeafEqualityFact?
shell_fact : OiPureScalarEqualTreeShellFact?
child_fact : OiScalarTreeChildEqualityFact?
}
///|
fn OiScalarTreeEqualityFact::empty() -> OiScalarTreeEqualityFact {
{
kind: ScalarTreeEqualityUnsupported,
leaf_fact: None,
shell_fact: None,
child_fact: None,
}
}
///|
fn OiScalarTreeEqualityFact::from_nodes(
func : @ir.HotFunc,
left_id : Int32,
right_id : Int32,
include_stable_state_read~ : Bool = false,
depth~ : Int32 = 0,
) -> OiScalarTreeEqualityFact {
// Source-backed subset of Binaryen v130 `ExpressionAnalyzer::equal` used by
// OI's equality facts. Keep leaf kind, branchless scalar-shell equality, and
// deterministic memory/table-size reads as nested facts so callers can
// distinguish pure equality from effect-sensitive equality that still needs an
// enclosing `orderedBefore` / invalidation check.
if depth > 16 {
return OiScalarTreeEqualityFact::empty()
}
let left = @ir.hot_node_get(func, left_id)
let right = @ir.hot_node_get(func, right_id)
match
OiScalarTreeLeafEqualityFact::from_nodes(
func, left_id, right_id, left, right, include_stable_state_read,
) {
Some(leaf_fact) =>
return {
kind: ScalarTreeEqualityLeaf,
leaf_fact: Some(leaf_fact),
shell_fact: None,
child_fact: None,
}
None => ()
}
if !optimize_instructions_scalar_tree_metadata_equal(
func, left_id, right_id, left, right,
) {
return OiScalarTreeEqualityFact::empty()
}
let mut children_equal = true
let mut includes_stable_state_read = false
for slot = 0; slot < left.child_count; slot = slot + 1 {
let child_fact = OiScalarTreeEqualityFact::from_nodes(
func,
@ir.hot_child_get(func, left_id, slot),
@ir.hot_child_get(func, right_id, slot),
include_stable_state_read~,
depth=depth + 1,
)
if !child_fact.proves_equal() {
children_equal = false
}
if child_fact.proves_includes_stable_state_read() {
includes_stable_state_read = true
}
}
{
kind: ScalarTreeEqualityBranchlessShell,
leaf_fact: None,
shell_fact: Some(OiPureScalarEqualTreeShellFact::from_op(left.op)),
child_fact: Some({ children_equal, includes_stable_state_read, }),
}
}
///|
fn OiScalarTreeEqualityFact::proves_equal(
self : OiScalarTreeEqualityFact,
) -> Bool {
match self.kind {
ScalarTreeEqualityLeaf =>
match self.leaf_fact {
Some(fact) => fact.proves_equal()
None => false
}
ScalarTreeEqualityBranchlessShell => {
let shell_equal = match self.shell_fact {
Some(fact) => fact.proves_shell()
None => false
}
let children_equal = match self.child_fact {
Some(fact) => fact.proves_children_equal()
None => false
}
shell_equal && children_equal
}
ScalarTreeEqualityUnsupported => false
}
}
///|
fn OiScalarTreeEqualityFact::proves_includes_stable_state_read(
self : OiScalarTreeEqualityFact,
) -> Bool {
match self.kind {
ScalarTreeEqualityLeaf =>
match self.leaf_fact {
Some(fact) => fact.includes_stable_state_read()
None => false
}
ScalarTreeEqualityBranchlessShell =>
match self.child_fact {
Some(fact) => fact.proves_includes_stable_state_read()
None => false
}
ScalarTreeEqualityUnsupported => false
}
}
///|
fn OiScalarTreeEqualityFact::proves_pure_equal(
self : OiScalarTreeEqualityFact,
) -> Bool {
self.proves_equal() && !self.proves_includes_stable_state_read()
}
///|
fn OiScalarTreeEqualityFact::proves_stable_state_scalar_equal(
self : OiScalarTreeEqualityFact,
) -> Bool {
self.proves_equal() && self.proves_includes_stable_state_read()
}
///|
priv enum OiStableMemoryReadEqualityKind {
StableMemoryReadUnsupported
StableMemoryReadLoad
StableMemoryReadAtomicLoad
}
///|
fn OiStableMemoryReadEqualityKind::from_op_and_instr(
op : @ir.HotOp,
instr : @lib.Instruction,
) -> OiStableMemoryReadEqualityKind {
match op {
@ir.HotOp::Load => StableMemoryReadLoad
@ir.HotOp::Atomic =>
match instr {
@lib.Instruction::I32AtomicLoad(_, _)
| @lib.Instruction::I64AtomicLoad(_, _)
| @lib.Instruction::I32AtomicLoad8U(_, _)
| @lib.Instruction::I32AtomicLoad16U(_, _)
| @lib.Instruction::I64AtomicLoad8U(_, _)
| @lib.Instruction::I64AtomicLoad16U(_, _)
| @lib.Instruction::I64AtomicLoad32U(_, _) => StableMemoryReadAtomicLoad
_ => StableMemoryReadUnsupported
}
_ => StableMemoryReadUnsupported
}
}
///|
priv struct OiStableMemoryReadInstructionRelationFact {
left_instr : @lib.Instruction
right_instr : @lib.Instruction
}
///|
fn OiStableMemoryReadInstructionRelationFact::proves_same_instruction(
self : OiStableMemoryReadInstructionRelationFact,
) -> Bool {
self.left_instr == self.right_instr
}
///|
priv struct OiStableMemoryReadEqualityFact {
kind : OiStableMemoryReadEqualityKind
instruction_relation : OiStableMemoryReadInstructionRelationFact?
address_fact : OiScalarTreeEqualityFact
}
///|
fn OiStableMemoryReadEqualityFact::empty() -> OiStableMemoryReadEqualityFact {
{
kind: StableMemoryReadUnsupported,
instruction_relation: None,
address_fact: OiScalarTreeEqualityFact::empty(),
}
}
///|
fn OiStableMemoryReadEqualityFact::from_nodes(
ctx : HotPassContext,
func : @ir.HotFunc,
left_id : Int32,
right_id : Int32,
) -> OiStableMemoryReadEqualityFact {
// Equal addresses establish equal reads only for an unshared memory with
// no intervening writes. Shared reads can observe another agent's store.
let left = @ir.hot_node_get(func, left_id)
let right = @ir.hot_node_get(func, right_id)
if left.child_count != 1 || right.child_count != 1 {
return OiStableMemoryReadEqualityFact::empty()
}
match
(
@ir.hot_node_exact_instr(func, left_id),
@ir.hot_node_exact_instr(func, right_id),
) {
(Some(left_instr), Some(right_instr)) => {
let memarg = match left_instr {
@lib.I32Load(arg)
| @lib.I64Load(arg)
| @lib.F32Load(arg)
| @lib.F64Load(arg)
| @lib.I32Load8S(arg)
| @lib.I32Load8U(arg)
| @lib.I32Load16S(arg)
| @lib.I32Load16U(arg)
| @lib.I64Load8S(arg)
| @lib.I64Load8U(arg)
| @lib.I64Load16S(arg)
| @lib.I64Load16U(arg)
| @lib.I64Load32S(arg)
| @lib.I64Load32U(arg)
| @lib.I32AtomicLoad(_, arg)
| @lib.I64AtomicLoad(_, arg)
| @lib.I32AtomicLoad8U(_, arg)
| @lib.I32AtomicLoad16U(_, arg)
| @lib.I64AtomicLoad8U(_, arg)
| @lib.I64AtomicLoad16U(_, arg)
| @lib.I64AtomicLoad32U(_, arg) => arg
_ => return OiStableMemoryReadEqualityFact::empty()
}
let @lib.MemArg(_, selected_memory, _) = memarg
let unshared = match ctx.module_ctx {
Some(module_ctx) =>
match
@ir.hot_module_resolve_mem_type(
module_ctx,
selected_memory.unwrap_or(@lib.MemIdx::new(0U)),
) {
Some(@lib.MemType(_, false)) => true
_ => false
}
None => false
}
if !unshared {
return OiStableMemoryReadEqualityFact::empty()
}
let left_kind = OiStableMemoryReadEqualityKind::from_op_and_instr(
left.op,
left_instr,
)
let right_kind = OiStableMemoryReadEqualityKind::from_op_and_instr(
right.op,
right_instr,
)
let kind = match (left_kind, right_kind) {
(StableMemoryReadLoad, StableMemoryReadLoad) => StableMemoryReadLoad
(StableMemoryReadAtomicLoad, StableMemoryReadAtomicLoad) =>
StableMemoryReadAtomicLoad
_ => StableMemoryReadUnsupported
}
match kind {
StableMemoryReadUnsupported => OiStableMemoryReadEqualityFact::empty()
StableMemoryReadLoad | StableMemoryReadAtomicLoad =>
{
kind,
instruction_relation: Some({ left_instr, right_instr, }),
address_fact: OiScalarTreeEqualityFact::from_nodes(
func,
@ir.hot_child_get(func, left_id, 0),
@ir.hot_child_get(func, right_id, 0),
include_stable_state_read=true,
),
}
}
}
_ => OiStableMemoryReadEqualityFact::empty()
}
}
///|
fn OiStableMemoryReadEqualityFact::proves_equal(
self : OiStableMemoryReadEqualityFact,
) -> Bool {
let matching_read_kind = match self.kind {
StableMemoryReadLoad | StableMemoryReadAtomicLoad => true
StableMemoryReadUnsupported => false
}
let same_instruction = match self.instruction_relation {
Some(fact) => fact.proves_same_instruction()
None => false
}
matching_read_kind && same_instruction && self.address_fact.proves_equal()
}
///|
fn optimize_instructions_left_fallthrough_input_for_equality(
func : @ir.HotFunc,
node_masks : Array[UInt32],
node_id : Int32,
right_fallthrough_id : Int32,
) -> OiLeftFallthroughInputFact {
let node = @ir.hot_node_get(func, node_id)
if node.child_count == 1 {
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull)
| Some(@lib.Instruction::RefCast(_, _, _)) =>
// Binaryen v131's fallthrough equality sees through reference checks:
// if the wrapper returns, it returns its child unchanged. The wrapper
// itself remains in the selected preserved operand so its trap is not
// lost merely because equality was proved.
return optimize_instructions_left_fallthrough_input_for_equality(
func,
node_masks,
@ir.hot_child_get(func, node_id, 0),
right_fallthrough_id,
)
_ => ()
}
}
match node.op {
@ir.HotOp::Block if node.child_count > 0 =>
optimize_instructions_left_fallthrough_input_for_equality(
func,
node_masks,
@ir.hot_child_get(func, node_id, node.child_count - 1),
right_fallthrough_id,
)
@ir.HotOp::LocalTee if node.child_count > 0 => {
let right = @ir.hot_node_get(func, right_fallthrough_id)
match right.op {
@ir.HotOp::LocalGet if node.imm0 == right.imm0 =>
return {
fallthrough_id: node_id,
after_value_effect_mask: 0U,
after_value_write_mask: 0U,
after_value_state_writes: OiStateWriteSetFact::empty(),
unsupported_after_value_control: false,
}
_ => ()
}
let child_id = @ir.hot_child_get(func, node_id, 0)
let nested = optimize_instructions_left_fallthrough_input_for_equality(
func, node_masks, child_id, right_fallthrough_id,
)
let state_writes = OiStateWriteSetFact::empty()
state_writes.append_from(nested.after_value_state_writes)
state_writes.add_local(node.imm0)
{
fallthrough_id: nested.fallthrough_id,
after_value_effect_mask: nested.after_value_effect_mask |
@ir.EFFECT_MASK_LOCAL_STATE,
after_value_write_mask: nested.after_value_write_mask |
@ir.EFFECT_MASK_LOCAL_STATE,
after_value_state_writes: state_writes,
unsupported_after_value_control: nested.unsupported_after_value_control,
}
}
@ir.HotOp::BrIf =>
// Binaryen v130 `areConsecutiveInputsEqual` peels a value-carrying left
// `br_if` to its payload and records the condition as an after-value
// interfering effect. Keep Starshine's equality fact closed here until
// value-carrying `br_if` operands can pass HOT/pass verification; see
// docs/wiki/binaryen/passes/optimize-instructions/implementation-structure-and-tests.md.
{
fallthrough_id: node_id,
after_value_effect_mask: 0U,
after_value_write_mask: 0U,
after_value_state_writes: OiStateWriteSetFact::empty(),
unsupported_after_value_control: true,
}
_ =>
{
fallthrough_id: node_id,
after_value_effect_mask: 0U,
after_value_write_mask: 0U,
after_value_state_writes: OiStateWriteSetFact::empty(),
unsupported_after_value_control: false,
}
}
}
///|
fn optimize_instructions_right_fallthrough_input_for_equality(
func : @ir.HotFunc,
node_masks : Array[UInt32],
node_id : Int32,
) -> OiRightFallthroughInputFact {
let node = @ir.hot_node_get(func, node_id)
if node.child_count == 1 {
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull)
| Some(@lib.Instruction::RefCast(_, _, _)) =>
return optimize_instructions_right_fallthrough_input_for_equality(
func,
node_masks,
@ir.hot_child_get(func, node_id, 0),
)
_ => ()
}
}
match node.op {
@ir.HotOp::Block if node.child_count > 0 => {
let mut prefix_effect_mask = 0U
let mut prefix_write_mask = 0U
let prefix_state_writes = OiStateWriteSetFact::empty()
for slot = 0; slot < node.child_count - 1; slot = slot + 1 {
let child_id = @ir.hot_child_get(func, node_id, slot)
let child_mask = node_masks[child_id]
if (child_mask & @ir.EFFECT_MASK_CONTROL) != 0U {
return {
fallthrough_id: node_id,
prefix_effect_mask: 0U,
prefix_write_mask: 0U,
prefix_state_writes: OiStateWriteSetFact::empty(),
unsupported_prefix_control: true,
peeled_local_tee: false,
}
}
let child_effect_fact = OiSubtreeReorderEffectFact::from_node(
func, node_masks, child_id,
)
prefix_effect_mask = prefix_effect_mask | child_effect_fact.effect_mask
prefix_write_mask = prefix_write_mask | child_effect_fact.write_mask
prefix_state_writes.append_from(child_effect_fact.state_writes)
}
let nested = optimize_instructions_right_fallthrough_input_for_equality(
func,
node_masks,
@ir.hot_child_get(func, node_id, node.child_count - 1),
)
prefix_state_writes.append_from(nested.prefix_state_writes)
{
fallthrough_id: nested.fallthrough_id,
prefix_effect_mask: prefix_effect_mask | nested.prefix_effect_mask,
prefix_write_mask: prefix_write_mask | nested.prefix_write_mask,
prefix_state_writes,
unsupported_prefix_control: nested.unsupported_prefix_control,
peeled_local_tee: nested.peeled_local_tee,
}
}
@ir.HotOp::LocalTee if node.child_count > 0 => {
// Binaryen v130 `areConsecutiveInputsEqual` peels the right-side
// immediate fallthrough with `FallthroughBehavior::AllowTeeBrIf`, so a
// right `local.tee` contributes its value child to the equality proof. The
// tee write is not an intervening prefix effect for the right value; it is
// preserved later when the original operand is dropped.
let nested = optimize_instructions_right_fallthrough_input_for_equality(
func,
node_masks,
@ir.hot_child_get(func, node_id, 0),
)
{
fallthrough_id: nested.fallthrough_id,
prefix_effect_mask: nested.prefix_effect_mask,
prefix_write_mask: nested.prefix_write_mask,
prefix_state_writes: nested.prefix_state_writes,
unsupported_prefix_control: nested.unsupported_prefix_control,
peeled_local_tee: true,
}
}
_ =>
{
fallthrough_id: node_id,
prefix_effect_mask: 0U,
prefix_write_mask: 0U,
prefix_state_writes: OiStateWriteSetFact::empty(),
unsupported_prefix_control: false,
peeled_local_tee: false,
}
}
}
///|
fn OiInterveningEffectFact::from_fallthrough_facts(
left : OiLeftFallthroughInputFact,
right : OiRightFallthroughInputFact,
) -> OiInterveningEffectFact {
let state_writes = OiStateWriteSetFact::empty()
state_writes.append_from(left.after_value_state_writes)
state_writes.append_from(right.prefix_state_writes)
{
effect_mask: left.after_value_effect_mask | right.prefix_effect_mask,
write_mask: left.after_value_write_mask | right.prefix_write_mask,
state_writes,
unsupported_control: left.unsupported_after_value_control ||
right.unsupported_prefix_control,
}
}
///|
fn OiInterveningEffectFact::ordered_before_right_fallthrough(
self : OiInterveningEffectFact,
func : @ir.HotFunc,
node_masks : Array[UInt32],
right_id : Int32,
module_ctx~ : @ir.HotModuleContext? = None,
) -> Bool {
// Represented subset of Binaryen v130
// `EffectAnalyzer::orderedBefore(effects(right))` / `invalidates` used by
// OI consecutive-input equality. Coarse effect bits prove memory/table/call
// interference, while `OiStateWriteSetFact` proves exact local/global misses
// so `local.get $x` can be reused across writes to other locals and
// `global.get $x` can be reused across non-call writes to provably
// disjoint global storage.
if self.unsupported_control {
return true
}
let right_effect_fact = OiSubtreeReorderEffectFact::from_node(
func, node_masks, right_id,
)
let right_mask = right_effect_fact.effect_mask
let right_write_mask = right_effect_fact.write_mask
let mut effect_reads = self.effect_mask &
OiSubtreeReorderEffectFact::read_effect_mask()
let mut effective_write_mask = self.write_mask
let mut right_reads = right_effect_fact.read_mask
let mut effective_right_write_mask = right_write_mask
let right = @ir.hot_node_get(func, right_id)
match right.op {
@ir.HotOp::LocalGet if !self.state_writes.writes_local(right.imm0) => {
effective_write_mask = optimize_instructions_mask_without_local_state(
effective_write_mask,
)
effect_reads = optimize_instructions_mask_without_local_state(
effect_reads,
)
right_reads = optimize_instructions_mask_without_local_state(right_reads)
effective_right_write_mask = optimize_instructions_mask_without_local_state(
effective_right_write_mask,
)
}
@ir.HotOp::GlobalGet if (self.effect_mask & @ir.EFFECT_MASK_CALL) == 0U &&
!self.state_writes.writes_global(right.imm0, module_ctx~) => {
effective_write_mask = optimize_instructions_mask_without_global_state(
effective_write_mask,
)
effect_reads = optimize_instructions_mask_without_global_state(
effect_reads,
)
right_reads = optimize_instructions_mask_without_global_state(right_reads)
effective_right_write_mask = optimize_instructions_mask_without_global_state(
effective_right_write_mask,
)
}
_ => ()
}
let region_conflict = (
(effect_reads | effective_write_mask) & effective_right_write_mask
) !=
0U ||
((right_reads | effective_right_write_mask) & effective_write_mask) != 0U
if region_conflict {
return true
}
let effects_disrupt = (
self.effect_mask & (@ir.EFFECT_MASK_TRAP | @ir.EFFECT_MASK_THROW)
) !=
0U
let right_side_effect = right_write_mask != 0U ||
(right_mask & @ir.EFFECT_MASK_THROW) != 0U
effects_disrupt && right_side_effect
}
///|
priv struct OiRightLocalTeeFallthroughValueFact {
value_id : Int32
}
///|
fn OiRightLocalTeeFallthroughValueFact::from_node(
func : @ir.HotFunc,
right_id : Int32,
) -> OiRightLocalTeeFallthroughValueFact? {
// Binaryen v130 `areConsecutiveInputsEqual` peels the right operand with
// `Properties::getImmediateFallthrough(...AllowTeeBrIf)`, exposing the value
// child of a direct `local.tee`. Keep that shell/value proof separate from
// the later scalar equality proof so the equality fact no longer stores
// parallel local.tee and value-match booleans.
let right = @ir.hot_node_get(func, right_id)
match right.op {
@ir.HotOp::LocalTee if right.child_count == 1 =>
Some({ value_id: @ir.hot_child_get(func, right_id, 0), })
_ => None
}
}
///|
priv struct OiRightLocalTeeFallthroughEqualityFact {
value_fact : OiRightLocalTeeFallthroughValueFact?
equality_fact : OiScalarTreeEqualityFact?
}
///|
fn OiRightLocalTeeFallthroughEqualityFact::from_nodes(
func : @ir.HotFunc,
left_fallthrough_id : Int32,
right_id : Int32,
) -> OiRightLocalTeeFallthroughEqualityFact {
// The currently represented Starshine subset is intentionally narrow: direct
// right `local.tee` values that match the left fallthrough by pure scalar
// equality. The tee write is intentionally not an intervening prefix effect
// because it happens after the returned value is produced and is preserved
// when the original right operand is later dropped.
let value_fact = OiRightLocalTeeFallthroughValueFact::from_node(
func, right_id,
)
match value_fact {
Some(fact) =>
{
value_fact: Some(fact),
equality_fact: Some(
OiScalarTreeEqualityFact::from_nodes(
func,
left_fallthrough_id,
fact.value_id,
),
),
}
None => { value_fact: None, equality_fact: None, }
}
}
///|
fn OiRightLocalTeeFallthroughEqualityFact::proves_equal(
self : OiRightLocalTeeFallthroughEqualityFact,
) -> Bool {
match (self.value_fact, self.equality_fact) {
(Some(_), Some(fact)) => fact.proves_pure_equal()
_ => false
}
}
///|
fn OiConsecutiveInputEqualityProofFact::from_source_facts(
tee_and_get : Bool,
scalar_tree_fact : OiScalarTreeEqualityFact,
stable_state_read_fact : OiStableStateReadEqualityFact,
stable_state_scalar_tree_fact : OiScalarTreeEqualityFact,
stable_memory_read_fact : OiStableMemoryReadEqualityFact,
right_local_tee_fact : OiRightLocalTeeFallthroughEqualityFact,
idempotent_call_fact : OiIdempotentCallEqualityFact,
) -> OiConsecutiveInputEqualityProofFact {
// Binaryen v130 `areConsecutiveInputsEqual` accepts any one represented
// equality source after fallthrough peeling, then checks intervening effects
// separately. Keep the selected proof as a single nested fact instead of
// carrying a parallel set of scanner booleans on the aggregate equality fact.
let kind = if tee_and_get {
ConsecutiveInputLocalTeeAndGet
} else if scalar_tree_fact.proves_pure_equal() {
ConsecutiveInputPureScalarTree
} else if stable_state_read_fact.proves_equal() {
ConsecutiveInputStableStateRead
} else if stable_state_scalar_tree_fact.proves_stable_state_scalar_equal() {
ConsecutiveInputStableStateScalarTree
} else if stable_memory_read_fact.proves_equal() {
ConsecutiveInputStableMemoryRead
} else if right_local_tee_fact.proves_equal() {
ConsecutiveInputRightLocalTeeFallthrough
} else if idempotent_call_fact.proves_equal() {
ConsecutiveInputIdempotentCall
} else {
ConsecutiveInputNoKnownEquality
}
{ kind, }
}
///|
fn OiConsecutiveInputEqualityProofFact::proves_equal(
self : OiConsecutiveInputEqualityProofFact,
) -> Bool {
match self.kind {
ConsecutiveInputNoKnownEquality => false
ConsecutiveInputLocalTeeAndGet
| ConsecutiveInputStoredScalarValue
| ConsecutiveInputPureScalarTree
| ConsecutiveInputStableStateRead
| ConsecutiveInputStableStateScalarTree
| ConsecutiveInputStableMemoryRead
| ConsecutiveInputRightLocalTeeFallthrough
| ConsecutiveInputIdempotentCall => true
}
}
///|
fn OiConsecutiveInputInterferenceFact::from_intervening_effect(
func : @ir.HotFunc,
node_masks : Array[UInt32],
right_fallthrough_id : Int32,
intervening_effect_fact : OiInterveningEffectFact,
module_ctx~ : @ir.HotModuleContext? = None,
) -> OiConsecutiveInputInterferenceFact {
// Keep the Binaryen `EffectAnalyzer::orderedBefore(effects(right))` blocker
// as its own nested fact so equality proof selection and effect invalidation
// remain independent source-backed decisions.
let kind = if intervening_effect_fact.ordered_before_right_fallthrough(
func,
node_masks,
right_fallthrough_id,
module_ctx~,
) {
ConsecutiveInputEffectsOrderedBeforeRight
} else {
ConsecutiveInputEffectsIndependent
}
{ kind, }
}
///|
// Resolve only the value stored at this read, never a function-wide current
// value. Expanded LocalGraph covers operand-nested control; ambiguous joins
// do not establish identity, and the bound rejects copy cycles.
fn oi_stored_scalar_source(
graph : @ir.HotLocalGraph,
func : @ir.HotFunc,
input : Int32,
) -> Int32? {
let mut node = input
for _ in 0..<16 {
if node < 0 || !@ir.hot_node_is_live(func, node) {
return None
}
let data = @ir.hot_node_get(func, node)
match data.op {
@ir.HotOp::LocalGet => {
let sources = @ir.local_graph_reaching_sources_for_get(graph, node)
if sources.length() != 1 {
return None
}
match sources[0] {
@ir.HotLocalGraphSource::SetSource(write) => {
if @ir.hot_child_count(func, write) != 1 {
return None
}
node = @ir.hot_child_get(func, write, 0)
}
@ir.HotLocalGraphSource::EntrySource(local_id) =>
return Some(-local_id - 1)
}
}
@ir.HotOp::LocalTee => node = @ir.hot_child_get(func, node, 0)
_ => return Some(node)
}
}
None
}
///|
fn OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
left_id : Int32,
right_id : Int32,
) -> OiConsecutiveInputEqualityFact {
let effects = pass_require_effects(ctx, func)
let right_fallthrough = optimize_instructions_right_fallthrough_input_for_equality(
func,
effects.node_masks,
right_id,
)
let right_fallthrough_id = right_fallthrough.fallthrough_id
let left_fallthrough = optimize_instructions_left_fallthrough_input_for_equality(
func,
effects.node_masks,
left_id,
right_fallthrough_id,
)
let left_fallthrough_id = left_fallthrough.fallthrough_id
let left = @ir.hot_node_get(func, left_fallthrough_id)
let right = @ir.hot_node_get(func, right_fallthrough_id)
let tee_and_get = match (left.op, right.op) {
(@ir.HotOp::LocalTee, @ir.HotOp::LocalGet) => left.imm0 == right.imm0
_ => false
}
let scalar_tree_fact = OiScalarTreeEqualityFact::from_nodes(
func, left_fallthrough_id, right_fallthrough_id,
)
let state_scalar_tree_fact = OiScalarTreeEqualityFact::from_nodes(
func,
left_fallthrough_id,
right_fallthrough_id,
include_stable_state_read=true,
)
let stable_state_read_fact = OiStableStateReadEqualityFact::from_nodes(
left, right,
)
let stable_memory_read_fact = OiStableMemoryReadEqualityFact::from_nodes(
ctx, func, left_fallthrough_id, right_fallthrough_id,
)
let right_local_tee_fact = OiRightLocalTeeFallthroughEqualityFact::from_nodes(
func, left_fallthrough_id, right_id,
)
let idempotent_call_fact = OiIdempotentCallEqualityFact::from_nodes(
ctx, func, left_fallthrough_id, right_fallthrough_id,
)
let ordinary_proof = OiConsecutiveInputEqualityProofFact::from_source_facts(
tee_and_get, scalar_tree_fact, stable_state_read_fact, state_scalar_tree_fact,
stable_memory_read_fact, right_local_tee_fact, idempotent_call_fact,
)
let equality_proof : OiConsecutiveInputEqualityProofFact = if !ordinary_proof.proves_equal() &&
left.op is @ir.HotOp::LocalGet &&
right.op is @ir.HotOp::LocalGet &&
@ir.hot_node_type(func, left_fallthrough_id) ==
@ir.hot_node_type(func, right_fallthrough_id) {
let graph = @ir.local_graph_build(
func,
@ir.cfg_build(func, expand_operand_control=true),
)
match
(
oi_stored_scalar_source(graph, func, left_fallthrough_id),
oi_stored_scalar_source(graph, func, right_fallthrough_id),
) {
(Some(a), Some(b)) if a == b =>
{ kind: ConsecutiveInputStoredScalarValue, }
_ => ordinary_proof
}
} else {
ordinary_proof
}
let intervening_effect_fact = OiInterveningEffectFact::from_fallthrough_facts(
left_fallthrough, right_fallthrough,
)
let interference_fact = OiConsecutiveInputInterferenceFact::from_intervening_effect(
func,
effects.node_masks,
right_fallthrough_id,
intervening_effect_fact,
module_ctx=ctx.module_ctx,
)
{
left_fallthrough_id,
right_fallthrough_id,
equality_proof,
interference_fact,
}
}
///|
fn OiConsecutiveInputEqualityFact::proves_equal(
self : OiConsecutiveInputEqualityFact,
func : @ir.HotFunc,
) -> Bool {
ignore(func)
ignore(self.left_fallthrough_id)
ignore(self.right_fallthrough_id)
match self.interference_fact.kind {
ConsecutiveInputEffectsOrderedBeforeRight => false
ConsecutiveInputEffectsIndependent => self.equality_proof.proves_equal()
}
}
///|
priv struct OiBulkMemoryConstSizeFact {
size : Int32
}
///|
priv struct OiBulkMemoryFact {
dst_id : Int32
src_or_value_id : Int32
size_id : Int32
const_size_fact : OiBulkMemoryConstSizeFact?
dst_mem_idx : Int32
src_mem_idx : Int32
}
///|
priv struct OiBulkMemorySameAddressCopyDropFact {
bulk_fact : OiBulkMemoryFact
}
///|
priv struct OiBulkMemoryZeroSizeDropFact {
bulk_fact : OiBulkMemoryFact
}
///|
fn OiBulkMemoryConstSizeFact::from_size_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
size_id : Int32,
) -> OiBulkMemoryConstSizeFact? {
match optimize_instructions_const_memory_size(func, dead, size_id) {
Some(size) => Some({ size, })
None => None
}
}
///|
fn OiBulkMemoryFact::from_copy(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBulkMemoryFact? {
let node = @ir.hot_node_get(func, node_id)
if node.child_count != 3 {
return None
}
let size_id = @ir.hot_child_get(func, node_id, 2)
Some({
dst_id: @ir.hot_child_get(func, node_id, 0),
src_or_value_id: @ir.hot_child_get(func, node_id, 1),
size_id,
const_size_fact: OiBulkMemoryConstSizeFact::from_size_operand(
func, dead, size_id,
),
dst_mem_idx: node.imm0,
src_mem_idx: node.imm1,
})
}
///|
fn OiBulkMemoryFact::from_fill(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBulkMemoryFact? {
let node = @ir.hot_node_get(func, node_id)
if node.child_count != 3 {
return None
}
let size_id = @ir.hot_child_get(func, node_id, 2)
Some({
dst_id: @ir.hot_child_get(func, node_id, 0),
src_or_value_id: @ir.hot_child_get(func, node_id, 1),
size_id,
const_size_fact: OiBulkMemoryConstSizeFact::from_size_operand(
func, dead, size_id,
),
dst_mem_idx: node.imm0,
src_mem_idx: node.imm0,
})
}
///|
fn OiBulkMemoryFact::dst_memarg(self : OiBulkMemoryFact) -> @lib.MemArg {
optimize_instructions_memarg_for_mem_idx(self.dst_mem_idx)
}
///|
fn OiBulkMemoryFact::src_memarg(self : OiBulkMemoryFact) -> @lib.MemArg {
optimize_instructions_memarg_for_mem_idx(self.src_mem_idx)
}
///|
fn OiBulkMemoryFact::dest_and_source_are_consecutive_equal_address(
self : OiBulkMemoryFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Bool {
// Binaryen v130 `areConsecutiveInputsEqual` compares adjacent operands via
// fallthrough facts, plus a deliberate fast path for a fallthrough
// `local.tee` immediately followed by a matching `local.get`. Starshine keeps
// this fact intentionally narrower than Binaryen's full effect-interference
// helper: right-operand peeling and non-matching left `local.tee` peeling carry
// accumulated intervening effects, including precise local-write ids for the
// direct local/global-get fallthrough subset and conservative closure for
// nested pure scalar trees. Equality currently covers matching tee/get,
// structurally identical scalar trees made of local.get, global.get,
// constants, unary, binary, compare, and convert nodes, effect-sensitive
// same-index memory.size/table.size state reads, scalar trees with those
// deterministic state-read leaves, and same-access load/atomic-load reads
// whose address proofs include deterministic state-read leaves.
OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx,
func,
self.dst_id,
self.src_or_value_id,
).proves_equal(func)
}
///|
fn OiBulkMemorySameAddressCopyDropFact::from_copy(
ctx : HotPassContext,
func : @ir.HotFunc,
bulk_fact : OiBulkMemoryFact,
) -> OiBulkMemorySameAddressCopyDropFact? {
// Binaryen v130 drops same-address memory.copy only after the trap-relaxed
// flags make the source/destination bounds checks irrelevant, the memories
// match, and `areConsecutiveInputsEqual` proves the address operands equal.
// Keep that as a drop fact rather than a helper-style `can_drop` predicate.
if !ctx.traps_never_happen && !ctx.ignore_implicit_traps {
return None
}
if bulk_fact.dst_mem_idx != bulk_fact.src_mem_idx {
return None
}
if !bulk_fact.dest_and_source_are_consecutive_equal_address(ctx, func) {
return None
}
Some({ bulk_fact, })
}
///|
fn OiBulkMemoryZeroSizeDropFact::from_bulk(
ctx : HotPassContext,
func : @ir.HotFunc,
bulk_fact : OiBulkMemoryFact,
) -> OiBulkMemoryZeroSizeDropFact? {
// Binaryen v130 can erase zero-size bulk memory only in trap-relaxed mode.
// The represented HOT route first records the dead-aware constant-size fact;
// the exact fallback preserves the previous live-constant boundary without
// using `-1` as a nonconstant-size sentinel.
if !ctx.traps_never_happen && !ctx.ignore_implicit_traps {
return None
}
match bulk_fact.const_size_fact {
Some(fact) if fact.size == 0 => return Some({ bulk_fact, })
_ => ()
}
match optimize_instructions_const_memory_size_exact(func, bulk_fact.size_id) {
Some(0) => Some({ bulk_fact, })
_ => None
}
}
///|
priv struct OiTinyMemoryCopyFact {
size : Int32
}
///|
fn OiTinyMemoryCopyFact::from_size(
ctx : HotPassContext,
size : Int32,
) -> OiTinyMemoryCopyFact? {
match size {
1 | 2 | 4 | 8 => Some({ size, })
16 =>
// Binaryen v130 `optimizeMemoryCopy` only emits the SIMD 16-byte
// load/store lane when `shrinkLevel == 0`; higher shrink levels keep the
// bulk-memory spelling to avoid the extra bytes. The module contract
// must also permit the new SIMD load and store.
if ctx.shrink_level == 0 && ctx.allows_simd_introduction {
Some({ size, })
} else {
None
}
_ => None
}
}
///|
fn OiTinyMemoryCopyFact::load_type(self : OiTinyMemoryCopyFact) -> @lib.ValType {
match self.size {
8 => @lib.ValType::i64()
16 => @lib.ValType::v128()
_ => @lib.ValType::i32()
}
}
///|
fn OiTinyMemoryCopyFact::load_instr(
self : OiTinyMemoryCopyFact,
memarg : @lib.MemArg,
) -> @lib.Instruction {
match self.size {
1 => @lib.Instruction::i32_load8u(memarg)
2 => @lib.Instruction::i32_load16u(memarg)
4 => @lib.Instruction::i32_load(memarg)
8 => @lib.Instruction::i64_load(memarg)
16 => @lib.Instruction::v128_load(memarg)
_ => abort("unreachable tiny memory.copy load fact")
}
}
///|
fn OiTinyMemoryCopyFact::store_instr(
self : OiTinyMemoryCopyFact,
memarg : @lib.MemArg,
) -> @lib.Instruction {
match self.size {
1 => @lib.Instruction::i32_store8(memarg)
2 => @lib.Instruction::i32_store16(memarg)
4 => @lib.Instruction::i32_store(memarg)
8 => @lib.Instruction::i64_store(memarg)
16 => @lib.Instruction::v128_store(memarg)
_ => abort("unreachable tiny memory.copy store fact")
}
}
///|
fn optimize_instructions_replace_with_store_exact(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
mem_idx : Int32,
address_id : Int32,
value_id : Int32,
instr_for_memarg : (@lib.MemArg) -> @lib.Instruction,
) -> Unit {
let memarg = optimize_instructions_memarg_for_mem_idx(mem_idx)
let temp = @ir.hot_build_store(func, memarg, address_id, value_id)
@ir.hot_node_exact_instr_set(func, temp, instr_for_memarg(memarg))
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_repeated_fill_i32(
raw : Int32,
size : Int32,
) -> @lib.I32? {
let byte = raw.reinterpret_as_uint() & 0xff
match size {
1 => Some(@lib.I32(byte.reinterpret_as_int()))
2 => Some(@lib.I32((byte | (byte << 8)).reinterpret_as_int()))
4 =>
Some(
@lib.I32(
(byte | (byte << 8) | (byte << 16) | (byte << 24)).reinterpret_as_int(),
),
)
_ => None
}
}
///|
fn optimize_instructions_repeated_fill_i64(raw : Int32) -> @lib.I64 {
let byte = (raw.reinterpret_as_uint() & 0xff).to_uint64()
@lib.I64(
(byte |
(byte << 8) |
(byte << 16) |
(byte << 24) |
(byte << 32) |
(byte << 40) |
(byte << 48) |
(byte << 56)).reinterpret_as_int64(),
)
}
///|
fn optimize_instructions_build_repeated_fill_v128_const(
func : @ir.HotFunc,
raw : Int32,
) -> Int32 {
let byte = (raw.reinterpret_as_uint() & 0xff).to_byte()
@ir.hot_build_const_v128_splat(func, byte)
}
///|
priv enum OiTinyMemoryFillStoreFact {
Store8
Store16
Store32
Store64
StoreV128
}
///|
priv enum OiTinyMemoryFillValueFact {
RepeatedByte(Int32)
OriginalI32Value
}
///|
priv struct OiTinyMemoryFillFact {
size : Int32
store_fact : OiTinyMemoryFillStoreFact
value_fact : OiTinyMemoryFillValueFact
}
///|
fn OiTinyMemoryFillStoreFact::from_size(
size : Int32,
) -> OiTinyMemoryFillStoreFact? {
match size {
1 => Some(Store8)
2 => Some(Store16)
4 => Some(Store32)
8 => Some(Store64)
16 => Some(StoreV128)
_ => None
}
}
///|
fn OiTinyMemoryFillStoreFact::instruction(
self : OiTinyMemoryFillStoreFact,
memarg : @lib.MemArg,
) -> @lib.Instruction {
match self {
Store8 => @lib.Instruction::i32_store8(memarg)
Store16 => @lib.Instruction::i32_store16(memarg)
Store32 => @lib.Instruction::i32_store(memarg)
Store64 => @lib.Instruction::i64_store(memarg)
StoreV128 => @lib.Instruction::v128_store(memarg)
}
}
///|
fn OiTinyMemoryFillFact::from_repeated_byte(
ctx : HotPassContext,
size : Int32,
byte : Int32,
) -> OiTinyMemoryFillFact? {
let store_fact = match OiTinyMemoryFillStoreFact::from_size(size) {
Some(fact) => fact
None => return None
}
// Binaryen v130 `optimizeMemoryFill` always lowers constant sizes 1/2, only
// lowers constant sizes 4/8 for zero bytes or `shrinkLevel == 0`, and only
// lowers constant size 16 at `shrinkLevel == 0` when the module contract
// permits SIMD introduction. Keep memory.fill when SIMD is forbidden: two
// scalar stores could write the first half before the second half traps.
let may_lower = match size {
1 | 2 => true
4 | 8 => byte == 0 || ctx.shrink_level == 0
16 => ctx.shrink_level == 0 && ctx.allows_simd_introduction
_ => false
}
if may_lower {
Some({ size, store_fact, value_fact: RepeatedByte(byte), })
} else {
None
}
}
///|
fn OiTinyMemoryFillFact::from_size_and_value(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
size : Int32,
value_id : Int32,
) -> OiTinyMemoryFillFact? {
match optimize_instructions_i32_const_for_memory_fill(func, dead, value_id) {
Some(@lib.I32(raw)) => {
let byte = (raw.reinterpret_as_uint() & 0xff).reinterpret_as_int()
OiTinyMemoryFillFact::from_repeated_byte(ctx, size, byte)
}
None =>
if size == 1 {
Some({ size, store_fact: Store8, value_fact: OriginalI32Value, })
} else {
None
}
}
}
///|
fn OiTinyMemoryFillFact::store_instr(
self : OiTinyMemoryFillFact,
memarg : @lib.MemArg,
) -> @lib.Instruction {
self.store_fact.instruction(memarg)
}
///|
fn OiTinyMemoryFillFact::build_store_value(
self : OiTinyMemoryFillFact,
func : @ir.HotFunc,
original_value_id : Int32,
) -> Int32? {
match self.value_fact {
RepeatedByte(raw) =>
match self.store_fact {
Store8 | Store16 | Store32 =>
match optimize_instructions_repeated_fill_i32(raw, self.size) {
Some(value) => Some(@ir.hot_build_const_i32(func, value))
None => None
}
Store64 =>
Some(
@ir.hot_build_const_i64(
func,
optimize_instructions_repeated_fill_i64(raw),
),
)
StoreV128 =>
Some(optimize_instructions_build_repeated_fill_v128_const(func, raw))
}
OriginalI32Value =>
if self.size == 1 {
Some(original_value_id)
} else {
None
}
}
}
///|
fn optimize_instructions_build_i32_binary(
func : @ir.HotFunc,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Int32 {
let node_id = @ir.hot_build_binary(func, @lib.ValType::i32(), lhs_id, rhs_id)
@ir.hot_node_exact_instr_set(func, node_id, instr)
node_id
}
///|
priv struct OiMemoryAccessFact {
instr : @lib.Instruction
load_result_type : @lib.ValType?
store_value_bits : Int32?
store_value_is_i64 : Bool
}
///|
fn OiMemoryAccessFact::from_instr(
instr : @lib.Instruction,
) -> OiMemoryAccessFact? {
let load_result_type = match instr {
@lib.Instruction::I32Load(_)
| @lib.Instruction::I32Load8S(_)
| @lib.Instruction::I32Load8U(_)
| @lib.Instruction::I32Load16S(_)
| @lib.Instruction::I32Load16U(_) => Some(@lib.ValType::i32())
@lib.Instruction::I64Load(_)
| @lib.Instruction::I64Load8S(_)
| @lib.Instruction::I64Load8U(_)
| @lib.Instruction::I64Load16S(_)
| @lib.Instruction::I64Load16U(_)
| @lib.Instruction::I64Load32S(_)
| @lib.Instruction::I64Load32U(_) => Some(@lib.ValType::i64())
@lib.Instruction::F32Load(_) => Some(@lib.ValType::f32())
@lib.Instruction::F64Load(_) => Some(@lib.ValType::f64())
_ => None
}
let store_value_bits = match instr {
@lib.Instruction::I32Store8(_) | @lib.Instruction::I64Store8(_) => Some(8)
@lib.Instruction::I32Store16(_) | @lib.Instruction::I64Store16(_) =>
Some(16)
@lib.Instruction::I64Store32(_) => Some(32)
_ => None
}
let store_value_is_i64 = match instr {
@lib.Instruction::I64Store8(_)
| @lib.Instruction::I64Store16(_)
| @lib.Instruction::I64Store32(_) => true
_ => false
}
match instr {
@lib.Instruction::I32Load(_)
| @lib.Instruction::I64Load(_)
| @lib.Instruction::F32Load(_)
| @lib.Instruction::F64Load(_)
| @lib.Instruction::I32Load8S(_)
| @lib.Instruction::I32Load8U(_)
| @lib.Instruction::I32Load16S(_)
| @lib.Instruction::I32Load16U(_)
| @lib.Instruction::I64Load8S(_)
| @lib.Instruction::I64Load8U(_)
| @lib.Instruction::I64Load16S(_)
| @lib.Instruction::I64Load16U(_)
| @lib.Instruction::I64Load32S(_)
| @lib.Instruction::I64Load32U(_)
| @lib.Instruction::I32Store(_)
| @lib.Instruction::I64Store(_)
| @lib.Instruction::F32Store(_)
| @lib.Instruction::F64Store(_)
| @lib.Instruction::I32Store8(_)
| @lib.Instruction::I32Store16(_)
| @lib.Instruction::I64Store8(_)
| @lib.Instruction::I64Store16(_)
| @lib.Instruction::I64Store32(_) =>
Some({ instr, load_result_type, store_value_bits, store_value_is_i64, })
_ => None
}
}
///|
fn OiMemoryAccessFact::with_memarg(
self : OiMemoryAccessFact,
memarg : @lib.MemArg,
) -> @lib.Instruction? {
match self.instr {
@lib.Instruction::I32Load(_) => Some(@lib.Instruction::i32_load(memarg))
@lib.Instruction::I64Load(_) => Some(@lib.Instruction::i64_load(memarg))
@lib.Instruction::F32Load(_) => Some(@lib.Instruction::f32_load(memarg))
@lib.Instruction::F64Load(_) => Some(@lib.Instruction::f64_load(memarg))
@lib.Instruction::I32Load8S(_) => Some(@lib.Instruction::i32_load8s(memarg))
@lib.Instruction::I32Load8U(_) => Some(@lib.Instruction::i32_load8u(memarg))
@lib.Instruction::I32Load16S(_) =>
Some(@lib.Instruction::i32_load16s(memarg))
@lib.Instruction::I32Load16U(_) =>
Some(@lib.Instruction::i32_load16u(memarg))
@lib.Instruction::I64Load8S(_) => Some(@lib.Instruction::i64_load8s(memarg))
@lib.Instruction::I64Load8U(_) => Some(@lib.Instruction::i64_load8u(memarg))
@lib.Instruction::I64Load16S(_) =>
Some(@lib.Instruction::i64_load16s(memarg))
@lib.Instruction::I64Load16U(_) =>
Some(@lib.Instruction::i64_load16u(memarg))
@lib.Instruction::I64Load32S(_) =>
Some(@lib.Instruction::i64_load32s(memarg))
@lib.Instruction::I64Load32U(_) =>
Some(@lib.Instruction::i64_load32u(memarg))
@lib.Instruction::I32Store(_) => Some(@lib.Instruction::i32_store(memarg))
@lib.Instruction::I64Store(_) => Some(@lib.Instruction::i64_store(memarg))
@lib.Instruction::F32Store(_) => Some(@lib.Instruction::f32_store(memarg))
@lib.Instruction::F64Store(_) => Some(@lib.Instruction::f64_store(memarg))
@lib.Instruction::I32Store8(_) => Some(@lib.Instruction::i32_store8(memarg))
@lib.Instruction::I32Store16(_) =>
Some(@lib.Instruction::i32_store16(memarg))
@lib.Instruction::I64Store8(_) => Some(@lib.Instruction::i64_store8(memarg))
@lib.Instruction::I64Store16(_) =>
Some(@lib.Instruction::i64_store16(memarg))
@lib.Instruction::I64Store32(_) =>
Some(@lib.Instruction::i64_store32(memarg))
_ => None
}
}
///|
priv struct OiStoredValueFact {
instr : @lib.Instruction
address_id : Int32
value_id : Int32
access_fact : OiMemoryAccessFact
}
///|
fn OiStoredValueFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiStoredValueFact? {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Store => ()
_ => return None
}
if node.child_count != 2 {
return None
}
let instr = match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) => instr
None => return None
}
let access_fact = match OiMemoryAccessFact::from_instr(instr) {
Some(fact) => fact
None => return None
}
Some({
instr,
address_id: @ir.hot_child_get(func, node_id, 0),
value_id: @ir.hot_child_get(func, node_id, 1),
access_fact,
})
}
///|
priv struct OiStoredValueTruncatedConstFact {
stored_fact : OiStoredValueFact
new_value_id : Int32
}
///|
fn OiStoredValueTruncatedConstFact::from_stored_value(
stored_fact : OiStoredValueFact,
func : @ir.HotFunc,
dead : @bitset.BitSet,
) -> OiStoredValueTruncatedConstFact? {
// Binaryen v130 `optimizeStoredValue` truncates constants before narrow
// stores by deriving exactly the low bits preserved by the store access.
// Keep the constructed replacement constant as a named fact before the plan
// layer rewrites the store value child.
let bits = match stored_fact.access_fact.store_value_bits {
Some(bits) => bits
None => return None
}
let new_value_id = if stored_fact.access_fact.store_value_is_i64 {
let raw = match
optimize_instructions_i64_const(func, dead, stored_fact.value_id) {
Some(@lib.I64(raw)) => raw
_ => return None
}
let mask = (1L << bits) - 1L
let truncated = raw & mask
if truncated == raw {
return None
}
@ir.hot_build_const_i64(func, @lib.I64(truncated))
} else {
let raw = match
optimize_instructions_i32_const(func, dead, stored_fact.value_id) {
Some(@lib.I32(raw)) => raw
_ => return None
}
let mask = (1 << bits) - 1
let truncated = raw & mask
if truncated == raw {
return None
}
@ir.hot_build_const_i32(func, @lib.I32(truncated))
}
Some({ stored_fact, new_value_id, })
}
///|
priv struct OiStoredValueLowBitMaskFact {
stored_fact : OiStoredValueFact
unmasked_value_id : Int32
}
///|
fn OiStoredValueLowBitMaskFact::from_stored_value(
stored_fact : OiStoredValueFact,
func : @ir.HotFunc,
dead : @bitset.BitSet,
) -> OiStoredValueLowBitMaskFact? {
let bits = match stored_fact.access_fact.store_value_bits {
Some(bits) => bits
None => return None
}
// Binaryen version_130 `optimizeStoredValue` only drops exact i32 low-bit
// masks before i32 narrow stores. It preserves superset masks and all i64
// masks before i64 narrow stores, even though those masks are semantically
// redundant for the store.
if stored_fact.access_fact.store_value_is_i64 {
return None
}
if !OiLiveNodeFact::proves_node_live(func, dead, stored_fact.value_id) {
return None
}
let value = @ir.hot_node_get(func, stored_fact.value_id)
match value.op {
@ir.HotOp::Binary => ()
_ => return None
}
if value.child_count != 2 {
return None
}
if @ir.hot_node_exact_instr(func, stored_fact.value_id) !=
Some(@lib.Instruction::i32_and()) {
return None
}
let lhs_id = @ir.hot_child_get(func, stored_fact.value_id, 0)
let rhs_id = @ir.hot_child_get(func, stored_fact.value_id, 1)
let (mask, unmasked_value_id) = match
optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => (raw, lhs_id)
_ =>
match optimize_instructions_i32_const(func, dead, lhs_id) {
Some(@lib.I32(raw)) => (raw, rhs_id)
_ => return None
}
}
let required_mask = (1 << bits) - 1
if mask != required_mask {
return None
}
Some({ stored_fact, unmasked_value_id, })
}
///|
priv struct OiStoredValueReinterpretFact {
stored_fact : OiStoredValueFact
new_instr : @lib.Instruction
new_value_id : Int32
}
///|
fn OiStoredValueReinterpretFact::from_stored_value(
stored_fact : OiStoredValueFact,
func : @ir.HotFunc,
dead : @bitset.BitSet,
) -> OiStoredValueReinterpretFact? {
// Binaryen v130 `optimizeStoredValue` rewrites representation stores by
// changing the store opcode and forwarding the direct reinterpret child.
if !OiLiveNodeFact::proves_node_live(func, dead, stored_fact.value_id) {
return None
}
let value = @ir.hot_node_get(func, stored_fact.value_id)
if value.child_count != 1 {
return None
}
let new_instr = match
(stored_fact.instr, @ir.hot_node_exact_instr(func, stored_fact.value_id)) {
(
@lib.Instruction::F32Store(memarg),
Some(@lib.Instruction::F32ReinterpretI32),
) => @lib.Instruction::i32_store(memarg)
(
@lib.Instruction::F64Store(memarg),
Some(@lib.Instruction::F64ReinterpretI64),
) => @lib.Instruction::i64_store(memarg)
(
@lib.Instruction::I32Store(memarg),
Some(@lib.Instruction::I32ReinterpretF32),
) => @lib.Instruction::f32_store(memarg)
(
@lib.Instruction::I64Store(memarg),
Some(@lib.Instruction::I64ReinterpretF64),
) => @lib.Instruction::f64_store(memarg)
_ => return None
}
Some({
stored_fact,
new_instr,
new_value_id: @ir.hot_child_get(func, stored_fact.value_id, 0),
})
}
///|
priv struct OiStoredValueWrapI64WidenFact {
stored_fact : OiStoredValueFact
new_instr : @lib.Instruction
new_value_id : Int32
}
///|
fn OiStoredValueWrapI64WidenFact::from_stored_value(
stored_fact : OiStoredValueFact,
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
) -> OiStoredValueWrapI64WidenFact? {
// Storing the low N bits of (i32.wrap_i64 x) is equivalent to storing the low
// N bits of x directly via the matching i64 narrow store, so a direct one-use
// wrap value child can be dropped by widening the store.
let new_instr = match stored_fact.instr {
@lib.Instruction::I32Store8(ma) => @lib.Instruction::i64_store8(ma)
@lib.Instruction::I32Store16(ma) => @lib.Instruction::i64_store16(ma)
@lib.Instruction::I32Store(ma) => @lib.Instruction::i64_store32(ma)
_ => return None
}
if !OiLiveNodeFact::proves_node_live(func, dead, stored_fact.value_id) {
return None
}
match @ir.hot_node_exact_instr(func, stored_fact.value_id) {
Some(@lib.Instruction::I32WrapI64) => ()
_ => return None
}
let value = @ir.hot_node_get(func, stored_fact.value_id)
if value.child_count != 1 {
return None
}
// Only drop the wrap when this store is its sole use, so the wrap node can be
// cleaned up instead of leaving a dangling i32 producer.
if pass_node_use_count(ctx, func, stored_fact.value_id) != 1 {
return None
}
Some({
stored_fact,
new_instr,
new_value_id: @ir.hot_child_get(func, stored_fact.value_id, 0),
})
}
///|
priv enum OiStoredValueRewritePlanKind {
StoredValueTruncateConst
StoredValueDropLowBitMask
StoredValueRewriteReinterpret
StoredValueWidenWrapI64
}
///|
priv struct OiStoredValueRewritePlanFact {
kind : OiStoredValueRewritePlanKind
stored_fact : OiStoredValueFact
new_instr : @lib.Instruction
new_value_id : Int32
}
///|
fn OiStoredValueRewritePlanFact::from_truncated_const(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiStoredValueRewritePlanFact? {
// Binaryen v130 `optimizeStoredValue` rewrites narrow-store constants by
// deriving the preserved low bits from the store access fact. Keep the final
// replacement as a plan fact so the visitor does not own a separate store
// scanner for each OI-G stored-value family.
let stored_fact = match OiStoredValueFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let truncate_fact = match
OiStoredValueTruncatedConstFact::from_stored_value(stored_fact, func, dead) {
Some(fact) => fact
None => return None
}
Some({
kind: StoredValueTruncateConst,
stored_fact: truncate_fact.stored_fact,
new_instr: truncate_fact.stored_fact.instr,
new_value_id: truncate_fact.new_value_id,
})
}
///|
fn OiStoredValueRewritePlanFact::from_low_bit_mask(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiStoredValueRewritePlanFact? {
let stored_fact = match OiStoredValueFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let mask_fact = match
OiStoredValueLowBitMaskFact::from_stored_value(stored_fact, func, dead) {
Some(fact) => fact
None => return None
}
Some({
kind: StoredValueDropLowBitMask,
stored_fact: mask_fact.stored_fact,
new_instr: mask_fact.stored_fact.instr,
new_value_id: mask_fact.unmasked_value_id,
})
}
///|
fn OiStoredValueRewritePlanFact::from_reinterpret(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiStoredValueRewritePlanFact? {
let stored_fact = match OiStoredValueFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let reinterpret_fact = match
OiStoredValueReinterpretFact::from_stored_value(stored_fact, func, dead) {
Some(fact) => fact
None => return None
}
Some({
kind: StoredValueRewriteReinterpret,
stored_fact: reinterpret_fact.stored_fact,
new_instr: reinterpret_fact.new_instr,
new_value_id: reinterpret_fact.new_value_id,
})
}
///|
fn OiStoredValueRewritePlanFact::from_wrap_i64_widen(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiStoredValueRewritePlanFact? {
let stored_fact = match OiStoredValueFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let widen_fact = match
OiStoredValueWrapI64WidenFact::from_stored_value(
stored_fact, ctx, func, dead,
) {
Some(fact) => fact
None => return None
}
Some({
kind: StoredValueWidenWrapI64,
stored_fact: widen_fact.stored_fact,
new_instr: widen_fact.new_instr,
new_value_id: widen_fact.new_value_id,
})
}
///|
fn OiStoredValueRewritePlanFact::apply(
self : OiStoredValueRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
StoredValueTruncateConst
| StoredValueDropLowBitMask
| StoredValueRewriteReinterpret
| StoredValueWidenWrapI64 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
self.new_instr,
[self.stored_fact.address_id, self.new_value_id],
)
true
}
}
}
///|
priv struct OiLoadResultFact {
consumer_instr : @lib.Instruction
load_instr : @lib.Instruction
address_id : Int32
}
///|
fn OiLoadResultFact::from_unary_consumer(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiLoadResultFact? {
let node = @ir.hot_node_get(func, node_id)
if node.child_count != 1 {
return None
}
let load_id = @ir.hot_child_get(func, node_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, load_id) ||
pass_node_use_count(ctx, func, load_id) != 1 {
return None
}
let load = @ir.hot_node_get(func, load_id)
match load.op {
@ir.HotOp::Load => ()
_ => return None
}
if load.child_count != 1 {
return None
}
let consumer_instr = match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) => instr
None => return None
}
let load_instr = match @ir.hot_node_exact_instr(func, load_id) {
Some(instr) => instr
None => return None
}
Some({
consumer_instr,
load_instr,
address_id: @ir.hot_child_get(func, load_id, 0),
})
}
///|
priv struct OiLoadResultReinterpretFact {
load_fact : OiLoadResultFact
new_instr : @lib.Instruction
}
///|
fn OiLoadResultReinterpretFact::from_load_result(
load_fact : OiLoadResultFact,
) -> OiLoadResultReinterpretFact? {
// Binaryen v130 `visitUnary` rewrites one-use non-atomic representation loads
// through reinterpret consumers by mutating the load type in place. Keep the
// consumer/load opcode relation as an explicit fact before the rewrite-plan
// layer rebuilds the root.
let new_instr = match (load_fact.load_instr, load_fact.consumer_instr) {
(@lib.Instruction::I32Load(memarg), @lib.Instruction::F32ReinterpretI32) =>
@lib.Instruction::f32_load(memarg)
(@lib.Instruction::I64Load(memarg), @lib.Instruction::F64ReinterpretI64) =>
@lib.Instruction::f64_load(memarg)
(@lib.Instruction::F32Load(memarg), @lib.Instruction::I32ReinterpretF32) =>
@lib.Instruction::i32_load(memarg)
(@lib.Instruction::F64Load(memarg), @lib.Instruction::I64ReinterpretF64) =>
@lib.Instruction::i64_load(memarg)
_ => return None
}
Some({ load_fact, new_instr, })
}
///|
priv struct OiLoadResultI64ExtendI32Fact {
load_fact : OiLoadResultFact
new_instr : @lib.Instruction
}
///|
fn OiLoadResultI64ExtendI32Fact::from_load_result(
load_fact : OiLoadResultFact,
) -> OiLoadResultI64ExtendI32Fact? {
// Binaryen v130 rewrites one-use i32 loads under i64.extend_i32 when the
// signedness/atomic constraints are represented by an equivalent i64 load.
// Keep that signedness/result-width relation as a named fact so it cannot be
// confused with the reinterpret-load fact above.
let new_instr = match (load_fact.load_instr, load_fact.consumer_instr) {
(@lib.Instruction::I32Load(memarg), @lib.Instruction::I64ExtendI32U) =>
@lib.Instruction::i64_load32u(memarg)
(@lib.Instruction::I32Load(memarg), @lib.Instruction::I64ExtendI32S) =>
@lib.Instruction::i64_load32s(memarg)
(
@lib.Instruction::I32Load8U(memarg),
@lib.Instruction::I64ExtendI32U
| @lib.Instruction::I64ExtendI32S,
) => @lib.Instruction::i64_load8u(memarg)
(
@lib.Instruction::I32Load16U(memarg),
@lib.Instruction::I64ExtendI32U
| @lib.Instruction::I64ExtendI32S,
) => @lib.Instruction::i64_load16u(memarg)
(@lib.Instruction::I32Load8S(memarg), @lib.Instruction::I64ExtendI32S) =>
@lib.Instruction::i64_load8s(memarg)
(@lib.Instruction::I32Load16S(memarg), @lib.Instruction::I64ExtendI32S) =>
@lib.Instruction::i64_load16s(memarg)
_ => return None
}
Some({ load_fact, new_instr, })
}
///|
priv enum OiLoadResultRewritePlanKind {
LoadResultRewriteReinterpret
LoadResultRewriteI64ExtendI32
}
///|
priv struct OiLoadResultRewritePlanFact {
kind : OiLoadResultRewritePlanKind
load_fact : OiLoadResultFact
new_instr : @lib.Instruction
}
///|
fn OiLoadResultRewritePlanFact::from_reinterpret(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiLoadResultRewritePlanFact? {
// Binaryen v130 `visitUnary` mutates one-use representation loads underneath
// reinterpret consumers. Keep the mutation target as a plan fact beside the
// load-result fact instead of repeating the unary/load scanner in each
// visitor rewrite branch.
let load_fact = match
OiLoadResultFact::from_unary_consumer(ctx, func, dead, node_id) {
Some(fact) => fact
None => return None
}
let rewrite_fact = match
OiLoadResultReinterpretFact::from_load_result(load_fact) {
Some(fact) => fact
None => return None
}
Some({
kind: LoadResultRewriteReinterpret,
load_fact: rewrite_fact.load_fact,
new_instr: rewrite_fact.new_instr,
})
}
///|
fn OiLoadResultRewritePlanFact::from_i64_extend_i32(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiLoadResultRewritePlanFact? {
let load_fact = match
OiLoadResultFact::from_unary_consumer(ctx, func, dead, node_id) {
Some(fact) => fact
None => return None
}
let rewrite_fact = match
OiLoadResultI64ExtendI32Fact::from_load_result(load_fact) {
Some(fact) => fact
None => return None
}
Some({
kind: LoadResultRewriteI64ExtendI32,
load_fact: rewrite_fact.load_fact,
new_instr: rewrite_fact.new_instr,
})
}
///|
fn OiLoadResultRewritePlanFact::apply(
self : OiLoadResultRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
LoadResultRewriteReinterpret | LoadResultRewriteI64ExtendI32 => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
self.new_instr,
[self.load_fact.address_id],
)
true
}
}
}
///|
priv struct OiAtomicAccessFact {
instr : @lib.Instruction
}
///|
fn OiAtomicAccessFact::from_instr(
instr : @lib.Instruction,
) -> OiAtomicAccessFact? {
match instr {
@lib.Instruction::I32AtomicLoad(_, _)
| @lib.Instruction::I64AtomicLoad(_, _)
| @lib.Instruction::I32AtomicLoad8U(_, _)
| @lib.Instruction::I32AtomicLoad16U(_, _)
| @lib.Instruction::I64AtomicLoad8U(_, _)
| @lib.Instruction::I64AtomicLoad16U(_, _)
| @lib.Instruction::I64AtomicLoad32U(_, _)
| @lib.Instruction::I32AtomicStore(_, _)
| @lib.Instruction::I64AtomicStore(_, _)
| @lib.Instruction::I32AtomicStore8(_, _)
| @lib.Instruction::I32AtomicStore16(_, _)
| @lib.Instruction::I64AtomicStore8(_, _)
| @lib.Instruction::I64AtomicStore16(_, _)
| @lib.Instruction::I64AtomicStore32(_, _) => Some({ instr, })
_ => None
}
}
///|
fn OiAtomicAccessFact::with_memarg(
self : OiAtomicAccessFact,
memarg : @lib.MemArg,
) -> @lib.Instruction? {
match self.instr {
@lib.Instruction::I32AtomicLoad(order, _) =>
Some(@lib.Instruction::i32_atomic_load(memarg, order~))
@lib.Instruction::I64AtomicLoad(order, _) =>
Some(@lib.Instruction::i64_atomic_load(memarg, order~))
@lib.Instruction::I32AtomicLoad8U(order, _) =>
Some(@lib.Instruction::i32_atomic_load8_u(memarg, order~))
@lib.Instruction::I32AtomicLoad16U(order, _) =>
Some(@lib.Instruction::i32_atomic_load16_u(memarg, order~))
@lib.Instruction::I64AtomicLoad8U(order, _) =>
Some(@lib.Instruction::i64_atomic_load8_u(memarg, order~))
@lib.Instruction::I64AtomicLoad16U(order, _) =>
Some(@lib.Instruction::i64_atomic_load16_u(memarg, order~))
@lib.Instruction::I64AtomicLoad32U(order, _) =>
Some(@lib.Instruction::i64_atomic_load32_u(memarg, order~))
@lib.Instruction::I32AtomicStore(order, _) =>
Some(@lib.Instruction::i32_atomic_store(memarg, order~))
@lib.Instruction::I64AtomicStore(order, _) =>
Some(@lib.Instruction::i64_atomic_store(memarg, order~))
@lib.Instruction::I32AtomicStore8(order, _) =>
Some(@lib.Instruction::i32_atomic_store8(memarg, order~))
@lib.Instruction::I32AtomicStore16(order, _) =>
Some(@lib.Instruction::i32_atomic_store16(memarg, order~))
@lib.Instruction::I64AtomicStore8(order, _) =>
Some(@lib.Instruction::i64_atomic_store8(memarg, order~))
@lib.Instruction::I64AtomicStore16(order, _) =>
Some(@lib.Instruction::i64_atomic_store16(memarg, order~))
@lib.Instruction::I64AtomicStore32(order, _) =>
Some(@lib.Instruction::i64_atomic_store32(memarg, order~))
_ => None
}
}
///|
// Binaryen v130 `optimizeMemoryAccess` folds a constant address plus a static
// memory offset into a zero-offset access under memory32/memory64 overflow
// guards. Keep that proof in one OI-G fact instead of open-coding the
// load/store/atomic scanner at each rewrite site.
priv struct OiMemoryOffsetFoldFact {
instr : @lib.Instruction
address_id : Int32
new_memarg : @lib.MemArg
folded_i32_address : @lib.I32?
folded_i64_address : @lib.I64?
}
///|
fn OiMemoryOffsetFoldFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMemoryOffsetFoldFact? {
let node = @ir.hot_node_get(func, node_id)
if node.child_count == 0 {
return None
}
match node.op {
@ir.HotOp::Load | @ir.HotOp::Store | @ir.HotOp::Atomic => ()
_ => return None
}
let instr = match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) => instr
None => return None
}
match instr {
@lib.Instruction::WaitqueueNotify
| @lib.Instruction::StructWait(_, _)
| @lib.Instruction::StructAtomicGet(_, _, _)
| @lib.Instruction::StructAtomicGetS(_, _, _)
| @lib.Instruction::StructAtomicGetU(_, _, _)
| @lib.Instruction::StructAtomicRmw(_, _, _, _)
| @lib.Instruction::StructAtomicCmpxchg(_, _, _)
| @lib.Instruction::ArrayAtomicGet(_, _)
| @lib.Instruction::ArrayAtomicGetS(_, _)
| @lib.Instruction::ArrayAtomicGetU(_, _)
| @lib.Instruction::ArrayAtomicRmw(_, _, _)
| @lib.Instruction::AtomicFence(_)
| @lib.Instruction::StructAtomicSet(_, _, _)
| @lib.Instruction::ArrayAtomicSet(_, _)
| @lib.Instruction::ArrayAtomicCmpxchg(_, _) => return None
_ => ()
}
let @lib.MemArg(align, mem, @lib.U64(offset)) = @ir.hot_memarg_get(
func,
node.imm0,
).memarg
if offset == 0UL {
return None
}
let address_id = @ir.hot_child_get(func, node_id, 0)
let address = @ir.hot_node_get(func, address_id)
match address.op {
@ir.HotOp::Const => ()
_ => return None
}
let (folded_i32_address, folded_i64_address) = match
@ir.hot_const_get(func, address.imm0) {
@ir.HotConstPayload::I32Const(@lib.I32(raw)) => {
if raw < 0 {
return None
}
let raw_u = raw.reinterpret_as_uint()
if raw_u > 2147483647U {
return None
}
let sum = raw_u.to_uint64() + offset
if sum > 2147483647UL {
return None
}
(Some(@lib.I32(sum.to_uint().reinterpret_as_int())), None)
}
@ir.HotConstPayload::I64Const(@lib.I64(raw)) => {
let raw_u = raw.reinterpret_as_uint64()
if raw_u > 18446744073709551615UL - offset {
return None
}
let sum = raw_u + offset
(None, Some(@lib.I64(sum.reinterpret_as_int64())))
}
_ => return None
}
Some({
instr,
address_id,
new_memarg: @lib.MemArg::new(align, mem, @lib.U64(0UL)),
folded_i32_address,
folded_i64_address,
})
}
///|
fn OiMemoryOffsetFoldFact::build_address_const(
self : OiMemoryOffsetFoldFact,
func : @ir.HotFunc,
) -> Int32? {
ignore(self.address_id)
match (self.folded_i32_address, self.folded_i64_address) {
(Some(value), None) => Some(@ir.hot_build_const_i32(func, value))
(None, Some(value)) => Some(@ir.hot_build_const_i64(func, value))
_ => None
}
}
///|
priv struct OiMemoryOffsetFoldAtomicFact {
new_address_id : Int32
new_instr : @lib.Instruction
}
///|
priv struct OiMemoryOffsetFoldLoadFact {
offset_fact : OiMemoryOffsetFoldFact
new_address_id : Int32
new_instr : @lib.Instruction
result_type : @lib.ValType
}
///|
priv struct OiMemoryOffsetFoldStoreFact {
offset_fact : OiMemoryOffsetFoldFact
new_address_id : Int32
new_instr : @lib.Instruction
store_value_id : Int32
}
///|
fn OiMemoryOffsetFoldAtomicFact::from_offset(
offset_fact : OiMemoryOffsetFoldFact,
new_address_id : Int32,
) -> OiMemoryOffsetFoldAtomicFact? {
// Binaryen v130 `optimizeMemoryAccess` rebuilds load/store atomics by
// replacing only the memarg after the source address fold is proven. Keep the
// atomic opcode projection here so the plan no longer carries sentinel
// load/store fields or asks the offset fact for a generic optional access.
let atomic_fact = match OiAtomicAccessFact::from_instr(offset_fact.instr) {
Some(fact) => fact
None => return None
}
let new_instr = match atomic_fact.with_memarg(offset_fact.new_memarg) {
Some(instr) => instr
None => return None
}
Some({ new_address_id, new_instr, })
}
///|
fn OiMemoryOffsetFoldLoadFact::from_offset(
offset_fact : OiMemoryOffsetFoldFact,
new_address_id : Int32,
) -> OiMemoryOffsetFoldLoadFact? {
// Binaryen v130 scalar loads need the rebuilt load result type and zero-offset
// memarg together. Keep both in the load-specific fact instead of sharing a
// plan with optional store-only payloads.
let access_fact = match OiMemoryAccessFact::from_instr(offset_fact.instr) {
Some(fact) => fact
None => return None
}
let result_type = match access_fact.load_result_type {
Some(result_type) => result_type
None => return None
}
let new_instr = match access_fact.with_memarg(offset_fact.new_memarg) {
Some(instr) => instr
None => return None
}
Some({ offset_fact, new_address_id, new_instr, result_type, })
}
///|
fn OiMemoryOffsetFoldStoreFact::from_offset(
func : @ir.HotFunc,
node_id : Int32,
offset_fact : OiMemoryOffsetFoldFact,
new_address_id : Int32,
) -> OiMemoryOffsetFoldStoreFact? {
// Binaryen v130 scalar stores preserve the value child while replacing the
// address child and memarg. Capture that store-only child in a store-specific
// fact so non-store plans no longer encode a `-1` placeholder.
let access_fact = match OiMemoryAccessFact::from_instr(offset_fact.instr) {
Some(fact) => fact
None => return None
}
let new_instr = match access_fact.with_memarg(offset_fact.new_memarg) {
Some(instr) => instr
None => return None
}
Some({
offset_fact,
new_address_id,
new_instr,
store_value_id: @ir.hot_child_get(func, node_id, 1),
})
}
///|
priv enum OiMemoryOffsetFoldRewritePlanFact {
MemoryOffsetFoldAtomicPlan(OiMemoryOffsetFoldAtomicFact)
MemoryOffsetFoldLoadPlan(OiMemoryOffsetFoldLoadFact)
MemoryOffsetFoldStorePlan(OiMemoryOffsetFoldStoreFact)
}
///|
fn OiMemoryOffsetFoldRewritePlanFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMemoryOffsetFoldRewritePlanFact? {
// Binaryen v130 `optimizeMemoryAccess` derives one source-level address
// folding fact, then rebuilds the matching load/store/atomic access with the
// zero-offset memarg. Keep the replacement choice here as explicit
// access-kind facts so the visitor does not own a memory scanner and the plan
// carries no irrelevant sentinel payloads.
let node = @ir.hot_node_get(func, node_id)
let offset_fact = match OiMemoryOffsetFoldFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let new_address_id = match offset_fact.build_address_const(func) {
Some(address_id) => address_id
None => return None
}
match node.op {
@ir.HotOp::Atomic => {
if node.child_count != 1 && node.child_count != 2 {
return None
}
match
OiMemoryOffsetFoldAtomicFact::from_offset(offset_fact, new_address_id) {
Some(fact) => Some(MemoryOffsetFoldAtomicPlan(fact))
None => None
}
}
@ir.HotOp::Load => {
if node.child_count != 1 {
return None
}
match
OiMemoryOffsetFoldLoadFact::from_offset(offset_fact, new_address_id) {
Some(fact) => Some(MemoryOffsetFoldLoadPlan(fact))
None => None
}
}
@ir.HotOp::Store => {
if node.child_count != 2 {
return None
}
match
OiMemoryOffsetFoldStoreFact::from_offset(
func, node_id, offset_fact, new_address_id,
) {
Some(fact) => Some(MemoryOffsetFoldStorePlan(fact))
None => None
}
}
_ => None
}
}
///|
fn OiMemoryOffsetFoldRewritePlanFact::apply(
self : OiMemoryOffsetFoldRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self {
MemoryOffsetFoldAtomicPlan(fact) => {
@ir.hot_child_set(func, node_id, 0, fact.new_address_id)
@ir.hot_node_exact_instr_set(func, node_id, fact.new_instr)
pass_mark_mutated(ctx, func)
true
}
MemoryOffsetFoldLoadPlan(fact) => {
let temp = @ir.hot_build_load(
func,
fact.result_type,
fact.offset_fact.new_memarg,
fact.new_address_id,
)
@ir.hot_node_exact_instr_set(func, temp, fact.new_instr)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
true
}
MemoryOffsetFoldStorePlan(fact) => {
let temp = @ir.hot_build_store(
func,
fact.offset_fact.new_memarg,
fact.new_address_id,
fact.store_value_id,
)
@ir.hot_node_exact_instr_set(func, temp, fact.new_instr)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
true
}
}
}
///|
fn optimize_instructions_struct_atomic_field_type(
ctx : HotPassContext,
type_idx : @lib.TypeIdx,
field_idx : @lib.U32,
) -> @lib.ValType? {
let module_ctx = match ctx.module_ctx {
Some(module_ctx) => module_ctx
None => return None
}
let @lib.U32(raw_field) = field_idx
let fields = match
@ir.hot_module_resolve_struct_fields(module_ctx, type_idx) {
Some(fields) => fields
None => return None
}
let field = match fields.get(raw_field.reinterpret_as_int()) {
Some(field) => field
None => return None
}
if !field.is_mutable() || field.get_storage_type().is_packed() {
return None
}
let value_type = field.unpack()
if value_type == @lib.ValType::i32() || value_type == @lib.ValType::i64() {
Some(value_type)
} else {
None
}
}
///|
fn optimize_instructions_add_temp_local(
func : @ir.HotFunc,
value_type : @lib.ValType,
) -> Int32 {
let local_id = @ir.hot_local_count(func)
func.locals.locals.push(value_type)
local_id
}
///|
fn optimize_instructions_build_exact_heap(
func : @ir.HotFunc,
instr : @lib.Instruction,
result_type : @lib.ValType?,
children : Array[Int32],
) -> Int32 {
let result_types = match result_type {
Some(value_type) => [value_type]
None => []
}
@ir.hot_build_heap(func, result_types, instr, children)
}
///|
fn optimize_instructions_build_exact_atomic(
func : @ir.HotFunc,
instr : @lib.Instruction,
result_type : @lib.ValType?,
children : Array[Int32],
) -> Int32 {
let result_types = match result_type {
Some(value_type) => [value_type]
None => []
}
@ir.hot_build_atomic(func, result_types, instr, children)
}
///|
fn optimize_instructions_build_exact_binary_for_value_type(
func : @ir.HotFunc,
value_type : @lib.ValType,
instr32 : @lib.Instruction,
instr64 : @lib.Instruction,
lhs : Int32,
rhs : Int32,
) -> Int32? {
let instr = if value_type == @lib.ValType::i32() {
instr32
} else if value_type == @lib.ValType::i64() {
instr64
} else {
return None
}
let node_id = @ir.hot_build_binary(func, value_type, lhs, rhs)
@ir.hot_node_exact_instr_set(func, node_id, instr)
Some(node_id)
}
///|
fn optimize_instructions_build_exact_compare_for_value_type(
func : @ir.HotFunc,
value_type : @lib.ValType,
instr32 : @lib.Instruction,
instr64 : @lib.Instruction,
lhs : Int32,
rhs : Int32,
) -> Int32? {
let instr = if value_type == @lib.ValType::i32() {
instr32
} else if value_type == @lib.ValType::i64() {
instr64
} else {
return None
}
let node_id = @ir.hot_build_compare(func, lhs, rhs)
@ir.hot_node_exact_instr_set(func, node_id, instr)
Some(node_id)
}
///|
fn optimize_instructions_replace_with_value_block(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
result_type : @lib.ValType,
body : Array[Int32],
) -> Unit {
@ir.hot_node_promote_to_block(
func,
node_id,
@lib.BlockType::val_type(result_type),
body,
)
for root_idx = 0
root_idx + 1 < @ir.hot_root_count(func)
root_idx = root_idx + 1 {
if @ir.hot_root_get(func, root_idx) == node_id {
let next_root = @ir.hot_root_get(func, root_idx + 1)
match @ir.hot_node_get(func, next_root).op {
@ir.HotOp::Unreachable => {
ignore(@ir.hot_root_remove(func, root_idx + 1))
break
}
_ => ()
}
}
}
pass_mark_mutated(ctx, func)
}
///|
priv enum OiStructAtomicIdentityLiteralKind {
ZeroStructAtomicIdentityLiteral
AllOnesStructAtomicIdentityLiteral
}
///|
fn OiStructAtomicIdentityLiteralKind::expected_i32(self : Self) -> Int32 {
match self {
ZeroStructAtomicIdentityLiteral => 0
AllOnesStructAtomicIdentityLiteral => -1
}
}
///|
fn OiStructAtomicIdentityLiteralKind::expected_i64(self : Self) -> Int64 {
match self {
ZeroStructAtomicIdentityLiteral => 0L
AllOnesStructAtomicIdentityLiteral => -1L
}
}
///|
priv struct OiStructAtomicIdentityOperatorFact {
literal_kind : OiStructAtomicIdentityLiteralKind
}
///|
fn OiStructAtomicIdentityOperatorFact::from_op(
op : @lib.AggregateAtomicRmwOp,
) -> OiStructAtomicIdentityOperatorFact? {
// Binaryen v130 `visitStructRMW` recognizes two identity literal families:
// add/sub/or/xor use zero, while and uses all-ones. Keep the op-to-literal
// relation as a named fact instead of threading width-specific expected
// integer arguments through the value recognizers.
match op {
@lib.AggregateAtomicRmwAdd
| @lib.AggregateAtomicRmwSub
| @lib.AggregateAtomicRmwOr
| @lib.AggregateAtomicRmwXor =>
Some({ literal_kind: ZeroStructAtomicIdentityLiteral, })
@lib.AggregateAtomicRmwAnd =>
Some({ literal_kind: AllOnesStructAtomicIdentityLiteral, })
_ => None
}
}
///|
priv struct OiStructAtomicI32IdentityValueFact {
value_id : Int32
}
///|
fn OiStructAtomicI32IdentityValueFact::from_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
value_id : Int32,
literal_kind : OiStructAtomicIdentityLiteralKind,
) -> OiStructAtomicI32IdentityValueFact? {
// Binaryen v130 `visitStructRMW` recognizes aggregate-atomic identity
// constants by concrete field width. The shared literal-kind fact supplies
// the i32 payload expected for the represented operator family.
match optimize_instructions_i32_const(func, dead, value_id) {
Some(@lib.I32(raw)) if raw == literal_kind.expected_i32() =>
Some({ value_id, })
_ => None
}
}
///|
priv struct OiStructAtomicI64IdentityValueFact {
value_id : Int32
}
///|
fn OiStructAtomicI64IdentityValueFact::from_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
value_id : Int32,
literal_kind : OiStructAtomicIdentityLiteralKind,
) -> OiStructAtomicI64IdentityValueFact? {
// Same Binaryen v130 aggregate-atomic identity literal proof for the i64
// lane, with the width projection localized to the literal-kind fact.
match optimize_instructions_i64_const(func, dead, value_id) {
Some(@lib.I64(raw)) if raw == literal_kind.expected_i64() =>
Some({ value_id, })
_ => None
}
}
///|
priv struct OiStructAtomicFieldFact {
type_idx : @lib.TypeIdx
field_idx : @lib.U32
value_type : @lib.ValType
}
///|
fn OiStructAtomicFieldFact::from_index(
ctx : HotPassContext,
type_idx : @lib.TypeIdx,
field_idx : @lib.U32,
) -> OiStructAtomicFieldFact? {
// Binaryen v130 `visitStructRMW` / `visitStructCmpxchg` only continue when
// the receiver is a struct and the aggregate atomic produces a concrete field
// value. Starshine's HOT fact is intentionally narrower: it accepts resolved,
// mutable, unpacked i32/i64 struct fields, matching the represented OI-L
// lowering surface and leaving packed/ref fields to documented reopeners.
match
optimize_instructions_struct_atomic_field_type(ctx, type_idx, field_idx) {
Some(value_type) => Some({ type_idx, field_idx, value_type, })
None => None
}
}
///|
priv struct OiStructAtomicReceiverFact {
receiver_id : Int32
}
///|
fn OiStructAtomicReceiverFact::from_expr(
func : @ir.HotFunc,
receiver_id : Int32,
) -> OiStructAtomicReceiverFact? {
// Binaryen's replacement can reuse the receiver via `getResultOfFirst` or a
// fresh local. The current HOT OI-L surface keeps this fact deliberately
// explicit and accepts only trivially duplicable receiver producers that prior
// Binaryen probes exercised for Starshine: local.get and global.get.
match @ir.hot_node_get(func, receiver_id).op {
@ir.HotOp::LocalGet | @ir.HotOp::GlobalGet => Some({ receiver_id, })
_ => None
}
}
///|
priv struct OiStructAtomicIdentityOperandFact {
value_id : Int32
}
///|
fn OiStructAtomicIdentityOperandFact::from_rmw(
func : @ir.HotFunc,
dead : @bitset.BitSet,
field_fact : OiStructAtomicFieldFact,
op : @lib.AggregateAtomicRmwOp,
value_id : Int32,
) -> OiStructAtomicIdentityOperandFact? {
// Binaryen v130 folds RMW add/sub/or/xor with zero and and with all-ones to a
// read-only struct.atomic.get when the value is a single constant fallthrough.
// Route the represented HOT surface through explicit op-literal and
// width-literal facts rather than ad hoc expected-value parameters.
let operator_fact = OiStructAtomicIdentityOperatorFact::from_op(op)
match operator_fact {
Some({ literal_kind, }) => {
let identity_value_id = if field_fact.value_type == @lib.ValType::i32() {
match
OiStructAtomicI32IdentityValueFact::from_value(
func, dead, value_id, literal_kind,
) {
Some(fact) => Some(fact.value_id)
None => None
}
} else {
match
OiStructAtomicI64IdentityValueFact::from_value(
func, dead, value_id, literal_kind,
) {
Some(fact) => Some(fact.value_id)
None => None
}
}
match identity_value_id {
Some(id) => Some({ value_id: id, })
None => None
}
}
None => None
}
}
///|
priv struct OiStructAtomicCmpxchgSameValueFact {
expected_id : Int32
replacement_id : Int32
}
///|
fn OiStructAtomicCmpxchgSameValueFact::from_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
expected_id : Int32,
replacement_id : Int32,
) -> OiStructAtomicCmpxchgSameValueFact? {
// Binaryen v130 `visitStructCmpxchg` uses `areConsecutiveInputsEqual` to
// prove that `expected` and `replacement` cannot modify the field and then
// rewrites cmpxchg to a read-only struct.atomic.get. Starshine routes the
// represented same-value cmpxchg surface through the shared consecutive-input
// equality fact used by the OI-G bulk-memory facts: matching tee/get,
// structurally identical pure scalar fallthrough trees, deterministic
// state-read scalar trees, and effect-sensitive stable reads, with explicit
// intervening-effect interference checks.
let equality_fact = OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx, func, expected_id, replacement_id,
)
if equality_fact.proves_equal(func) {
Some({ expected_id, replacement_id, })
} else {
None
}
}
///|
fn optimize_instructions_try_rewrite_struct_atomic_identity_get(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
field_fact : OiStructAtomicFieldFact,
order : @lib.AtomicOrder,
receiver_fact : OiStructAtomicReceiverFact,
operands_to_drop : Array[Int32],
) -> Bool {
let body : Array[Int32] = []
for operand in operands_to_drop {
if !OiLiveNodeFact::proves_node_live(func, dead, operand) ||
!OiSideEffectFreeExpressionFact::proves_node_free(func, dead, operand) {
return false
}
body.push(@ir.hot_build_drop(func, operand))
}
let read = if order == @lib.AtomicOrder::acq_rel() &&
optimize_instructions_struct_is_unshared(ctx, field_fact.type_idx) {
optimize_instructions_build_exact_heap(
func,
@lib.Instruction::struct_get(field_fact.type_idx, field_fact.field_idx),
Some(field_fact.value_type),
[receiver_fact.receiver_id],
)
} else {
optimize_instructions_build_exact_atomic(
func,
@lib.Instruction::struct_atomic_get(
order,
field_fact.type_idx,
field_fact.field_idx,
),
Some(field_fact.value_type),
[receiver_fact.receiver_id],
)
}
body.push(read)
optimize_instructions_replace_with_value_block(
ctx,
func,
node_id,
field_fact.value_type,
body,
)
true
}
///|
priv enum OiStructAtomicRmwLoweringOpFact {
StructAtomicRmwLowerAdd
StructAtomicRmwLowerSub
StructAtomicRmwLowerAnd
StructAtomicRmwLowerOr
StructAtomicRmwLowerXor
StructAtomicRmwLowerXchg
}
///|
fn OiStructAtomicRmwLoweringOpFact::from_op(
op : @lib.AggregateAtomicRmwOp,
) -> OiStructAtomicRmwLoweringOpFact? {
// Binaryen v130 lowers all non-identity unshared struct RMW operators except
// none: add/sub/and/or/xor become a struct.get, binary op, struct.set, and old
// value; xchg stores the replacement value directly. This fact owns that
// operator table so the visitor no longer hard-codes an add/xchg whitelist.
match op {
@lib.AggregateAtomicRmwAdd => Some(StructAtomicRmwLowerAdd)
@lib.AggregateAtomicRmwSub => Some(StructAtomicRmwLowerSub)
@lib.AggregateAtomicRmwAnd => Some(StructAtomicRmwLowerAnd)
@lib.AggregateAtomicRmwOr => Some(StructAtomicRmwLowerOr)
@lib.AggregateAtomicRmwXor => Some(StructAtomicRmwLowerXor)
@lib.AggregateAtomicRmwXchg => Some(StructAtomicRmwLowerXchg)
}
}
///|
fn OiStructAtomicRmwLoweringOpFact::build_new_value(
self : OiStructAtomicRmwLoweringOpFact,
func : @ir.HotFunc,
value_type : @lib.ValType,
old_get_for_new : Int32,
value_get_for_new : Int32,
) -> Int32? {
match self {
StructAtomicRmwLowerAdd =>
optimize_instructions_build_exact_binary_for_value_type(
func,
value_type,
@lib.Instruction::i32_add(),
@lib.Instruction::i64_add(),
old_get_for_new,
value_get_for_new,
)
StructAtomicRmwLowerSub =>
optimize_instructions_build_exact_binary_for_value_type(
func,
value_type,
@lib.Instruction::i32_sub(),
@lib.Instruction::i64_sub(),
old_get_for_new,
value_get_for_new,
)
StructAtomicRmwLowerAnd =>
optimize_instructions_build_exact_binary_for_value_type(
func,
value_type,
@lib.Instruction::i32_and(),
@lib.Instruction::i64_and(),
old_get_for_new,
value_get_for_new,
)
StructAtomicRmwLowerOr =>
optimize_instructions_build_exact_binary_for_value_type(
func,
value_type,
@lib.Instruction::i32_or(),
@lib.Instruction::i64_or(),
old_get_for_new,
value_get_for_new,
)
StructAtomicRmwLowerXor =>
optimize_instructions_build_exact_binary_for_value_type(
func,
value_type,
@lib.Instruction::i32_xor(),
@lib.Instruction::i64_xor(),
old_get_for_new,
value_get_for_new,
)
StructAtomicRmwLowerXchg => Some(value_get_for_new)
}
}
///|
fn optimize_instructions_try_rewrite_struct_atomic_rmw(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
order : @lib.AtomicOrder,
op : @lib.AggregateAtomicRmwOp,
field_fact : OiStructAtomicFieldFact,
) -> Bool {
if @ir.hot_child_count(func, node_id) != 2 {
return false
}
// Every shared RMW contributes a write event even when it stores the old
// value. AcqRel writes also contribute release behavior, so identity
// simplification is restricted to unshared structs.
let unshared = optimize_instructions_struct_is_unshared(
ctx,
field_fact.type_idx,
)
if !unshared {
return false
}
let receiver_id = @ir.hot_child_get(func, node_id, 0)
let value_id = @ir.hot_child_get(func, node_id, 1)
let identity_operand_fact = OiStructAtomicIdentityOperandFact::from_rmw(
func, dead, field_fact, op, value_id,
)
match identity_operand_fact {
Some(identity_fact) => {
let receiver_fact = match
OiStructAtomicReceiverFact::from_expr(func, receiver_id) {
Some(fact) => fact
None => return false
}
return optimize_instructions_try_rewrite_struct_atomic_identity_get(
ctx,
func,
dead,
node_id,
field_fact,
order,
receiver_fact,
[identity_fact.value_id],
)
}
None => ()
}
let lowering_op = match OiStructAtomicRmwLoweringOpFact::from_op(op) {
Some(fact) => fact
None => return false
}
let receiver_fact = match
OiStructAtomicReceiverFact::from_expr(func, receiver_id) {
Some(fact) => fact
None => return false
}
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, value_id) {
return false
}
let receiver_local = optimize_instructions_add_temp_local(
func,
@lib.ValType::ref_type(
@lib.RefType::new(true, @lib.HeapType::new(field_fact.type_idx)),
),
)
let value_local = optimize_instructions_add_temp_local(
func,
field_fact.value_type,
)
let old_local = optimize_instructions_add_temp_local(
func,
field_fact.value_type,
)
// Build temporary definitions before their reads. HOT source order is also
// used to recover carried stack dependencies; prebuilding the arithmetic
// would make lowering preserve an extra evaluation before these writes.
let body : Array[Int32] = [
@ir.hot_build_local_set(func, receiver_local, receiver_fact.receiver_id),
@ir.hot_build_local_set(func, value_local, value_id),
@ir.hot_build_local_set(
func,
old_local,
optimize_instructions_build_exact_heap(
func,
@lib.Instruction::struct_get(field_fact.type_idx, field_fact.field_idx),
Some(field_fact.value_type),
[@ir.hot_build_local_get(func, receiver_local)],
),
),
]
let old_get_for_new = @ir.hot_build_local_get(func, old_local)
let value_get_for_new = @ir.hot_build_local_get(func, value_local)
let new_value = match
lowering_op.build_new_value(
func,
field_fact.value_type,
old_get_for_new,
value_get_for_new,
) {
Some(id) => id
None => return false
}
body.push(
optimize_instructions_build_exact_heap(
func,
@lib.Instruction::struct_set(field_fact.type_idx, field_fact.field_idx),
None,
[@ir.hot_build_local_get(func, receiver_local), new_value],
),
)
body.push(@ir.hot_build_local_get(func, old_local))
optimize_instructions_replace_with_value_block(
ctx,
func,
node_id,
field_fact.value_type,
body,
)
true
}
///|
fn optimize_instructions_try_rewrite_struct_atomic_cmpxchg(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
order : @lib.AtomicOrder,
field_fact : OiStructAtomicFieldFact,
) -> Bool {
if @ir.hot_child_count(func, node_id) != 3 {
return false
}
// A successful shared cmpxchg contributes a write event even when expected
// and replacement are equal. AcqRel success also contributes release
// behavior, so same-value simplification is restricted to unshared structs.
let unshared = optimize_instructions_struct_is_unshared(
ctx,
field_fact.type_idx,
)
if !unshared {
return false
}
let receiver_id = @ir.hot_child_get(func, node_id, 0)
let expected_id = @ir.hot_child_get(func, node_id, 1)
let replacement_id = @ir.hot_child_get(func, node_id, 2)
let same_value_fact = OiStructAtomicCmpxchgSameValueFact::from_operands(
ctx, func, expected_id, replacement_id,
)
match same_value_fact {
Some(fact) => {
let receiver_fact = match
OiStructAtomicReceiverFact::from_expr(func, receiver_id) {
Some(fact) => fact
None => return false
}
return optimize_instructions_try_rewrite_struct_atomic_identity_get(
ctx,
func,
dead,
node_id,
field_fact,
order,
receiver_fact,
[fact.expected_id, fact.replacement_id],
)
}
None => ()
}
let receiver_fact = match
OiStructAtomicReceiverFact::from_expr(func, receiver_id) {
Some(fact) => fact
None => return false
}
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, expected_id) ||
!OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, replacement_id,
) {
return false
}
let receiver_local = optimize_instructions_add_temp_local(
func,
@lib.ValType::ref_type(
@lib.RefType::new(true, @lib.HeapType::new(field_fact.type_idx)),
),
)
let expected_local = optimize_instructions_add_temp_local(
func,
field_fact.value_type,
)
let replacement_local = optimize_instructions_add_temp_local(
func,
field_fact.value_type,
)
let old_local = optimize_instructions_add_temp_local(
func,
field_fact.value_type,
)
let old_for_compare = @ir.hot_build_local_tee(
func,
old_local,
optimize_instructions_build_exact_heap(
func,
@lib.Instruction::struct_get(field_fact.type_idx, field_fact.field_idx),
Some(field_fact.value_type),
[@ir.hot_build_local_get(func, receiver_local)],
),
)
let expected_for_compare = @ir.hot_build_local_get(func, expected_local)
let condition = match
optimize_instructions_build_exact_compare_for_value_type(
func,
field_fact.value_type,
@lib.Instruction::i32_eq(),
@lib.Instruction::i64_eq(),
old_for_compare,
expected_for_compare,
) {
Some(id) => id
None => return false
}
let conditional_store = @ir.hot_build_if(
func,
@lib.BlockType::void_(),
condition,
[
optimize_instructions_build_exact_heap(
func,
@lib.Instruction::struct_set(field_fact.type_idx, field_fact.field_idx),
None,
[
@ir.hot_build_local_get(func, receiver_local),
@ir.hot_build_local_get(func, replacement_local),
],
),
],
)
let body : Array[Int32] = [
@ir.hot_build_local_set(func, receiver_local, receiver_fact.receiver_id),
@ir.hot_build_local_set(func, expected_local, expected_id),
@ir.hot_build_local_set(func, replacement_local, replacement_id),
conditional_store,
@ir.hot_build_local_get(func, old_local),
]
optimize_instructions_replace_with_value_block(
ctx,
func,
node_id,
field_fact.value_type,
body,
)
true
}
///|
fn optimize_instructions_struct_is_unshared(
ctx : HotPassContext,
type_idx : @lib.TypeIdx,
) -> Bool {
let module_ctx = match ctx.module_ctx {
Some(module_ctx) => module_ctx
None => return false
}
match @ir.hot_module_resolve_subtype(module_ctx, type_idx) {
Some(subtype) => !subtype.metadata().is_shared()
None => false
}
}
///|
fn optimize_instructions_try_rewrite_aggregate_atomic(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let instr = match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) => instr
None => return false
}
match instr {
@lib.Instruction::StructAtomicGet(@lib.AcqRel, type_idx, field_idx)
| @lib.Instruction::StructAtomicGetS(@lib.AcqRel, type_idx, field_idx)
| @lib.Instruction::StructAtomicGetU(@lib.AcqRel, type_idx, field_idx) => {
if !optimize_instructions_struct_is_unshared(ctx, type_idx) ||
@ir.hot_child_count(func, node_id) != 1 {
return false
}
let results = @ir.hot_type_results(func, @ir.hot_node_type(func, node_id))
if results.length() != 1 {
return false
}
let ordinary = match instr {
@lib.Instruction::StructAtomicGetS(_, _, _) =>
@lib.Instruction::struct_get_s(type_idx, field_idx)
@lib.Instruction::StructAtomicGetU(_, _, _) =>
@lib.Instruction::struct_get_u(type_idx, field_idx)
_ => @lib.Instruction::struct_get(type_idx, field_idx)
}
let replacement = optimize_instructions_build_exact_heap(
func,
ordinary,
Some(results[0]),
[@ir.hot_child_get(func, node_id, 0)],
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
true
}
@lib.Instruction::StructAtomicSet(@lib.AcqRel, type_idx, field_idx) => {
if !optimize_instructions_struct_is_unshared(ctx, type_idx) ||
@ir.hot_child_count(func, node_id) != 2 {
return false
}
// The v132 TODO does not authorize relaxing SeqCst or Relaxed here.
// Reuse both children in order so a nullable receiver still traps after
// evaluating the stored value, just as the original atomic store does.
let replacement = optimize_instructions_build_exact_heap(
func,
@lib.Instruction::struct_set(type_idx, field_idx),
None,
[
@ir.hot_child_get(func, node_id, 0),
@ir.hot_child_get(func, node_id, 1),
],
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
true
}
@lib.Instruction::StructAtomicRmw(order, op, type_idx, field_idx) => {
if @ir.hot_child_count(func, node_id) != 2 {
return false
}
let field_fact = match
OiStructAtomicFieldFact::from_index(ctx, type_idx, field_idx) {
Some(fact) => fact
None => return false
}
optimize_instructions_try_rewrite_struct_atomic_rmw(
ctx, func, dead, node_id, order, op, field_fact,
)
}
@lib.Instruction::StructAtomicCmpxchg(order, type_idx, field_idx) => {
if @ir.hot_child_count(func, node_id) != 3 {
return false
}
let field_fact = match
OiStructAtomicFieldFact::from_index(ctx, type_idx, field_idx) {
Some(fact) => fact
None => return false
}
optimize_instructions_try_rewrite_struct_atomic_cmpxchg(
ctx, func, dead, node_id, order, field_fact,
)
}
@lib.Instruction::ArrayAtomicGet(_, _)
| @lib.Instruction::ArrayAtomicGetS(_, _)
| @lib.Instruction::ArrayAtomicGetU(_, _)
| @lib.Instruction::ArrayAtomicRmw(_, _, _)
| @lib.Instruction::ArrayAtomicCmpxchg(_, _) => false
_ => false
}
}
///|
fn optimize_instructions_try_fold_const_memory_access_offset(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
ignore(dead)
match OiMemoryOffsetFoldRewritePlanFact::from_node(func, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv enum OiTupleLaneLocalizeFact {
TupleLaneDirectLocalizable
TupleLaneControlBoundary
}
///|
fn OiTupleLaneLocalizeFact::allows_direct(
self : OiTupleLaneLocalizeFact,
) -> Bool {
match self {
TupleLaneDirectLocalizable => true
TupleLaneControlBoundary => false
}
}
///|
priv enum OiTupleLaneLocalizeChildTraversalKind {
TupleLaneLocalizeNoChildTraversal
TupleLaneLocalizeChildrenDirect
TupleLaneLocalizeChildControlBoundary
}
///|
priv struct OiTupleLaneLocalizeChildTraversalFact {
kind : OiTupleLaneLocalizeChildTraversalKind
}
///|
fn OiTupleLaneLocalizeChildTraversalFact::allows_direct(
self : OiTupleLaneLocalizeChildTraversalFact,
) -> Bool {
match self.kind {
TupleLaneLocalizeNoChildTraversal | TupleLaneLocalizeChildrenDirect => true
TupleLaneLocalizeChildControlBoundary => false
}
}
///|
fn OiTupleLaneLocalizeChildTraversalFact::from_region(
func : @ir.HotFunc,
region : @ir.HotRegionRef,
) -> OiTupleLaneLocalizeChildTraversalFact {
// Binaryen v130 `getDroppedChildrenAndAppend(make, get)` recursively inspects
// tuple-lane children before deciding whether lane materialization can stay
// expression-local. Keep the child walk as its own fact so structured-control
// blockers are not encoded as an inline tuple-lane scanner loop.
let root_count = @ir.hot_region_root_count(func, region)
if root_count == 0 {
return { kind: TupleLaneLocalizeNoChildTraversal, }
}
for i = 0; i < root_count; i = i + 1 {
if !OiTupleLaneLocalizeFact::from_node(
func,
@ir.hot_region_get(func, region, i),
).allows_direct() {
return { kind: TupleLaneLocalizeChildControlBoundary, }
}
}
{ kind: TupleLaneLocalizeChildrenDirect, }
}
///|
fn OiTupleLaneLocalizeChildTraversalFact::from_node_children(
func : @ir.HotFunc,
node_id : Int32,
) -> OiTupleLaneLocalizeChildTraversalFact {
let node = @ir.hot_node_get(func, node_id)
if node.child_count == 0 {
return { kind: TupleLaneLocalizeNoChildTraversal, }
}
for child_idx = 0; child_idx < node.child_count; child_idx = child_idx + 1 {
let child_id = @ir.hot_child_get(func, node_id, child_idx)
if child_id >= 0 &&
!OiTupleLaneLocalizeFact::from_node(func, child_id).allows_direct() {
return { kind: TupleLaneLocalizeChildControlBoundary, }
}
}
{ kind: TupleLaneLocalizeChildrenDirect, }
}
///|
fn OiTupleLaneLocalizeFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiTupleLaneLocalizeFact {
// Binaryen v130 `visitTupleExtract` delegates discarded-value sequencing to
// `getDroppedChildrenAndAppend(make, get)`, which can localize expression
// children but must not synthesize through structured control, branches,
// returns, or EH roots. Keep root classification and child traversal as
// explicit tuple-lane facts instead of an open-ended tuple scanner whitelist.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Block => {
let body = @ir.hot_region_body_ref(func, node_id)
if OiTupleLaneLocalizeChildTraversalFact::from_region(func, body).allows_direct() {
return TupleLaneDirectLocalizable
}
return TupleLaneControlBoundary
}
@ir.HotOp::Loop
| @ir.HotOp::If
| @ir.HotOp::Try
| @ir.HotOp::TryTable
| @ir.HotOp::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail
| @ir.HotOp::Return
| @ir.HotOp::ReturnCall
| @ir.HotOp::ReturnCallIndirect
| @ir.HotOp::ReturnCallRef
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate
| @ir.HotOp::Catch
| @ir.HotOp::CatchAll => return TupleLaneControlBoundary
_ => ()
}
if !OiTupleLaneLocalizeChildTraversalFact::from_node_children(func, node_id).allows_direct() {
return TupleLaneControlBoundary
}
TupleLaneDirectLocalizable
}
///|
priv enum OiTupleLaneTrapFact {
TupleLaneMayTrapWithoutEffectMask
TupleLaneNoKnownTrapWithoutEffectMask
}
///|
fn OiTupleLaneTrapFact::may_trap(self : OiTupleLaneTrapFact) -> Bool {
match self {
TupleLaneMayTrapWithoutEffectMask => true
TupleLaneNoKnownTrapWithoutEffectMask => false
}
}
///|
priv enum OiTupleLaneTrapChildTraversalKind {
TupleLaneTrapNoChildTraversal
TupleLaneTrapChildrenNoKnownTrap
TupleLaneTrapChildMayTrapWithoutEffectMask
}
///|
priv struct OiTupleLaneTrapChildTraversalFact {
kind : OiTupleLaneTrapChildTraversalKind
}
///|
fn OiTupleLaneTrapChildTraversalFact::may_trap(
self : OiTupleLaneTrapChildTraversalFact,
) -> Bool {
match self.kind {
TupleLaneTrapChildMayTrapWithoutEffectMask => true
TupleLaneTrapNoChildTraversal | TupleLaneTrapChildrenNoKnownTrap => false
}
}
///|
fn OiTupleLaneTrapChildTraversalFact::from_node_children(
func : @ir.HotFunc,
node_id : Int32,
) -> OiTupleLaneTrapChildTraversalFact {
// This child-traversal fact is the trap-preservation sibling of
// `OiTupleLaneLocalizeChildTraversalFact`: direct opcode trap facts are checked
// first, then recursive children decide whether dropping this lane would hide a
// pure trap.
let node = @ir.hot_node_get(func, node_id)
if node.child_count == 0 {
return { kind: TupleLaneTrapNoChildTraversal, }
}
for child_idx = 0; child_idx < node.child_count; child_idx = child_idx + 1 {
let child_id = @ir.hot_child_get(func, node_id, child_idx)
if child_id >= 0 &&
OiTupleLaneTrapFact::from_node(func, child_id).may_trap() {
return { kind: TupleLaneTrapChildMayTrapWithoutEffectMask, }
}
}
{ kind: TupleLaneTrapChildrenNoKnownTrap, }
}
///|
fn OiTupleLaneTrapFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiTupleLaneTrapFact {
// This is the tuple-local Binaryen `getDroppedChildrenAndAppend` preservation
// fact for pure instructions that still may trap when dropped as an unselected
// lane. It intentionally remains narrower than general EffectAnalyzer facts;
// direct opcode evidence and recursive child evidence are split explicitly.
match @ir.hot_node_exact_instr(func, node_id) {
Some(
@lib.Instruction::I32DivS
| @lib.Instruction::I32DivU
| @lib.Instruction::I32RemS
| @lib.Instruction::I32RemU
| @lib.Instruction::I64DivS
| @lib.Instruction::I64DivU
| @lib.Instruction::I64RemS
| @lib.Instruction::I64RemU
| @lib.Instruction::RefAsNonNull
| @lib.Instruction::I31GetS
| @lib.Instruction::I31GetU
) => return TupleLaneMayTrapWithoutEffectMask
_ => ()
}
if OiTupleLaneTrapChildTraversalFact::from_node_children(func, node_id).may_trap() {
return TupleLaneMayTrapWithoutEffectMask
}
TupleLaneNoKnownTrapWithoutEffectMask
}
///|
priv enum OiTupleLaneDiscardFact {
TupleLaneCanEraseWhenDiscarded
TupleLaneMustPreserveWhenDiscarded
}
///|
fn OiTupleLaneDiscardFact::must_preserve(self : OiTupleLaneDiscardFact) -> Bool {
match self {
TupleLaneCanEraseWhenDiscarded => false
TupleLaneMustPreserveWhenDiscarded => true
}
}
///|
fn OiTupleLaneDiscardFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
localize_fact : OiTupleLaneLocalizeFact,
) -> OiTupleLaneDiscardFact {
if !localize_fact.allows_direct() {
return TupleLaneMustPreserveWhenDiscarded
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Block => {
let body = @ir.hot_region_body_ref(func, node_id)
for i = 0; i < @ir.hot_region_root_count(func, body); i = i + 1 {
let child_id = @ir.hot_region_get(func, body, i)
let child_localize = OiTupleLaneLocalizeFact::from_node(func, child_id)
if OiTupleLaneDiscardFact::from_node(
ctx, func, child_id, child_localize,
).must_preserve() {
return TupleLaneMustPreserveWhenDiscarded
}
}
return TupleLaneCanEraseWhenDiscarded
}
@ir.HotOp::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail
| @ir.HotOp::Return
| @ir.HotOp::ReturnCall
| @ir.HotOp::ReturnCallIndirect
| @ir.HotOp::ReturnCallRef
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate
| @ir.HotOp::Catch
| @ir.HotOp::CatchAll => return TupleLaneMustPreserveWhenDiscarded
_ => ()
}
if !optimize_instructions_node_effects_are_pure(ctx, func, node_id) ||
OiTupleLaneTrapFact::from_node(func, node_id).may_trap() {
TupleLaneMustPreserveWhenDiscarded
} else {
TupleLaneCanEraseWhenDiscarded
}
}
///|
fn optimize_instructions_tuple_extract_child_for_flat_index(
func : @ir.HotFunc,
tuple_id : Int32,
index : Int32,
) -> (Int32, Int32)? {
let tuple = @ir.hot_node_get(func, tuple_id)
let mut first_result = 0
for slot = 0; slot < tuple.child_count; slot = slot + 1 {
let child_id = @ir.hot_child_get(func, tuple_id, slot)
let child_results = @ir.hot_type_results(
func,
@ir.hot_node_get(func, child_id).ty,
)
let next_result = first_result + child_results.length()
if index >= first_result && index < next_result {
return Some((slot, index - first_result))
}
first_result = next_result
}
None
}
///|
priv enum OiTupleLaneMaterializeFact {
TupleLaneEraseDiscarded
TupleLaneAppendEffectRoot
TupleLaneDropSingleResult
TupleLaneStoreMultiResultLocals
}
///|
fn OiTupleLaneMaterializeFact::from_lane(
results : Array[@lib.ValType],
discard_fact : OiTupleLaneDiscardFact,
) -> OiTupleLaneMaterializeFact {
// Binaryen v130 `getDroppedChildrenAndAppend` materializes discarded tuple
// lanes by preserving no-result effects directly, dropping single-result
// effects, or storing each result of a multivalue effect into scratch locals.
// Keep that emission choice as a tuple-lane fact instead of a body-builder
// side branch hidden inside the tuple.extract rewrite.
if !discard_fact.must_preserve() {
return TupleLaneEraseDiscarded
}
match results.length() {
0 => TupleLaneAppendEffectRoot
1 => TupleLaneDropSingleResult
_ => TupleLaneStoreMultiResultLocals
}
}
///|
fn OiTupleLaneMaterializeFact::must_emit(
self : OiTupleLaneMaterializeFact,
) -> Bool {
match self {
TupleLaneEraseDiscarded => false
TupleLaneAppendEffectRoot
| TupleLaneDropSingleResult
| TupleLaneStoreMultiResultLocals => true
}
}
///|
fn OiTupleLaneMaterializeFact::append_discarded_child(
self : OiTupleLaneMaterializeFact,
func : @ir.HotFunc,
child_id : Int32,
child_results : Array[@lib.ValType],
body : Array[Int32],
) -> Unit {
match self {
TupleLaneEraseDiscarded => ()
TupleLaneAppendEffectRoot => body.push(child_id)
TupleLaneDropSingleResult => body.push(@ir.hot_build_drop(func, child_id))
TupleLaneStoreMultiResultLocals => {
let discard_locals : Array[Int32] = []
for result in child_results {
discard_locals.push(@ir.hot_append_body_local(func, result))
}
let mut lane = child_results.length()
while lane > 0 {
lane = lane - 1
body.push(@ir.hot_build_local_set(func, discard_locals[lane], child_id))
}
}
}
}
///|
priv struct OiTupleLaneFact {
node_id : Int32
results : Array[@lib.ValType]
localize_fact : OiTupleLaneLocalizeFact
discard_fact : OiTupleLaneDiscardFact
materialize_fact : OiTupleLaneMaterializeFact
}
///|
fn OiTupleLaneFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiTupleLaneFact {
let localize_fact = OiTupleLaneLocalizeFact::from_node(func, node_id)
let results = @ir.hot_type_results(func, @ir.hot_node_type(func, node_id))
let discard_fact = OiTupleLaneDiscardFact::from_node(
ctx, func, node_id, localize_fact,
)
{
node_id,
results,
localize_fact,
discard_fact,
materialize_fact: OiTupleLaneMaterializeFact::from_lane(
results, discard_fact,
),
}
}
///|
priv enum OiTupleExtractStructuralKind {
TupleExtractUnsupportedRoot
TupleExtractDeadOrSharedTuple
TupleExtractMissingTupleMake
TupleExtractUnsupportedIndex
TupleExtractTupleMake
}
///|
priv struct OiTupleExtractTupleMakePayloadFact {
tuple_id : Int32
index : Int32
}
///|
priv struct OiTupleExtractStructuralFact {
kind : OiTupleExtractStructuralKind
tuple_make_payload : OiTupleExtractTupleMakePayloadFact?
}
///|
fn OiTupleExtractStructuralFact::from_extract(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiTupleExtractStructuralFact {
// Binaryen v130 `visitTupleExtract` only rewrites a `tuple.extract` fed by a
// single-use `tuple.make` with a concrete lane index. Keep each structural
// blocker and the successful tuple.make payload in an explicit fact so the
// tuple rewrite fact owns only source-backed lane/discard/materialization
// decisions.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::TupleExtract if node.child_count == 1 => ()
_ => return { kind: TupleExtractUnsupportedRoot, tuple_make_payload: None, }
}
let tuple_id = @ir.hot_child_get(func, node_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, tuple_id) ||
pass_node_use_count(ctx, func, tuple_id) != 1 {
return { kind: TupleExtractDeadOrSharedTuple, tuple_make_payload: None, }
}
let tuple = @ir.hot_node_get(func, tuple_id)
match tuple.op {
@ir.HotOp::TupleMake => ()
_ =>
return { kind: TupleExtractMissingTupleMake, tuple_make_payload: None, }
}
let index = node.imm0
if index < 0 {
return { kind: TupleExtractUnsupportedIndex, tuple_make_payload: None, }
}
{
kind: TupleExtractTupleMake,
tuple_make_payload: Some({ tuple_id, index, }),
}
}
///|
priv struct OiTupleSiblingEffectFact {
has_preserved_sibling : Bool
has_later_preserved_sibling : Bool
}
///|
fn OiTupleSiblingEffectFact::from_lanes(
lane_facts : Array[OiTupleLaneFact],
selected_slot : Int32,
) -> OiTupleSiblingEffectFact {
// Binaryen v130 `visitTupleExtract` rewrites the selected tuple lane to a
// temporary and lets `getDroppedChildrenAndAppend(make, get)` preserve only
// discarded siblings whose effects/traps must remain observable. Keep the
// sibling-order summary as an explicit fact so direct selected-lane
// forwarding and selected-lane materialization consume the same source-backed
// evidence instead of re-scanning tuple lanes independently.
let mut has_preserved_sibling = false
let mut has_later_preserved_sibling = false
for slot = 0; slot < lane_facts.length(); slot = slot + 1 {
if slot == selected_slot {
continue
}
if lane_facts[slot].discard_fact.must_preserve() {
has_preserved_sibling = true
if slot > selected_slot {
has_later_preserved_sibling = true
}
}
}
{ has_preserved_sibling, has_later_preserved_sibling, }
}
///|
fn OiTupleSiblingEffectFact::allows_selected_forward(
self : OiTupleSiblingEffectFact,
) -> Bool {
!self.has_preserved_sibling
}
///|
fn OiTupleSiblingEffectFact::needs_selected_local(
self : OiTupleSiblingEffectFact,
) -> Bool {
self.has_later_preserved_sibling
}
///|
priv struct OiTupleExtractFact {
selected_slot : Int32
selected_lane : Int32
selected_id : Int32
selected_results : Array[@lib.ValType]
lane_facts : Array[OiTupleLaneFact]
sibling_effect_fact : OiTupleSiblingEffectFact
}
///|
fn OiTupleExtractFact::from_extract(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiTupleExtractFact? {
let structural_fact = OiTupleExtractStructuralFact::from_extract(
ctx, func, dead, node_id,
)
match structural_fact.kind {
TupleExtractUnsupportedRoot => return None
TupleExtractDeadOrSharedTuple => return None
TupleExtractMissingTupleMake => return None
TupleExtractUnsupportedIndex => return None
TupleExtractTupleMake => ()
}
let tuple_make_payload = match structural_fact.tuple_make_payload {
Some(fact) => fact
None => return None
}
let tuple_id = tuple_make_payload.tuple_id
let tuple = @ir.hot_node_get(func, tuple_id)
let index = tuple_make_payload.index
let lane_facts : Array[OiTupleLaneFact] = []
for slot = 0; slot < tuple.child_count; slot = slot + 1 {
let lane_fact = OiTupleLaneFact::from_node(
ctx,
func,
@ir.hot_child_get(func, tuple_id, slot),
)
if !lane_fact.localize_fact.allows_direct() {
return None
}
lane_facts.push(lane_fact)
}
let (selected_slot, selected_lane) = match
optimize_instructions_tuple_extract_child_for_flat_index(
func, tuple_id, index,
) {
Some(value) => value
None => return None
}
let selected_fact = lane_facts[selected_slot]
if selected_fact.results.length() == 0 {
return None
}
Some({
selected_slot,
selected_lane,
selected_id: selected_fact.node_id,
selected_results: selected_fact.results,
sibling_effect_fact: OiTupleSiblingEffectFact::from_lanes(
lane_facts, selected_slot,
),
lane_facts,
})
}
///|
fn OiTupleExtractFact::can_forward_selected_directly(
self : OiTupleExtractFact,
) -> Bool {
self.sibling_effect_fact.allows_selected_forward() &&
self.selected_results.length() == 1
}
///|
priv enum OiTupleSelectedMaterializeKind {
TupleSelectedDirectValue
TupleSelectedSingleLocal
TupleSelectedMultiResultLocals
}
///|
priv struct OiTupleSelectedLocalFact {
selected_local : Int32
}
///|
priv struct OiTupleSelectedResultLocalsFact {
selected_result_locals : Array[Int32]
}
///|
priv struct OiTupleSelectedMaterializeFact {
kind : OiTupleSelectedMaterializeKind
selected_local_fact : OiTupleSelectedLocalFact?
selected_result_locals_fact : OiTupleSelectedResultLocalsFact?
}
///|
fn OiTupleSelectedMaterializeFact::direct_value() -> OiTupleSelectedMaterializeFact {
{
kind: TupleSelectedDirectValue,
selected_local_fact: None,
selected_result_locals_fact: None,
}
}
///|
fn OiTupleSelectedMaterializeFact::single_local(
selected_local : Int32,
) -> OiTupleSelectedMaterializeFact {
{
kind: TupleSelectedSingleLocal,
selected_local_fact: Some({ selected_local, }),
selected_result_locals_fact: None,
}
}
///|
fn OiTupleSelectedMaterializeFact::multi_result_locals(
selected_result_locals : Array[Int32],
) -> OiTupleSelectedMaterializeFact {
{
kind: TupleSelectedMultiResultLocals,
selected_local_fact: None,
selected_result_locals_fact: Some({ selected_result_locals, }),
}
}
///|
fn OiTupleSelectedMaterializeFact::from_extract(
func : @ir.HotFunc,
tuple_fact : OiTupleExtractFact,
) -> OiTupleSelectedMaterializeFact {
// Binaryen v130 `visitTupleExtract` stores the selected lane in a temporary
// local tee before delegating dropped sibling sequencing to
// `getDroppedChildrenAndAppend(make, get)`. Starshine's HOT tuple shape can
// directly forward the selected child when no later preserved sibling can
// reorder around it, but the selected-lane materialization choice still lives
// in an explicit fact rather than the tuple body builder. Keep the single-local
// and multi-result-local payloads distinct so the fact does not use a `-1`
// selected-local sentinel or parallel helper predicates.
if tuple_fact.selected_results.length() > 1 {
let selected_result_locals : Array[Int32] = []
for result in tuple_fact.selected_results {
selected_result_locals.push(@ir.hot_append_body_local(func, result))
}
return OiTupleSelectedMaterializeFact::multi_result_locals(
selected_result_locals,
)
}
if tuple_fact.sibling_effect_fact.needs_selected_local() {
return OiTupleSelectedMaterializeFact::single_local(
@ir.hot_append_body_local(func, tuple_fact.selected_results[0]),
)
}
OiTupleSelectedMaterializeFact::direct_value()
}
///|
fn OiTupleSelectedMaterializeFact::append_selected_capture(
self : OiTupleSelectedMaterializeFact,
func : @ir.HotFunc,
tuple_fact : OiTupleExtractFact,
body : Array[Int32],
) -> Unit {
match self.kind {
TupleSelectedMultiResultLocals => {
let result_locals_fact = match self.selected_result_locals_fact {
Some(fact) => fact
None => return
}
let mut lane = tuple_fact.selected_results.length()
while lane > 0 {
lane = lane - 1
body.push(
@ir.hot_build_local_set(
func,
result_locals_fact.selected_result_locals[lane],
tuple_fact.selected_id,
),
)
}
}
TupleSelectedSingleLocal => {
let local_fact = match self.selected_local_fact {
Some(fact) => fact
None => return
}
body.push(
@ir.hot_build_local_set(
func,
local_fact.selected_local,
tuple_fact.selected_id,
),
)
}
TupleSelectedDirectValue => ()
}
}
///|
fn OiTupleSelectedMaterializeFact::append_selected_result(
self : OiTupleSelectedMaterializeFact,
func : @ir.HotFunc,
tuple_fact : OiTupleExtractFact,
body : Array[Int32],
) -> Unit {
match self.kind {
TupleSelectedMultiResultLocals => {
let result_locals_fact = match self.selected_result_locals_fact {
Some(fact) => fact
None => return
}
body.push(
@ir.hot_build_local_get(
func,
result_locals_fact.selected_result_locals[tuple_fact.selected_lane],
),
)
}
TupleSelectedSingleLocal => {
let local_fact = match self.selected_local_fact {
Some(fact) => fact
None => return
}
body.push(@ir.hot_build_local_get(func, local_fact.selected_local))
}
TupleSelectedDirectValue => body.push(tuple_fact.selected_id)
}
}
///|
priv enum OiTupleExtractRewritePlanKind {
TupleExtractForwardSelected
TupleExtractMaterializeSelected
}
///|
priv struct OiTupleExtractRewritePlanFact {
kind : OiTupleExtractRewritePlanKind
tuple_fact : OiTupleExtractFact
selected_materialize_fact : OiTupleSelectedMaterializeFact?
}
///|
fn OiTupleExtractRewritePlanFact::from_extract(
func : @ir.HotFunc,
tuple_fact : OiTupleExtractFact,
) -> OiTupleExtractRewritePlanFact {
// Binaryen v130 `visitTupleExtract` either forwards the selected child when
// sibling preservation is unnecessary or materializes the selected child
// before `getDroppedChildrenAndAppend` emits preserved discarded lanes. Keep
// that visitor-level choice as an explicit plan fact instead of a body-builder
// branch in the pass visitor.
if tuple_fact.can_forward_selected_directly() {
return {
kind: TupleExtractForwardSelected,
tuple_fact,
selected_materialize_fact: None,
}
}
{
kind: TupleExtractMaterializeSelected,
tuple_fact,
selected_materialize_fact: Some(
OiTupleSelectedMaterializeFact::from_extract(func, tuple_fact),
),
}
}
///|
fn OiTupleExtractRewritePlanFact::apply(
self : OiTupleExtractRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
TupleExtractForwardSelected => {
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.tuple_fact.selected_id,
)
true
}
TupleExtractMaterializeSelected =>
self.apply_materialized(ctx, func, node_id)
}
}
///|
fn OiTupleExtractRewritePlanFact::apply_materialized(
self : OiTupleExtractRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let selected_materialize_fact = match self.selected_materialize_fact {
Some(fact) => fact
None => return false
}
let body : Array[Int32] = []
for slot = 0; slot < self.tuple_fact.lane_facts.length(); slot = slot + 1 {
let lane_fact = self.tuple_fact.lane_facts[slot]
if slot == self.tuple_fact.selected_slot {
selected_materialize_fact.append_selected_capture(
func,
self.tuple_fact,
body,
)
continue
}
if !lane_fact.materialize_fact.must_emit() {
continue
}
lane_fact.materialize_fact.append_discarded_child(
func,
lane_fact.node_id,
lane_fact.results,
body,
)
}
selected_materialize_fact.append_selected_result(func, self.tuple_fact, body)
let replacement = @ir.hot_build_block(
func,
@lib.BlockType::val_type(
self.tuple_fact.selected_results[self.tuple_fact.selected_lane],
),
body,
)
optimize_instructions_replace_with_node_id(ctx, func, node_id, replacement)
true
}
///|
fn optimize_instructions_try_fold_tuple_extract_tuple_make(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let tuple_fact = match
OiTupleExtractFact::from_extract(ctx, func, dead, node_id) {
Some(fact) => fact
None => return false
}
OiTupleExtractRewritePlanFact::from_extract(func, tuple_fact).apply(
ctx, func, node_id,
)
}
///|
priv enum OiOrderedOperandControlShapeFact {
OrderedOperandExpressionRoot
OrderedOperandControlBoundary
}
///|
fn OiOrderedOperandControlShapeFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiOrderedOperandControlShapeFact {
// Binaryen v130 ordered-effect rewrites rely on ChildLocalizer-like
// root-shape facts: scalar expression roots can be sequenced by appending
// their effects, while structured control, branches, returns, and EH roots
// need a broader control-aware localizer. Keep that boundary as an explicit
// fact rather than a helper-owned opcode predicate.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Block
| @ir.HotOp::Loop
| @ir.HotOp::If
| @ir.HotOp::Try
| @ir.HotOp::TryTable
| @ir.HotOp::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail
| @ir.HotOp::Return
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate
| @ir.HotOp::Catch
| @ir.HotOp::CatchAll => OrderedOperandControlBoundary
_ => OrderedOperandExpressionRoot
}
}
///|
priv enum OiOrderedOperandDiscardFact {
OrderedOperandAppendEffectRoot
OrderedOperandDropSingleResult
OrderedOperandUnsupportedMultiResult
}
///|
fn OiOrderedOperandDiscardFact::from_results(
results : Array[@lib.ValType],
) -> OiOrderedOperandDiscardFact {
// Binaryen v130 `getDroppedChildrenAndAppend` can preserve a no-result
// effect root directly or drop a single-result effect. Starshine's current
// ordered-effect fact boundary does not synthesize scratch locals for
// multi-result operands, so keep that unsupported case visible as a fact.
match results.length() {
0 => OrderedOperandAppendEffectRoot
1 => OrderedOperandDropSingleResult
_ => OrderedOperandUnsupportedMultiResult
}
}
///|
fn OiOrderedOperandDiscardFact::append(
self : OiOrderedOperandDiscardFact,
func : @ir.HotFunc,
node_id : Int32,
body : Array[Int32],
) -> Bool {
match self {
OrderedOperandAppendEffectRoot => body.push(node_id)
OrderedOperandDropSingleResult =>
body.push(@ir.hot_build_drop(func, node_id))
OrderedOperandUnsupportedMultiResult => return false
}
true
}
///|
priv enum OiOrderedOperandPurityKind {
OrderedOperandTriviallyPure
OrderedOperandEffectPure
OrderedOperandEffectful
}
///|
priv struct OiOrderedOperandPurityFact {
kind : OiOrderedOperandPurityKind
}
///|
fn OiOrderedOperandPurityFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiOrderedOperandPurityFact {
// Binaryen's ordered-effect localizer drops pure children while preserving
// effectful children. Keep the two Starshine purity sources as a single
// ordered-operand fact instead of parallel booleans on the consumer record.
let effects = pass_require_effects(ctx, func)
let kind = if OiTriviallyPureOperandFact::proves_node_pure(func, node_id) {
OrderedOperandTriviallyPure
} else if OiNodeEffectPurityFact::from_node_masks(effects.node_masks, node_id).proves_pure() {
OrderedOperandEffectPure
} else {
OrderedOperandEffectful
}
{ kind, }
}
///|
fn OiOrderedOperandPurityFact::proves_pure(
self : OiOrderedOperandPurityFact,
) -> Bool {
match self.kind {
OrderedOperandTriviallyPure | OrderedOperandEffectPure => true
OrderedOperandEffectful => false
}
}
///|
priv enum OiOrderedOperandPreservationKind {
OrderedOperandPreservesEffectOrder
OrderedOperandBlockedByControlBoundary
OrderedOperandBlockedByMultiResultDiscard
}
///|
priv struct OiOrderedOperandPreservationFact {
kind : OiOrderedOperandPreservationKind
}
///|
fn OiOrderedOperandPreservationFact::from_facts(
control_shape_fact : OiOrderedOperandControlShapeFact,
discard_fact : OiOrderedOperandDiscardFact,
) -> OiOrderedOperandPreservationFact {
// Binaryen v130 `getDroppedChildrenAndAppend` needs both an expression-root
// operand and a materializable discard shape. Encode the combined outcome as
// a preservation fact rather than open-coded `can_*` predicates.
let kind = match control_shape_fact {
OrderedOperandControlBoundary => OrderedOperandBlockedByControlBoundary
OrderedOperandExpressionRoot =>
match discard_fact {
OrderedOperandAppendEffectRoot | OrderedOperandDropSingleResult =>
OrderedOperandPreservesEffectOrder
OrderedOperandUnsupportedMultiResult =>
OrderedOperandBlockedByMultiResultDiscard
}
}
{ kind, }
}
///|
fn OiOrderedOperandPreservationFact::proves_order_preserved(
self : OiOrderedOperandPreservationFact,
) -> Bool {
match self.kind {
OrderedOperandPreservesEffectOrder => true
OrderedOperandBlockedByControlBoundary
| OrderedOperandBlockedByMultiResultDiscard => false
}
}
///|
priv struct OiOrderedOperandFact {
node_id : Int32
discard_fact : OiOrderedOperandDiscardFact
purity_fact : OiOrderedOperandPurityFact
preservation_fact : OiOrderedOperandPreservationFact
}
///|
fn OiOrderedOperandFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiOrderedOperandFact {
let results = @ir.hot_type_results(func, @ir.hot_node_type(func, node_id))
let control_shape_fact = OiOrderedOperandControlShapeFact::from_node(
func, node_id,
)
let discard_fact = OiOrderedOperandDiscardFact::from_results(results)
{
node_id,
discard_fact,
purity_fact: OiOrderedOperandPurityFact::from_node(ctx, func, node_id),
preservation_fact: OiOrderedOperandPreservationFact::from_facts(
control_shape_fact, discard_fact,
),
}
}
///|
fn OiOrderedOperandFact::append_discard(
self : OiOrderedOperandFact,
func : @ir.HotFunc,
body : Array[Int32],
) -> Bool {
self.discard_fact.append(func, self.node_id, body)
}
///|
fn optimize_instructions_append_ordered_effects(
ctx : HotPassContext,
func : @ir.HotFunc,
operand_ids : Array[Int32],
body : Array[Int32],
) -> Bool {
for operand_id in operand_ids {
let operand_fact = OiOrderedOperandFact::from_node(ctx, func, operand_id)
if !operand_fact.preservation_fact.proves_order_preserved() {
return false
}
if operand_fact.purity_fact.proves_pure() {
continue
}
if !operand_fact.append_discard(func, body) {
return false
}
}
true
}
///|
priv struct OiChildLocalizationPlanFact {
source_ids : Array[Int32]
drop_ids : Array[Int32]
}
///|
priv enum OiArrayNewFixedRepresentativeDefaultKind {
ArrayNewFixedRepresentativeDefaultValue
ArrayNewFixedRepresentativeNonDefaultValue
}
///|
priv struct OiArrayNewFixedRepresentativeDefaultFact {
kind : OiArrayNewFixedRepresentativeDefaultKind
default_value_fact : OiArrayDefaultValueFact
}
///|
priv struct OiArrayNewFixedRepeatedValueFact {
node_id : Int32
type_idx : @lib.TypeIdx
size : Int32
representative_id : Int32
representative_default_fact : OiArrayNewFixedRepresentativeDefaultFact
non_default_localization_plan : OiChildLocalizationPlanFact
}
///|
priv enum OiArrayDefaultFallthroughKind {
ArrayDefaultFallthroughDepthExhausted
ArrayDefaultFallthroughLocalTeeChild
ArrayDefaultFallthroughBlockStoppedByPrefixControl
ArrayDefaultFallthroughBlockTailChild
ArrayDefaultFallthroughLoopBodyTail
ArrayDefaultFallthroughLoopWithoutTail
ArrayDefaultFallthroughOneSidedIfTail
ArrayDefaultFallthroughIfWithoutElse
ArrayDefaultFallthroughIfWithoutProvenRegion
ArrayDefaultFallthroughUnsupported
}
///|
priv struct OiArrayDefaultFallthroughTraversalFact {
fallthrough_id : Int32
kind : OiArrayDefaultFallthroughKind
}
///|
priv struct OiArrayDefaultFallthroughFact {
traversal_fact : OiArrayDefaultFallthroughTraversalFact
}
///|
priv enum OiArrayDefaultPreservationKind {
ArrayDefaultReachedFallthrough
ArrayDefaultDepthExhausted
ArrayDefaultLocalTeeWrapper
ArrayDefaultBlockPrefixEffectful
ArrayDefaultNestedBlockPreserves
ArrayDefaultLoopEffectful
ArrayDefaultIfControlShape
ArrayDefaultSubtreeEffectful
ArrayDefaultNoPreservationNeeded
}
///|
priv struct OiArrayDefaultPreservationFact {
kind : OiArrayDefaultPreservationKind
}
///|
priv enum OiArrayDefaultValueKind {
ArrayDefaultNoModuleContext
ArrayDefaultMissingField
ArrayDefaultStorageNonDefault
ArrayDefaultStorageDefault
}
///|
priv struct OiArrayDefaultValueFact {
kind : OiArrayDefaultValueKind
preservation_plan : OiChildLocalizationPlanFact
}
///|
priv enum OiArrayStorageDefaultValueKind {
ArrayStorageI32ZeroDefault
ArrayStorageI64ZeroDefault
ArrayStorageF32ZeroDefault
ArrayStorageF64ZeroDefault
ArrayStorageNullableRefNullDefault
ArrayStorageNonDefault
}
///|
priv struct OiArrayStorageDefaultValueFact {
kind : OiArrayStorageDefaultValueKind
}
///|
fn OiArrayStorageDefaultValueFact::from_value(
func : @ir.HotFunc,
storage : @lib.StorageType,
value_id : Int32,
) -> OiArrayStorageDefaultValueFact {
// Binaryen's array default rewrites prove the fallthrough initializer equals
// the field storage default before erasing or replacing allocation operands.
// Keep the exact represented zero/null set as a reusable fact; broader SIMD
// and nullable array constructor cases remain documented parser/HOT blockers.
let value = @ir.hot_node_get(func, value_id)
let kind = match storage.unpack() {
@lib.ValType::NumTypeValType(@lib.NumType::I32NumType) =>
match value.op {
@ir.HotOp::Const if @ir.hot_const_get(func, value.imm0) ==
@ir.HotConstPayload::i32_const(@lib.I32(0)) =>
ArrayStorageI32ZeroDefault
_ => ArrayStorageNonDefault
}
@lib.ValType::NumTypeValType(@lib.NumType::I64NumType) =>
match value.op {
@ir.HotOp::Const if @ir.hot_const_get(func, value.imm0) ==
@ir.HotConstPayload::i64_const(@lib.I64(0L)) =>
ArrayStorageI64ZeroDefault
_ => ArrayStorageNonDefault
}
@lib.ValType::NumTypeValType(@lib.NumType::F32NumType) =>
match value.op {
@ir.HotOp::Const if pass_hot_const_bits_equal(
@ir.hot_const_get(func, value.imm0),
@ir.HotConstPayload::f32_const(@lib.F32(0.0)),
) => ArrayStorageF32ZeroDefault
_ => ArrayStorageNonDefault
}
@lib.ValType::NumTypeValType(@lib.NumType::F64NumType) =>
match value.op {
@ir.HotOp::Const if pass_hot_const_bits_equal(
@ir.hot_const_get(func, value.imm0),
@ir.HotConstPayload::f64_const(@lib.F64(0.0)),
) => ArrayStorageF64ZeroDefault
_ => ArrayStorageNonDefault
}
@lib.ValType::RefTypeValType(ref_type) =>
if !ref_type.is_nullable() {
ArrayStorageNonDefault
} else {
match value.op {
@ir.HotOp::RefNull => ArrayStorageNullableRefNullDefault
@ir.HotOp::Const =>
match @ir.hot_const_get(func, value.imm0) {
@ir.HotConstPayload::RefNullConst(_) =>
ArrayStorageNullableRefNullDefault
_ => ArrayStorageNonDefault
}
_ => ArrayStorageNonDefault
}
}
_ => ArrayStorageNonDefault
}
{ kind, }
}
///|
fn OiArrayStorageDefaultValueFact::proves_default(
self : OiArrayStorageDefaultValueFact,
) -> Bool {
match self.kind {
ArrayStorageI32ZeroDefault
| ArrayStorageI64ZeroDefault
| ArrayStorageF32ZeroDefault
| ArrayStorageF64ZeroDefault
| ArrayStorageNullableRefNullDefault => true
ArrayStorageNonDefault => false
}
}
///|
fn OiArrayStorageDefaultValueFact::proves_value_default_for_storage(
func : @ir.HotFunc,
storage : @lib.StorageType,
value_id : Int32,
) -> Bool {
OiArrayStorageDefaultValueFact::from_value(func, storage, value_id).proves_default()
}
///|
fn OiArrayDefaultFallthroughTraversalFact::from_kind(
fallthrough_id : Int32,
kind : OiArrayDefaultFallthroughKind,
) -> OiArrayDefaultFallthroughTraversalFact {
{ fallthrough_id, kind, }
}
///|
fn OiArrayDefaultFallthroughTraversalFact::from_value(
func : @ir.HotFunc,
node_masks : Array[UInt32],
value_id : Int32,
depth~ : Int32 = 0,
) -> OiArrayDefaultFallthroughTraversalFact {
// Binaryen v130 `visitArrayNew` and `visitArrayNewFixed` use
// `Properties::getFallthrough` to decide whether an initializer reaches a
// storage-default value. Keep the represented HOT traversal as a fact with a
// source-shaped stop/descend kind instead of a local opcode scanner.
if depth > 8 {
return OiArrayDefaultFallthroughTraversalFact::from_kind(
value_id,
ArrayDefaultFallthroughDepthExhausted,
)
}
let value = @ir.hot_node_get(func, value_id)
match value.op {
@ir.HotOp::LocalTee if value.child_count == 1 => {
let child_fact = OiArrayDefaultFallthroughTraversalFact::from_value(
func,
node_masks,
@ir.hot_child_get(func, value_id, 0),
depth=depth + 1,
)
OiArrayDefaultFallthroughTraversalFact::from_kind(
child_fact.fallthrough_id,
ArrayDefaultFallthroughLocalTeeChild,
)
}
@ir.HotOp::Block if value.child_count > 0 => {
for slot = 0; slot < value.child_count - 1; slot = slot + 1 {
let child_id = @ir.hot_child_get(func, value_id, slot)
if (node_masks[child_id] & @ir.EFFECT_MASK_CONTROL) != 0U {
return OiArrayDefaultFallthroughTraversalFact::from_kind(
value_id,
ArrayDefaultFallthroughBlockStoppedByPrefixControl,
)
}
}
let child_fact = OiArrayDefaultFallthroughTraversalFact::from_value(
func,
node_masks,
@ir.hot_child_get(func, value_id, value.child_count - 1),
depth=depth + 1,
)
OiArrayDefaultFallthroughTraversalFact::from_kind(
child_fact.fallthrough_id,
ArrayDefaultFallthroughBlockTailChild,
)
}
@ir.HotOp::Loop =>
match
optimize_instructions_region_tail_id(
func,
@ir.hot_region_body_ref(func, value_id),
) {
Some(tail_id) => {
let child_fact = OiArrayDefaultFallthroughTraversalFact::from_value(
func,
node_masks,
tail_id,
depth=depth + 1,
)
OiArrayDefaultFallthroughTraversalFact::from_kind(
child_fact.fallthrough_id,
ArrayDefaultFallthroughLoopBodyTail,
)
}
None =>
OiArrayDefaultFallthroughTraversalFact::from_kind(
value_id,
ArrayDefaultFallthroughLoopWithoutTail,
)
}
@ir.HotOp::If =>
match @ir.hot_region_else_ref(func, value_id) {
Some(else_ref) => {
let then_ref = @ir.hot_region_then_ref(func, value_id)
let one_sided_fact = OiIfOneSidedFallthroughRegionFact::from_regions(
func, then_ref, else_ref,
)
match one_sided_fact.proven_fallthrough_region() {
Some(region_ref) =>
match optimize_instructions_region_tail_id(func, region_ref) {
Some(tail_id) => {
let child_fact = OiArrayDefaultFallthroughTraversalFact::from_value(
func,
node_masks,
tail_id,
depth=depth + 1,
)
OiArrayDefaultFallthroughTraversalFact::from_kind(
child_fact.fallthrough_id,
ArrayDefaultFallthroughOneSidedIfTail,
)
}
None =>
OiArrayDefaultFallthroughTraversalFact::from_kind(
value_id,
ArrayDefaultFallthroughIfWithoutProvenRegion,
)
}
None =>
OiArrayDefaultFallthroughTraversalFact::from_kind(
value_id,
ArrayDefaultFallthroughIfWithoutProvenRegion,
)
}
}
None =>
OiArrayDefaultFallthroughTraversalFact::from_kind(
value_id,
ArrayDefaultFallthroughIfWithoutElse,
)
}
_ =>
OiArrayDefaultFallthroughTraversalFact::from_kind(
value_id,
ArrayDefaultFallthroughUnsupported,
)
}
}
///|
fn OiArrayDefaultFallthroughTraversalFact::proves_recorded_traversal_kind(
self : OiArrayDefaultFallthroughTraversalFact,
) -> Bool {
match self.kind {
ArrayDefaultFallthroughDepthExhausted
| ArrayDefaultFallthroughLocalTeeChild
| ArrayDefaultFallthroughBlockStoppedByPrefixControl
| ArrayDefaultFallthroughBlockTailChild
| ArrayDefaultFallthroughLoopBodyTail
| ArrayDefaultFallthroughLoopWithoutTail
| ArrayDefaultFallthroughOneSidedIfTail
| ArrayDefaultFallthroughIfWithoutElse
| ArrayDefaultFallthroughIfWithoutProvenRegion
| ArrayDefaultFallthroughUnsupported => true
}
}
///|
fn OiArrayDefaultPreservationFact::from_kind(
kind : OiArrayDefaultPreservationKind,
) -> OiArrayDefaultPreservationFact {
{ kind, }
}
///|
fn OiArrayDefaultPreservationFact::from_value(
func : @ir.HotFunc,
node_masks : Array[UInt32],
value_id : Int32,
fallthrough_id : Int32,
depth~ : Int32 = 0,
) -> OiArrayDefaultPreservationFact {
// Binaryen v130 `getDroppedChildrenAndAppend` preserves only wrappers whose
// evaluation can remain observable while replacing an array default value
// with the canonical default initializer. Keep the represented Starshine
// subset as an explicit preservation fact: matching fallthroughs and the
// depth guard erase, local.tee and `if` wrappers preserve, blocks preserve if
// any prefix child has effects or if the fallthrough child recursively needs
// preservation, and loops/other subtrees preserve only when HOT effects are
// not pure. The result is one source-shaped kind instead of parallel
// preservation booleans.
if value_id == fallthrough_id {
return OiArrayDefaultPreservationFact::from_kind(
ArrayDefaultReachedFallthrough,
)
}
if depth > 8 {
return OiArrayDefaultPreservationFact::from_kind(ArrayDefaultDepthExhausted)
}
let value = @ir.hot_node_get(func, value_id)
match value.op {
@ir.HotOp::LocalTee if value.child_count == 1 =>
OiArrayDefaultPreservationFact::from_kind(ArrayDefaultLocalTeeWrapper)
@ir.HotOp::Block if value.child_count > 0 => {
let mut block_prefix_effectful = false
for slot = 0; slot < value.child_count - 1; slot = slot + 1 {
let child_id = @ir.hot_child_get(func, value_id, slot)
if !OiNodeEffectPurityFact::from_node_masks(node_masks, child_id).proves_pure() {
block_prefix_effectful = true
}
}
if block_prefix_effectful {
OiArrayDefaultPreservationFact::from_kind(
ArrayDefaultBlockPrefixEffectful,
)
} else {
let nested = OiArrayDefaultPreservationFact::from_value(
func,
node_masks,
@ir.hot_child_get(func, value_id, value.child_count - 1),
fallthrough_id,
depth=depth + 1,
)
if nested.proves_preserve_needed() {
OiArrayDefaultPreservationFact::from_kind(
ArrayDefaultNestedBlockPreserves,
)
} else {
OiArrayDefaultPreservationFact::from_kind(
ArrayDefaultNoPreservationNeeded,
)
}
}
}
@ir.HotOp::Loop =>
if !OiNodeEffectPurityFact::from_node_masks(node_masks, value_id).proves_pure() {
OiArrayDefaultPreservationFact::from_kind(ArrayDefaultLoopEffectful)
} else {
OiArrayDefaultPreservationFact::from_kind(
ArrayDefaultNoPreservationNeeded,
)
}
@ir.HotOp::If =>
OiArrayDefaultPreservationFact::from_kind(ArrayDefaultIfControlShape)
_ =>
if !OiNodeEffectPurityFact::from_node_masks(node_masks, value_id).proves_pure() {
OiArrayDefaultPreservationFact::from_kind(ArrayDefaultSubtreeEffectful)
} else {
OiArrayDefaultPreservationFact::from_kind(
ArrayDefaultNoPreservationNeeded,
)
}
}
}
///|
fn OiArrayDefaultPreservationFact::proves_preserve_needed(
self : OiArrayDefaultPreservationFact,
) -> Bool {
match self.kind {
ArrayDefaultLocalTeeWrapper
| ArrayDefaultBlockPrefixEffectful
| ArrayDefaultNestedBlockPreserves
| ArrayDefaultLoopEffectful
| ArrayDefaultIfControlShape
| ArrayDefaultSubtreeEffectful => true
ArrayDefaultReachedFallthrough
| ArrayDefaultDepthExhausted
| ArrayDefaultNoPreservationNeeded => false
}
}
///|
fn OiArrayDefaultFallthroughFact::from_value(
func : @ir.HotFunc,
node_masks : Array[UInt32],
value_id : Int32,
) -> OiArrayDefaultFallthroughFact {
{
traversal_fact: OiArrayDefaultFallthroughTraversalFact::from_value(
func, node_masks, value_id,
),
}
}
///|
fn OiArrayDefaultFallthroughFact::fallthrough_id(
self : OiArrayDefaultFallthroughFact,
) -> Int32 {
ignore(self.traversal_fact.proves_recorded_traversal_kind())
self.traversal_fact.fallthrough_id
}
///|
fn OiChildLocalizationPlanFact::empty() -> OiChildLocalizationPlanFact {
{ source_ids: [], drop_ids: [], }
}
///|
fn OiChildLocalizationPlanFact::from_repeated_array_new_fixed_non_default(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiChildLocalizationPlanFact {
// Binaryen v130 `visitArrayNewFixed` hands the non-default repeated-value
// rewrite to `ChildLocalizer`: operands whose evaluation must remain visible
// are materialized before the replacement `array.new`, while trivially pure
// duplicate scalars are dropped. Represent that decision as a reusable plan
// fact instead of storing an ad hoc list on the array constructor fact.
let effects = pass_require_effects(ctx, func)
let node = @ir.hot_node_get(func, node_id)
let source_ids : Array[Int32] = []
for slot = 0; slot < node.child_count; slot = slot + 1 {
let child_id = @ir.hot_child_get(func, node_id, slot)
if !OiNodeEffectPurityFact::from_node_masks(effects.node_masks, child_id).proves_pure() {
source_ids.push(child_id)
}
}
{ source_ids, drop_ids: [], }
}
///|
fn OiChildLocalizationPlanFact::needs_localization(
self : OiChildLocalizationPlanFact,
) -> Bool {
self.source_ids.length() > 0
}
///|
fn OiChildLocalizationPlanFact::from_array_default_fallthrough(
func : @ir.HotFunc,
node_masks : Array[UInt32],
value_id : Int32,
fallthrough_id : Int32,
) -> OiChildLocalizationPlanFact {
// Binaryen v130 uses `getDroppedChildrenAndAppend` for both `array.new`
// default-initializer rewrites and default repeated `array.new_fixed`
// rewrites. Represent the dropped-child preservation decision as the same
// ChildLocalizer-shaped fact used by non-default repeated fixed arrays: pure
// default-producing wrappers are erased, while value wrappers with observable
// effects are explicitly dropped before the replacement allocation.
if OiArrayDefaultPreservationFact::from_value(
func, node_masks, value_id, fallthrough_id,
).proves_preserve_needed() {
{ source_ids: [], drop_ids: [value_id], }
} else {
OiChildLocalizationPlanFact::empty()
}
}
///|
fn OiChildLocalizationPlanFact::from_array_new_default_initializer(
value_id : Int32,
) -> OiChildLocalizationPlanFact {
// Binaryen v130 `visitArrayNew` always sequences `drop(init)` before the
// converted `array.new_default`, even when the fallthrough default itself is
// pure. Keep that separate from the fixed-array default plan, where
// `getDroppedChildrenAndAppend` can erase pure duplicate default operands.
{ source_ids: [], drop_ids: [value_id], }
}
///|
fn OiChildLocalizationPlanFact::needs_dropped_children(
self : OiChildLocalizationPlanFact,
) -> Bool {
self.drop_ids.length() > 0
}
///|
fn OiChildLocalizationPlanFact::append_dropped_children(
self : OiChildLocalizationPlanFact,
func : @ir.HotFunc,
body : Array[Int32],
) -> Unit {
for source_id in self.drop_ids {
body.push(@ir.hot_build_drop(func, source_id))
}
}
///|
fn OiChildLocalizationPlanFact::append_localized_inputs(
self : OiChildLocalizationPlanFact,
func : @ir.HotFunc,
value_type : @lib.ValType,
representative_id : Int32,
body : Array[Int32],
) -> Int32 {
let mut representative_local = -1
for source_id in self.source_ids {
let local_id = optimize_instructions_add_temp_local(func, value_type)
if source_id == representative_id && representative_local < 0 {
representative_local = local_id
}
body.push(@ir.hot_build_local_set(func, local_id, source_id))
}
if representative_local >= 0 {
@ir.hot_build_local_get(func, representative_local)
} else {
representative_id
}
}
///|
fn OiArrayDefaultValueFact::from_kind(
kind : OiArrayDefaultValueKind,
preservation_plan~ : OiChildLocalizationPlanFact = OiChildLocalizationPlanFact::empty(),
) -> OiArrayDefaultValueFact {
{ kind, preservation_plan, }
}
///|
fn OiArrayDefaultValueFact::proves_default(
self : OiArrayDefaultValueFact,
) -> Bool {
match self.kind {
ArrayDefaultStorageDefault => true
ArrayDefaultNoModuleContext
| ArrayDefaultMissingField
| ArrayDefaultStorageNonDefault => false
}
}
///|
fn OiArrayNewFixedRepresentativeDefaultFact::from_default_value_fact(
default_value_fact : OiArrayDefaultValueFact,
) -> OiArrayNewFixedRepresentativeDefaultFact {
let kind = if default_value_fact.proves_default() {
ArrayNewFixedRepresentativeDefaultValue
} else {
ArrayNewFixedRepresentativeNonDefaultValue
}
{ kind, default_value_fact, }
}
///|
fn OiArrayNewFixedRepresentativeDefaultFact::proves_default(
self : OiArrayNewFixedRepresentativeDefaultFact,
) -> Bool {
match self.kind {
ArrayNewFixedRepresentativeDefaultValue => true
ArrayNewFixedRepresentativeNonDefaultValue => false
}
}
///|
fn OiArrayDefaultValueFact::from_array_value(
ctx : HotPassContext,
func : @ir.HotFunc,
type_idx : @lib.TypeIdx,
value_id : Int32,
) -> OiArrayDefaultValueFact {
// Binaryen v130 `visitArrayNew` and `visitArrayNewFixed` check
// `getFallthrough(value)` before deciding whether an array initializer or an
// equal fixed-array representative is the storage default, then delegate
// effect preservation to `getDroppedChildrenAndAppend`. Keep the represented
// fallthrough/default result as a fact so constructor rewrites do not inline a
// one-off local.tee, block, loop, or one-unreachable-arm `if` scanner. Broader
// Binaryen `getFallthrough` shapes stay documented until they have
// source-probed preservation facts.
let module_ctx = match ctx.module_ctx {
Some(module_ctx) => module_ctx
None =>
return OiArrayDefaultValueFact::from_kind(ArrayDefaultNoModuleContext)
}
let field = match @ir.hot_module_resolve_array_field(module_ctx, type_idx) {
Some(field) => field
None => return OiArrayDefaultValueFact::from_kind(ArrayDefaultMissingField)
}
let effects = pass_require_effects(ctx, func)
let fallthrough_fact = OiArrayDefaultFallthroughFact::from_value(
func,
effects.node_masks,
value_id,
)
let fallthrough_id = fallthrough_fact.fallthrough_id()
let default_source = if @ir.hot_node_get(func, fallthrough_id).op
is @ir.HotOp::LocalGet {
let graph = @ir.local_graph_build(
func,
@ir.cfg_build(func, expand_operand_control=true),
)
match oi_stored_scalar_source(graph, func, fallthrough_id) {
Some(node) if node >= 0 => node
_ => fallthrough_id
}
} else {
fallthrough_id
}
if !OiArrayStorageDefaultValueFact::proves_value_default_for_storage(
func,
field.get_storage_type(),
default_source,
) {
return OiArrayDefaultValueFact::from_kind(ArrayDefaultStorageNonDefault)
}
OiArrayDefaultValueFact::from_kind(
ArrayDefaultStorageDefault,
preservation_plan=OiChildLocalizationPlanFact::from_array_default_fallthrough(
func,
effects.node_masks,
value_id,
fallthrough_fact.fallthrough_id(),
),
)
}
///|
fn OiArrayNewFixedRepeatedValueFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiArrayNewFixedRepeatedValueFact? {
let node = @ir.hot_node_get(func, node_id)
let (type_idx, size) = match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::ArrayNewFixed(type_idx, @lib.U32(size))) =>
(type_idx, size.reinterpret_as_int())
_ => return None
}
if size <= 0 || size != node.child_count {
return None
}
for slot = 0; slot < node.child_count - 1; slot = slot + 1 {
let equality_fact = OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx,
func,
@ir.hot_child_get(func, node_id, slot),
@ir.hot_child_get(func, node_id, slot + 1),
)
if !equality_fact.proves_equal(func) {
return None
}
}
let representative_id = @ir.hot_child_get(func, node_id, 0)
let default_fact = OiArrayDefaultValueFact::from_array_value(
ctx, func, type_idx, representative_id,
)
let representative_default_fact = OiArrayNewFixedRepresentativeDefaultFact::from_default_value_fact(
default_fact,
)
let non_default_localization_plan = if representative_default_fact.proves_default() {
OiChildLocalizationPlanFact::empty()
} else {
OiChildLocalizationPlanFact::from_repeated_array_new_fixed_non_default(
ctx, func, node_id,
)
}
Some({
node_id,
type_idx,
size,
representative_id,
representative_default_fact,
non_default_localization_plan,
})
}
///|
fn OiArrayNewFixedRepeatedValueFact::is_rewritable(
self : OiArrayNewFixedRepeatedValueFact,
) -> Bool {
self.representative_default_fact.proves_default() || self.size >= 2
}
///|
fn OiArrayNewFixedRepeatedValueFact::apply_non_default_localized_rewrite(
self : OiArrayNewFixedRepeatedValueFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Bool {
// Binaryen v130 routes the non-default `array.new_fixed` -> `array.new`
// collapse through `getDroppedChildrenAndAppend`, which materializes
// state-reading/effectful fixed operands into scratch locals before the new
// repeated-value allocation. Keep that preservation as data on the repeated
// value fact instead of letting the rewrite drop every non-representative
// operand after equality succeeds.
let value_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, self.representative_id)) {
[value_type] => value_type
_ => return false
}
let original = @ir.hot_node_get(func, self.node_id)
let body : Array[Int32] = []
let representative_value_id = self.non_default_localization_plan.append_localized_inputs(
func,
value_type,
self.representative_id,
body,
)
let len_id = @ir.hot_build_const_i32(func, @lib.I32(self.size))
let temp = @ir.hot_build_node(
func,
original.op,
@ir.hot_node_type(func, self.node_id),
children=[representative_value_id, len_id],
)
@ir.hot_node_exact_instr_set(
func,
temp,
@lib.Instruction::array_new(self.type_idx),
)
body.push(temp)
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, self.node_id)) {
[val_type] => @lib.BlockType::val_type(val_type)
_ => @lib.BlockType::void_()
}
let replacement_id = @ir.hot_build_block(func, block_type, body)
let replacement = @ir.hot_node_get(func, replacement_id)
@ir.hot_replace_node(func, self.node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, self.node_id),
self.node_id,
)
@ir.hot_delete_node(func, replacement_id)
pass_mark_mutated(ctx, func)
true
}
///|
fn OiArrayNewFixedRepeatedValueFact::apply_rewrite(
self : OiArrayNewFixedRepeatedValueFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Unit {
if !self.representative_default_fact.proves_default() &&
self.non_default_localization_plan.needs_localization() &&
self.apply_non_default_localized_rewrite(ctx, func) {
return
}
let body : Array[Int32] = []
if self.representative_default_fact.proves_default() {
for slot = 0
slot < @ir.hot_child_count(func, self.node_id)
slot = slot + 1 {
let child = @ir.hot_child_get(func, self.node_id, slot)
let default_fact = OiArrayDefaultValueFact::from_array_value(
ctx,
func,
self.type_idx,
child,
)
if default_fact.proves_default() {
default_fact.preservation_plan.append_dropped_children(func, body)
} else {
// Equality proves the resulting value, not that evaluation is disposable.
body.push(@ir.hot_build_drop(func, child))
}
}
}
let original = @ir.hot_node_get(func, self.node_id)
let len_id = @ir.hot_build_const_i32(func, @lib.I32(self.size))
let temp = if self.representative_default_fact.proves_default() {
@ir.hot_build_node(
func,
original.op,
@ir.hot_node_type(func, self.node_id),
children=[len_id],
)
} else {
@ir.hot_build_node(
func,
original.op,
@ir.hot_node_type(func, self.node_id),
children=[self.representative_id, len_id],
)
}
@ir.hot_node_exact_instr_set(
func,
temp,
if self.representative_default_fact.proves_default() {
@lib.Instruction::array_new_default(self.type_idx)
} else {
@lib.Instruction::array_new(self.type_idx)
},
)
if !body.is_empty() {
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, self.node_id)) {
[val_type] => @lib.BlockType::val_type(val_type)
_ => @lib.BlockType::void_()
}
body.push(temp)
let replacement_id = @ir.hot_build_block(func, block_type, body)
let replacement = @ir.hot_node_get(func, replacement_id)
@ir.hot_replace_node(func, self.node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, self.node_id),
self.node_id,
)
@ir.hot_delete_node(func, replacement_id)
} else {
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, self.node_id, replacement)
}
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_try_rewrite_array_new_fixed_repeated_values(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let fact = match
OiArrayNewFixedRepeatedValueFact::from_node(ctx, func, node_id) {
Some(fact) => fact
None => return false
}
if !fact.is_rewritable() {
return false
}
fact.apply_rewrite(ctx, func)
true
}
///|
priv struct OiArrayNewDefaultInitRewriteFact {
node_id : Int32
type_idx : @lib.TypeIdx
preservation_plan : OiChildLocalizationPlanFact
len_id : Int32
}
///|
priv struct OiArrayNewSingleFixedFact {
node_id : Int32
type_idx : @lib.TypeIdx
init_id : Int32
}
///|
fn OiArrayNewDefaultInitRewriteFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiArrayNewDefaultInitRewriteFact? {
let node = @ir.hot_node_get(func, node_id)
let type_idx = match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::ArrayNew(type_idx)) => type_idx
_ => return None
}
if node.child_count != 2 {
return None
}
let init_id = @ir.hot_child_get(func, node_id, 0)
let default_fact = OiArrayDefaultValueFact::from_array_value(
ctx, func, type_idx, init_id,
)
if !default_fact.proves_default() {
return None
}
Some({
node_id,
type_idx,
preservation_plan: OiChildLocalizationPlanFact::from_array_new_default_initializer(
init_id,
),
len_id: @ir.hot_child_get(func, node_id, 1),
})
}
///|
fn OiArrayNewDefaultInitRewriteFact::apply_rewrite(
self : OiArrayNewDefaultInitRewriteFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Unit {
let original = @ir.hot_node_get(func, self.node_id)
let temp = @ir.hot_build_node(
func,
original.op,
@ir.hot_node_type(func, self.node_id),
children=[self.len_id],
)
@ir.hot_node_exact_instr_set(
func,
temp,
@lib.Instruction::array_new_default(self.type_idx),
)
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, self.node_id)) {
[val_type] => @lib.BlockType::val_type(val_type)
_ => @lib.BlockType::void_()
}
let body : Array[Int32] = []
self.preservation_plan.append_dropped_children(func, body)
body.push(temp)
let replacement_id = @ir.hot_build_block(func, block_type, body)
let replacement = @ir.hot_node_get(func, replacement_id)
@ir.hot_replace_node(func, self.node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, self.node_id),
self.node_id,
)
@ir.hot_delete_node(func, replacement_id)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_try_rewrite_array_new_default_init(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let fact = match
OiArrayNewDefaultInitRewriteFact::from_node(ctx, func, node_id) {
Some(fact) => fact
None => return false
}
fact.apply_rewrite(ctx, func)
true
}
///|
fn OiArrayNewSingleFixedFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiArrayNewSingleFixedFact? {
let node = @ir.hot_node_get(func, node_id)
let type_idx = match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::ArrayNew(type_idx)) => type_idx
_ => return None
}
if node.child_count != 2 {
return None
}
let len_id = @ir.hot_child_get(func, node_id, 1)
match @ir.hot_node_get(func, len_id).op {
@ir.HotOp::Const =>
if @ir.hot_const_get(func, @ir.hot_node_get(func, len_id).imm0) !=
@ir.HotConstPayload::i32_const(@lib.I32(1)) {
return None
}
_ => return None
}
Some({ node_id, type_idx, init_id: @ir.hot_child_get(func, node_id, 0), })
}
///|
fn OiArrayNewSingleFixedFact::apply_rewrite(
self : OiArrayNewSingleFixedFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Unit {
let original = @ir.hot_node_get(func, self.node_id)
let temp = @ir.hot_build_node(
func,
original.op,
@ir.hot_node_type(func, self.node_id),
children=[self.init_id],
)
@ir.hot_node_exact_instr_set(
func,
temp,
@lib.Instruction::array_new_fixed(self.type_idx, @lib.U32(1U)),
)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, self.node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_try_rewrite_array_new_single_fixed(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let fact = match OiArrayNewSingleFixedFact::from_node(func, node_id) {
Some(fact) => fact
None => return false
}
fact.apply_rewrite(ctx, func)
true
}
///|
fn optimize_instructions_try_truncate_narrow_store_const_value(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match
OiStoredValueRewritePlanFact::from_truncated_const(func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_drop_narrow_store_value_mask(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match OiStoredValueRewritePlanFact::from_low_bit_mask(func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_rewrite_reinterpret_store_value(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match OiStoredValueRewritePlanFact::from_reinterpret(func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_rewrite_reinterpret_load_result(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match
OiLoadResultRewritePlanFact::from_reinterpret(ctx, func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_rewrite_i64_extend_i32_load_result(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match
OiLoadResultRewritePlanFact::from_i64_extend_i32(ctx, func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_widen_store_of_wrap_i64(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match
OiStoredValueRewritePlanFact::from_wrap_i64_widen(ctx, func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_replace_with_dropped_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
operands : Array[Int32],
) -> Unit {
let body : Array[Int32] = []
for operand in operands {
body.push(@ir.hot_build_drop(func, operand))
}
let temp = @ir.hot_build_block(func, @lib.BlockType::void_(), body)
let replacement = @ir.hot_node_get(func, temp)
// The built block's label initially belongs to the detached temporary node.
// Rehome it before deleting that wrapper so nested parent analysis never sees
// a live label whose owner is dead.
@ir.hot_label_owner_set(func, replacement.imm0, node_id)
@ir.hot_delete_detached_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
}
///|
priv enum OiDeadShellRewritePlanKind {
DeadShellBulkMemoryCopy
DeadShellBulkMemoryFill
}
///|
priv struct OiDeadShellRewritePlanFact {
kind : OiDeadShellRewritePlanKind
bulk_plan : OiBulkMemoryRewritePlanFact
}
///|
priv enum OiBulkMemoryRewritePlanKind {
BulkMemoryDropSameAddressCopy
BulkMemoryDropZeroSizeCopy
BulkMemoryDropZeroSizeFill
BulkMemoryTinyCopy
BulkMemoryTinyFill
}
///|
priv struct OiBulkMemoryRewritePlanFact {
kind : OiBulkMemoryRewritePlanKind
bulk_fact : OiBulkMemoryFact
copy_fact : OiTinyMemoryCopyFact?
fill_fact : OiTinyMemoryFillFact?
}
///|
fn OiBulkMemoryRewritePlanFact::from_copy(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBulkMemoryRewritePlanFact? {
// Binaryen v130 `optimizeMemoryCopy` first handles trap-relaxed identical
// addresses, then trap-relaxed zero sizes, then constant tiny load/store
// lowering. Keep that source order as a plan fact so the visitor does not own
// a branch cascade for the OI-G bulk-memory rewrite families.
let bulk_fact = match OiBulkMemoryFact::from_copy(func, dead, node_id) {
Some(fact) => fact
None => return None
}
match OiBulkMemorySameAddressCopyDropFact::from_copy(ctx, func, bulk_fact) {
Some(fact) =>
return Some({
kind: BulkMemoryDropSameAddressCopy,
bulk_fact: fact.bulk_fact,
copy_fact: None,
fill_fact: None,
})
None => ()
}
match OiBulkMemoryZeroSizeDropFact::from_bulk(ctx, func, bulk_fact) {
Some(fact) =>
return Some({
kind: BulkMemoryDropZeroSizeCopy,
bulk_fact: fact.bulk_fact,
copy_fact: None,
fill_fact: None,
})
None => ()
}
let const_size_fact = match bulk_fact.const_size_fact {
Some(fact) => fact
None => return None
}
let copy_fact = match
OiTinyMemoryCopyFact::from_size(ctx, const_size_fact.size) {
Some(fact) => fact
None => return None
}
Some({
kind: BulkMemoryTinyCopy,
bulk_fact,
copy_fact: Some(copy_fact),
fill_fact: None,
})
}
///|
fn OiBulkMemoryRewritePlanFact::from_fill(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiBulkMemoryRewritePlanFact? {
// Binaryen v130 `optimizeMemoryFill` requires a constant size, handles the
// trap-relaxed zero-size drop, then lowers constant-byte fills and the
// nonconstant size-1 store8 case. SIMD-forbidden size-16 fills keep their
// atomic bounds-check/write behavior rather than split into scalar stores.
let bulk_fact = match OiBulkMemoryFact::from_fill(func, dead, node_id) {
Some(fact) => fact
None => return None
}
match OiBulkMemoryZeroSizeDropFact::from_bulk(ctx, func, bulk_fact) {
Some(fact) =>
return Some({
kind: BulkMemoryDropZeroSizeFill,
bulk_fact: fact.bulk_fact,
copy_fact: None,
fill_fact: None,
})
None => ()
}
let const_size_fact = match bulk_fact.const_size_fact {
Some(fact) => fact
None => return None
}
let fill_fact = match
OiTinyMemoryFillFact::from_size_and_value(
ctx,
func,
dead,
const_size_fact.size,
bulk_fact.src_or_value_id,
) {
Some(fact) => fact
None => return None
}
Some({
kind: BulkMemoryTinyFill,
bulk_fact,
copy_fact: None,
fill_fact: Some(fill_fact),
})
}
///|
fn OiBulkMemoryRewritePlanFact::apply(
self : OiBulkMemoryRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
BulkMemoryDropSameAddressCopy => {
optimize_instructions_replace_with_dropped_operands(ctx, func, node_id, [
self.bulk_fact.dst_id,
self.bulk_fact.src_or_value_id,
self.bulk_fact.size_id,
])
true
}
BulkMemoryDropZeroSizeCopy | BulkMemoryDropZeroSizeFill => {
optimize_instructions_replace_with_dropped_operands(ctx, func, node_id, [
self.bulk_fact.dst_id,
self.bulk_fact.src_or_value_id,
])
true
}
BulkMemoryTinyCopy => self.apply_tiny_copy(ctx, func, node_id)
BulkMemoryTinyFill => self.apply_tiny_fill(ctx, func, node_id)
}
}
///|
fn OiBulkMemoryRewritePlanFact::apply_tiny_copy(
self : OiBulkMemoryRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let copy_fact = match self.copy_fact {
Some(fact) => fact
None => return false
}
let src_memarg = self.bulk_fact.src_memarg()
let load = @ir.hot_build_load(
func,
copy_fact.load_type(),
src_memarg,
self.bulk_fact.src_or_value_id,
)
@ir.hot_node_exact_instr_set(func, load, copy_fact.load_instr(src_memarg))
let dst_memarg = self.bulk_fact.dst_memarg()
let temp = @ir.hot_build_store(func, dst_memarg, self.bulk_fact.dst_id, load)
@ir.hot_node_exact_instr_set(func, temp, copy_fact.store_instr(dst_memarg))
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_delete_node(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
pass_mark_mutated(ctx, func)
true
}
///|
fn OiBulkMemoryRewritePlanFact::apply_tiny_fill(
self : OiBulkMemoryRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let fill_fact = match self.fill_fact {
Some(fact) => fact
None => return false
}
let stored_id = match
fill_fact.build_store_value(func, self.bulk_fact.src_or_value_id) {
Some(stored_id) => stored_id
None => return false
}
optimize_instructions_replace_with_store_exact(
ctx,
func,
node_id,
self.bulk_fact.dst_mem_idx,
self.bulk_fact.dst_id,
stored_id,
fn(memarg) { fill_fact.store_instr(memarg) },
)
true
}
///|
fn optimize_instructions_try_expand_tiny_memory_copy(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match OiBulkMemoryRewritePlanFact::from_copy(ctx, func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_expand_tiny_memory_fill(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match OiBulkMemoryRewritePlanFact::from_fill(ctx, func, dead, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn OiDeadShellRewritePlanFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDeadShellRewritePlanFact? {
// Binaryen still optimizes selected bulk-memory children under syntactically
// unreachable shells during the OI walk. Starshine keeps that dead-shell
// parity boundary as an explicit plan fact instead of a visitor-owned opcode
// whitelist: only OI-G bulk-memory rewrite plans may run on dead roots, and
// every accepted shape still passes through the ordinary bulk fact/plan layer.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::MemoryCopy =>
match OiBulkMemoryRewritePlanFact::from_copy(ctx, func, dead, node_id) {
Some(bulk_plan) => Some({ kind: DeadShellBulkMemoryCopy, bulk_plan, })
None => None
}
@ir.HotOp::MemoryFill =>
match OiBulkMemoryRewritePlanFact::from_fill(ctx, func, dead, node_id) {
Some(bulk_plan) => Some({ kind: DeadShellBulkMemoryFill, bulk_plan, })
None => None
}
_ => None
}
}
///|
fn OiDeadShellRewritePlanFact::apply(
self : OiDeadShellRewritePlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
DeadShellBulkMemoryCopy | DeadShellBulkMemoryFill =>
self.bulk_plan.apply(ctx, func, node_id)
}
}
///|
priv enum OiOrderedBinaryLocalLocalEqualityKind {
OrderedBinaryLocalLocalNoMatch
OrderedBinaryI32LocalLocalMatch
OrderedBinaryI64LocalLocalMatch
OrderedBinaryF32LocalLocalMatch
OrderedBinaryF64LocalLocalMatch
}
///|
priv struct OiOrderedBinaryLocalLocalEqualityFact {
kind : OiOrderedBinaryLocalLocalEqualityKind
}
///|
fn OiOrderedBinaryLocalLocalEqualityFact::from_nodes(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs : Int32,
rhs : Int32,
) -> OiOrderedBinaryLocalLocalEqualityFact {
// Binaryen v130 `ExpressionAnalyzer::equal` treats equal binary nodes as the
// same operation with recursively equal ordered operands. For the select-arm
// `!condition.invalidates(value)` path, this Starshine fact records the
// represented nontrapping ordered local+local subset whose operands cannot be
// invalidated by an effectful condition. The kind names the single scalar
// proof class accepted by the current no-widening subset.
let mut kind = OrderedBinaryLocalLocalNoMatch
match
(
@ir.hot_node_exact_instr(func, lhs),
@ir.hot_node_exact_instr(func, rhs),
@ir.hot_node_get(func, lhs).child_count,
@ir.hot_node_get(func, rhs).child_count,
) {
(Some(lhs_instr), Some(rhs_instr), 2, 2) if lhs_instr == rhs_instr => {
let lhs_left = @ir.hot_child_get(func, lhs, 0)
let lhs_right = @ir.hot_child_get(func, lhs, 1)
let rhs_left = @ir.hot_child_get(func, rhs, 0)
let rhs_right = @ir.hot_child_get(func, rhs, 1)
let mut matching_ordered_local_operands = false
match
(
OiLocalGetLeafFact::from_node(func, dead, lhs_left).local_id,
OiLocalGetLeafFact::from_node(func, dead, rhs_left).local_id,
OiLocalGetLeafFact::from_node(func, dead, lhs_right).local_id,
OiLocalGetLeafFact::from_node(func, dead, rhs_right).local_id,
) {
(
Some(lhs_left_local),
Some(rhs_left_local),
Some(lhs_right_local),
Some(rhs_right_local),
) =>
matching_ordered_local_operands = lhs_left_local == rhs_left_local &&
lhs_right_local == rhs_right_local
_ => ()
}
if matching_ordered_local_operands {
if lhs_instr == @lib.Instruction::i32_add() ||
lhs_instr == @lib.Instruction::i32_sub() ||
lhs_instr == @lib.Instruction::i32_mul() ||
lhs_instr == @lib.Instruction::i32_and() ||
lhs_instr == @lib.Instruction::i32_or() ||
lhs_instr == @lib.Instruction::i32_xor() ||
lhs_instr == @lib.Instruction::i32_shl() ||
lhs_instr == @lib.Instruction::i32_shr_s() ||
lhs_instr == @lib.Instruction::i32_shr_u() ||
lhs_instr == @lib.Instruction::i32_rotl() ||
lhs_instr == @lib.Instruction::i32_rotr() ||
lhs_instr == @lib.Instruction::i32_eq() ||
lhs_instr == @lib.Instruction::i32_ne() ||
lhs_instr == @lib.Instruction::i32_lt_s() ||
lhs_instr == @lib.Instruction::i32_lt_u() ||
lhs_instr == @lib.Instruction::i32_gt_s() ||
lhs_instr == @lib.Instruction::i32_gt_u() ||
lhs_instr == @lib.Instruction::i32_le_s() ||
lhs_instr == @lib.Instruction::i32_le_u() ||
lhs_instr == @lib.Instruction::i32_ge_s() ||
lhs_instr == @lib.Instruction::i32_ge_u() {
kind = OrderedBinaryI32LocalLocalMatch
} else if lhs_instr == @lib.Instruction::i64_add() ||
lhs_instr == @lib.Instruction::i64_sub() ||
lhs_instr == @lib.Instruction::i64_mul() ||
lhs_instr == @lib.Instruction::i64_and() ||
lhs_instr == @lib.Instruction::i64_or() ||
lhs_instr == @lib.Instruction::i64_xor() ||
lhs_instr == @lib.Instruction::i64_shl() ||
lhs_instr == @lib.Instruction::i64_shr_s() ||
lhs_instr == @lib.Instruction::i64_shr_u() ||
lhs_instr == @lib.Instruction::i64_rotl() ||
lhs_instr == @lib.Instruction::i64_rotr() ||
lhs_instr == @lib.Instruction::i64_eq() ||
lhs_instr == @lib.Instruction::i64_ne() ||
lhs_instr == @lib.Instruction::i64_lt_s() ||
lhs_instr == @lib.Instruction::i64_lt_u() ||
lhs_instr == @lib.Instruction::i64_gt_s() ||
lhs_instr == @lib.Instruction::i64_gt_u() ||
lhs_instr == @lib.Instruction::i64_le_s() ||
lhs_instr == @lib.Instruction::i64_le_u() ||
lhs_instr == @lib.Instruction::i64_ge_s() ||
lhs_instr == @lib.Instruction::i64_ge_u() {
kind = OrderedBinaryI64LocalLocalMatch
} else if lhs_instr == @lib.Instruction::f32_add() ||
lhs_instr == @lib.Instruction::f32_sub() ||
lhs_instr == @lib.Instruction::f32_mul() ||
lhs_instr == @lib.Instruction::f32_div() ||
lhs_instr == @lib.Instruction::f32_min() ||
lhs_instr == @lib.Instruction::f32_max() ||
lhs_instr == @lib.Instruction::f32_copysign() ||
lhs_instr == @lib.Instruction::f32_eq() ||
lhs_instr == @lib.Instruction::f32_ne() ||
lhs_instr == @lib.Instruction::f32_lt() ||
lhs_instr == @lib.Instruction::f32_gt() ||
lhs_instr == @lib.Instruction::f32_le() ||
lhs_instr == @lib.Instruction::f32_ge() {
kind = OrderedBinaryF32LocalLocalMatch
} else if lhs_instr == @lib.Instruction::f64_add() ||
lhs_instr == @lib.Instruction::f64_sub() ||
lhs_instr == @lib.Instruction::f64_mul() ||
lhs_instr == @lib.Instruction::f64_div() ||
lhs_instr == @lib.Instruction::f64_min() ||
lhs_instr == @lib.Instruction::f64_max() ||
lhs_instr == @lib.Instruction::f64_copysign() ||
lhs_instr == @lib.Instruction::f64_eq() ||
lhs_instr == @lib.Instruction::f64_ne() ||
lhs_instr == @lib.Instruction::f64_lt() ||
lhs_instr == @lib.Instruction::f64_gt() ||
lhs_instr == @lib.Instruction::f64_le() ||
lhs_instr == @lib.Instruction::f64_ge() {
kind = OrderedBinaryF64LocalLocalMatch
}
}
}
_ => ()
}
{ kind, }
}
///|
fn OiOrderedBinaryLocalLocalEqualityFact::proves_i32_nontrapping_scalar(
self : OiOrderedBinaryLocalLocalEqualityFact,
) -> Bool {
match self.kind {
OrderedBinaryI32LocalLocalMatch => true
_ => false
}
}
///|
fn OiOrderedBinaryLocalLocalEqualityFact::proves_i64_nontrapping_scalar(
self : OiOrderedBinaryLocalLocalEqualityFact,
) -> Bool {
match self.kind {
OrderedBinaryI64LocalLocalMatch => true
_ => false
}
}
///|
fn OiOrderedBinaryLocalLocalEqualityFact::proves_f32_nontrapping_scalar(
self : OiOrderedBinaryLocalLocalEqualityFact,
) -> Bool {
match self.kind {
OrderedBinaryF32LocalLocalMatch => true
_ => false
}
}
///|
fn OiOrderedBinaryLocalLocalEqualityFact::proves_f64_nontrapping_scalar(
self : OiOrderedBinaryLocalLocalEqualityFact,
) -> Bool {
match self.kind {
OrderedBinaryF64LocalLocalMatch => true
_ => false
}
}
///|
priv enum OiOrderedBinaryLocalConstEqualityKind {
OrderedBinaryLocalConstNoMatch
OrderedBinaryI32LocalConstMatch
OrderedBinaryI64LocalConstMatch
OrderedBinaryF32LocalConstMatch
OrderedBinaryF64LocalConstMatch
}
///|
priv struct OiOrderedBinaryLocalConstEqualityFact {
instr : @lib.Instruction?
kind : OiOrderedBinaryLocalConstEqualityKind
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::from_nodes(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs : Int32,
rhs : Int32,
) -> OiOrderedBinaryLocalConstEqualityFact {
// Binaryen v130 `ExpressionAnalyzer::equal` treats equal binary nodes as the
// same operation with recursively equal ordered operands. For the select-arm
// `!condition.invalidates(value)` path, this fact records the represented
// nontrapping ordered local+constant subset whose local operand cannot be
// invalidated by an effectful condition and whose constant operand is stable.
let mut instr : @lib.Instruction? = None
let mut kind = OrderedBinaryLocalConstNoMatch
match
(
@ir.hot_node_exact_instr(func, lhs),
@ir.hot_node_exact_instr(func, rhs),
@ir.hot_node_get(func, lhs).child_count,
@ir.hot_node_get(func, rhs).child_count,
) {
(Some(lhs_instr), Some(rhs_instr), 2, 2) if lhs_instr == rhs_instr => {
instr = Some(lhs_instr)
let lhs_left = @ir.hot_child_get(func, lhs, 0)
let lhs_right = @ir.hot_child_get(func, lhs, 1)
let rhs_left = @ir.hot_child_get(func, rhs, 0)
let rhs_right = @ir.hot_child_get(func, rhs, 1)
let mut matching_local_operand = false
match
(
OiLocalGetLeafFact::from_node(func, dead, lhs_left).local_id,
OiLocalGetLeafFact::from_node(func, dead, rhs_left).local_id,
) {
(Some(lhs_local), Some(rhs_local)) =>
matching_local_operand = lhs_local == rhs_local
_ => ()
}
if matching_local_operand {
match
(
optimize_instructions_i32_const(func, dead, lhs_right),
optimize_instructions_i32_const(func, dead, rhs_right),
) {
(Some(lhs_const), Some(rhs_const)) if lhs_const == rhs_const =>
kind = OrderedBinaryI32LocalConstMatch
_ => ()
}
match
(
optimize_instructions_i64_const(func, dead, lhs_right),
optimize_instructions_i64_const(func, dead, rhs_right),
) {
(Some(lhs_const), Some(rhs_const)) if lhs_const == rhs_const =>
kind = OrderedBinaryI64LocalConstMatch
_ => ()
}
match
(
optimize_instructions_f32_const(func, dead, lhs_right),
optimize_instructions_f32_const(func, dead, rhs_right),
) {
(Some(lhs_const), Some(rhs_const)) if lhs_const == rhs_const =>
kind = OrderedBinaryF32LocalConstMatch
_ => ()
}
match
(
optimize_instructions_f64_const(func, dead, lhs_right),
optimize_instructions_f64_const(func, dead, rhs_right),
) {
(Some(lhs_const), Some(rhs_const)) if lhs_const == rhs_const =>
kind = OrderedBinaryF64LocalConstMatch
_ => ()
}
}
}
_ => ()
}
{ instr, kind, }
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_instr(
self : OiOrderedBinaryLocalConstEqualityFact,
instr : @lib.Instruction,
) -> Bool {
match self.instr {
Some(actual) => actual == instr
None => false
}
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_i32_instr(
self : OiOrderedBinaryLocalConstEqualityFact,
instr : @lib.Instruction,
) -> Bool {
match self.kind {
OrderedBinaryI32LocalConstMatch => self.proves_instr(instr)
_ => false
}
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_i64_instr(
self : OiOrderedBinaryLocalConstEqualityFact,
instr : @lib.Instruction,
) -> Bool {
match self.kind {
OrderedBinaryI64LocalConstMatch => self.proves_instr(instr)
_ => false
}
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_f32_instr(
self : OiOrderedBinaryLocalConstEqualityFact,
instr : @lib.Instruction,
) -> Bool {
match self.kind {
OrderedBinaryF32LocalConstMatch => self.proves_instr(instr)
_ => false
}
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_f64_instr(
self : OiOrderedBinaryLocalConstEqualityFact,
instr : @lib.Instruction,
) -> Bool {
match self.kind {
OrderedBinaryF64LocalConstMatch => self.proves_instr(instr)
_ => false
}
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_i32_nontrapping_scalar(
self : OiOrderedBinaryLocalConstEqualityFact,
) -> Bool {
self.proves_i32_instr(@lib.Instruction::i32_add()) ||
self.proves_i32_instr(@lib.Instruction::i32_sub()) ||
self.proves_i32_instr(@lib.Instruction::i32_mul()) ||
self.proves_i32_instr(@lib.Instruction::i32_and()) ||
self.proves_i32_instr(@lib.Instruction::i32_or()) ||
self.proves_i32_instr(@lib.Instruction::i32_xor()) ||
self.proves_i32_instr(@lib.Instruction::i32_shl()) ||
self.proves_i32_instr(@lib.Instruction::i32_shr_s()) ||
self.proves_i32_instr(@lib.Instruction::i32_shr_u()) ||
self.proves_i32_instr(@lib.Instruction::i32_rotl()) ||
self.proves_i32_instr(@lib.Instruction::i32_rotr()) ||
self.proves_i32_instr(@lib.Instruction::i32_eq()) ||
self.proves_i32_instr(@lib.Instruction::i32_ne()) ||
self.proves_i32_instr(@lib.Instruction::i32_lt_s()) ||
self.proves_i32_instr(@lib.Instruction::i32_lt_u()) ||
self.proves_i32_instr(@lib.Instruction::i32_gt_s()) ||
self.proves_i32_instr(@lib.Instruction::i32_gt_u()) ||
self.proves_i32_instr(@lib.Instruction::i32_le_s()) ||
self.proves_i32_instr(@lib.Instruction::i32_le_u()) ||
self.proves_i32_instr(@lib.Instruction::i32_ge_s()) ||
self.proves_i32_instr(@lib.Instruction::i32_ge_u())
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_i64_nontrapping_scalar(
self : OiOrderedBinaryLocalConstEqualityFact,
) -> Bool {
self.proves_i64_instr(@lib.Instruction::i64_add()) ||
self.proves_i64_instr(@lib.Instruction::i64_sub()) ||
self.proves_i64_instr(@lib.Instruction::i64_mul()) ||
self.proves_i64_instr(@lib.Instruction::i64_and()) ||
self.proves_i64_instr(@lib.Instruction::i64_or()) ||
self.proves_i64_instr(@lib.Instruction::i64_xor()) ||
self.proves_i64_instr(@lib.Instruction::i64_shl()) ||
self.proves_i64_instr(@lib.Instruction::i64_shr_s()) ||
self.proves_i64_instr(@lib.Instruction::i64_shr_u()) ||
self.proves_i64_instr(@lib.Instruction::i64_rotl()) ||
self.proves_i64_instr(@lib.Instruction::i64_rotr()) ||
self.proves_i64_instr(@lib.Instruction::i64_eq()) ||
self.proves_i64_instr(@lib.Instruction::i64_ne()) ||
self.proves_i64_instr(@lib.Instruction::i64_lt_s()) ||
self.proves_i64_instr(@lib.Instruction::i64_lt_u()) ||
self.proves_i64_instr(@lib.Instruction::i64_gt_s()) ||
self.proves_i64_instr(@lib.Instruction::i64_gt_u()) ||
self.proves_i64_instr(@lib.Instruction::i64_le_s()) ||
self.proves_i64_instr(@lib.Instruction::i64_le_u()) ||
self.proves_i64_instr(@lib.Instruction::i64_ge_s()) ||
self.proves_i64_instr(@lib.Instruction::i64_ge_u())
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_f32_nontrapping_scalar(
self : OiOrderedBinaryLocalConstEqualityFact,
) -> Bool {
self.proves_f32_instr(@lib.Instruction::f32_add()) ||
self.proves_f32_instr(@lib.Instruction::f32_sub()) ||
self.proves_f32_instr(@lib.Instruction::f32_mul()) ||
self.proves_f32_instr(@lib.Instruction::f32_div()) ||
self.proves_f32_instr(@lib.Instruction::f32_min()) ||
self.proves_f32_instr(@lib.Instruction::f32_max()) ||
self.proves_f32_instr(@lib.Instruction::f32_copysign()) ||
self.proves_f32_instr(@lib.Instruction::f32_eq()) ||
self.proves_f32_instr(@lib.Instruction::f32_ne()) ||
self.proves_f32_instr(@lib.Instruction::f32_lt()) ||
self.proves_f32_instr(@lib.Instruction::f32_gt()) ||
self.proves_f32_instr(@lib.Instruction::f32_le()) ||
self.proves_f32_instr(@lib.Instruction::f32_ge())
}
///|
fn OiOrderedBinaryLocalConstEqualityFact::proves_f64_nontrapping_scalar(
self : OiOrderedBinaryLocalConstEqualityFact,
) -> Bool {
self.proves_f64_instr(@lib.Instruction::f64_add()) ||
self.proves_f64_instr(@lib.Instruction::f64_sub()) ||
self.proves_f64_instr(@lib.Instruction::f64_mul()) ||
self.proves_f64_instr(@lib.Instruction::f64_div()) ||
self.proves_f64_instr(@lib.Instruction::f64_min()) ||
self.proves_f64_instr(@lib.Instruction::f64_max()) ||
self.proves_f64_instr(@lib.Instruction::f64_copysign()) ||
self.proves_f64_instr(@lib.Instruction::f64_eq()) ||
self.proves_f64_instr(@lib.Instruction::f64_ne()) ||
self.proves_f64_instr(@lib.Instruction::f64_lt()) ||
self.proves_f64_instr(@lib.Instruction::f64_gt()) ||
self.proves_f64_instr(@lib.Instruction::f64_le()) ||
self.proves_f64_instr(@lib.Instruction::f64_ge())
}
///|
priv enum OiUnaryLocalEqualityKind {
UnaryLocalNoMatch
UnaryI32LocalMatch
UnaryI64LocalMatch
UnaryF32LocalMatch
UnaryF64LocalMatch
UnaryV128SplatLocalMatch
}
///|
priv struct OiUnaryLocalEqualityFact {
kind : OiUnaryLocalEqualityKind
}
///|
fn OiUnaryLocalEqualityFact::from_nodes(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs : Int32,
rhs : Int32,
) -> OiUnaryLocalEqualityFact {
// Binaryen v130 `ExpressionAnalyzer::equal` treats equal unary nodes as the
// same operation with recursively equal operands. For the select-arm
// `!condition.invalidates(value)` path, this Starshine fact records the
// represented nontrapping subset whose operand is a direct local and can be
// re-evaluated after an effectful condition without reading mutable state.
let mut kind = UnaryLocalNoMatch
match
(
@ir.hot_node_exact_instr(func, lhs),
@ir.hot_node_exact_instr(func, rhs),
@ir.hot_node_get(func, lhs).child_count,
@ir.hot_node_get(func, rhs).child_count,
) {
(Some(lhs_instr), Some(rhs_instr), 1, 1) if lhs_instr == rhs_instr => {
let lhs_child = @ir.hot_child_get(func, lhs, 0)
let rhs_child = @ir.hot_child_get(func, rhs, 0)
let matching_local_operand = match
(
OiLocalGetLeafFact::from_node(func, dead, lhs_child).local_id,
OiLocalGetLeafFact::from_node(func, dead, rhs_child).local_id,
) {
(Some(lhs_local), Some(rhs_local)) => lhs_local == rhs_local
_ => false
}
if matching_local_operand {
if lhs_instr == @lib.Instruction::i32_eqz() ||
lhs_instr == @lib.Instruction::i32_clz() ||
lhs_instr == @lib.Instruction::i32_ctz() ||
lhs_instr == @lib.Instruction::i32_popcnt() ||
lhs_instr == @lib.Instruction::i32_extend8s() ||
lhs_instr == @lib.Instruction::i32_extend16s() ||
lhs_instr == @lib.Instruction::i32_trunc_sat_f32s() ||
lhs_instr == @lib.Instruction::i32_trunc_sat_f32u() ||
lhs_instr == @lib.Instruction::i32_trunc_sat_f64s() ||
lhs_instr == @lib.Instruction::i32_trunc_sat_f64u() ||
lhs_instr == @lib.Instruction::i32_reinterpret_f32() ||
lhs_instr == @lib.Instruction::i32_wrap_i64() {
kind = UnaryI32LocalMatch
} else if lhs_instr == @lib.Instruction::i64_eqz() ||
lhs_instr == @lib.Instruction::i64_clz() ||
lhs_instr == @lib.Instruction::i64_ctz() ||
lhs_instr == @lib.Instruction::i64_popcnt() ||
lhs_instr == @lib.Instruction::i64_extend8s() ||
lhs_instr == @lib.Instruction::i64_extend16s() ||
lhs_instr == @lib.Instruction::i64_extend32s() ||
lhs_instr == @lib.Instruction::i64_extend_i32s() ||
lhs_instr == @lib.Instruction::i64_extend_i32u() ||
lhs_instr == @lib.Instruction::i64_trunc_sat_f32s() ||
lhs_instr == @lib.Instruction::i64_trunc_sat_f32u() ||
lhs_instr == @lib.Instruction::i64_trunc_sat_f64s() ||
lhs_instr == @lib.Instruction::i64_trunc_sat_f64u() ||
lhs_instr == @lib.Instruction::i64_reinterpret_f64() {
kind = UnaryI64LocalMatch
} else if lhs_instr == @lib.Instruction::f32_abs() ||
lhs_instr == @lib.Instruction::f32_neg() ||
lhs_instr == @lib.Instruction::f32_ceil() ||
lhs_instr == @lib.Instruction::f32_floor() ||
lhs_instr == @lib.Instruction::f32_trunc() ||
lhs_instr == @lib.Instruction::f32_nearest() ||
lhs_instr == @lib.Instruction::f32_sqrt() ||
lhs_instr == @lib.Instruction::f32_convert_i32s() ||
lhs_instr == @lib.Instruction::f32_convert_i32u() ||
lhs_instr == @lib.Instruction::f32_convert_i64s() ||
lhs_instr == @lib.Instruction::f32_convert_i64u() ||
lhs_instr == @lib.Instruction::f32_demote_f64() ||
lhs_instr == @lib.Instruction::f32_reinterpret_i32() {
kind = UnaryF32LocalMatch
} else if lhs_instr == @lib.Instruction::f64_abs() ||
lhs_instr == @lib.Instruction::f64_neg() ||
lhs_instr == @lib.Instruction::f64_ceil() ||
lhs_instr == @lib.Instruction::f64_floor() ||
lhs_instr == @lib.Instruction::f64_trunc() ||
lhs_instr == @lib.Instruction::f64_nearest() ||
lhs_instr == @lib.Instruction::f64_sqrt() ||
lhs_instr == @lib.Instruction::f64_convert_i32s() ||
lhs_instr == @lib.Instruction::f64_convert_i32u() ||
lhs_instr == @lib.Instruction::f64_convert_i64s() ||
lhs_instr == @lib.Instruction::f64_convert_i64u() ||
lhs_instr == @lib.Instruction::f64_promote_f32() ||
lhs_instr == @lib.Instruction::f64_reinterpret_i64() {
kind = UnaryF64LocalMatch
} else if lhs_instr == @lib.Instruction::i8x16_splat() ||
lhs_instr == @lib.Instruction::i16x8_splat() ||
lhs_instr == @lib.Instruction::i32x4_splat() ||
lhs_instr == @lib.Instruction::i64x2_splat() ||
lhs_instr == @lib.Instruction::f32x4_splat() ||
lhs_instr == @lib.Instruction::f64x2_splat() {
kind = UnaryV128SplatLocalMatch
}
}
}
_ => ()
}
{ kind, }
}
///|
fn OiUnaryLocalEqualityFact::proves_i32_nontrapping_scalar(
self : OiUnaryLocalEqualityFact,
) -> Bool {
match self.kind {
UnaryI32LocalMatch => true
_ => false
}
}
///|
fn OiUnaryLocalEqualityFact::proves_i64_nontrapping_scalar(
self : OiUnaryLocalEqualityFact,
) -> Bool {
match self.kind {
UnaryI64LocalMatch => true
_ => false
}
}
///|
fn OiUnaryLocalEqualityFact::proves_f32_nontrapping_scalar(
self : OiUnaryLocalEqualityFact,
) -> Bool {
match self.kind {
UnaryF32LocalMatch => true
_ => false
}
}
///|
fn OiUnaryLocalEqualityFact::proves_f64_nontrapping_scalar(
self : OiUnaryLocalEqualityFact,
) -> Bool {
match self.kind {
UnaryF64LocalMatch => true
_ => false
}
}
///|
fn OiUnaryLocalEqualityFact::proves_v128_splat(
self : OiUnaryLocalEqualityFact,
) -> Bool {
match self.kind {
UnaryV128SplatLocalMatch => true
_ => false
}
}
///|
priv enum OiSimdLeafEqualityKind {
SimdLeafNoMatch
SimdLeafV128ConstMatch
SimdLeafV128LocalSplatMatch
}
///|
priv struct OiSimdLeafEqualityFact {
kind : OiSimdLeafEqualityKind
}
///|
fn OiSimdLeafEqualityFact::from_nodes(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs : Int32,
rhs : Int32,
) -> OiSimdLeafEqualityFact {
// Binaryen v130 `ExpressionAnalyzer::equal` treats equal SIMD constants as
// equal leaves and equal splats as equal unary nodes. Keep Starshine's
// represented select-arm SIMD subset as a selected fact kind so the pure-arm
// fold no longer relies on standalone helper-level SIMD scanners or parallel
// boolean markers.
let mut kind = SimdLeafNoMatch
match
(@ir.hot_node_exact_instr(func, lhs), @ir.hot_node_exact_instr(func, rhs)) {
(
Some(
@lib.Instruction::V128Const(
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_
) as lhs_instr
),
Some(
@lib.Instruction::V128Const(
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_,
_
) as rhs_instr
),
) if lhs_instr == rhs_instr => kind = SimdLeafV128ConstMatch
_ => ()
}
match kind {
SimdLeafNoMatch =>
if OiUnaryLocalEqualityFact::from_nodes(func, dead, lhs, rhs).proves_v128_splat() {
kind = SimdLeafV128LocalSplatMatch
}
_ => ()
}
{ kind, }
}
///|
fn OiSimdLeafEqualityFact::proves_v128_const_leaf(
self : OiSimdLeafEqualityFact,
) -> Bool {
match self.kind {
SimdLeafV128ConstMatch => true
_ => false
}
}
///|
fn OiSimdLeafEqualityFact::proves_v128_local_splat(
self : OiSimdLeafEqualityFact,
) -> Bool {
match self.kind {
SimdLeafV128LocalSplatMatch => true
_ => false
}
}
///|
priv enum OiRefI31PayloadEqualityKind {
RefI31PayloadNoMatch
RefI31PayloadLocalMatch
RefI31PayloadConstMatch
RefI31PayloadI32LocalLocalScalarMatch
RefI31PayloadI32LocalConstScalarMatch
RefI31PayloadI32UnaryLocalScalarMatch
}
///|
priv struct OiRefI31PayloadEqualityFact {
kind : OiRefI31PayloadEqualityKind
}
///|
fn OiRefI31PayloadEqualityFact::from_nodes(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs : Int32,
rhs : Int32,
) -> OiRefI31PayloadEqualityFact {
// Binaryen v130 `ExpressionAnalyzer::equal` compares `ref.i31` by comparing
// its scalar payload. When the enclosing select condition is effectful,
// `visitSelect` also asks `EffectAnalyzer::invalidates` whether the value can
// be moved after the condition. Keep Starshine's represented nontrapping,
// reorderable i31-payload subset as a selected fact kind so callers do not
// re-open a visitor-local ref.i31 scanner or parallel payload booleans.
let mut kind = RefI31PayloadNoMatch
match
(
@ir.hot_node_exact_instr(func, lhs),
@ir.hot_node_exact_instr(func, rhs),
@ir.hot_node_get(func, lhs).child_count,
@ir.hot_node_get(func, rhs).child_count,
) {
(Some(@lib.Instruction::RefI31), Some(@lib.Instruction::RefI31), 1, 1) => {
let lhs_payload = @ir.hot_child_get(func, lhs, 0)
let rhs_payload = @ir.hot_child_get(func, rhs, 0)
match
(
OiLocalGetLeafFact::from_node(func, dead, lhs_payload).local_id,
OiLocalGetLeafFact::from_node(func, dead, rhs_payload).local_id,
) {
(Some(lhs_local), Some(rhs_local)) if lhs_local == rhs_local =>
kind = RefI31PayloadLocalMatch
_ => ()
}
match kind {
RefI31PayloadNoMatch =>
match
(
optimize_instructions_i32_const(func, dead, lhs_payload),
optimize_instructions_i32_const(func, dead, rhs_payload),
) {
(Some(lhs_const), Some(rhs_const)) if lhs_const == rhs_const =>
kind = RefI31PayloadConstMatch
_ => ()
}
_ => ()
}
match kind {
RefI31PayloadNoMatch =>
if OiOrderedBinaryLocalLocalEqualityFact::from_nodes(
func, dead, lhs_payload, rhs_payload,
).proves_i32_nontrapping_scalar() {
kind = RefI31PayloadI32LocalLocalScalarMatch
}
_ => ()
}
match kind {
RefI31PayloadNoMatch =>
if OiOrderedBinaryLocalConstEqualityFact::from_nodes(
func, dead, lhs_payload, rhs_payload,
).proves_i32_nontrapping_scalar() {
kind = RefI31PayloadI32LocalConstScalarMatch
}
_ => ()
}
match kind {
RefI31PayloadNoMatch =>
if OiUnaryLocalEqualityFact::from_nodes(
func, dead, lhs_payload, rhs_payload,
).proves_i32_nontrapping_scalar() {
kind = RefI31PayloadI32UnaryLocalScalarMatch
}
_ => ()
}
}
_ => ()
}
{ kind, }
}
///|
fn OiRefI31PayloadEqualityFact::proves_nontrapping_reorderable(
self : OiRefI31PayloadEqualityFact,
) -> Bool {
match self.kind {
RefI31PayloadNoMatch => false
RefI31PayloadLocalMatch => true
RefI31PayloadConstMatch => true
RefI31PayloadI32LocalLocalScalarMatch => true
RefI31PayloadI32LocalConstScalarMatch => true
RefI31PayloadI32UnaryLocalScalarMatch => true
}
}
///|
priv enum OiNontrappingReorderableExpressionEqualityKind {
NontrappingReorderableNoMatch
NontrappingReorderableRefI31PayloadMatch
NontrappingReorderableI32LocalLocalScalarMatch
NontrappingReorderableI64LocalLocalScalarMatch
NontrappingReorderableI32LocalConstScalarMatch
NontrappingReorderableI64LocalConstScalarMatch
NontrappingReorderableI32UnaryLocalScalarMatch
NontrappingReorderableI64UnaryLocalScalarMatch
NontrappingReorderableF32LocalLocalScalarMatch
NontrappingReorderableF32UnaryLocalScalarMatch
NontrappingReorderableF32LocalConstScalarMatch
NontrappingReorderableF64LocalLocalScalarMatch
NontrappingReorderableF64UnaryLocalScalarMatch
NontrappingReorderableF64LocalConstScalarMatch
}
///|
priv struct OiNontrappingReorderableExpressionEqualityFact {
kind : OiNontrappingReorderableExpressionEqualityKind
}
///|
fn OiNontrappingReorderableExpressionEqualityFact::from_select_arms(
func : @ir.HotFunc,
dead : @bitset.BitSet,
if_true : Int32,
if_false : Int32,
) -> OiNontrappingReorderableExpressionEqualityFact {
// Binaryen v130 `visitSelect` preserves an effectful condition before a
// folded identical arm only when the condition does not invalidate the value's
// effects (`!condition.invalidates(value)`). This source-backed fact records
// one represented nontrapping, local/constant-only equality family that can
// safely be re-evaluated after the dropped condition instead of leaving them
// as a visitor-local opcode cascade or parallel match booleans.
if OiRefI31PayloadEqualityFact::from_nodes(func, dead, if_true, if_false).proves_nontrapping_reorderable() {
return { kind: NontrappingReorderableRefI31PayloadMatch, }
}
let ordered_binary_local_local = OiOrderedBinaryLocalLocalEqualityFact::from_nodes(
func, dead, if_true, if_false,
)
if ordered_binary_local_local.proves_i32_nontrapping_scalar() {
return { kind: NontrappingReorderableI32LocalLocalScalarMatch, }
}
if ordered_binary_local_local.proves_i64_nontrapping_scalar() {
return { kind: NontrappingReorderableI64LocalLocalScalarMatch, }
}
let ordered_binary_local_const = OiOrderedBinaryLocalConstEqualityFact::from_nodes(
func, dead, if_true, if_false,
)
if ordered_binary_local_const.proves_i32_nontrapping_scalar() {
return { kind: NontrappingReorderableI32LocalConstScalarMatch, }
}
if ordered_binary_local_const.proves_i64_nontrapping_scalar() {
return { kind: NontrappingReorderableI64LocalConstScalarMatch, }
}
let unary_local = OiUnaryLocalEqualityFact::from_nodes(
func, dead, if_true, if_false,
)
if unary_local.proves_i32_nontrapping_scalar() {
return { kind: NontrappingReorderableI32UnaryLocalScalarMatch, }
}
if unary_local.proves_i64_nontrapping_scalar() {
return { kind: NontrappingReorderableI64UnaryLocalScalarMatch, }
}
if ordered_binary_local_local.proves_f32_nontrapping_scalar() {
return { kind: NontrappingReorderableF32LocalLocalScalarMatch, }
}
if unary_local.proves_f32_nontrapping_scalar() {
return { kind: NontrappingReorderableF32UnaryLocalScalarMatch, }
}
if ordered_binary_local_const.proves_f32_nontrapping_scalar() {
return { kind: NontrappingReorderableF32LocalConstScalarMatch, }
}
if ordered_binary_local_local.proves_f64_nontrapping_scalar() {
return { kind: NontrappingReorderableF64LocalLocalScalarMatch, }
}
if unary_local.proves_f64_nontrapping_scalar() {
return { kind: NontrappingReorderableF64UnaryLocalScalarMatch, }
}
if ordered_binary_local_const.proves_f64_nontrapping_scalar() {
return { kind: NontrappingReorderableF64LocalConstScalarMatch, }
}
{ kind: NontrappingReorderableNoMatch, }
}
///|
fn OiNontrappingReorderableExpressionEqualityFact::proves_equal(
self : OiNontrappingReorderableExpressionEqualityFact,
) -> Bool {
match self.kind {
NontrappingReorderableNoMatch => false
NontrappingReorderableRefI31PayloadMatch => true
NontrappingReorderableI32LocalLocalScalarMatch => true
NontrappingReorderableI64LocalLocalScalarMatch => true
NontrappingReorderableI32LocalConstScalarMatch => true
NontrappingReorderableI64LocalConstScalarMatch => true
NontrappingReorderableI32UnaryLocalScalarMatch => true
NontrappingReorderableI64UnaryLocalScalarMatch => true
NontrappingReorderableF32LocalLocalScalarMatch => true
NontrappingReorderableF32UnaryLocalScalarMatch => true
NontrappingReorderableF32LocalConstScalarMatch => true
NontrappingReorderableF64LocalLocalScalarMatch => true
NontrappingReorderableF64UnaryLocalScalarMatch => true
NontrappingReorderableF64LocalConstScalarMatch => true
}
}
///|
priv enum OiIdenticalPureExpressionKind {
IdenticalPureNoMatch
IdenticalPureEffectUnsafe
IdenticalPureScalarTreeMatch
IdenticalPureStableStateReadMatch
IdenticalPureStableStateScalarTreeMatch
IdenticalPureReferenceLeafMatch
IdenticalPureRefI31ScalarTreeMatch
IdenticalPureRefI31NontrappingLocalScalarTreeMatch
IdenticalPureV128ConstLeafMatch
IdenticalPureV128LocalSplatMatch
IdenticalPureNontrappingReorderableScalarTreeMatch
}
///|
priv struct OiIdenticalPureExpressionFact {
kind : OiIdenticalPureExpressionKind
}
///|
priv enum OiReferenceLeafEqualityKind {
ReferenceLeafNoMatch
ReferenceLeafRefNullMatch
ReferenceLeafRefFuncMatch
}
///|
priv struct OiReferenceLeafEqualityFact {
kind : OiReferenceLeafEqualityKind
}
///|
fn OiReferenceLeafEqualityFact::from_nodes(
func : @ir.HotFunc,
lhs_id : Int32,
rhs_id : Int32,
) -> OiReferenceLeafEqualityFact {
// Binaryen v130 `ExpressionAnalyzer::equal` compares reference leaves by
// their literal heap/function target. Starshine also accepts HOT-lowered
// RefNull/RefFunc node payloads whose result types preserve the reference
// type. Keep that represented subset as a selected fact kind instead of an
// arm-local helper scanner or parallel match booleans.
match (@ir.hot_node_get(func, lhs_id), @ir.hot_node_get(func, rhs_id)) {
(
{ op: @ir.HotOp::RefNull, ty: lhs_ty, .. },
{ op: @ir.HotOp::RefNull, ty: rhs_ty, .. },
) if lhs_ty == rhs_ty => { kind: ReferenceLeafRefNullMatch, }
(
{ op: @ir.HotOp::RefFunc, ty: lhs_ty, imm0: lhs_func, .. },
{ op: @ir.HotOp::RefFunc, ty: rhs_ty, imm0: rhs_func, .. },
) if lhs_ty == rhs_ty && lhs_func == rhs_func =>
{ kind: ReferenceLeafRefFuncMatch, }
_ =>
match
(
@ir.hot_node_exact_instr(func, lhs_id),
@ir.hot_node_exact_instr(func, rhs_id),
) {
(
Some(@lib.Instruction::RefNull(lhs)),
Some(@lib.Instruction::RefNull(rhs)),
) if lhs == rhs => { kind: ReferenceLeafRefNullMatch, }
(
Some(@lib.Instruction::RefFunc(lhs)),
Some(@lib.Instruction::RefFunc(rhs)),
) if lhs == rhs => { kind: ReferenceLeafRefFuncMatch, }
_ => { kind: ReferenceLeafNoMatch, }
}
}
}
///|
fn OiReferenceLeafEqualityFact::proves_equal(
self : OiReferenceLeafEqualityFact,
) -> Bool {
match self.kind {
ReferenceLeafNoMatch => false
ReferenceLeafRefNullMatch => true
ReferenceLeafRefFuncMatch => true
}
}
///|
priv struct OiLocalConstSelectScalarTreeFact {
nontrapping_reorderable : Bool
}
///|
fn OiLocalConstSelectScalarTreeFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
depth~ : Int32 = 0,
) -> OiLocalConstSelectScalarTreeFact {
// Same-value select-arm folding across an effectful condition moves the kept
// value after that condition. This fact is the source-backed, represented
// scalar subset that cannot trap and cannot observe calls, globals, memory,
// or tables: local/const leaves plus branchless select shells whose arms and
// condition recursively have the same property.
if depth > 16 {
return { nontrapping_reorderable: false, }
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet | @ir.HotOp::Const => { nontrapping_reorderable: true, }
@ir.HotOp::Select => {
for slot = 0; slot < node.child_count; slot = slot + 1 {
if !OiLocalConstSelectScalarTreeFact::from_node(
func,
@ir.hot_child_get(func, node_id, slot),
depth=depth + 1,
).proves_nontrapping_reorderable() {
return { nontrapping_reorderable: false, }
}
}
{ nontrapping_reorderable: true, }
}
_ => { nontrapping_reorderable: false, }
}
}
///|
fn OiLocalConstSelectScalarTreeFact::proves_nontrapping_reorderable(
self : OiLocalConstSelectScalarTreeFact,
) -> Bool {
self.nontrapping_reorderable
}
///|
priv enum OiRefI31ScalarTreeEqualityKind {
RefI31ScalarTreeNoMatch
RefI31ScalarTreePayloadPureMatch
RefI31ScalarTreePayloadNontrappingReorderableMatch
}
///|
priv struct OiRefI31ScalarTreeEqualityFact {
kind : OiRefI31ScalarTreeEqualityKind
}
///|
fn OiRefI31ScalarTreeEqualityFact::from_nodes(
func : @ir.HotFunc,
lhs_id : Int32,
rhs_id : Int32,
) -> OiRefI31ScalarTreeEqualityFact {
// Binaryen v130 `ExpressionAnalyzer::equal` compares `ref.i31` shells by
// recursively comparing their i32 payloads. Binaryen `visitSelect` then uses
// `EffectAnalyzer::invalidates` before moving equal arms across an effectful
// condition; Starshine's represented safe-move subset is local/const/select
// scalar payload trees. Keep payload equality and movability as a selected
// source-backed fact kind rather than helper-level ref.i31 tree scanners or
// parallel payload booleans.
let lhs = @ir.hot_node_get(func, lhs_id)
let rhs = @ir.hot_node_get(func, rhs_id)
if lhs.ty == rhs.ty && lhs.child_count == 1 && rhs.child_count == 1 {
match
(
@ir.hot_node_exact_instr(func, lhs_id),
@ir.hot_node_exact_instr(func, rhs_id),
) {
(Some(@lib.Instruction::RefI31), Some(@lib.Instruction::RefI31)) => {
let lhs_payload = @ir.hot_child_get(func, lhs_id, 0)
let rhs_payload = @ir.hot_child_get(func, rhs_id, 0)
if OiScalarTreeEqualityFact::from_nodes(func, lhs_payload, rhs_payload).proves_pure_equal() {
if OiLocalConstSelectScalarTreeFact::from_node(func, lhs_payload).proves_nontrapping_reorderable() &&
OiLocalConstSelectScalarTreeFact::from_node(func, rhs_payload).proves_nontrapping_reorderable() {
return { kind: RefI31ScalarTreePayloadNontrappingReorderableMatch, }
}
return { kind: RefI31ScalarTreePayloadPureMatch, }
}
}
_ => ()
}
}
{ kind: RefI31ScalarTreeNoMatch, }
}
///|
fn OiRefI31ScalarTreeEqualityFact::proves_pure_equal(
self : OiRefI31ScalarTreeEqualityFact,
) -> Bool {
match self.kind {
RefI31ScalarTreeNoMatch => false
RefI31ScalarTreePayloadPureMatch => true
RefI31ScalarTreePayloadNontrappingReorderableMatch => true
}
}
///|
fn OiRefI31ScalarTreeEqualityFact::proves_nontrapping_reorderable_equal(
self : OiRefI31ScalarTreeEqualityFact,
) -> Bool {
match self.kind {
RefI31ScalarTreePayloadNontrappingReorderableMatch => true
_ => false
}
}
///|
fn OiIdenticalPureExpressionFact::from_select_arms(
func : @ir.HotFunc,
dead : @bitset.BitSet,
if_true : Int32,
if_false : Int32,
) -> OiIdenticalPureExpressionFact {
// Binaryen v130 `visitSelect` folds identical arms via
// `ExpressionAnalyzer::equal(ifTrue, ifFalse)` after checking whether the
// arms have effects. Model Starshine's represented no-side-effect/effect-safe
// subset as an explicit fact shared by nested scalar/select trees,
// deterministic state-read leaves, deterministic reference leaves, represented
// ref.i31 scalar-payload trees, and the currently represented SIMD constant /
// local-splat leaves instead of growing the old opcode-by-opcode arm scanner.
// State-read leaves stay separated from pure scalar equality so
// effectful-condition folds do not move memory/table-size reads across possible
// growth.
let left = @ir.hot_node_get(func, if_true)
let right = @ir.hot_node_get(func, if_false)
let scalar_tree_fact = OiScalarTreeEqualityFact::from_nodes(
func, if_true, if_false,
)
let state_scalar_tree_fact = OiScalarTreeEqualityFact::from_nodes(
func,
if_true,
if_false,
include_stable_state_read=true,
)
let scalar_tree = scalar_tree_fact.proves_pure_equal()
let state_read = OiStableStateReadEqualityFact::from_nodes(left, right).proves_equal()
let state_scalar_tree = state_scalar_tree_fact.proves_stable_state_scalar_equal()
let reference_leaf = OiReferenceLeafEqualityFact::from_nodes(
func, if_true, if_false,
).proves_equal()
let ref_i31_scalar_tree_fact = OiRefI31ScalarTreeEqualityFact::from_nodes(
func, if_true, if_false,
)
let ref_i31_scalar_tree = ref_i31_scalar_tree_fact.proves_pure_equal()
let ref_i31_nontrapping_local_scalar_tree = ref_i31_scalar_tree_fact.proves_nontrapping_reorderable_equal()
let simd_leaf = OiSimdLeafEqualityFact::from_nodes(
func, dead, if_true, if_false,
)
let nontrapping_reorderable_scalar_tree = OiNontrappingReorderableExpressionEqualityFact::from_select_arms(
func, dead, if_true, if_false,
).proves_equal()
let effect_safe = if state_read || state_scalar_tree {
true
} else {
OiSideEffectFreeExpressionFact::proves_node_free(func, dead, if_true) &&
OiSideEffectFreeExpressionFact::proves_node_free(func, dead, if_false)
}
if !effect_safe {
return { kind: IdenticalPureEffectUnsafe, }
}
if ref_i31_nontrapping_local_scalar_tree {
return { kind: IdenticalPureRefI31NontrappingLocalScalarTreeMatch, }
}
if simd_leaf.proves_v128_const_leaf() {
return { kind: IdenticalPureV128ConstLeafMatch, }
}
if simd_leaf.proves_v128_local_splat() {
return { kind: IdenticalPureV128LocalSplatMatch, }
}
if nontrapping_reorderable_scalar_tree {
return { kind: IdenticalPureNontrappingReorderableScalarTreeMatch, }
}
if scalar_tree {
return { kind: IdenticalPureScalarTreeMatch, }
}
if state_read {
return { kind: IdenticalPureStableStateReadMatch, }
}
if state_scalar_tree {
return { kind: IdenticalPureStableStateScalarTreeMatch, }
}
if reference_leaf {
return { kind: IdenticalPureReferenceLeafMatch, }
}
if ref_i31_scalar_tree {
return { kind: IdenticalPureRefI31ScalarTreeMatch, }
}
{ kind: IdenticalPureNoMatch, }
}
///|
fn OiIdenticalPureExpressionFact::proves_equal(
self : OiIdenticalPureExpressionFact,
) -> Bool {
match self.kind {
IdenticalPureNoMatch => false
IdenticalPureEffectUnsafe => false
IdenticalPureScalarTreeMatch => true
IdenticalPureStableStateReadMatch => true
IdenticalPureStableStateScalarTreeMatch => true
IdenticalPureReferenceLeafMatch => true
IdenticalPureRefI31ScalarTreeMatch => true
IdenticalPureRefI31NontrappingLocalScalarTreeMatch => true
IdenticalPureV128ConstLeafMatch => true
IdenticalPureV128LocalSplatMatch => true
IdenticalPureNontrappingReorderableScalarTreeMatch => true
}
}
///|
fn OiIdenticalPureExpressionFact::proves_equal_after_condition_preserved(
self : OiIdenticalPureExpressionFact,
) -> Bool {
// If the select condition itself has effects, Binaryen v130 keeps that
// condition by evaluating it before the selected equal arm. Only represented
// arms that are known movable/reorderable across the condition may use that
// block-shaped rewrite; other pure equal arms still require a pure condition.
match self.kind {
IdenticalPureRefI31NontrappingLocalScalarTreeMatch => true
IdenticalPureV128ConstLeafMatch => true
IdenticalPureV128LocalSplatMatch => true
IdenticalPureNontrappingReorderableScalarTreeMatch => true
_ => false
}
}
///|
fn optimize_instructions_select_identical_immutable_global_arms(
ctx : HotPassContext,
func : @ir.HotFunc,
if_true : Int32,
if_false : Int32,
) -> Bool {
match (@ir.hot_node_get(func, if_true), @ir.hot_node_get(func, if_false)) {
(
{ op: @ir.HotOp::GlobalGet, imm0: lhs, .. },
{ op: @ir.HotOp::GlobalGet, imm0: rhs, .. },
) if lhs == rhs =>
match ctx.module_ctx {
Some(module_ctx) =>
match
@ir.hot_module_resolve_global_type(
module_ctx,
@lib.GlobalIdx::new(lhs.reinterpret_as_uint()),
) {
Some(@lib.GlobalType(_, false)) => true
_ => false
}
None => false
}
_ => false
}
}
///|
fn optimize_instructions_inverted_select_compare(
instr : @lib.Instruction,
) -> @lib.Instruction? {
match instr {
@lib.Instruction::I32Eq => Some(@lib.Instruction::i32_ne())
@lib.Instruction::I32Ne => Some(@lib.Instruction::i32_eq())
@lib.Instruction::I64Eq => Some(@lib.Instruction::i64_ne())
@lib.Instruction::I64Ne => Some(@lib.Instruction::i64_eq())
_ => optimize_instructions_inverted_eqz_compare(instr)
}
}
///|
priv enum OiIdempotentCallOperandTreeKind {
IdempotentCallOperandTreeUnsupported
IdempotentCallOperandTreeLocalConstLeaf
IdempotentCallOperandTreeImmutableGlobalLeaf
IdempotentCallOperandTreeI32EqzShell
IdempotentCallOperandTreeI32EqShell
IdempotentCallOperandTreeI32AddShell
IdempotentCallOperandTreeI32MulShell
IdempotentCallOperandTreeI64AddShell
}
///|
priv struct OiIdempotentCallOperandTreeFact {
kind : OiIdempotentCallOperandTreeKind
children_foldable : Bool
}
///|
fn OiIdempotentCallOperandTreeFact::empty() -> OiIdempotentCallOperandTreeFact {
{ kind: IdempotentCallOperandTreeUnsupported, children_foldable: false, }
}
///|
fn OiIdempotentCallOperandTreeFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
depth~ : Int32 = 0,
) -> OiIdempotentCallOperandTreeFact {
// Binaryen v130's idempotent-call exception builds an EffectAnalyzer over
// every operand subtree, rejects unremovable child effects, and then asks a
// shallow call analyzer whether the call can invalidate those effects. Keep
// that proof separate from structural equality: direct local/constant leaves,
// immutable-global reads, and the source-probed nontrapping i32.eqz, i32.eq,
// i32.add, i32.mul, and i64.add shells are the represented recursive subset.
if depth > 16 {
return OiIdempotentCallOperandTreeFact::empty()
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet | @ir.HotOp::Const =>
return {
kind: IdempotentCallOperandTreeLocalConstLeaf,
children_foldable: true,
}
@ir.HotOp::GlobalGet =>
match ctx.module_ctx {
Some(module_ctx) =>
match
@ir.hot_module_resolve_global_type(
module_ctx,
@lib.GlobalIdx::new(node.imm0.reinterpret_as_uint()),
) {
Some(@lib.GlobalType(_, false)) =>
return {
kind: IdempotentCallOperandTreeImmutableGlobalLeaf,
children_foldable: true,
}
_ => return OiIdempotentCallOperandTreeFact::empty()
}
None => return OiIdempotentCallOperandTreeFact::empty()
}
_ => ()
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Eqz) if node.child_count == 1 => {
let child_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 0),
depth=depth + 1,
)
{
kind: IdempotentCallOperandTreeI32EqzShell,
children_foldable: child_fact.proves_foldable(),
}
}
Some(@lib.Instruction::I32Eq) if node.child_count == 2 => {
let left_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 0),
depth=depth + 1,
)
let right_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 1),
depth=depth + 1,
)
{
kind: IdempotentCallOperandTreeI32EqShell,
children_foldable: left_fact.proves_foldable() &&
right_fact.proves_foldable(),
}
}
Some(@lib.Instruction::I32Add) if node.child_count == 2 => {
let left_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 0),
depth=depth + 1,
)
let right_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 1),
depth=depth + 1,
)
{
kind: IdempotentCallOperandTreeI32AddShell,
children_foldable: left_fact.proves_foldable() &&
right_fact.proves_foldable(),
}
}
Some(@lib.Instruction::I32Mul) if node.child_count == 2 => {
let left_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 0),
depth=depth + 1,
)
let right_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 1),
depth=depth + 1,
)
{
kind: IdempotentCallOperandTreeI32MulShell,
children_foldable: left_fact.proves_foldable() &&
right_fact.proves_foldable(),
}
}
Some(@lib.Instruction::I64Add) if node.child_count == 2 => {
let left_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 0),
depth=depth + 1,
)
let right_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx,
func,
@ir.hot_child_get(func, node_id, 1),
depth=depth + 1,
)
{
kind: IdempotentCallOperandTreeI64AddShell,
children_foldable: left_fact.proves_foldable() &&
right_fact.proves_foldable(),
}
}
_ => OiIdempotentCallOperandTreeFact::empty()
}
}
///|
fn OiIdempotentCallOperandTreeFact::proves_foldable(
self : OiIdempotentCallOperandTreeFact,
) -> Bool {
match self.kind {
IdempotentCallOperandTreeLocalConstLeaf
| IdempotentCallOperandTreeImmutableGlobalLeaf => true
IdempotentCallOperandTreeI32EqzShell
| IdempotentCallOperandTreeI32EqShell
| IdempotentCallOperandTreeI32AddShell
| IdempotentCallOperandTreeI32MulShell
| IdempotentCallOperandTreeI64AddShell => self.children_foldable
IdempotentCallOperandTreeUnsupported => false
}
}
///|
priv enum OiIdempotentCallOperandKind {
IdempotentCallNoOperands
IdempotentCallPureOperandTrees
IdempotentCallUnsupportedOperands
}
///|
priv struct OiIdempotentCallOperandFact {
kind : OiIdempotentCallOperandKind
operand_ids : Array[Int32]
tree_facts : Array[OiIdempotentCallOperandTreeFact]
}
///|
priv struct OiIdempotentCallFact {
call_id : Int32
func_idx : Int32
operand_fact : OiIdempotentCallOperandFact
}
///|
fn OiIdempotentCallFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiIdempotentCallFact? {
// Binaryen v130 asks `Intrinsics::getCallAnnotations(...).idempotent` before
// treating a direct call as effect-free for consecutive-input folding. The
// represented operand fact includes no operands plus recursively proven
// local/constant/immutable-global leaves under the exact nontrapping scalar
// shells selected by `OiIdempotentCallOperandTreeFact`. Immutable globals are
// state reads, but the annotated call shell cannot invalidate them.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Call if node.imm0 >= 0 => ()
_ => return None
}
let module_ctx = match ctx.module_ctx {
Some(module_ctx) => module_ctx
None => return None
}
let func_idx = node.imm0
if !@ir.hot_module_func_has_annotation(
module_ctx,
@lib.FuncIdx::new(func_idx.reinterpret_as_uint()),
"binaryen.idempotent",
) {
return None
}
let operand_ids : Array[Int32] = []
let tree_facts : Array[OiIdempotentCallOperandTreeFact] = []
let mut all_supported = true
for slot = 0; slot < node.child_count; slot = slot + 1 {
let operand_id = @ir.hot_child_get(func, node_id, slot)
operand_ids.push(operand_id)
let tree_fact = OiIdempotentCallOperandTreeFact::from_node(
ctx, func, operand_id,
)
if !tree_fact.proves_foldable() {
all_supported = false
}
tree_facts.push(tree_fact)
}
let kind = if node.child_count == 0 {
IdempotentCallNoOperands
} else if all_supported {
IdempotentCallPureOperandTrees
} else {
IdempotentCallUnsupportedOperands
}
Some({
call_id: node_id,
func_idx,
operand_fact: { kind, operand_ids, tree_facts, },
})
}
///|
fn OiIdempotentCallOperandFact::proves_foldable(
self : OiIdempotentCallOperandFact,
) -> Bool {
match self.kind {
IdempotentCallNoOperands | IdempotentCallPureOperandTrees => true
IdempotentCallUnsupportedOperands => false
}
}
///|
fn OiIdempotentCallOperandFact::proves_equal(
self : OiIdempotentCallOperandFact,
func : @ir.HotFunc,
other : OiIdempotentCallOperandFact,
) -> Bool {
if !self.proves_foldable() ||
!other.proves_foldable() ||
self.operand_ids.length() != other.operand_ids.length() ||
self.tree_facts.length() != other.tree_facts.length() {
return false
}
for fact in self.tree_facts {
if !fact.proves_foldable() {
return false
}
}
for fact in other.tree_facts {
if !fact.proves_foldable() {
return false
}
}
for slot = 0; slot < self.operand_ids.length(); slot = slot + 1 {
if !OiScalarTreeEqualityFact::from_nodes(
func,
self.operand_ids[slot],
other.operand_ids[slot],
).proves_pure_equal() {
return false
}
}
true
}
///|
fn OiIdempotentCallFact::proves_foldable(self : OiIdempotentCallFact) -> Bool {
self.operand_fact.proves_foldable()
}
///|
priv struct OiIdempotentCallEqualityFact {
left_fact : OiIdempotentCallFact?
right_fact : OiIdempotentCallFact?
same_call : Bool
}
///|
fn OiIdempotentCallEqualityFact::from_nodes(
ctx : HotPassContext,
func : @ir.HotFunc,
left_id : Int32,
right_id : Int32,
) -> OiIdempotentCallEqualityFact {
let left_fact = OiIdempotentCallFact::from_node(ctx, func, left_id)
let right_fact = OiIdempotentCallFact::from_node(ctx, func, right_id)
let same_call = match (left_fact, right_fact) {
(Some(left), Some(right)) =>
left.func_idx == right.func_idx &&
@ir.hot_node_type(func, left.call_id) ==
@ir.hot_node_type(func, right.call_id) &&
left.proves_foldable() &&
right.proves_foldable() &&
left.operand_fact.proves_equal(func, right.operand_fact)
_ => false
}
{ left_fact, right_fact, same_call, }
}
///|
fn OiIdempotentCallEqualityFact::proves_equal(
self : OiIdempotentCallEqualityFact,
) -> Bool {
ignore(self.left_fact)
ignore(self.right_fact)
self.same_call
}
///|
priv enum OiConsecutiveInputFoldabilityKind {
ConsecutiveInputFoldablePure
ConsecutiveInputFoldableIdempotentCall
ConsecutiveInputNotFoldable
}
///|
priv struct OiConsecutiveInputFoldabilityFact {
right_id : Int32
kind : OiConsecutiveInputFoldabilityKind
}
///|
fn OiConsecutiveInputFoldabilityFact::from_right(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
right_id : Int32,
) -> OiConsecutiveInputFoldabilityFact {
// Binaryen v130 `areConsecutiveInputsEqualAndFoldable` separately requires
// the later equal input to have no unremovable effects before it can be
// folded into the earlier input. Annotated direct calls with no operands or
// operands accepted by the explicit recursive child-effect/invalidation fact
// are the represented exceptions to the ordinary pure-expression proof.
let kind = if OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, right_id,
) {
ConsecutiveInputFoldablePure
} else {
match OiIdempotentCallFact::from_node(ctx, func, right_id) {
Some(fact) if fact.proves_foldable() =>
ConsecutiveInputFoldableIdempotentCall
_ => ConsecutiveInputNotFoldable
}
}
{ right_id, kind, }
}
///|
fn OiConsecutiveInputFoldabilityFact::proves_foldable(
self : OiConsecutiveInputFoldabilityFact,
) -> Bool {
ignore(self.right_id)
match self.kind {
ConsecutiveInputFoldablePure | ConsecutiveInputFoldableIdempotentCall =>
true
ConsecutiveInputNotFoldable => false
}
}
///|
priv enum OiConsecutiveSelfSelectKind {
ConsecutiveSelfSelectI32Truthy
ConsecutiveSelfSelectI32Falsy
}
///|
priv struct OiConsecutiveSelfSelectFact {
kind : OiConsecutiveSelfSelectKind
selected_id : Int32
zero_id : Int32
equality_fact : OiConsecutiveInputEqualityFact
foldability_fact : OiConsecutiveInputFoldabilityFact
}
///|
fn OiConsecutiveSelfSelectFact::from_select(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
if_true : Int32,
if_false : Int32,
condition_id : Int32,
) -> OiConsecutiveSelfSelectFact? {
// Binaryen v130 `optimizeSelect`'s paired SCCP compatibility rules rewrite
// `i32(x) ? i32(x) : 0` to the earlier `x`, and `i32(x) ? 0 : i32(x)` to
// `drop(x); 0`, when the selected arm and condition are consecutive equal
// foldable inputs. Keep the literal/type shell, equality proof, and
// right-input erasure proof together as one OI-F fact.
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[result_type] if result_type == @lib.ValType::i32() => ()
_ => return None
}
let (kind, selected_id, zero_id) = match
(
optimize_instructions_i32_const(func, dead, if_true),
optimize_instructions_i32_const(func, dead, if_false),
) {
(_, Some(@lib.I32(0))) =>
(ConsecutiveSelfSelectI32Truthy, if_true, if_false)
(Some(@lib.I32(0)), _) => (ConsecutiveSelfSelectI32Falsy, if_false, if_true)
_ => return None
}
let equality_fact = OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx, func, selected_id, condition_id,
)
let foldability_fact = OiConsecutiveInputFoldabilityFact::from_right(
ctx, func, dead, condition_id,
)
if !equality_fact.proves_equal(func) || !foldability_fact.proves_foldable() {
return None
}
Some({ kind, selected_id, zero_id, equality_fact, foldability_fact, })
}
///|
fn OiConsecutiveSelfSelectFact::apply(
self : OiConsecutiveSelfSelectFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if !self.equality_fact.proves_equal(func) ||
!self.foldability_fact.proves_foldable() {
return false
}
match self.kind {
ConsecutiveSelfSelectI32Truthy =>
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.selected_id,
)
ConsecutiveSelfSelectI32Falsy => {
let replacement = @ir.hot_build_block(
func,
@lib.BlockType::val_type(@lib.ValType::i32()),
[@ir.hot_build_drop(func, self.selected_id), self.zero_id],
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
}
}
true
}
///|
priv enum OiI64ZeroTestConditionKind {
I64ZeroTestEqz
I64ZeroTestNeZero
}
///|
priv struct OiI64ZeroTestConditionFact {
inner_id : Int32
kind : OiI64ZeroTestConditionKind
}
///|
fn OiI64ZeroTestConditionFact::from_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
condition_id : Int32,
) -> OiI64ZeroTestConditionFact? {
// Binaryen v130's consecutive i64 self-select rules match either direct
// `i64.eqz(y)` or the exact `i64.ne(y, 0)` spelling. Keep that polarity and
// surviving inner input separate from the later arm/equality decision.
match @ir.hot_node_exact_instr(func, condition_id) {
Some(@lib.Instruction::I64Eqz) if @ir.hot_child_count(func, condition_id) ==
1 =>
Some({
inner_id: @ir.hot_child_get(func, condition_id, 0),
kind: I64ZeroTestEqz,
})
Some(@lib.Instruction::I64Ne) if @ir.hot_child_count(func, condition_id) ==
2 => {
let rhs_id = @ir.hot_child_get(func, condition_id, 1)
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(0L)) =>
Some({
inner_id: @ir.hot_child_get(func, condition_id, 0),
kind: I64ZeroTestNeZero,
})
_ => None
}
}
_ => None
}
}
///|
priv enum OiI64ZeroTestSelfSelectKind {
I64ZeroTestSelfSelectForward
I64ZeroTestSelfSelectDropToZero
}
///|
priv struct OiI64ZeroTestSelfSelectFact {
kind : OiI64ZeroTestSelfSelectKind
selected_id : Int32
zero_id : Int32
condition_fact : OiI64ZeroTestConditionFact
equality_fact : OiConsecutiveInputEqualityFact
foldability_fact : OiConsecutiveInputFoldabilityFact
}
///|
fn OiI64ZeroTestSelfSelectFact::from_select(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
if_true : Int32,
if_false : Int32,
condition_id : Int32,
) -> OiI64ZeroTestSelfSelectFact? {
// Binaryen v130 `optimizeSelect` has four paired i64 SCCP compatibility
// forms. Depending on eqz/ne polarity and which arm is zero, they either
// forward the earlier selected `x` or preserve it with a drop before zero.
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[result_type] if result_type == @lib.ValType::i64() => ()
_ => return None
}
let condition_fact = match
OiI64ZeroTestConditionFact::from_condition(func, dead, condition_id) {
Some(fact) => fact
None => return None
}
let true_is_zero = match
optimize_instructions_i64_const(func, dead, if_true) {
Some(@lib.I64(0L)) => true
_ => false
}
let false_is_zero = match
optimize_instructions_i64_const(func, dead, if_false) {
Some(@lib.I64(0L)) => true
_ => false
}
let (kind, selected_id, zero_id) = match condition_fact.kind {
I64ZeroTestEqz if true_is_zero =>
(I64ZeroTestSelfSelectForward, if_false, if_true)
I64ZeroTestNeZero if false_is_zero =>
(I64ZeroTestSelfSelectForward, if_true, if_false)
I64ZeroTestEqz if false_is_zero =>
(I64ZeroTestSelfSelectDropToZero, if_true, if_false)
I64ZeroTestNeZero if true_is_zero =>
(I64ZeroTestSelfSelectDropToZero, if_false, if_true)
_ => return None
}
let equality_fact = OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx,
func,
selected_id,
condition_fact.inner_id,
)
let foldability_fact = OiConsecutiveInputFoldabilityFact::from_right(
ctx,
func,
dead,
condition_fact.inner_id,
)
if !equality_fact.proves_equal(func) || !foldability_fact.proves_foldable() {
return None
}
Some({
kind,
selected_id,
zero_id,
condition_fact,
equality_fact,
foldability_fact,
})
}
///|
fn OiI64ZeroTestSelfSelectFact::apply(
self : OiI64ZeroTestSelfSelectFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
ignore(self.condition_fact)
if !self.equality_fact.proves_equal(func) ||
!self.foldability_fact.proves_foldable() {
return false
}
match self.kind {
I64ZeroTestSelfSelectForward =>
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.selected_id,
)
I64ZeroTestSelfSelectDropToZero => {
let replacement = @ir.hot_build_block(
func,
@lib.BlockType::val_type(@lib.ValType::i64()),
[@ir.hot_build_drop(func, self.selected_id), self.zero_id],
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
}
}
true
}
///|
priv struct OiOneBitI32ValueFact {
producer_fact : OiI32MaxBitsProducerFact
}
///|
fn OiOneBitI32ValueFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
) -> OiOneBitI32ValueFact? {
// Binaryen v130 `optimizeSelect` asks `Bits::getMaxBits` for the condition
// and both arms before applying boolean algebra. Reuse the fact-owned i32
// maxBits producer instead of adding a select-local opcode whitelist.
match OiI32MaxBitsProducerFact::from_node(func, dead, local_facts, node_id) {
Some(producer_fact) =>
match producer_fact.max_value {
Some(max_value) if OiUnsignedMaxValueFact::i32_bits_for_max(max_value) <=
1 => Some({ producer_fact, })
_ => None
}
None => None
}
}
///|
fn OiOneBitI32ValueFact::value_id(self : OiOneBitI32ValueFact) -> Int32 {
self.producer_fact.value_id
}
///|
priv enum OiBooleanSelectAlgebraKind {
BooleanSelectAndFalseZero
BooleanSelectOrTrueOne
}
///|
priv struct OiSynthesizedI32BinaryFact {
instr : @lib.Instruction
left_id : Int32
right_id : Int32
}
///|
fn OiSynthesizedI32BinaryFact::new(
instr : @lib.Instruction,
left_id : Int32,
right_id : Int32,
) -> OiSynthesizedI32BinaryFact {
{ instr, left_id, right_id, }
}
///|
fn OiSynthesizedI32BinaryFact::apply(
self : OiSynthesizedI32BinaryFact,
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Unit {
let replacement = optimize_instructions_build_i32_binary(
func,
self.instr,
self.left_id,
self.right_id,
)
optimize_instructions_replace_with_node_id(ctx, func, node_id, replacement)
// A producer that creates a binary after its ordinary post-order slot owns
// the local reuse of normal binary finishing facts. Keep this bounded to the
// current root: no body revisit, consumer admission widening, or new scanner.
ignore(
optimize_instructions_try_fold_identity_binary(
ctx,
func,
dead,
node_id,
self.instr,
self.left_id,
self.right_id,
) ||
optimize_instructions_try_fold_absorbing_binary(
ctx,
func,
dead,
node_id,
self.instr,
self.left_id,
self.right_id,
) ||
optimize_instructions_try_fold_exact_const_binary(
ctx,
func,
dead,
node_id,
self.instr,
self.left_id,
self.right_id,
),
)
}
///|
priv struct OiBooleanSelectAlgebraFact {
kind : OiBooleanSelectAlgebraKind
condition_fact : OiOneBitI32ValueFact
if_true_fact : OiOneBitI32ValueFact
if_false_fact : OiOneBitI32ValueFact
}
///|
fn OiBooleanSelectAlgebraFact::from_select(
func : @ir.HotFunc,
dead : @bitset.BitSet,
local_facts : OptimizeInstructionsLocalFacts?,
node_id : Int32,
if_true : Int32,
if_false : Int32,
condition_id : Int32,
) -> OiBooleanSelectAlgebraFact? {
// Binaryen v130 `optimizeSelect` rewrites `select(y, 0, x)` to `y & x`
// and `select(1, y, x)` to `y | x` only after all three i32 inputs are
// proven one-bit values. Keep the selected maxBits proofs with the exact
// false-zero or true-one shell.
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[result_type] if result_type == @lib.ValType::i32() => ()
_ => return None
}
let kind = match
(
optimize_instructions_i32_const(func, dead, if_true),
optimize_instructions_i32_const(func, dead, if_false),
) {
(_, Some(@lib.I32(0))) => BooleanSelectAndFalseZero
(Some(@lib.I32(1)), _) => BooleanSelectOrTrueOne
_ => return None
}
let condition_fact = match
OiOneBitI32ValueFact::from_node(func, dead, local_facts, condition_id) {
Some(fact) => fact
None => return None
}
let if_true_fact = match
OiOneBitI32ValueFact::from_node(func, dead, local_facts, if_true) {
Some(fact) => fact
None => return None
}
let if_false_fact = match
OiOneBitI32ValueFact::from_node(func, dead, local_facts, if_false) {
Some(fact) => fact
None => return None
}
Some({ kind, condition_fact, if_true_fact, if_false_fact, })
}
///|
fn OiBooleanSelectAlgebraFact::apply(
self : OiBooleanSelectAlgebraFact,
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let (instr, left_id) = match self.kind {
BooleanSelectAndFalseZero =>
(@lib.Instruction::i32_and(), self.if_true_fact.value_id())
BooleanSelectOrTrueOne =>
(@lib.Instruction::i32_or(), self.if_false_fact.value_id())
}
OiSynthesizedI32BinaryFact::new(
instr,
left_id,
self.condition_fact.value_id(),
).apply(ctx, func, dead, node_id)
true
}
///|
priv enum OiEqzSelectArmKind {
EqzSelectArmI32
EqzSelectArmI64
}
///|
priv struct OiEqzSelectArmFact {
kind : OiEqzSelectArmKind
inner_id : Int32
}
///|
fn OiEqzSelectArmFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiEqzSelectArmFact? {
// Binaryen v130 `optimizeTernary` matches a direct abstract `EqZ` arm and
// later rebuilds its child type under the inner ternary. Keep the width and
// surviving child as an explicit arm fact.
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
let inner_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Eqz) => Some({ kind: EqzSelectArmI32, inner_id, })
Some(@lib.Instruction::I64Eqz) => Some({ kind: EqzSelectArmI64, inner_id, })
_ => None
}
}
///|
priv struct OiTernaryBooleanConstFact {
node_id : Int32
flipped_value : Int32
}
///|
fn OiTernaryBooleanConstFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiTernaryBooleanConstFact? {
// Binaryen's `bval` matcher accepts exactly i32 zero or one here, then flips
// the value before changing it to the hoisted eqz input type.
match optimize_instructions_i32_const(func, dead, node_id) {
Some(@lib.I32(0)) => Some({ node_id, flipped_value: 1, })
Some(@lib.I32(1)) => Some({ node_id, flipped_value: 0, })
_ => None
}
}
///|
priv enum OiEqzBooleanSelectHoistKind {
EqzBooleanSelectTrueArm
EqzBooleanSelectFalseArm
}
///|
priv struct OiEqzBooleanSelectHoistFact {
kind : OiEqzBooleanSelectHoistKind
eqz_arm_fact : OiEqzSelectArmFact
boolean_const_fact : OiTernaryBooleanConstFact
condition_id : Int32
}
///|
fn OiEqzBooleanSelectHoistFact::from_select(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiEqzBooleanSelectHoistFact? {
// Binaryen v130 `optimizeTernary` hoists one direct eqz arm across a sibling
// boolean constant, after ordinary `optimizeSelect` rules have declined.
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[result_type] if result_type == @lib.ValType::i32() => ()
_ => return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Select if node.child_count == 3 => ()
_ => return None
}
let if_true = @ir.hot_child_get(func, node_id, 0)
let if_false = @ir.hot_child_get(func, node_id, 1)
let condition_id = @ir.hot_child_get(func, node_id, 2)
match
(
OiEqzSelectArmFact::from_node(func, if_true),
OiTernaryBooleanConstFact::from_node(func, dead, if_false),
) {
(Some(eqz_arm_fact), Some(boolean_const_fact)) =>
return Some({
kind: EqzBooleanSelectTrueArm,
eqz_arm_fact,
boolean_const_fact,
condition_id,
})
_ => ()
}
match
(
OiEqzSelectArmFact::from_node(func, if_false),
OiTernaryBooleanConstFact::from_node(func, dead, if_true),
) {
(Some(eqz_arm_fact), Some(boolean_const_fact)) =>
Some({
kind: EqzBooleanSelectFalseArm,
eqz_arm_fact,
boolean_const_fact,
condition_id,
})
_ => None
}
}
///|
fn OiEqzBooleanSelectHoistFact::apply(
self : OiEqzBooleanSelectHoistFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
ignore(self.boolean_const_fact.node_id)
let (input_type, eqz_instr, flipped_const) = match self.eqz_arm_fact.kind {
EqzSelectArmI32 =>
(
@lib.ValType::i32(),
@lib.Instruction::i32_eqz(),
@ir.hot_build_const_i32(
func,
@lib.I32(self.boolean_const_fact.flipped_value),
),
)
EqzSelectArmI64 =>
(
@lib.ValType::i64(),
@lib.Instruction::i64_eqz(),
@ir.hot_build_const_i64(
func,
@lib.I64(self.boolean_const_fact.flipped_value.to_int64()),
),
)
}
let (if_true, if_false) = match self.kind {
EqzBooleanSelectTrueArm => (self.eqz_arm_fact.inner_id, flipped_const)
EqzBooleanSelectFalseArm => (flipped_const, self.eqz_arm_fact.inner_id)
}
let inner_select = @ir.hot_build_select(
func,
input_type,
if_true,
if_false,
self.condition_id,
)
let replacement = @ir.hot_build_unary(func, @lib.ValType::i32(), inner_select)
@ir.hot_node_exact_instr_set(func, replacement, eqz_instr)
optimize_instructions_replace_with_node_id(ctx, func, node_id, replacement)
true
}
///|
fn optimize_instructions_try_hoist_eqz_boolean_select(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
match OiEqzBooleanSelectHoistFact::from_select(func, dead, node_id) {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv enum OiMatchingUnarySelectShellKind {
MatchingSelectI32Eqz
MatchingSelectI64Eqz
MatchingSelectI32Clz
MatchingSelectI32Ctz
MatchingSelectI32Popcnt
MatchingSelectI64Clz
MatchingSelectI64Ctz
MatchingSelectI64Popcnt
MatchingSelectI32WrapI64
MatchingSelectI64ExtendI32S
MatchingSelectI64ExtendI32U
MatchingSelectI32Extend8S
MatchingSelectI32Extend16S
MatchingSelectI64Extend8S
MatchingSelectI64Extend16S
MatchingSelectI64Extend32S
MatchingSelectF32Abs
MatchingSelectF32Neg
MatchingSelectF32Ceil
MatchingSelectF32Floor
MatchingSelectF32Trunc
MatchingSelectF32Nearest
MatchingSelectF32Sqrt
MatchingSelectF64Abs
MatchingSelectF64Neg
MatchingSelectF64Ceil
MatchingSelectF64Floor
MatchingSelectF64Trunc
MatchingSelectF64Nearest
MatchingSelectF64Sqrt
MatchingSelectF32DemoteF64
MatchingSelectF64PromoteF32
MatchingSelectI32ReinterpretF32
MatchingSelectI64ReinterpretF64
MatchingSelectF32ReinterpretI32
MatchingSelectF64ReinterpretI64
MatchingSelectF32ConvertI32S
MatchingSelectF32ConvertI32U
MatchingSelectF32ConvertI64S
MatchingSelectF32ConvertI64U
MatchingSelectF64ConvertI32S
MatchingSelectF64ConvertI32U
MatchingSelectF64ConvertI64S
MatchingSelectF64ConvertI64U
MatchingSelectI32TruncSatF32S
MatchingSelectI32TruncSatF32U
MatchingSelectI32TruncSatF64S
MatchingSelectI32TruncSatF64U
MatchingSelectI64TruncSatF32S
MatchingSelectI64TruncSatF32U
MatchingSelectI64TruncSatF64S
MatchingSelectI64TruncSatF64U
MatchingSelectI32x4ExtractLane
}
///|
priv struct OiMatchingUnarySelectArmFact {
kind : OiMatchingUnarySelectShellKind
instr : @lib.Instruction
result_type : @lib.ValType
child_type : @lib.ValType
child_id : Int32
}
///|
fn OiMatchingUnarySelectArmFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMatchingUnarySelectArmFact? {
// Binaryen v130's `optimizeTernary` shallow-equal loop first excludes control
// structures, requires one child, and later requires equal child types. This
// narrow starter fact admits only direct eqz shells, whose shallow operation
// is side-effect-free, and records both the output and selectable child type.
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
let (kind, instr, result_type, child_type) = match
@ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Eqz) =>
(
MatchingSelectI32Eqz,
@lib.Instruction::i32_eqz(),
@lib.ValType::i32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I64Eqz) =>
(
MatchingSelectI64Eqz,
@lib.Instruction::i64_eqz(),
@lib.ValType::i32(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I32Clz) =>
(
MatchingSelectI32Clz,
@lib.Instruction::i32_clz(),
@lib.ValType::i32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I32Ctz) =>
(
MatchingSelectI32Ctz,
@lib.Instruction::i32_ctz(),
@lib.ValType::i32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I32Popcnt) =>
(
MatchingSelectI32Popcnt,
@lib.Instruction::i32_popcnt(),
@lib.ValType::i32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I64Clz) =>
(
MatchingSelectI64Clz,
@lib.Instruction::i64_clz(),
@lib.ValType::i64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I64Ctz) =>
(
MatchingSelectI64Ctz,
@lib.Instruction::i64_ctz(),
@lib.ValType::i64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I64Popcnt) =>
(
MatchingSelectI64Popcnt,
@lib.Instruction::i64_popcnt(),
@lib.ValType::i64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I32WrapI64) =>
(
MatchingSelectI32WrapI64,
@lib.Instruction::i32_wrap_i64(),
@lib.ValType::i32(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I64ExtendI32S) =>
(
MatchingSelectI64ExtendI32S,
@lib.Instruction::i64_extend_i32s(),
@lib.ValType::i64(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I64ExtendI32U) =>
(
MatchingSelectI64ExtendI32U,
@lib.Instruction::i64_extend_i32u(),
@lib.ValType::i64(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I32Extend8S) =>
(
MatchingSelectI32Extend8S,
@lib.Instruction::i32_extend8s(),
@lib.ValType::i32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I32Extend16S) =>
(
MatchingSelectI32Extend16S,
@lib.Instruction::i32_extend16s(),
@lib.ValType::i32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::I64Extend8S) =>
(
MatchingSelectI64Extend8S,
@lib.Instruction::i64_extend8s(),
@lib.ValType::i64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I64Extend16S) =>
(
MatchingSelectI64Extend16S,
@lib.Instruction::i64_extend16s(),
@lib.ValType::i64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I64Extend32S) =>
(
MatchingSelectI64Extend32S,
@lib.Instruction::i64_extend32s(),
@lib.ValType::i64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::F32Abs) =>
(
MatchingSelectF32Abs,
@lib.Instruction::f32_abs(),
@lib.ValType::f32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::F32Neg) =>
(
MatchingSelectF32Neg,
@lib.Instruction::f32_neg(),
@lib.ValType::f32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::F32Ceil) =>
(
MatchingSelectF32Ceil,
@lib.Instruction::f32_ceil(),
@lib.ValType::f32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::F32Floor) =>
(
MatchingSelectF32Floor,
@lib.Instruction::f32_floor(),
@lib.ValType::f32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::F32Trunc) =>
(
MatchingSelectF32Trunc,
@lib.Instruction::f32_trunc(),
@lib.ValType::f32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::F32Nearest) =>
(
MatchingSelectF32Nearest,
@lib.Instruction::f32_nearest(),
@lib.ValType::f32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::F32Sqrt) =>
(
MatchingSelectF32Sqrt,
@lib.Instruction::f32_sqrt(),
@lib.ValType::f32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::F64Abs) =>
(
MatchingSelectF64Abs,
@lib.Instruction::f64_abs(),
@lib.ValType::f64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F64Neg) =>
(
MatchingSelectF64Neg,
@lib.Instruction::f64_neg(),
@lib.ValType::f64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F64Ceil) =>
(
MatchingSelectF64Ceil,
@lib.Instruction::f64_ceil(),
@lib.ValType::f64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F64Floor) =>
(
MatchingSelectF64Floor,
@lib.Instruction::f64_floor(),
@lib.ValType::f64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F64Trunc) =>
(
MatchingSelectF64Trunc,
@lib.Instruction::f64_trunc(),
@lib.ValType::f64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F64Nearest) =>
(
MatchingSelectF64Nearest,
@lib.Instruction::f64_nearest(),
@lib.ValType::f64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F64Sqrt) =>
(
MatchingSelectF64Sqrt,
@lib.Instruction::f64_sqrt(),
@lib.ValType::f64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F32DemoteF64) =>
(
MatchingSelectF32DemoteF64,
@lib.Instruction::f32_demote_f64(),
@lib.ValType::f32(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F64PromoteF32) =>
(
MatchingSelectF64PromoteF32,
@lib.Instruction::f64_promote_f32(),
@lib.ValType::f64(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::I32ReinterpretF32) =>
(
MatchingSelectI32ReinterpretF32,
@lib.Instruction::i32_reinterpret_f32(),
@lib.ValType::i32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::I64ReinterpretF64) =>
(
MatchingSelectI64ReinterpretF64,
@lib.Instruction::i64_reinterpret_f64(),
@lib.ValType::i64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::F32ReinterpretI32) =>
(
MatchingSelectF32ReinterpretI32,
@lib.Instruction::f32_reinterpret_i32(),
@lib.ValType::f32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::F64ReinterpretI64) =>
(
MatchingSelectF64ReinterpretI64,
@lib.Instruction::f64_reinterpret_i64(),
@lib.ValType::f64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::F32ConvertI32S) =>
(
MatchingSelectF32ConvertI32S,
@lib.Instruction::f32_convert_i32s(),
@lib.ValType::f32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::F32ConvertI32U) =>
(
MatchingSelectF32ConvertI32U,
@lib.Instruction::f32_convert_i32u(),
@lib.ValType::f32(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::F32ConvertI64S) =>
(
MatchingSelectF32ConvertI64S,
@lib.Instruction::f32_convert_i64s(),
@lib.ValType::f32(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::F32ConvertI64U) =>
(
MatchingSelectF32ConvertI64U,
@lib.Instruction::f32_convert_i64u(),
@lib.ValType::f32(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::F64ConvertI32S) =>
(
MatchingSelectF64ConvertI32S,
@lib.Instruction::f64_convert_i32s(),
@lib.ValType::f64(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::F64ConvertI32U) =>
(
MatchingSelectF64ConvertI32U,
@lib.Instruction::f64_convert_i32u(),
@lib.ValType::f64(),
@lib.ValType::i32(),
)
Some(@lib.Instruction::F64ConvertI64S) =>
(
MatchingSelectF64ConvertI64S,
@lib.Instruction::f64_convert_i64s(),
@lib.ValType::f64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::F64ConvertI64U) =>
(
MatchingSelectF64ConvertI64U,
@lib.Instruction::f64_convert_i64u(),
@lib.ValType::f64(),
@lib.ValType::i64(),
)
Some(@lib.Instruction::I32TruncSatF32S) =>
(
MatchingSelectI32TruncSatF32S,
@lib.Instruction::i32_trunc_sat_f32s(),
@lib.ValType::i32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::I32TruncSatF32U) =>
(
MatchingSelectI32TruncSatF32U,
@lib.Instruction::i32_trunc_sat_f32u(),
@lib.ValType::i32(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::I32TruncSatF64S) =>
(
MatchingSelectI32TruncSatF64S,
@lib.Instruction::i32_trunc_sat_f64s(),
@lib.ValType::i32(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::I32TruncSatF64U) =>
(
MatchingSelectI32TruncSatF64U,
@lib.Instruction::i32_trunc_sat_f64u(),
@lib.ValType::i32(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::I64TruncSatF32S) =>
(
MatchingSelectI64TruncSatF32S,
@lib.Instruction::i64_trunc_sat_f32s(),
@lib.ValType::i64(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::I64TruncSatF32U) =>
(
MatchingSelectI64TruncSatF32U,
@lib.Instruction::i64_trunc_sat_f32u(),
@lib.ValType::i64(),
@lib.ValType::f32(),
)
Some(@lib.Instruction::I64TruncSatF64S) =>
(
MatchingSelectI64TruncSatF64S,
@lib.Instruction::i64_trunc_sat_f64s(),
@lib.ValType::i64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::I64TruncSatF64U) =>
(
MatchingSelectI64TruncSatF64U,
@lib.Instruction::i64_trunc_sat_f64u(),
@lib.ValType::i64(),
@lib.ValType::f64(),
)
Some(@lib.Instruction::I32x4ExtractLane(lane)) =>
(
MatchingSelectI32x4ExtractLane,
@lib.Instruction::i32x4_extract_lane(lane),
@lib.ValType::i32(),
@lib.ValType::v128(),
)
_ => return None
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[actual_result] if actual_result == result_type => ()
_ => return None
}
let child_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_type_results(func, @ir.hot_node_type(func, child_id)) {
[actual_child] if actual_child == child_type => ()
_ => return None
}
Some({ kind, instr, result_type, child_type, child_id, })
}
///|
priv struct OiMatchingUnarySelectArmPairFact {
shell_fact : OiMatchingUnarySelectArmFact
if_true_child : Int32
if_false_child : Int32
}
///|
fn OiMatchingUnarySelectArmPairFact::from_arms(
func : @ir.HotFunc,
if_true : Int32,
if_false : Int32,
) -> OiMatchingUnarySelectArmPairFact? {
let true_fact = match OiMatchingUnarySelectArmFact::from_node(func, if_true) {
Some(fact) => fact
None => return None
}
let false_fact = match
OiMatchingUnarySelectArmFact::from_node(func, if_false) {
Some(fact) => fact
None => return None
}
match (true_fact.kind, false_fact.kind) {
(MatchingSelectI32Eqz, MatchingSelectI32Eqz) => ()
(MatchingSelectI64Eqz, MatchingSelectI64Eqz) => ()
(MatchingSelectI32Clz, MatchingSelectI32Clz) => ()
(MatchingSelectI32Ctz, MatchingSelectI32Ctz) => ()
(MatchingSelectI32Popcnt, MatchingSelectI32Popcnt) => ()
(MatchingSelectI64Clz, MatchingSelectI64Clz) => ()
(MatchingSelectI64Ctz, MatchingSelectI64Ctz) => ()
(MatchingSelectI64Popcnt, MatchingSelectI64Popcnt) => ()
(MatchingSelectI32WrapI64, MatchingSelectI32WrapI64) => ()
(MatchingSelectI64ExtendI32S, MatchingSelectI64ExtendI32S) => ()
(MatchingSelectI64ExtendI32U, MatchingSelectI64ExtendI32U) => ()
(MatchingSelectI32Extend8S, MatchingSelectI32Extend8S) => ()
(MatchingSelectI32Extend16S, MatchingSelectI32Extend16S) => ()
(MatchingSelectI64Extend8S, MatchingSelectI64Extend8S) => ()
(MatchingSelectI64Extend16S, MatchingSelectI64Extend16S) => ()
(MatchingSelectI64Extend32S, MatchingSelectI64Extend32S) => ()
(MatchingSelectF32Abs, MatchingSelectF32Abs) => ()
(MatchingSelectF32Neg, MatchingSelectF32Neg) => ()
(MatchingSelectF32Ceil, MatchingSelectF32Ceil) => ()
(MatchingSelectF32Floor, MatchingSelectF32Floor) => ()
(MatchingSelectF32Trunc, MatchingSelectF32Trunc) => ()
(MatchingSelectF32Nearest, MatchingSelectF32Nearest) => ()
(MatchingSelectF32Sqrt, MatchingSelectF32Sqrt) => ()
(MatchingSelectF64Abs, MatchingSelectF64Abs) => ()
(MatchingSelectF64Neg, MatchingSelectF64Neg) => ()
(MatchingSelectF64Ceil, MatchingSelectF64Ceil) => ()
(MatchingSelectF64Floor, MatchingSelectF64Floor) => ()
(MatchingSelectF64Trunc, MatchingSelectF64Trunc) => ()
(MatchingSelectF64Nearest, MatchingSelectF64Nearest) => ()
(MatchingSelectF64Sqrt, MatchingSelectF64Sqrt) => ()
(MatchingSelectF32DemoteF64, MatchingSelectF32DemoteF64) => ()
(MatchingSelectF64PromoteF32, MatchingSelectF64PromoteF32) => ()
(MatchingSelectI32ReinterpretF32, MatchingSelectI32ReinterpretF32) => ()
(MatchingSelectI64ReinterpretF64, MatchingSelectI64ReinterpretF64) => ()
(MatchingSelectF32ReinterpretI32, MatchingSelectF32ReinterpretI32) => ()
(MatchingSelectF64ReinterpretI64, MatchingSelectF64ReinterpretI64) => ()
(MatchingSelectF32ConvertI32S, MatchingSelectF32ConvertI32S) => ()
(MatchingSelectF32ConvertI32U, MatchingSelectF32ConvertI32U) => ()
(MatchingSelectF32ConvertI64S, MatchingSelectF32ConvertI64S) => ()
(MatchingSelectF32ConvertI64U, MatchingSelectF32ConvertI64U) => ()
(MatchingSelectF64ConvertI32S, MatchingSelectF64ConvertI32S) => ()
(MatchingSelectF64ConvertI32U, MatchingSelectF64ConvertI32U) => ()
(MatchingSelectF64ConvertI64S, MatchingSelectF64ConvertI64S) => ()
(MatchingSelectF64ConvertI64U, MatchingSelectF64ConvertI64U) => ()
(MatchingSelectI32TruncSatF32S, MatchingSelectI32TruncSatF32S) => ()
(MatchingSelectI32TruncSatF32U, MatchingSelectI32TruncSatF32U) => ()
(MatchingSelectI32TruncSatF64S, MatchingSelectI32TruncSatF64S) => ()
(MatchingSelectI32TruncSatF64U, MatchingSelectI32TruncSatF64U) => ()
(MatchingSelectI64TruncSatF32S, MatchingSelectI64TruncSatF32S) => ()
(MatchingSelectI64TruncSatF32U, MatchingSelectI64TruncSatF32U) => ()
(MatchingSelectI64TruncSatF64S, MatchingSelectI64TruncSatF64S) => ()
(MatchingSelectI64TruncSatF64U, MatchingSelectI64TruncSatF64U) => ()
(MatchingSelectI32x4ExtractLane, MatchingSelectI32x4ExtractLane) => ()
_ => return None
}
if true_fact.instr != false_fact.instr ||
true_fact.child_type != false_fact.child_type ||
true_fact.result_type != false_fact.result_type {
return None
}
Some({
shell_fact: false_fact,
if_true_child: true_fact.child_id,
if_false_child: false_fact.child_id,
})
}
///|
priv struct OiMatchingUnarySelectHoistFact {
shell_facts : Array[OiMatchingUnarySelectArmFact]
inner_select_type : @lib.ValType
if_true_child : Int32
if_false_child : Int32
condition_id : Int32
}
///|
fn OiMatchingUnarySelectHoistFact::from_select(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMatchingUnarySelectHoistFact? {
// This is the direct-select subset of Binaryen v130's repeated shallow-equal
// one-child hoist. Equal selected fact kinds model `shallowEqual`; the exact
// scalar child type check models both equal child types and the currently
// representable `canEmitSelectWithArms` boundary. Collecting the pair facts
// mirrors the source loop so an entire supported matching chain is rebuilt
// around one final child select instead of stopping after the outer shell.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Select if node.child_count == 3 => ()
_ => return None
}
let mut if_true_child = @ir.hot_child_get(func, node_id, 0)
let mut if_false_child = @ir.hot_child_get(func, node_id, 1)
let condition_id = @ir.hot_child_get(func, node_id, 2)
let mut inner_select_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[select_result] => select_result
_ => return None
}
let shell_facts : Array[OiMatchingUnarySelectArmFact] = []
let mut scanning = true
while scanning {
match
OiMatchingUnarySelectArmPairFact::from_arms(
func, if_true_child, if_false_child,
) {
Some(pair_fact) =>
if pair_fact.shell_fact.result_type != inner_select_type {
scanning = false
} else {
inner_select_type = pair_fact.shell_fact.child_type
if_true_child = pair_fact.if_true_child
if_false_child = pair_fact.if_false_child
shell_facts.push(pair_fact.shell_fact)
}
None => scanning = false
}
}
if shell_facts.length() == 0 {
return None
}
Some({
shell_facts,
inner_select_type,
if_true_child,
if_false_child,
condition_id,
})
}
///|
fn OiMatchingUnarySelectHoistFact::apply(
self : OiMatchingUnarySelectHoistFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let mut replacement = @ir.hot_build_select(
func,
self.inner_select_type,
self.if_true_child,
self.if_false_child,
self.condition_id,
)
let mut remaining = self.shell_facts.length()
while remaining > 0 {
remaining = remaining - 1
let shell_fact = self.shell_facts[remaining]
let outer = @ir.hot_build_unary(func, shell_fact.result_type, replacement)
@ir.hot_node_exact_instr_set(func, outer, shell_fact.instr)
replacement = outer
}
optimize_instructions_replace_with_node_id(ctx, func, node_id, replacement)
true
}
///|
fn optimize_instructions_try_hoist_matching_unary_select(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match OiMatchingUnarySelectHoistFact::from_select(func, node_id) {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv struct OiMatchingUnaryIfHoistFact {
shell_facts : Array[OiMatchingUnarySelectArmFact]
inner_if_type : @lib.ValType
if_true_child : Int32
if_false_child : Int32
condition_id : Int32
root_index : Int32
}
///|
fn OiMatchingUnaryIfHoistFact::from_if(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMatchingUnaryIfHoistFact? {
// Binaryen v130 instantiates `optimizeTernary` for If as well as Select and
// repeatedly moves matching one-child shells. Reuse the exact scalar shell
// pair facts, but keep the first safe HOT writeback at a direct function root.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => ()
_ => return None
}
let mut root_index = -1
for idx = 0; idx < @ir.hot_root_count(func); idx = idx + 1 {
if @ir.hot_root_get(func, idx) == node_id {
root_index = idx
break
}
}
if root_index < 0 {
return None
}
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return None
}
let then_region = @ir.hot_region_then_ref(func, node_id)
if @ir.hot_region_root_count(func, then_region) != 1 ||
@ir.hot_region_root_count(func, else_region) != 1 {
return None
}
let mut if_true_child = @ir.hot_region_get(func, then_region, 0)
let mut if_false_child = @ir.hot_region_get(func, else_region, 0)
let mut inner_if_type = match @ir.hot_type_results(func, node.ty) {
[result] => result
_ => return None
}
let shell_facts : Array[OiMatchingUnarySelectArmFact] = []
let mut scanning = true
while scanning {
match
OiMatchingUnarySelectArmPairFact::from_arms(
func, if_true_child, if_false_child,
) {
Some(pair_fact) =>
if pair_fact.shell_fact.result_type != inner_if_type {
scanning = false
} else {
inner_if_type = pair_fact.shell_fact.child_type
if_true_child = pair_fact.if_true_child
if_false_child = pair_fact.if_false_child
shell_facts.push(pair_fact.shell_fact)
}
None => scanning = false
}
}
if shell_facts.length() == 0 {
return None
}
Some({
shell_facts,
inner_if_type,
if_true_child,
if_false_child,
condition_id: @ir.hot_child_get(func, node_id, 0),
root_index,
})
}
///|
fn OiMatchingUnaryIfHoistFact::apply(
self : OiMatchingUnaryIfHoistFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Bool {
let mut replacement = @ir.hot_build_if(
func,
@lib.BlockType::val_type(self.inner_if_type),
self.condition_id,
[self.if_true_child],
else_body=Some([self.if_false_child]),
)
let mut remaining = self.shell_facts.length()
while remaining > 0 {
remaining = remaining - 1
let shell_fact = self.shell_facts[remaining]
let outer = @ir.hot_build_unary(func, shell_fact.result_type, replacement)
@ir.hot_node_exact_instr_set(func, outer, shell_fact.instr)
replacement = outer
}
@ir.hot_root_set(func, self.root_index, replacement)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_hoist_matching_unary_if(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match OiMatchingUnaryIfHoistFact::from_if(func, node_id) {
Some(fact) => fact.apply(ctx, func)
None => false
}
}
///|
priv enum OiMatchingReferenceSelectShellKind {
MatchingSelectRefIsNull
MatchingSelectRefTest
}
///|
priv struct OiMatchingReferenceSelectArmFact {
kind : OiMatchingReferenceSelectShellKind
shell_id : Int32
instr : @lib.Instruction?
child_type : @lib.ValType
child_id : Int32
}
///|
fn OiMatchingReferenceSelectArmFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMatchingReferenceSelectArmFact? {
// Binaryen v130's generic `optimizeTernary` loop accepts shallow-equal
// one-child shells, equal child types, and side-effect-free select shells.
// Keep reference-shaped shells separate from exact numeric Unary facts.
let node = @ir.hot_node_get(func, node_id)
let (kind, instr) = match node.op {
@ir.HotOp::RefIsNull if node.child_count == 1 =>
(MatchingSelectRefIsNull, None)
@ir.HotOp::RefTest if node.child_count == 1 =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(instr) =>
match instr {
@lib.Instruction::RefTest(_, _, _) =>
(MatchingSelectRefTest, Some(instr))
_ => return None
}
None => return None
}
_ => return None
}
match @ir.hot_type_results(func, node.ty) {
[result] if result == @lib.ValType::i32() => ()
_ => return None
}
let child_id = @ir.hot_child_get(func, node_id, 0)
let child_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, child_id)) {
[result] => result
_ => return None
}
Some({ kind, shell_id: node_id, instr, child_type, child_id, })
}
///|
priv struct OiMatchingReferenceSelectHoistFact {
kind : OiMatchingReferenceSelectShellKind
shell_id : Int32
instr : @lib.Instruction?
child_type : @lib.ValType
if_true_child : Int32
if_false_child : Int32
condition_id : Int32
}
///|
fn OiMatchingReferenceSelectHoistFact::from_select(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMatchingReferenceSelectHoistFact? {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Select if node.child_count == 3 => ()
_ => return None
}
match @ir.hot_type_results(func, node.ty) {
[result] if result == @lib.ValType::i32() => ()
_ => return None
}
let true_fact = match
OiMatchingReferenceSelectArmFact::from_node(
func,
@ir.hot_child_get(func, node_id, 0),
) {
Some(fact) => fact
None => return None
}
let false_fact = match
OiMatchingReferenceSelectArmFact::from_node(
func,
@ir.hot_child_get(func, node_id, 1),
) {
Some(fact) => fact
None => return None
}
match (true_fact.kind, false_fact.kind) {
(MatchingSelectRefIsNull, MatchingSelectRefIsNull) => ()
(MatchingSelectRefTest, MatchingSelectRefTest) => ()
_ => return None
}
if true_fact.instr != false_fact.instr ||
true_fact.child_type != false_fact.child_type {
return None
}
Some({
kind: false_fact.kind,
shell_id: false_fact.shell_id,
instr: false_fact.instr,
child_type: false_fact.child_type,
if_true_child: true_fact.child_id,
if_false_child: false_fact.child_id,
condition_id: @ir.hot_child_get(func, node_id, 2),
})
}
///|
fn OiMatchingReferenceSelectHoistFact::apply(
self : OiMatchingReferenceSelectHoistFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let inner = @ir.hot_build_select(
func,
self.child_type,
self.if_true_child,
self.if_false_child,
self.condition_id,
)
let replacement = match self.kind {
MatchingSelectRefIsNull =>
@ir.hot_build_node(
func,
@ir.hot_node_get(func, self.shell_id).op,
@ir.hot_type_intern_value(func, @lib.ValType::i32()),
children=[inner],
)
MatchingSelectRefTest => {
let instr = match self.instr {
Some(instr) => instr
None => return false
}
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(instr),
)
@ir.hot_build_node(
func,
@ir.hot_node_get(func, self.shell_id).op,
@ir.hot_type_intern_value(func, @lib.ValType::i32()),
imm1=payload_id,
children=[inner],
)
}
}
optimize_instructions_replace_with_node_id(ctx, func, node_id, replacement)
true
}
///|
fn optimize_instructions_try_hoist_matching_reference_select(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match OiMatchingReferenceSelectHoistFact::from_select(func, node_id) {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv struct OiMatchingReferenceIfHoistFact {
kind : OiMatchingReferenceSelectShellKind
shell_id : Int32
instr : @lib.Instruction?
child_type : @lib.ValType
if_true_child : Int32
if_false_child : Int32
condition_id : Int32
root_index : Int32
}
///|
fn OiMatchingReferenceIfHoistFact::from_if(
func : @ir.HotFunc,
node_id : Int32,
) -> OiMatchingReferenceIfHoistFact? {
// Binaryen v130 instantiates the same `optimizeTernary` shallow-equal loop
// for `If`. Unlike Select, moving a shallow shell does not reduce its
// execution count. Keep the direct one-root ref.is_null and exact ref.test
// arm subset so rebuilding the value regions and result type stays explicit.
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => ()
_ => return None
}
let mut root_index = -1
for idx = 0; idx < @ir.hot_root_count(func); idx = idx + 1 {
if @ir.hot_root_get(func, idx) == node_id {
root_index = idx
break
}
}
if root_index < 0 {
return None
}
match @ir.hot_type_results(func, node.ty) {
[result] if result == @lib.ValType::i32() => ()
_ => return None
}
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None => return None
}
let then_region = @ir.hot_region_then_ref(func, node_id)
if @ir.hot_region_root_count(func, then_region) != 1 ||
@ir.hot_region_root_count(func, else_region) != 1 {
return None
}
let true_fact = match
OiMatchingReferenceSelectArmFact::from_node(
func,
@ir.hot_region_get(func, then_region, 0),
) {
Some(fact) => fact
None => return None
}
let false_fact = match
OiMatchingReferenceSelectArmFact::from_node(
func,
@ir.hot_region_get(func, else_region, 0),
) {
Some(fact) => fact
None => return None
}
match (true_fact.kind, false_fact.kind) {
(MatchingSelectRefIsNull, MatchingSelectRefIsNull) => ()
(MatchingSelectRefTest, MatchingSelectRefTest) => ()
_ => return None
}
if true_fact.instr != false_fact.instr ||
true_fact.child_type != false_fact.child_type {
return None
}
Some({
kind: false_fact.kind,
shell_id: false_fact.shell_id,
instr: false_fact.instr,
child_type: false_fact.child_type,
if_true_child: true_fact.child_id,
if_false_child: false_fact.child_id,
condition_id: @ir.hot_child_get(func, node_id, 0),
root_index,
})
}
///|
fn OiMatchingReferenceIfHoistFact::apply(
self : OiMatchingReferenceIfHoistFact,
ctx : HotPassContext,
func : @ir.HotFunc,
_node_id : Int32,
) -> Bool {
let inner = @ir.hot_build_if(
func,
@lib.BlockType::val_type(self.child_type),
self.condition_id,
[self.if_true_child],
else_body=Some([self.if_false_child]),
)
let replacement = match self.kind {
MatchingSelectRefIsNull =>
@ir.hot_build_node(
func,
@ir.hot_node_get(func, self.shell_id).op,
@ir.hot_type_intern_value(func, @lib.ValType::i32()),
children=[inner],
)
MatchingSelectRefTest => {
let instr = match self.instr {
Some(instr) => instr
None => return false
}
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(instr),
)
@ir.hot_build_node(
func,
@ir.hot_node_get(func, self.shell_id).op,
@ir.hot_type_intern_value(func, @lib.ValType::i32()),
imm1=payload_id,
children=[inner],
)
}
}
// Keep the original structured node intact as a detached, label-owning node.
// HOT's in-place replacement helper cannot change a control node into a
// shallow shell without orphaning its label, so this first safe slice is
// deliberately bounded to direct function roots.
@ir.hot_root_set(func, self.root_index, replacement)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_hoist_matching_reference_if(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match OiMatchingReferenceIfHoistFact::from_if(func, node_id) {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv enum OiI32BooleanSelectArmKind {
I32BooleanSelectTrueFalse
I32BooleanSelectFalseTrue
}
///|
priv struct OiI32BooleanSelectArmFact {
kind : OiI32BooleanSelectArmKind
}
///|
fn OiI32BooleanSelectArmFact::from_select_arms(
func : @ir.HotFunc,
dead : @bitset.BitSet,
if_true : Int32,
if_false : Int32,
) -> OiI32BooleanSelectArmFact? {
// Binaryen v130 `optimizeBoolean` gives the exact i32 `1,0` and `0,1`
// select-arm shells special treatment across direct eqz, zero-compare,
// compare-condition, and final boolean-select fallbacks. Keep that literal
// arm proof in one source-backed fact so those condition facts no longer own
// parallel inline arm scanners.
match
(
optimize_instructions_i32_const(func, dead, if_true),
optimize_instructions_i32_const(func, dead, if_false),
) {
(Some(@lib.I32(1)), Some(@lib.I32(0))) =>
Some({ kind: I32BooleanSelectTrueFalse, })
(Some(@lib.I32(0)), Some(@lib.I32(1))) =>
Some({ kind: I32BooleanSelectFalseTrue, })
_ => None
}
}
///|
priv enum OiEqzConditionSelectRewriteKind {
EqzConditionSelectNoRewriteBoundary
EqzConditionSelectI64TrueFalse
EqzConditionSelectI64FalseTrue
EqzConditionSelectI32TrueFalseCompare
EqzConditionSelectI32TrueFalseEqz
EqzConditionSelectI32FalseTrueCompare
EqzConditionSelectI32FalseTrueNonzero
EqzConditionSelectI32SwapArms
}
///|
priv struct OiEqzConditionSelectCompareFact {
lhs_id : Int32
rhs_id : Int32
replacement_instr : @lib.Instruction
}
///|
priv struct OiEqzConditionSelectFact {
inner_condition_id : Int32
if_true_id : Int32
if_false_id : Int32
compare_fact : OiEqzConditionSelectCompareFact?
kind : OiEqzConditionSelectRewriteKind
}
///|
fn OiEqzConditionSelectFact::no_rewrite_boundary(
inner_condition_id : Int32,
if_true_id : Int32,
if_false_id : Int32,
) -> OiEqzConditionSelectFact {
{
inner_condition_id,
if_true_id,
if_false_id,
compare_fact: None,
kind: EqzConditionSelectNoRewriteBoundary,
}
}
///|
fn OiEqzConditionSelectFact::simple(
inner_condition_id : Int32,
if_true_id : Int32,
if_false_id : Int32,
kind : OiEqzConditionSelectRewriteKind,
) -> OiEqzConditionSelectFact {
{ inner_condition_id, if_true_id, if_false_id, compare_fact: None, kind, }
}
///|
fn OiEqzConditionSelectFact::compare(
inner_condition_id : Int32,
if_true_id : Int32,
if_false_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
replacement_instr : @lib.Instruction,
kind : OiEqzConditionSelectRewriteKind,
) -> OiEqzConditionSelectFact {
{
inner_condition_id,
if_true_id,
if_false_id,
compare_fact: Some({ lhs_id, rhs_id, replacement_instr, }),
kind,
}
}
///|
fn OiEqzConditionSelectFact::from_select_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
condition_id : Int32,
if_true : Int32,
if_false : Int32,
) -> OiEqzConditionSelectFact? {
// Binaryen v130 `optimizeBoolean` handles direct `eqz` select conditions
// before the final boolean-constant select fallback. This fact records the
// direct i32/i64 `eqz` child, exact boolean arm pairs, compare-child proof,
// live-child boundary, and value-arm swap payload so the visitor no longer
// owns a nested opcode scanner for this select-condition family.
match @ir.hot_node_exact_instr(func, condition_id) {
Some(@lib.Instruction::I64Eqz) if @ir.hot_child_count(func, condition_id) ==
1 => {
let inner_condition = @ir.hot_child_get(func, condition_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, inner_condition) {
return Some(
OiEqzConditionSelectFact::no_rewrite_boundary(
inner_condition, if_true, if_false,
),
)
}
match
OiI32BooleanSelectArmFact::from_select_arms(
func, dead, if_true, if_false,
) {
Some(boolean_arm_fact) =>
match boolean_arm_fact.kind {
I32BooleanSelectTrueFalse =>
Some(
OiEqzConditionSelectFact::simple(
inner_condition,
if_true,
if_false,
EqzConditionSelectI64TrueFalse,
),
)
I32BooleanSelectFalseTrue =>
Some(
OiEqzConditionSelectFact::simple(
inner_condition,
if_true,
if_false,
EqzConditionSelectI64FalseTrue,
),
)
}
None => None
}
}
Some(@lib.Instruction::I32Eqz) if @ir.hot_child_count(func, condition_id) ==
1 => {
let inner_condition = @ir.hot_child_get(func, condition_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, inner_condition) {
return Some(
OiEqzConditionSelectFact::no_rewrite_boundary(
inner_condition, if_true, if_false,
),
)
}
match
OiI32BooleanSelectArmFact::from_select_arms(
func, dead, if_true, if_false,
) {
Some(boolean_arm_fact) =>
match boolean_arm_fact.kind {
I32BooleanSelectTrueFalse => {
match @ir.hot_node_exact_instr(func, inner_condition) {
Some(inner_instr) if @ir.hot_child_count(func, inner_condition) ==
2 =>
match
optimize_instructions_inverted_select_compare(inner_instr) {
Some(inverted_instr) => {
let lhs_id = @ir.hot_child_get(func, inner_condition, 0)
let rhs_id = @ir.hot_child_get(func, inner_condition, 1)
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return Some(
OiEqzConditionSelectFact::no_rewrite_boundary(
inner_condition, if_true, if_false,
),
)
}
return Some(
OiEqzConditionSelectFact::compare(
inner_condition,
if_true,
if_false,
lhs_id,
rhs_id,
inverted_instr,
EqzConditionSelectI32TrueFalseCompare,
),
)
}
_ => ()
}
_ => ()
}
Some(
OiEqzConditionSelectFact::simple(
inner_condition,
if_true,
if_false,
EqzConditionSelectI32TrueFalseEqz,
),
)
}
I32BooleanSelectFalseTrue => {
match @ir.hot_node_exact_instr(func, inner_condition) {
Some(inner_instr) if @ir.hot_child_count(func, inner_condition) ==
2 =>
match
optimize_instructions_inverted_select_compare(inner_instr) {
Some(_) =>
return Some(
OiEqzConditionSelectFact::simple(
inner_condition,
if_true,
if_false,
EqzConditionSelectI32FalseTrueCompare,
),
)
_ => ()
}
_ => ()
}
Some(
OiEqzConditionSelectFact::simple(
inner_condition,
if_true,
if_false,
EqzConditionSelectI32FalseTrueNonzero,
),
)
}
}
None =>
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[_] =>
Some(
OiEqzConditionSelectFact::simple(
inner_condition,
if_true,
if_false,
EqzConditionSelectI32SwapArms,
),
)
_ => None
}
}
}
_ => None
}
}
///|
fn OiEqzConditionSelectFact::apply(
self : OiEqzConditionSelectFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
EqzConditionSelectNoRewriteBoundary => false
EqzConditionSelectI64TrueFalse => {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_eqz(),
self.inner_condition_id,
)
true
}
EqzConditionSelectI64FalseTrue => {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
self.inner_condition_id,
@ir.hot_build_const_i64(func, @lib.I64(0L)),
)
true
}
EqzConditionSelectI32TrueFalseCompare =>
match self.compare_fact {
Some(compare_fact) => {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
compare_fact.replacement_instr,
compare_fact.lhs_id,
compare_fact.rhs_id,
)
true
}
_ => abort("missing select eqz compare replacement")
}
EqzConditionSelectI32TrueFalseEqz => {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
self.inner_condition_id,
)
true
}
EqzConditionSelectI32FalseTrueCompare => {
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.inner_condition_id,
)
true
}
EqzConditionSelectI32FalseTrueNonzero => {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
self.inner_condition_id,
@ir.hot_build_const_i32(func, @lib.I32(0)),
)
true
}
EqzConditionSelectI32SwapArms =>
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[result_type] => {
let replacement = @ir.hot_build_select(
func,
result_type,
self.if_false_id,
self.if_true_id,
self.inner_condition_id,
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
true
}
_ => false
}
}
}
///|
priv enum OiZeroCompareConditionSelectRewriteKind {
ZeroCompareConditionSelectNoRewriteBoundary
ZeroCompareConditionSelectI32Eqz
ZeroCompareConditionSelectI32NeZero
ZeroCompareConditionSelectI64Eqz
ZeroCompareConditionSelectI64NeZero
ZeroCompareConditionSelectSwapArms
ZeroCompareConditionSelectKeepArms
}
///|
priv enum OiZeroCompareConditionCompareKind {
ZeroCompareConditionI32EqZero
ZeroCompareConditionI32NeZero
ZeroCompareConditionI64EqZero
ZeroCompareConditionI64NeZero
}
///|
priv struct OiZeroCompareConditionCompareFact {
inner_condition_id : Int32
kind : OiZeroCompareConditionCompareKind
}
///|
priv struct OiZeroCompareConditionSelectFact {
compare_fact : OiZeroCompareConditionCompareFact
if_true_id : Int32
if_false_id : Int32
kind : OiZeroCompareConditionSelectRewriteKind
}
///|
fn OiZeroCompareConditionCompareFact::from_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
condition_id : Int32,
) -> OiZeroCompareConditionCompareFact? {
// Binaryen v130 `optimizeBoolean` recognizes direct i32/i64 equality or
// inequality comparisons against literal zero as the select-condition shell.
// Keep the matched width/opcode and surviving inner operand in a compare fact
// instead of recomputing a local `invert_condition` boolean in each arm case.
match @ir.hot_node_exact_instr(func, condition_id) {
Some(@lib.Instruction::I32Eq) | Some(@lib.Instruction::I32Ne) if @ir.hot_child_count(
func, condition_id,
) ==
2 => {
let lhs_id = @ir.hot_child_get(func, condition_id, 0)
let rhs_id = @ir.hot_child_get(func, condition_id, 1)
let inner_condition = match
optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) if raw == 0 => Some(lhs_id)
_ =>
match optimize_instructions_i32_const(func, dead, lhs_id) {
Some(@lib.I32(raw)) if raw == 0 => Some(rhs_id)
_ => None
}
}
match inner_condition {
Some(inner_condition_id) => {
let kind = match @ir.hot_node_exact_instr(func, condition_id) {
Some(@lib.Instruction::I32Eq) => ZeroCompareConditionI32EqZero
Some(@lib.Instruction::I32Ne) => ZeroCompareConditionI32NeZero
_ => return None
}
Some({ inner_condition_id, kind, })
}
_ => None
}
}
Some(@lib.Instruction::I64Eq) | Some(@lib.Instruction::I64Ne) if @ir.hot_child_count(
func, condition_id,
) ==
2 => {
let lhs_id = @ir.hot_child_get(func, condition_id, 0)
let rhs_id = @ir.hot_child_get(func, condition_id, 1)
let inner_condition = match
optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) if raw == 0L => Some(lhs_id)
_ =>
match optimize_instructions_i64_const(func, dead, lhs_id) {
Some(@lib.I64(raw)) if raw == 0L => Some(rhs_id)
_ => None
}
}
match inner_condition {
Some(inner_condition_id) => {
let kind = match @ir.hot_node_exact_instr(func, condition_id) {
Some(@lib.Instruction::I64Eq) => ZeroCompareConditionI64EqZero
Some(@lib.Instruction::I64Ne) => ZeroCompareConditionI64NeZero
_ => return None
}
Some({ inner_condition_id, kind, })
}
_ => None
}
}
_ => None
}
}
///|
fn OiZeroCompareConditionCompareFact::boolean_true_false_rewrite_kind(
self : OiZeroCompareConditionCompareFact,
) -> OiZeroCompareConditionSelectRewriteKind {
match self.kind {
ZeroCompareConditionI32EqZero => ZeroCompareConditionSelectI32Eqz
ZeroCompareConditionI32NeZero => ZeroCompareConditionSelectI32NeZero
ZeroCompareConditionI64EqZero => ZeroCompareConditionSelectI64Eqz
ZeroCompareConditionI64NeZero => ZeroCompareConditionSelectI64NeZero
}
}
///|
fn OiZeroCompareConditionCompareFact::boolean_false_true_rewrite_kind(
self : OiZeroCompareConditionCompareFact,
) -> OiZeroCompareConditionSelectRewriteKind {
match self.kind {
ZeroCompareConditionI32EqZero => ZeroCompareConditionSelectI32NeZero
ZeroCompareConditionI32NeZero => ZeroCompareConditionSelectI32Eqz
ZeroCompareConditionI64EqZero => ZeroCompareConditionSelectI64NeZero
ZeroCompareConditionI64NeZero => ZeroCompareConditionSelectI64Eqz
}
}
///|
fn OiZeroCompareConditionCompareFact::value_select_rewrite_kind(
self : OiZeroCompareConditionCompareFact,
) -> OiZeroCompareConditionSelectRewriteKind {
match self.kind {
ZeroCompareConditionI32EqZero | ZeroCompareConditionI64EqZero =>
ZeroCompareConditionSelectSwapArms
ZeroCompareConditionI32NeZero | ZeroCompareConditionI64NeZero =>
ZeroCompareConditionSelectKeepArms
}
}
///|
fn OiZeroCompareConditionSelectFact::no_rewrite_boundary(
compare_fact : OiZeroCompareConditionCompareFact,
if_true_id : Int32,
if_false_id : Int32,
) -> OiZeroCompareConditionSelectFact {
{
compare_fact,
if_true_id,
if_false_id,
kind: ZeroCompareConditionSelectNoRewriteBoundary,
}
}
///|
fn OiZeroCompareConditionSelectFact::simple(
compare_fact : OiZeroCompareConditionCompareFact,
if_true_id : Int32,
if_false_id : Int32,
kind : OiZeroCompareConditionSelectRewriteKind,
) -> OiZeroCompareConditionSelectFact {
{ compare_fact, if_true_id, if_false_id, kind, }
}
///|
fn OiZeroCompareConditionSelectFact::from_select_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
condition_id : Int32,
if_true : Int32,
if_false : Int32,
) -> OiZeroCompareConditionSelectFact? {
// Binaryen v130 `optimizeBoolean` strips direct integer equality/inequality
// zero comparisons in select conditions before the generic boolean-arm
// fallback. This fact records the zero-compare shell, live inner operand,
// exact boolean-arm composition, and value-select arm-order payload so the
// visitor does not own the condition opcode/result scanner for this family.
let compare_fact = match
OiZeroCompareConditionCompareFact::from_condition(func, dead, condition_id) {
Some(fact) => fact
None => return None
}
if !OiLiveNodeFact::proves_node_live(
func,
dead,
compare_fact.inner_condition_id,
) {
return Some(
OiZeroCompareConditionSelectFact::no_rewrite_boundary(
compare_fact, if_true, if_false,
),
)
}
match
OiI32BooleanSelectArmFact::from_select_arms(func, dead, if_true, if_false) {
Some(boolean_arm_fact) =>
match boolean_arm_fact.kind {
I32BooleanSelectTrueFalse =>
Some(
OiZeroCompareConditionSelectFact::simple(
compare_fact,
if_true,
if_false,
compare_fact.boolean_true_false_rewrite_kind(),
),
)
I32BooleanSelectFalseTrue =>
Some(
OiZeroCompareConditionSelectFact::simple(
compare_fact,
if_true,
if_false,
compare_fact.boolean_false_true_rewrite_kind(),
),
)
}
None =>
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[_] =>
Some(
OiZeroCompareConditionSelectFact::simple(
compare_fact,
if_true,
if_false,
compare_fact.value_select_rewrite_kind(),
),
)
_ => None
}
}
}
///|
fn OiZeroCompareConditionSelectFact::apply(
self : OiZeroCompareConditionSelectFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let inner_condition_id = self.compare_fact.inner_condition_id
match self.kind {
ZeroCompareConditionSelectNoRewriteBoundary => false
ZeroCompareConditionSelectI32Eqz => {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
inner_condition_id,
)
true
}
ZeroCompareConditionSelectI32NeZero => {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
inner_condition_id,
@ir.hot_build_const_i32(func, @lib.I32(0)),
)
true
}
ZeroCompareConditionSelectI64Eqz => {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_eqz(),
inner_condition_id,
)
true
}
ZeroCompareConditionSelectI64NeZero => {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
inner_condition_id,
@ir.hot_build_const_i64(func, @lib.I64(0L)),
)
true
}
ZeroCompareConditionSelectSwapArms =>
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[result_type] => {
let replacement = @ir.hot_build_select(
func,
result_type,
self.if_false_id,
self.if_true_id,
inner_condition_id,
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
true
}
_ => false
}
ZeroCompareConditionSelectKeepArms =>
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[result_type] => {
let replacement = @ir.hot_build_select(
func,
result_type,
self.if_true_id,
self.if_false_id,
inner_condition_id,
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
true
}
_ => false
}
}
}
///|
priv enum OiIntegerEqualityConditionSelectRewriteKind {
IntegerEqualityConditionSelectNoRewriteBoundary
IntegerEqualityConditionSelectKeepCompare
IntegerEqualityConditionSelectInvertCompare
}
///|
priv struct OiIntegerEqualityConditionSelectInvertedCompareFact {
lhs_id : Int32
rhs_id : Int32
inverted_instr : @lib.Instruction
}
///|
priv struct OiIntegerEqualityConditionSelectFact {
condition_id : Int32
inverted_compare_fact : OiIntegerEqualityConditionSelectInvertedCompareFact?
kind : OiIntegerEqualityConditionSelectRewriteKind
}
///|
fn OiIntegerEqualityConditionSelectFact::no_rewrite_boundary(
condition_id : Int32,
) -> OiIntegerEqualityConditionSelectFact {
{
condition_id,
inverted_compare_fact: None,
kind: IntegerEqualityConditionSelectNoRewriteBoundary,
}
}
///|
fn OiIntegerEqualityConditionSelectFact::keep_compare(
condition_id : Int32,
) -> OiIntegerEqualityConditionSelectFact {
{
condition_id,
inverted_compare_fact: None,
kind: IntegerEqualityConditionSelectKeepCompare,
}
}
///|
fn OiIntegerEqualityConditionSelectFact::invert_compare(
condition_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
inverted_instr : @lib.Instruction,
) -> OiIntegerEqualityConditionSelectFact {
{
condition_id,
inverted_compare_fact: Some({ lhs_id, rhs_id, inverted_instr, }),
kind: IntegerEqualityConditionSelectInvertCompare,
}
}
///|
fn OiIntegerEqualityConditionSelectFact::from_select_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
condition_id : Int32,
if_true : Int32,
if_false : Int32,
) -> OiIntegerEqualityConditionSelectFact? {
// Binaryen v130 `optimizeBoolean` removes exact i32 boolean select arms
// around direct integer equality compare conditions. This fact records the
// direct i32/i64 `eq` / `ne` shell, preserved compare node or exact inverse
// payload, and live-child no-rewrite boundary before the generic boolean-arm
// fallback can wrap the compare in `ne zero` / `eqz`.
match @ir.hot_node_exact_instr(func, condition_id) {
Some(compare_instr) if @ir.hot_child_count(func, condition_id) == 2 => {
let inverted_instr = match compare_instr {
@lib.Instruction::I32Eq => Some(@lib.Instruction::i32_ne())
@lib.Instruction::I32Ne => Some(@lib.Instruction::i32_eq())
@lib.Instruction::I64Eq => Some(@lib.Instruction::i64_ne())
@lib.Instruction::I64Ne => Some(@lib.Instruction::i64_eq())
_ => None
}
match inverted_instr {
Some(inverted_instr) => {
let lhs_id = @ir.hot_child_get(func, condition_id, 0)
let rhs_id = @ir.hot_child_get(func, condition_id, 1)
match
OiI32BooleanSelectArmFact::from_select_arms(
func, dead, if_true, if_false,
) {
Some(boolean_arm_fact) =>
match boolean_arm_fact.kind {
I32BooleanSelectTrueFalse => {
if !OiLiveNodeFact::proves_node_live(func, dead, condition_id) {
return Some(
OiIntegerEqualityConditionSelectFact::no_rewrite_boundary(
condition_id,
),
)
}
Some(
OiIntegerEqualityConditionSelectFact::keep_compare(
condition_id,
),
)
}
I32BooleanSelectFalseTrue => {
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return Some(
OiIntegerEqualityConditionSelectFact::no_rewrite_boundary(
condition_id,
),
)
}
Some(
OiIntegerEqualityConditionSelectFact::invert_compare(
condition_id, lhs_id, rhs_id, inverted_instr,
),
)
}
}
None => None
}
}
_ => None
}
}
_ => None
}
}
///|
fn OiIntegerEqualityConditionSelectFact::apply(
self : OiIntegerEqualityConditionSelectFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
IntegerEqualityConditionSelectNoRewriteBoundary => false
IntegerEqualityConditionSelectKeepCompare => {
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.condition_id,
)
true
}
IntegerEqualityConditionSelectInvertCompare =>
match self.inverted_compare_fact {
Some(compare_fact) => {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
compare_fact.inverted_instr,
compare_fact.lhs_id,
compare_fact.rhs_id,
)
true
}
_ => abort("missing integer equality select inverse")
}
}
}
///|
priv enum OiDirectCompareConditionSelectRewriteKind {
DirectCompareConditionSelectNoRewriteBoundary
DirectCompareConditionSelectKeepCompare
DirectCompareConditionSelectInvertCompare
}
///|
priv struct OiDirectCompareConditionSelectInvertedCompareFact {
lhs_id : Int32
rhs_id : Int32
inverted_instr : @lib.Instruction
}
///|
priv struct OiDirectCompareConditionSelectFact {
condition_id : Int32
inverted_compare_fact : OiDirectCompareConditionSelectInvertedCompareFact?
kind : OiDirectCompareConditionSelectRewriteKind
}
///|
fn OiDirectCompareConditionSelectFact::no_rewrite_boundary(
condition_id : Int32,
) -> OiDirectCompareConditionSelectFact {
{
condition_id,
inverted_compare_fact: None,
kind: DirectCompareConditionSelectNoRewriteBoundary,
}
}
///|
fn OiDirectCompareConditionSelectFact::keep_compare(
condition_id : Int32,
) -> OiDirectCompareConditionSelectFact {
{
condition_id,
inverted_compare_fact: None,
kind: DirectCompareConditionSelectKeepCompare,
}
}
///|
fn OiDirectCompareConditionSelectFact::invert_compare(
condition_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
inverted_instr : @lib.Instruction,
) -> OiDirectCompareConditionSelectFact {
{
condition_id,
inverted_compare_fact: Some({ lhs_id, rhs_id, inverted_instr, }),
kind: DirectCompareConditionSelectInvertCompare,
}
}
///|
fn OiDirectCompareConditionSelectFact::from_select_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
condition_id : Int32,
if_true : Int32,
if_false : Int32,
) -> OiDirectCompareConditionSelectFact? {
// Binaryen v130 `optimizeBoolean` removes exact i32 boolean select arms
// around direct integer-relational and float equality compare conditions.
// Integer equality/inequality compares are intentionally handled by
// `OiIntegerEqualityConditionSelectFact` before this fact, matching the
// source-backed split between direct equality and the eqz-inversion table.
match @ir.hot_node_exact_instr(func, condition_id) {
Some(compare_instr) if @ir.hot_child_count(func, condition_id) == 2 =>
match optimize_instructions_inverted_eqz_compare(compare_instr) {
Some(inverted_instr) => {
let lhs_id = @ir.hot_child_get(func, condition_id, 0)
let rhs_id = @ir.hot_child_get(func, condition_id, 1)
match
OiI32BooleanSelectArmFact::from_select_arms(
func, dead, if_true, if_false,
) {
Some(boolean_arm_fact) =>
match boolean_arm_fact.kind {
I32BooleanSelectTrueFalse => {
if !OiLiveNodeFact::proves_node_live(func, dead, condition_id) {
return Some(
OiDirectCompareConditionSelectFact::no_rewrite_boundary(
condition_id,
),
)
}
Some(
OiDirectCompareConditionSelectFact::keep_compare(
condition_id,
),
)
}
I32BooleanSelectFalseTrue => {
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return Some(
OiDirectCompareConditionSelectFact::no_rewrite_boundary(
condition_id,
),
)
}
Some(
OiDirectCompareConditionSelectFact::invert_compare(
condition_id, lhs_id, rhs_id, inverted_instr,
),
)
}
}
None => None
}
}
_ => None
}
_ => None
}
}
///|
fn OiDirectCompareConditionSelectFact::apply(
self : OiDirectCompareConditionSelectFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
DirectCompareConditionSelectNoRewriteBoundary => false
DirectCompareConditionSelectKeepCompare => {
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.condition_id,
)
true
}
DirectCompareConditionSelectInvertCompare =>
match self.inverted_compare_fact {
Some(compare_fact) => {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
compare_fact.inverted_instr,
compare_fact.lhs_id,
compare_fact.rhs_id,
)
true
}
_ => abort("missing direct compare select inverse")
}
}
}
///|
priv enum OiBooleanConstSelectRewriteKind {
BooleanConstSelectConditionNonzero
BooleanConstSelectConditionEqz
}
///|
priv struct OiBooleanConstSelectFact {
condition_id : Int32
kind : OiBooleanConstSelectRewriteKind
}
///|
fn OiBooleanConstSelectFact::from_select_arms(
func : @ir.HotFunc,
dead : @bitset.BitSet,
condition_id : Int32,
if_true : Int32,
if_false : Int32,
) -> OiBooleanConstSelectFact? {
// Binaryen v130 `optimizeBoolean` rewrites exact i32 boolean select arms:
// `select 1 0 cond` becomes `i32.ne(cond, 0)` and `select 0 1 cond`
// becomes `i32.eqz(cond)`. Keep this final fallback as a fact with the
// preserved condition and literal-arm proof instead of an inline select-arm
// scanner; earlier eqz/compare condition facts keep their more specific
// output shapes before this fallback is queried.
match
OiI32BooleanSelectArmFact::from_select_arms(func, dead, if_true, if_false) {
Some(boolean_arm_fact) =>
match boolean_arm_fact.kind {
I32BooleanSelectTrueFalse =>
Some({ condition_id, kind: BooleanConstSelectConditionNonzero, })
I32BooleanSelectFalseTrue =>
Some({ condition_id, kind: BooleanConstSelectConditionEqz, })
}
None => None
}
}
///|
fn OiBooleanConstSelectFact::apply(
self : OiBooleanConstSelectFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.kind {
BooleanConstSelectConditionNonzero =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
self.condition_id,
@ir.hot_build_const_i32(func, @lib.I32(0)),
)
BooleanConstSelectConditionEqz =>
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
self.condition_id,
)
}
}
///|
priv enum OiV131IdenticalSelectPlanKind {
V131IdenticalSelectScratchFirst
V131IdenticalSelectPreserveBoth
}
///|
priv struct OiV131IdenticalSelectPlanFact {
kind : OiV131IdenticalSelectPlanKind
if_true : Int32
if_false : Int32
condition : Int32
result_type : @lib.ValType
}
///|
fn OiV131IdenticalSelectPlanFact::from_select(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
if_true : Int32,
if_false : Int32,
condition : Int32,
) -> OiV131IdenticalSelectPlanFact? {
// This is the v131 extension after the older cheap identical-arm paths have
// declined. The shared equality fact handles fallthrough values and annotated
// idempotent calls. Foldable arms use a scratch local only when the first arm
// cannot safely cross the condition; nonfoldable equal arms are retained only
// when the false arm can move after the condition without changing effects.
let result_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, if_true)) {
[result_type] => result_type
_ => return None
}
let equality_fact = OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx, func, if_true, if_false,
)
if !equality_fact.proves_equal(func) {
return None
}
let effects = pass_require_effects(ctx, func)
let condition_effects = OiSubtreeReorderEffectFact::from_node(
func,
effects.node_masks,
condition,
)
let foldability_fact = OiConsecutiveInputFoldabilityFact::from_right(
ctx, func, dead, if_false,
)
if foldability_fact.proves_foldable() {
let first_effects = OiSubtreeReorderEffectFact::from_node(
func,
effects.node_masks,
if_true,
)
if first_effects.can_swap_with(condition_effects) {
return None
}
match optimize_instructions_defaultable_scratch_type(result_type) {
Some(_) =>
Some({
kind: V131IdenticalSelectScratchFirst,
if_true,
if_false,
condition,
result_type,
})
None => None
}
} else {
let false_effects = OiSubtreeReorderEffectFact::from_node(
func,
effects.node_masks,
if_false,
)
if !false_effects.can_swap_with(condition_effects) {
return None
}
Some({
kind: V131IdenticalSelectPreserveBoth,
if_true,
if_false,
condition,
result_type,
})
}
}
///|
fn OiV131IdenticalSelectPlanFact::apply(
self : OiV131IdenticalSelectPlanFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match self.kind {
V131IdenticalSelectScratchFirst => {
let local_type = match
optimize_instructions_defaultable_scratch_type(self.result_type) {
Some(local_type) => local_type
None => return false
}
let local_id = @ir.hot_append_body_local(func, local_type)
let local_get = @ir.hot_build_local_get(func, local_id)
let restored = match
optimize_instructions_restore_localized_result(
func,
local_get,
local_type,
self.result_type,
) {
Some(result_id) => result_id
None => return false
}
let replacement = @ir.hot_build_block(
func,
@lib.BlockType::val_type(self.result_type),
[
@ir.hot_build_local_set(func, local_id, self.if_true),
@ir.hot_build_drop(func, self.condition),
restored,
],
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
}
V131IdenticalSelectPreserveBoth => {
let replacement = @ir.hot_build_block(
func,
@lib.BlockType::val_type(self.result_type),
[
@ir.hot_build_drop(func, self.if_true),
@ir.hot_build_drop(func, self.condition),
self.if_false,
],
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
}
}
true
}
///|
fn optimize_instructions_try_fold_const_select(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Bool {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Select if node.child_count == 3 => ()
_ => return false
}
let if_true = @ir.hot_child_get(func, node_id, 0)
let if_false = @ir.hot_child_get(func, node_id, 1)
let condition = @ir.hot_child_get(func, node_id, 2)
match
OiConsecutiveSelfSelectFact::from_select(
ctx, func, dead, node_id, if_true, if_false, condition,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
_ => ()
}
match
OiI64ZeroTestSelfSelectFact::from_select(
ctx, func, dead, node_id, if_true, if_false, condition,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
_ => ()
}
match
OiEqzConditionSelectFact::from_select_condition(
func, dead, node_id, condition, if_true, if_false,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
_ => ()
}
match
OiZeroCompareConditionSelectFact::from_select_condition(
func, dead, node_id, condition, if_true, if_false,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
_ => ()
}
match
OiIntegerEqualityConditionSelectFact::from_select_condition(
func, dead, condition, if_true, if_false,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
_ => ()
}
match
OiDirectCompareConditionSelectFact::from_select_condition(
func, dead, condition, if_true, if_false,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
_ => ()
}
match
OiBooleanConstSelectFact::from_select_arms(
func, dead, condition, if_true, if_false,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
_ => ()
}
match
OiBooleanSelectAlgebraFact::from_select(
func,
dead,
Some(scratch.local_facts),
node_id,
if_true,
if_false,
condition,
) {
Some(fact) => return fact.apply(ctx, func, dead, node_id)
_ => ()
}
let identical_arm_fact = OiIdenticalPureExpressionFact::from_select_arms(
func, dead, if_true, if_false,
)
if identical_arm_fact.proves_equal() {
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, condition) {
optimize_instructions_replace_with_node_id(ctx, func, node_id, if_true)
return true
}
if (
(
OiSharedCrossSafeLeafFact::from_node(func, dead, if_true).proves_safe() &&
OiSharedCrossSafeLeafFact::from_node(func, dead, if_false).proves_safe()
) ||
optimize_instructions_select_identical_immutable_global_arms(
ctx, func, if_true, if_false,
) ||
identical_arm_fact.proves_equal_after_condition_preserved()
) &&
@ir.hot_node_result_arity(func, if_true) == 1 &&
@ir.hot_node_result_arity(func, condition) == 1 {
match @ir.hot_type_results(func, @ir.hot_node_type(func, if_true)) {
[result_type] => {
let replacement = @ir.hot_build_block(
func,
@lib.BlockType::val_type(result_type),
[@ir.hot_build_drop(func, condition), if_true],
)
optimize_instructions_replace_with_node_id(
ctx, func, node_id, replacement,
)
return true
}
_ => ()
}
}
}
match
OiV131IdenticalSelectPlanFact::from_select(
ctx, func, dead, if_true, if_false, condition,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
None => ()
}
let (chosen, dead_arm) = match
optimize_instructions_i32_const(func, dead, condition) {
Some(@lib.I32(raw)) if raw == 0 => (if_false, if_true)
Some(@lib.I32(_)) => (if_true, if_false)
_ => return false
}
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, dead_arm) {
return false
}
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, chosen) {
let use_def = pass_require_use_def(ctx, func)
if @ir.node_use_count(use_def, chosen) != 1 {
return false
}
}
optimize_instructions_replace_with_node_id(ctx, func, node_id, chosen)
true
}
///|
priv struct OiDeMorganAndEqzFact {
lhs_operand_id : Int32
rhs_operand_id : Int32
}
///|
fn OiDeMorganAndEqzFact::from_binary(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiDeMorganAndEqzFact? {
// Binaryen v130 `optimizeBoolean` rewrites the direct
// `i32.and(i32.eqz(x), i32.eqz(y))` shell to `i32.eqz(i32.or(x, y))` while
// keeping the original operand order. Keep the exact source-backed shell and
// preserved operands as a fact rather than an inline binary-visitor opcode
// scanner; broader boolean algebra remains outside this fact.
match instr {
@lib.Instruction::I32And => ()
_ => return None
}
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return None
}
match
(
@ir.hot_node_exact_instr(func, lhs_id),
@ir.hot_node_exact_instr(func, rhs_id),
) {
(Some(@lib.Instruction::I32Eqz), Some(@lib.Instruction::I32Eqz)) => ()
_ => return None
}
if @ir.hot_child_count(func, lhs_id) != 1 ||
@ir.hot_child_count(func, rhs_id) != 1 {
return None
}
let lhs_operand_id = @ir.hot_child_get(func, lhs_id, 0)
let rhs_operand_id = @ir.hot_child_get(func, rhs_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_operand_id) ||
!OiLiveNodeFact::proves_node_live(func, dead, rhs_operand_id) {
return None
}
Some({ lhs_operand_id, rhs_operand_id, })
}
///|
fn OiDeMorganAndEqzFact::apply(
self : OiDeMorganAndEqzFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
let or_id = @ir.hot_build_binary(
func,
@lib.ValType::i32(),
self.lhs_operand_id,
self.rhs_operand_id,
)
@ir.hot_node_exact_instr_set(func, or_id, @lib.Instruction::i32_or())
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
or_id,
)
}
///|
fn optimize_instructions_try_apply_demorgan_and_eqz(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match OiDeMorganAndEqzFact::from_binary(func, dead, instr, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
fn optimize_instructions_inverted_eqz_compare(
instr : @lib.Instruction,
) -> @lib.Instruction? {
match instr {
@lib.Instruction::F32Eq => Some(@lib.Instruction::f32_ne())
@lib.Instruction::F32Ne => Some(@lib.Instruction::f32_eq())
@lib.Instruction::F64Eq => Some(@lib.Instruction::f64_ne())
@lib.Instruction::F64Ne => Some(@lib.Instruction::f64_eq())
@lib.Instruction::I32LtS => Some(@lib.Instruction::i32_ge_s())
@lib.Instruction::I32LtU => Some(@lib.Instruction::i32_ge_u())
@lib.Instruction::I32LeS => Some(@lib.Instruction::i32_gt_s())
@lib.Instruction::I32LeU => Some(@lib.Instruction::i32_gt_u())
@lib.Instruction::I32GtS => Some(@lib.Instruction::i32_le_s())
@lib.Instruction::I32GtU => Some(@lib.Instruction::i32_le_u())
@lib.Instruction::I32GeS => Some(@lib.Instruction::i32_lt_s())
@lib.Instruction::I32GeU => Some(@lib.Instruction::i32_lt_u())
@lib.Instruction::I64LtS => Some(@lib.Instruction::i64_ge_s())
@lib.Instruction::I64LtU => Some(@lib.Instruction::i64_ge_u())
@lib.Instruction::I64LeS => Some(@lib.Instruction::i64_gt_s())
@lib.Instruction::I64LeU => Some(@lib.Instruction::i64_gt_u())
@lib.Instruction::I64GtS => Some(@lib.Instruction::i64_le_s())
@lib.Instruction::I64GtU => Some(@lib.Instruction::i64_le_u())
@lib.Instruction::I64GeS => Some(@lib.Instruction::i64_lt_s())
@lib.Instruction::I64GeU => Some(@lib.Instruction::i64_lt_u())
_ => None
}
}
///|
priv struct OiNestedEqzRewriteFact {
inner_instr : @lib.Instruction
value_id : Int32
folded_result : @lib.I32?
}
///|
fn OiNestedEqzRewriteFact::from_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiNestedEqzRewriteFact? {
// Binaryen v130 rewrites `eqz(eqz(x))` in `visitUnary`; keep the inner
// boolean producer kind, preserved value, and literal-fold result together so
// the eqz visitor does not own a nested-unary opcode branch.
match instr {
@lib.Instruction::I32Eqz => ()
_ => return None
}
match
(func.nodes[operand_id].op, @ir.hot_node_exact_instr(func, operand_id)) {
(@ir.HotOp::Unary, Some(@lib.Instruction::I32Eqz)) => {
let value_id = @ir.hot_child_get(func, operand_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, value_id) {
return None
}
let folded_result = match
optimize_instructions_i32_const(func, dead, value_id) {
Some(@lib.I32(raw)) =>
Some(if raw == 0 { @lib.I32(0) } else { @lib.I32(1) })
None => None
}
Some({
inner_instr: @lib.Instruction::i32_eqz(),
value_id,
folded_result,
})
}
(@ir.HotOp::Unary, Some(@lib.Instruction::I64Eqz)) => {
let value_id = @ir.hot_child_get(func, operand_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, value_id) {
return None
}
let folded_result = match
optimize_instructions_i64_const(func, dead, value_id) {
Some(@lib.I64(raw)) =>
Some(if raw == 0L { @lib.I32(0) } else { @lib.I32(1) })
None => None
}
Some({
inner_instr: @lib.Instruction::i64_eqz(),
value_id,
folded_result,
})
}
_ => None
}
}
///|
fn OiNestedEqzRewriteFact::apply(
self : OiNestedEqzRewriteFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.folded_result {
Some(folded) =>
optimize_instructions_replace_with_const_i32(ctx, func, node_id, folded)
None =>
match self.inner_instr {
@lib.Instruction::I32Eqz =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
self.value_id,
@ir.hot_build_const_i32(func, @lib.I32(0)),
)
@lib.Instruction::I64Eqz =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
self.value_id,
@ir.hot_build_const_i64(func, @lib.I64(0L)),
)
_ => ()
}
}
}
///|
priv enum OiEqzZeroCompareRewriteKind {
EqzZeroCompareToI32Eqz
EqzZeroCompareToI64Eqz
EqzZeroCompareToI32NeZero
EqzZeroCompareToI64NeZero
}
///|
priv struct OiEqzZeroCompareFact {
kind : OiEqzZeroCompareRewriteKind
value_id : Int32
}
///|
fn OiEqzZeroCompareFact::from_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiEqzZeroCompareFact? {
// Binaryen v130 prefers the compact zero-compare spellings before broader
// compare inversion: `eqz(ne(x, 0))` becomes `eqz(x)`, while `eqz(eq(x, 0))`
// becomes `ne(x, 0)`. Keep the zero-side proof and replacement family as one
// fact instead of four visitor-local compare branches.
match instr {
@lib.Instruction::I32Eqz => ()
_ => return None
}
match
(func.nodes[operand_id].op, @ir.hot_node_exact_instr(func, operand_id)) {
(@ir.HotOp::Compare, Some(@lib.Instruction::I32Ne)) => {
let lhs_id = @ir.hot_child_get(func, operand_id, 0)
let rhs_id = @ir.hot_child_get(func, operand_id, 1)
match
(
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(Some(@lib.I32(raw)), _) if raw == 0 =>
Some({ kind: EqzZeroCompareToI32Eqz, value_id: rhs_id, })
(_, Some(@lib.I32(raw))) if raw == 0 =>
Some({ kind: EqzZeroCompareToI32Eqz, value_id: lhs_id, })
_ => None
}
}
(@ir.HotOp::Compare, Some(@lib.Instruction::I64Ne)) => {
let lhs_id = @ir.hot_child_get(func, operand_id, 0)
let rhs_id = @ir.hot_child_get(func, operand_id, 1)
match
(
optimize_instructions_i64_const(func, dead, lhs_id),
optimize_instructions_i64_const(func, dead, rhs_id),
) {
(Some(@lib.I64(raw)), _) if raw == 0L =>
Some({ kind: EqzZeroCompareToI64Eqz, value_id: rhs_id, })
(_, Some(@lib.I64(raw))) if raw == 0L =>
Some({ kind: EqzZeroCompareToI64Eqz, value_id: lhs_id, })
_ => None
}
}
(@ir.HotOp::Compare, Some(@lib.Instruction::I32Eq)) => {
let lhs_id = @ir.hot_child_get(func, operand_id, 0)
let rhs_id = @ir.hot_child_get(func, operand_id, 1)
match
(
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(Some(@lib.I32(raw)), _) if raw == 0 =>
Some({ kind: EqzZeroCompareToI32NeZero, value_id: rhs_id, })
(_, Some(@lib.I32(raw))) if raw == 0 =>
Some({ kind: EqzZeroCompareToI32NeZero, value_id: lhs_id, })
_ => None
}
}
(@ir.HotOp::Compare, Some(@lib.Instruction::I64Eq)) => {
let lhs_id = @ir.hot_child_get(func, operand_id, 0)
let rhs_id = @ir.hot_child_get(func, operand_id, 1)
match
(
optimize_instructions_i64_const(func, dead, lhs_id),
optimize_instructions_i64_const(func, dead, rhs_id),
) {
(Some(@lib.I64(raw)), _) if raw == 0L =>
Some({ kind: EqzZeroCompareToI64NeZero, value_id: rhs_id, })
(_, Some(@lib.I64(raw))) if raw == 0L =>
Some({ kind: EqzZeroCompareToI64NeZero, value_id: lhs_id, })
_ => None
}
}
_ => None
}
}
///|
fn OiEqzZeroCompareFact::apply(
self : OiEqzZeroCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.kind {
EqzZeroCompareToI32Eqz =>
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
self.value_id,
)
EqzZeroCompareToI64Eqz =>
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_eqz(),
self.value_id,
)
EqzZeroCompareToI32NeZero =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
self.value_id,
@ir.hot_build_const_i32(func, @lib.I32(0)),
)
EqzZeroCompareToI64NeZero =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
self.value_id,
@ir.hot_build_const_i64(func, @lib.I64(0L)),
)
}
}
///|
priv struct OiEqzCompareInversionFact {
inverted_instr : @lib.Instruction
lhs_id : Int32
rhs_id : Int32
}
///|
fn OiEqzCompareInversionFact::from_operand(
func : @ir.HotFunc,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiEqzCompareInversionFact? {
// Binaryen v130 inverts integer relational compares and float eq/ne compares
// under `i32.eqz`; NaN-sensitive float relational compares are intentionally
// absent from `optimize_instructions_inverted_eqz_compare`.
match instr {
@lib.Instruction::I32Eqz => ()
_ => return None
}
match
(func.nodes[operand_id].op, @ir.hot_node_exact_instr(func, operand_id)) {
(@ir.HotOp::Compare, Some(compare_instr)) =>
match optimize_instructions_inverted_eqz_compare(compare_instr) {
Some(inverted_instr) =>
Some({
inverted_instr,
lhs_id: @ir.hot_child_get(func, operand_id, 0),
rhs_id: @ir.hot_child_get(func, operand_id, 1),
})
None => None
}
_ => None
}
}
///|
fn OiEqzCompareInversionFact::apply(
self : OiEqzCompareInversionFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
self.inverted_instr,
self.lhs_id,
self.rhs_id,
)
}
///|
priv struct OiEqzSubCompareFact {
compare_instr : @lib.Instruction
lhs_id : Int32
rhs_id : Int32
}
///|
fn OiEqzSubCompareFact::from_operand(
func : @ir.HotFunc,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiEqzSubCompareFact? {
// Binaryen v130 rewrites `eqz(x - y)` to equality on the original operands.
match
(
instr,
func.nodes[operand_id].op,
@ir.hot_node_exact_instr(func, operand_id),
) {
(@lib.Instruction::I32Eqz, @ir.HotOp::Binary, Some(@lib.Instruction::I32Sub)
) =>
Some({
compare_instr: @lib.Instruction::i32_eq(),
lhs_id: @ir.hot_child_get(func, operand_id, 0),
rhs_id: @ir.hot_child_get(func, operand_id, 1),
})
(@lib.Instruction::I64Eqz, @ir.HotOp::Binary, Some(@lib.Instruction::I64Sub)
) =>
Some({
compare_instr: @lib.Instruction::i64_eq(),
lhs_id: @ir.hot_child_get(func, operand_id, 0),
rhs_id: @ir.hot_child_get(func, operand_id, 1),
})
_ => None
}
}
///|
fn OiEqzSubCompareFact::apply(
self : OiEqzSubCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
self.compare_instr,
self.lhs_id,
self.rhs_id,
)
}
///|
priv struct OiI32EqzAddConstCompareFact {
value_id : Int32
negated_const : @lib.I32
}
///|
fn OiI32EqzAddConstCompareFact::from_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiI32EqzAddConstCompareFact? {
// Binaryen v130 rewrites `eqz(x + c)` to `eq(x, -c)` for represented i32
// addition constants.
match
(
instr,
func.nodes[operand_id].op,
@ir.hot_node_exact_instr(func, operand_id),
) {
(@lib.Instruction::I32Eqz, @ir.HotOp::Binary, Some(@lib.Instruction::I32Add)
) => {
let lhs_id = @ir.hot_child_get(func, operand_id, 0)
let rhs_id = @ir.hot_child_get(func, operand_id, 1)
match
(
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(Some(@lib.I32(raw)), _) =>
Some({ value_id: rhs_id, negated_const: @lib.I32(-raw), })
(_, Some(@lib.I32(raw))) =>
Some({ value_id: lhs_id, negated_const: @lib.I32(-raw), })
_ => None
}
}
_ => None
}
}
///|
fn OiI32EqzAddConstCompareFact::apply(
self : OiI32EqzAddConstCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_eq(),
self.value_id,
@ir.hot_build_const_i32(func, self.negated_const),
)
}
///|
priv struct OiI64EqzAddConstCompareFact {
value_id : Int32
negated_const : @lib.I64
}
///|
fn OiI64EqzAddConstCompareFact::from_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
operand_id : Int32,
) -> OiI64EqzAddConstCompareFact? {
// Binaryen v130 rewrites `eqz(x + c)` to `eq(x, -c)` for represented i64
// addition constants.
match
(
instr,
func.nodes[operand_id].op,
@ir.hot_node_exact_instr(func, operand_id),
) {
(@lib.Instruction::I64Eqz, @ir.HotOp::Binary, Some(@lib.Instruction::I64Add)
) => {
let lhs_id = @ir.hot_child_get(func, operand_id, 0)
let rhs_id = @ir.hot_child_get(func, operand_id, 1)
match
(
optimize_instructions_i64_const(func, dead, lhs_id),
optimize_instructions_i64_const(func, dead, rhs_id),
) {
(Some(@lib.I64(raw)), _) =>
Some({ value_id: rhs_id, negated_const: @lib.I64(-raw), })
(_, Some(@lib.I64(raw))) =>
Some({ value_id: lhs_id, negated_const: @lib.I64(-raw), })
_ => None
}
}
_ => None
}
}
///|
fn OiI64EqzAddConstCompareFact::apply(
self : OiI64EqzAddConstCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i64_eq(),
self.value_id,
@ir.hot_build_const_i64(func, self.negated_const),
)
}
///|
fn optimize_instructions_try_fold_eqz(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
operand_id : Int32,
) -> Bool {
if !OiLiveNodeFact::proves_node_live(func, dead, operand_id) {
return false
}
match
OiEqzWrapI64MaxBitsFact::from_eqz_operand(
func, dead, scratch, instr, operand_id,
) {
Some(fact) => {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_eqz(),
fact.value_id,
)
return true
}
None => ()
}
match OiNestedEqzRewriteFact::from_operand(func, dead, instr, operand_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match OiEqzZeroCompareFact::from_operand(func, dead, instr, operand_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match OiEqzCompareInversionFact::from_operand(func, instr, operand_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match OiEqzSubCompareFact::from_operand(func, instr, operand_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match
OiI32EqzAddConstCompareFact::from_operand(func, dead, instr, operand_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match
OiI64EqzAddConstCompareFact::from_operand(func, dead, instr, operand_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
false
}
///|
fn optimize_instructions_try_rewrite_boolean_maxbits_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiBooleanMaxBitsBinaryRewriteFact::from_i32_binary(
ctx,
func,
dead,
Some(scratch.local_facts),
instr,
lhs_id,
rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match
OiBooleanMaxBitsBinaryRewriteFact::from_i32_binary(
ctx,
func,
dead,
Some(scratch.local_facts),
instr,
rhs_id,
lhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match
OiBooleanMaxBitsBinaryRewriteFact::from_i64_binary(
ctx,
func,
dead,
Some(scratch.local_facts),
instr,
lhs_id,
rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match
OiBooleanMaxBitsBinaryRewriteFact::from_i64_binary(
ctx,
func,
dead,
Some(scratch.local_facts),
instr,
rhs_id,
lhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
priv struct OiI32ExactConstBinaryFact {
result : @lib.I32
}
///|
priv struct OiI64ExactConstBinaryFact {
result : @lib.I64
}
///|
fn OiI32ExactConstBinaryFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32ExactConstBinaryFact? {
// Binaryen v130 `visitBinary` asks the literal-evaluable integer constant
// subset before identity/absorbing rewrites. Keep the represented i32 add/sub
// pair as a source-shaped fact so the visitor no longer owns a const-opcode
// whitelist or repeats the wrapping arithmetic spellings inline.
match
(
instr,
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(@lib.Instruction::I32Add, Some(@lib.I32(lhs_raw)), Some(@lib.I32(rhs_raw))) =>
Some({
result: @lib.I32(
(lhs_raw.reinterpret_as_uint() + rhs_raw.reinterpret_as_uint()).reinterpret_as_int(),
),
})
(@lib.Instruction::I32Sub, Some(@lib.I32(lhs_raw)), Some(@lib.I32(rhs_raw))) =>
Some({
result: @lib.I32(
(lhs_raw.reinterpret_as_uint() - rhs_raw.reinterpret_as_uint()).reinterpret_as_int(),
),
})
_ => None
}
}
///|
fn OiI64ExactConstBinaryFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64ExactConstBinaryFact? {
// Same Binaryen literal-evaluable constant add/sub fact for the i64 lane.
match
(
instr,
optimize_instructions_i64_const(func, dead, lhs_id),
optimize_instructions_i64_const(func, dead, rhs_id),
) {
(@lib.Instruction::I64Add, Some(@lib.I64(lhs_raw)), Some(@lib.I64(rhs_raw))) =>
Some({
result: @lib.I64(
(lhs_raw.reinterpret_as_uint64() + rhs_raw.reinterpret_as_uint64()).reinterpret_as_int64(),
),
})
(@lib.Instruction::I64Sub, Some(@lib.I64(lhs_raw)), Some(@lib.I64(rhs_raw))) =>
Some({
result: @lib.I64(
(lhs_raw.reinterpret_as_uint64() - rhs_raw.reinterpret_as_uint64()).reinterpret_as_int64(),
),
})
_ => None
}
}
///|
fn OiI32ExactConstBinaryFact::apply(
self : OiI32ExactConstBinaryFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i32(ctx, func, node_id, self.result)
}
///|
fn OiI64ExactConstBinaryFact::apply(
self : OiI64ExactConstBinaryFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i64(ctx, func, node_id, self.result)
}
///|
fn optimize_instructions_try_fold_exact_const_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiI32ExactConstBinaryFact::from_operands(func, dead, instr, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match
OiI64ExactConstBinaryFact::from_operands(func, dead, instr, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
priv struct OiI32BinaryIdentityOperandFact {
retained_id : Int32
}
///|
priv struct OiI64BinaryIdentityOperandFact {
retained_id : Int32
}
///|
fn OiI32BinaryIdentityOperandFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
identity : Int32,
) -> OiI32BinaryIdentityOperandFact? {
// Binaryen v130 `visitBinary` first asks whether a constant identity operand
// makes the binary operator a no-op. Keep the symmetric i32 proof as a fact
// so the visitor consumes the retained child instead of owning a constant-side
// whitelist. The lhs case intentionally wins when both sides are identities,
// preserving the previous replacement shape.
match
(
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(Some(@lib.I32(raw)), _) if raw == identity =>
Some({ retained_id: rhs_id, })
(_, Some(@lib.I32(raw))) if raw == identity =>
Some({ retained_id: lhs_id, })
_ => None
}
}
///|
fn OiI64BinaryIdentityOperandFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
identity : Int64,
) -> OiI64BinaryIdentityOperandFact? {
// Same Binaryen v130 constant-identity operand proof for the i64 lane.
match
(
optimize_instructions_i64_const(func, dead, lhs_id),
optimize_instructions_i64_const(func, dead, rhs_id),
) {
(Some(@lib.I64(raw)), _) if raw == identity =>
Some({ retained_id: rhs_id, })
(_, Some(@lib.I64(raw))) if raw == identity =>
Some({ retained_id: lhs_id, })
_ => None
}
}
///|
fn OiI32BinaryIdentityOperandFact::from_right_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
identity : Int32,
) -> OiI32BinaryIdentityOperandFact? {
// Noncommutative identity rewrites are one-sided: `x - 0`, `x / 1`, and
// rotates by zero retain only the left operand. Keep that asymmetry explicit.
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) if raw == identity => Some({ retained_id: lhs_id, })
_ => None
}
}
///|
fn OiI64BinaryIdentityOperandFact::from_right_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
identity : Int64,
) -> OiI64BinaryIdentityOperandFact? {
// Same one-sided noncommutative identity proof for the i64 lane.
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) if raw == identity => Some({ retained_id: lhs_id, })
_ => None
}
}
///|
priv struct OiI32RemainderOneFact {
replacement : @lib.I32
}
///|
priv struct OiI64RemainderOneFact {
replacement : @lib.I64
}
///|
fn OiI32RemainderOneFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32RemainderOneFact? {
// Binaryen folds `rem(_, 1)` to zero only when the dividend can be erased
// without losing effects. Keep that asymmetric trap/effect boundary as a fact
// next to the ordinary identity facts instead of a visitor-local predicate.
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(1)) if OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, lhs_id,
) => Some({ replacement: @lib.I32(0), })
_ => None
}
}
///|
fn OiI64RemainderOneFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64RemainderOneFact? {
// Same asymmetric Binaryen `rem(_, 1) -> 0` proof for i64.
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(1L)) if OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, lhs_id,
) => Some({ replacement: @lib.I64(0L), })
_ => None
}
}
///|
fn OiI32RemainderOneFact::apply(
self : OiI32RemainderOneFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.replacement,
)
}
///|
fn OiI64RemainderOneFact::apply(
self : OiI64RemainderOneFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i64(
ctx,
func,
node_id,
self.replacement,
)
}
///|
fn optimize_instructions_try_fold_identity_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
// A constant identity operand makes these binary ops a no-op. The symmetric
// cases allow the constant on either side; `sub` is asymmetric because
// `sub(0, x)` is `-x`, not `x`. `rem(_, 1) -> 0` is asymmetric and requires a
// pure lhs operand, because folding to the constant would otherwise drop the
// lhs effect (Binaryen keeps `rem(effect, 1)`).
match instr {
@lib.Instruction::I32RemS | @lib.Instruction::I32RemU =>
match OiI32RemainderOneFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
@lib.Instruction::I64RemS | @lib.Instruction::I64RemU =>
match OiI64RemainderOneFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
_ => ()
}
let keep_id = match instr {
@lib.Instruction::I32Or
| @lib.Instruction::I32Xor
| @lib.Instruction::I32Add =>
match
OiI32BinaryIdentityOperandFact::from_operands(
func, dead, lhs_id, rhs_id, 0,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I32Shl
| @lib.Instruction::I32ShrU
| @lib.Instruction::I32ShrS =>
match
OiI32BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 0,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I32And =>
match
OiI32BinaryIdentityOperandFact::from_operands(
func, dead, lhs_id, rhs_id, -1,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I32Mul =>
match
OiI32BinaryIdentityOperandFact::from_operands(
func, dead, lhs_id, rhs_id, 1,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I32Sub =>
match
OiI32BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 0,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I32DivS | @lib.Instruction::I32DivU =>
match
OiI32BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 1,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I32Rotl | @lib.Instruction::I32Rotr =>
match
OiI32BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 0,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I64Or
| @lib.Instruction::I64Xor
| @lib.Instruction::I64Add =>
match
OiI64BinaryIdentityOperandFact::from_operands(
func, dead, lhs_id, rhs_id, 0L,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I64Shl
| @lib.Instruction::I64ShrU
| @lib.Instruction::I64ShrS =>
match
OiI64BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 0L,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I64And =>
match
OiI64BinaryIdentityOperandFact::from_operands(
func, dead, lhs_id, rhs_id, -1L,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I64Mul =>
match
OiI64BinaryIdentityOperandFact::from_operands(
func, dead, lhs_id, rhs_id, 1L,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I64Sub =>
match
OiI64BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 0L,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I64DivS | @lib.Instruction::I64DivU =>
match
OiI64BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 1L,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
@lib.Instruction::I64Rotl | @lib.Instruction::I64Rotr =>
match
OiI64BinaryIdentityOperandFact::from_right_operand(
func, dead, lhs_id, rhs_id, 0L,
) {
Some(fact) => Some(fact.retained_id)
None => None
}
_ => None
}
match keep_id {
Some(id) => {
optimize_instructions_replace_with_node_id(ctx, func, node_id, id)
true
}
None => false
}
}
///|
priv struct OiI32AbsorbingZeroFact {
replacement : @lib.I32
}
///|
priv struct OiI64AbsorbingZeroFact {
replacement : @lib.I64
}
///|
fn OiI32AbsorbingZeroFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32AbsorbingZeroFact? {
// Binaryen v130 folds zero-absorbing integer ops to zero only when the other
// operand can be erased. Keep the constant-side and effect boundary explicit
// so the visitor no longer owns this opcode-local whitelist.
let zero_on_lhs = match optimize_instructions_i32_const(func, dead, lhs_id) {
Some(@lib.I32(0)) => true
_ => false
}
let zero_on_rhs = match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(0)) => true
_ => false
}
let pure_other = if zero_on_lhs && !zero_on_rhs {
OiSideEffectFreeExpressionFact::proves_node_free(func, dead, rhs_id)
} else if zero_on_rhs && !zero_on_lhs {
OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id)
} else {
zero_on_lhs && zero_on_rhs
}
if pure_other {
Some({ replacement: @lib.I32(0), })
} else {
None
}
}
///|
fn OiI64AbsorbingZeroFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64AbsorbingZeroFact? {
// Same Binaryen zero-absorbing fact for the i64 lane.
let zero_on_lhs = match optimize_instructions_i64_const(func, dead, lhs_id) {
Some(@lib.I64(0L)) => true
_ => false
}
let zero_on_rhs = match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(0L)) => true
_ => false
}
let pure_other = if zero_on_lhs && !zero_on_rhs {
OiSideEffectFreeExpressionFact::proves_node_free(func, dead, rhs_id)
} else if zero_on_rhs && !zero_on_lhs {
OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id)
} else {
zero_on_lhs && zero_on_rhs
}
if pure_other {
Some({ replacement: @lib.I64(0L), })
} else {
None
}
}
///|
fn OiI32AbsorbingZeroFact::apply(
self : OiI32AbsorbingZeroFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.replacement,
)
}
///|
fn OiI64AbsorbingZeroFact::apply(
self : OiI64AbsorbingZeroFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i64(
ctx,
func,
node_id,
self.replacement,
)
}
///|
priv struct OiI32AbsorbingAllOnesOrFact {
replacement : @lib.I32
drop_id : Int32?
}
///|
priv struct OiI64AbsorbingAllOnesOrFact {
replacement : @lib.I64
drop_id : Int32?
}
///|
fn OiI32AbsorbingAllOnesOrFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32AbsorbingAllOnesOrFact? {
// Binaryen v130 treats `or -1` as an absorbing all-ones result while keeping
// effects from the non-constant operand. Preserve that effectful-drop payload
// in the fact rather than burying it in the visitor branch.
let neg_one_on_lhs = match
optimize_instructions_i32_const(func, dead, lhs_id) {
Some(@lib.I32(-1)) => true
_ => false
}
let neg_one_on_rhs = match
optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(-1)) => true
_ => false
}
if !neg_one_on_lhs && !neg_one_on_rhs {
return None
}
let drop_id = if neg_one_on_lhs && !neg_one_on_rhs {
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, rhs_id) {
None
} else {
Some(rhs_id)
}
} else if neg_one_on_rhs && !neg_one_on_lhs {
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id) {
None
} else {
Some(lhs_id)
}
} else {
None
}
Some({ replacement: @lib.I32(-1), drop_id, })
}
///|
fn OiI64AbsorbingAllOnesOrFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64AbsorbingAllOnesOrFact? {
// Same all-ones absorbing OR fact for the i64 lane.
let neg_one_on_lhs = match
optimize_instructions_i64_const(func, dead, lhs_id) {
Some(@lib.I64(-1L)) => true
_ => false
}
let neg_one_on_rhs = match
optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(-1L)) => true
_ => false
}
if !neg_one_on_lhs && !neg_one_on_rhs {
return None
}
let drop_id = if neg_one_on_lhs && !neg_one_on_rhs {
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, rhs_id) {
None
} else {
Some(rhs_id)
}
} else if neg_one_on_rhs && !neg_one_on_lhs {
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id) {
None
} else {
Some(lhs_id)
}
} else {
None
}
Some({ replacement: @lib.I64(-1L), drop_id, })
}
///|
fn OiI32AbsorbingAllOnesOrFact::apply(
self : OiI32AbsorbingAllOnesOrFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.drop_id {
Some(drop_id) =>
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
drop_id,
self.replacement,
)
None =>
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.replacement,
)
}
}
///|
fn OiI64AbsorbingAllOnesOrFact::apply(
self : OiI64AbsorbingAllOnesOrFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.drop_id {
Some(drop_id) =>
optimize_instructions_replace_with_drop_then_const_i64(
ctx,
func,
node_id,
drop_id,
self.replacement,
)
None =>
optimize_instructions_replace_with_const_i64(
ctx,
func,
node_id,
self.replacement,
)
}
}
///|
priv struct OiI32RemainderNegOneFact {
replacement : @lib.I32
}
///|
priv struct OiI64RemainderNegOneFact {
replacement : @lib.I64
}
///|
fn OiI32RemainderNegOneFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32RemainderNegOneFact? {
// Binaryen also folds signed `rem(_, -1)` to zero only when the dividend can
// be erased. This remains intentionally separate from `rem(_, 1)` because it
// belongs to the absorbing family and applies only to signed remainder.
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(-1)) if OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, lhs_id,
) => Some({ replacement: @lib.I32(0), })
_ => None
}
}
///|
fn OiI64RemainderNegOneFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64RemainderNegOneFact? {
// Same signed-remainder absorbing fact for i64.
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(-1L)) if OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, lhs_id,
) => Some({ replacement: @lib.I64(0L), })
_ => None
}
}
///|
fn OiI32RemainderNegOneFact::apply(
self : OiI32RemainderNegOneFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.replacement,
)
}
///|
fn OiI64RemainderNegOneFact::apply(
self : OiI64RemainderNegOneFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i64(
ctx,
func,
node_id,
self.replacement,
)
}
///|
fn optimize_instructions_try_fold_absorbing_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
// Binaryen v130's absorbing binary folds are now expressed as width-specific
// facts that carry both constant-side proof and any effect-preserving payload.
match instr {
@lib.Instruction::I32Mul | @lib.Instruction::I32And =>
match OiI32AbsorbingZeroFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
@lib.Instruction::I64Mul | @lib.Instruction::I64And =>
match OiI64AbsorbingZeroFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
@lib.Instruction::I32RemS =>
match
OiI32RemainderNegOneFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
@lib.Instruction::I64RemS =>
match
OiI64RemainderNegOneFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
@lib.Instruction::I32Or =>
match
OiI32AbsorbingAllOnesOrFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
@lib.Instruction::I64Or =>
match
OiI64AbsorbingAllOnesOrFact::from_operands(func, dead, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
_ => false
}
}
///|
fn optimize_instructions_try_rewrite_float_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
// Binaryen v131 canonicalizes a negated right floating operand before the
// constant-specific rules below: `x + (-y)` becomes `x - y`, and
// `x - (-y)` becomes `x + y`. Unary float negation is exact bit-sign
// inversion, so removing it while flipping the binary opcode preserves NaN
// payload freedom, signed zero, evaluation order, and all operand effects.
if OiLiveNodeFact::proves_node_live(func, dead, rhs_id) &&
@ir.hot_child_count(func, rhs_id) == 1 {
let replacement_instr = match
(instr, @ir.hot_node_exact_instr(func, rhs_id)) {
(@lib.Instruction::F32Add, Some(@lib.Instruction::F32Neg)) =>
Some(@lib.Instruction::f32_sub())
(@lib.Instruction::F32Sub, Some(@lib.Instruction::F32Neg)) =>
Some(@lib.Instruction::f32_add())
(@lib.Instruction::F64Add, Some(@lib.Instruction::F64Neg)) =>
Some(@lib.Instruction::f64_sub())
(@lib.Instruction::F64Sub, Some(@lib.Instruction::F64Neg)) =>
Some(@lib.Instruction::f64_add())
_ => None
}
match replacement_instr {
Some(replacement_instr) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
replacement_instr,
[lhs_id, @ir.hot_child_get(func, rhs_id, 0)],
)
return true
}
None => ()
}
}
match (instr, optimize_instructions_f32_const(func, dead, rhs_id)) {
(@lib.Instruction::F32Sub, Some(@lib.F32(raw))) => {
let replacement_rhs = @ir.hot_build_const_f32(func, @lib.F32(-raw))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f32_add(),
[lhs_id, replacement_rhs],
)
return true
}
(@lib.Instruction::F32Div, Some(@lib.F32(raw))) if raw == 2.0 => {
let replacement_rhs = @ir.hot_build_const_f32(func, @lib.F32(0.5))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f32_mul(),
[lhs_id, replacement_rhs],
)
return true
}
(@lib.Instruction::F32Mul, Some(@lib.F32(raw))) if raw == -1.0 => {
let replacement_lhs = @ir.hot_build_const_f32(func, @lib.F32(-0.0))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f32_sub(),
[replacement_lhs, lhs_id],
)
return true
}
(@lib.Instruction::F32Div, Some(@lib.F32(raw))) if raw == 1.0 => {
let replacement_rhs = @ir.hot_build_const_f32(func, @lib.F32(1.0))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f32_mul(),
[lhs_id, replacement_rhs],
)
return true
}
(@lib.Instruction::F32Div, Some(@lib.F32(raw))) if raw == -1.0 => {
let replacement_lhs = @ir.hot_build_const_f32(func, @lib.F32(-0.0))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f32_sub(),
[replacement_lhs, lhs_id],
)
return true
}
_ => ()
}
match (instr, optimize_instructions_f64_const(func, dead, rhs_id)) {
(@lib.Instruction::F64Sub, Some(@lib.F64(raw))) => {
let replacement_rhs = @ir.hot_build_const_f64(func, @lib.F64(-raw))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f64_add(),
[lhs_id, replacement_rhs],
)
return true
}
(@lib.Instruction::F64Div, Some(@lib.F64(raw))) if raw == 2.0 => {
let replacement_rhs = @ir.hot_build_const_f64(func, @lib.F64(0.5))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f64_mul(),
[lhs_id, replacement_rhs],
)
return true
}
(@lib.Instruction::F64Mul, Some(@lib.F64(raw))) if raw == -1.0 => {
let replacement_lhs = @ir.hot_build_const_f64(func, @lib.F64(-0.0))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f64_sub(),
[replacement_lhs, lhs_id],
)
return true
}
(@lib.Instruction::F64Div, Some(@lib.F64(raw))) if raw == 1.0 => {
let replacement_rhs = @ir.hot_build_const_f64(func, @lib.F64(1.0))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f64_mul(),
[lhs_id, replacement_rhs],
)
return true
}
(@lib.Instruction::F64Div, Some(@lib.F64(raw))) if raw == -1.0 => {
let replacement_lhs = @ir.hot_build_const_f64(func, @lib.F64(-0.0))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::f64_sub(),
[replacement_lhs, lhs_id],
)
return true
}
_ => false
}
}
///|
fn optimize_instructions_wrap_i64_const_to_i32(value : Int64) -> @lib.I32 {
let lower64 = value & 4294967295L
if lower64 >= 2147483648L {
@lib.I32((lower64 - 4294967296L).to_int())
} else {
@lib.I32(lower64.to_int())
}
}
///|
fn optimize_instructions_try_replace_zero_bits_with_zero(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Bool {
match
OiZeroBitsReplacementFact::from_node(ctx, func, dead, scratch, node_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
fn optimize_instructions_exact_scalar_conversion_roundtrip_value(
func : @ir.HotFunc,
dead : @bitset.BitSet,
outer_instr : @lib.Instruction,
operand_id : Int32,
) -> Int32? {
if !OiLiveNodeFact::proves_node_live(func, dead, operand_id) {
return None
}
let operand = @ir.hot_node_get(func, operand_id)
if operand.child_count != 1 {
return None
}
let inner_instr = match @ir.hot_node_exact_instr(func, operand_id) {
Some(instr) => instr
None => return None
}
let exact = match (outer_instr, inner_instr) {
(
@lib.Instruction::I32WrapI64,
@lib.Instruction::I64ExtendI32S
| @lib.Instruction::I64ExtendI32U,
) => true
(@lib.Instruction::I32TruncSatF64S, @lib.Instruction::F64ConvertI32S) =>
true
(@lib.Instruction::I32TruncSatF64U, @lib.Instruction::F64ConvertI32U) =>
true
(@lib.Instruction::I32ReinterpretF32, @lib.Instruction::F32ReinterpretI32) =>
true
(@lib.Instruction::I64ReinterpretF64, @lib.Instruction::F64ReinterpretI64) =>
true
(@lib.Instruction::F32ReinterpretI32, @lib.Instruction::I32ReinterpretF32) =>
true
(@lib.Instruction::F64ReinterpretI64, @lib.Instruction::I64ReinterpretF64) =>
true
_ => false
}
if !exact {
return None
}
let value_id = @ir.hot_child_get(func, operand_id, 0)
if OiLiveNodeFact::proves_node_live(func, dead, value_id) {
Some(value_id)
} else {
None
}
}
///|
fn optimize_instructions_try_fold_convert(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
operand_id : Int32,
) -> Bool {
match
optimize_instructions_exact_scalar_conversion_roundtrip_value(
func, dead, instr, operand_id,
) {
Some(value_id) => {
optimize_instructions_replace_with_node_id(ctx, func, node_id, value_id)
return true
}
None => ()
}
match
OiExtendWrapI64MaxBitsFact::from_extend_operand(
func, dead, scratch, instr, operand_id,
) {
Some(fact) => {
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
fact.value_id,
)
return true
}
None => ()
}
match
OiSignedExtendWrapI64FallbackFact::from_extend_operand(
func, dead, instr, operand_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match (instr, optimize_instructions_i64_const(func, dead, operand_id)) {
(@lib.Instruction::I32WrapI64, Some(@lib.I64(raw))) => {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
optimize_instructions_wrap_i64_const_to_i32(raw),
)
true
}
_ => false
}
}
///|
fn optimize_instructions_i32_zero_sub_negated_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Int32? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
if node.child_count != 2 {
return None
}
match node.op {
@ir.HotOp::Binary if func.instr_payloads[node.imm0].instr ==
@lib.Instruction::i32_sub() => {
let zero_id = func.children[node.child_base]
let negated_id = func.children[node.child_base + 1]
if optimize_instructions_i32_const(func, dead, zero_id) ==
Some(@lib.I32(0)) {
Some(negated_id)
} else {
None
}
}
_ => None
}
}
///|
fn optimize_instructions_i64_zero_sub_negated_operand(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Int32? {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
if node.child_count != 2 {
return None
}
match node.op {
@ir.HotOp::Binary if func.instr_payloads[node.imm0].instr ==
@lib.Instruction::i64_sub() => {
let zero_id = func.children[node.child_base]
let negated_id = func.children[node.child_base + 1]
if optimize_instructions_i64_const(func, dead, zero_id) ==
Some(@lib.I64(0L)) {
Some(negated_id)
} else {
None
}
}
_ => None
}
}
///|
fn OiLeadingOperandSwapFact::from_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
a_id : Int32,
b_id : Int32,
) -> OiLeadingOperandSwapFact {
// Binaryen's leading-operand normalization asks `EffectAnalyzer` whether the
// two operand subtrees may exchange evaluation order. Keep the bounds check
// and both subtree effect facts as a named proof so callers consume the same
// OI-G reorder fact instead of a helper-level predicate.
let effects = pass_require_effects(ctx, func)
let left_in_bounds = a_id >= 0 && a_id < effects.node_masks.length()
let right_in_bounds = b_id >= 0 && b_id < effects.node_masks.length()
let left_effects = if left_in_bounds {
OiSubtreeReorderEffectFact::from_node(func, effects.node_masks, a_id)
} else {
OiSubtreeReorderEffectFact::empty()
}
let right_effects = if right_in_bounds {
OiSubtreeReorderEffectFact::from_node(func, effects.node_masks, b_id)
} else {
OiSubtreeReorderEffectFact::empty()
}
{ left_in_bounds, right_in_bounds, left_effects, right_effects, }
}
///|
fn OiLeadingOperandSwapFact::proves_swappable(
self : OiLeadingOperandSwapFact,
) -> Bool {
self.left_in_bounds &&
self.right_in_bounds &&
self.left_effects.can_swap_with(self.right_effects)
}
///|
fn optimize_instructions_try_remove_redundant_unsigned_mask(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match instr {
@lib.Instruction::I32And => {
let (mask, value_id) = match
optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => (raw, lhs_id)
None =>
match optimize_instructions_i32_const(func, dead, lhs_id) {
Some(@lib.I32(raw)) => (raw, rhs_id)
None => return false
}
}
if !OiI32LowOnesMaskFact::from_raw(mask).proves_low_ones() {
return false
}
match
optimize_instructions_i32_unsigned_max(func, dead, scratch, value_id) {
Some(max_value) if max_value <= mask => {
optimize_instructions_replace_with_node_id(
ctx, func, node_id, value_id,
)
true
}
_ => false
}
}
// Binaryen v130 has no analogous i64 low-ones mask removal in
// `visitBinary`; keep i64.and spellings even when maxBits proves the mask
// redundant.
_ => false
}
}
///|
fn optimize_instructions_try_rewrite_add_sub(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match instr {
@lib.Instruction::I32Add => {
let rhs = @ir.hot_node_get(func, rhs_id)
if rhs.child_count == 2 {
match rhs.op {
@ir.HotOp::Binary if func.instr_payloads[rhs.imm0].instr ==
@lib.Instruction::i32_sub() => {
let zero_id = func.children[rhs.child_base]
let sub_rhs_id = func.children[rhs.child_base + 1]
if optimize_instructions_i32_const(func, dead, zero_id) ==
Some(@lib.I32(0)) {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_sub(),
[lhs_id, sub_rhs_id],
)
return true
}
}
_ => ()
}
}
let lhs = @ir.hot_node_get(func, lhs_id)
if lhs.child_count == 2 {
match lhs.op {
@ir.HotOp::Binary if func.instr_payloads[lhs.imm0].instr ==
@lib.Instruction::i32_sub() => {
let zero_id = func.children[lhs.child_base]
let sub_rhs_id = func.children[lhs.child_base + 1]
if optimize_instructions_i32_const(func, dead, zero_id) ==
Some(@lib.I32(0)) &&
OiLeadingOperandSwapFact::from_operands(
ctx, func, rhs_id, sub_rhs_id,
).proves_swappable() {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_sub(),
[rhs_id, sub_rhs_id],
)
return true
}
}
_ => ()
}
}
}
@lib.Instruction::I64Add => {
let rhs = @ir.hot_node_get(func, rhs_id)
if rhs.child_count == 2 {
match rhs.op {
@ir.HotOp::Binary if func.instr_payloads[rhs.imm0].instr ==
@lib.Instruction::i64_sub() => {
let zero_id = func.children[rhs.child_base]
let sub_rhs_id = func.children[rhs.child_base + 1]
if optimize_instructions_i64_const(func, dead, zero_id) ==
Some(@lib.I64(0L)) {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_sub(),
[lhs_id, sub_rhs_id],
)
return true
}
}
_ => ()
}
}
let lhs = @ir.hot_node_get(func, lhs_id)
if lhs.child_count == 2 {
match lhs.op {
@ir.HotOp::Binary if func.instr_payloads[lhs.imm0].instr ==
@lib.Instruction::i64_sub() => {
let zero_id = func.children[lhs.child_base]
let sub_rhs_id = func.children[lhs.child_base + 1]
if optimize_instructions_i64_const(func, dead, zero_id) ==
Some(@lib.I64(0L)) &&
OiLeadingOperandSwapFact::from_operands(
ctx, func, rhs_id, sub_rhs_id,
).proves_swappable() {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_sub(),
[rhs_id, sub_rhs_id],
)
return true
}
}
_ => ()
}
}
}
_ => ()
}
match (instr, optimize_instructions_i32_const(func, dead, rhs_id)) {
(@lib.Instruction::I32Add, Some(@lib.I32(0)))
| (@lib.Instruction::I32Sub, Some(@lib.I32(0))) => {
optimize_instructions_replace_with_existing_node(ctx, func, node_id)
return true
}
(@lib.Instruction::I32Add, Some(@lib.I32(raw))) if raw < 0 &&
!optimize_instructions_prefer_negative_leb_i32(-raw) => {
let replacement_rhs = @ir.hot_build_const_i32(func, @lib.I32(-raw))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_sub(),
[lhs_id, replacement_rhs],
)
return true
}
(@lib.Instruction::I32Add, Some(@lib.I32(raw))) if raw > 0 &&
optimize_instructions_prefer_negative_leb_i32(raw) => {
let replacement_rhs = @ir.hot_build_const_i32(func, @lib.I32(-raw))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_sub(),
[lhs_id, replacement_rhs],
)
return true
}
_ => ()
}
match (instr, optimize_instructions_i64_const(func, dead, rhs_id)) {
(@lib.Instruction::I64Add, Some(@lib.I64(0L)))
| (@lib.Instruction::I64Sub, Some(@lib.I64(0L))) => {
optimize_instructions_replace_with_existing_node(ctx, func, node_id)
true
}
(@lib.Instruction::I64Add, Some(@lib.I64(raw))) if raw < 0L &&
!optimize_instructions_prefer_negative_leb_i64(-raw) => {
let replacement_rhs = @ir.hot_build_const_i64(func, @lib.I64(-raw))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_sub(),
[lhs_id, replacement_rhs],
)
true
}
(@lib.Instruction::I64Add, Some(@lib.I64(raw))) if raw > 0L &&
optimize_instructions_prefer_negative_leb_i64(raw) => {
let replacement_rhs = @ir.hot_build_const_i64(func, @lib.I64(-raw))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_sub(),
[lhs_id, replacement_rhs],
)
true
}
_ => false
}
}
///|
fn optimize_instructions_try_rewrite_mul_negation(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match instr {
@lib.Instruction::I32Mul =>
match
(
optimize_instructions_i32_zero_sub_negated_operand(func, dead, lhs_id),
optimize_instructions_i32_zero_sub_negated_operand(func, dead, rhs_id),
) {
(Some(lhs_negated), Some(rhs_negated)) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_mul(),
[lhs_negated, rhs_negated],
)
true
}
_ => false
}
@lib.Instruction::I64Mul =>
match
(
optimize_instructions_i64_zero_sub_negated_operand(func, dead, lhs_id),
optimize_instructions_i64_zero_sub_negated_operand(func, dead, rhs_id),
) {
(Some(lhs_negated), Some(rhs_negated)) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_mul(),
[lhs_negated, rhs_negated],
)
true
}
_ => false
}
_ => false
}
}
///|
fn optimize_instructions_try_rewrite_mul_shift(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match (instr, optimize_instructions_i32_const(func, dead, rhs_id)) {
(@lib.Instruction::I32Mul, Some(@lib.I32(raw))) =>
match optimize_instructions_i32_power_of_two_shift(raw) {
Some(shift) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_shl(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(shift))],
)
true
}
None => false
}
_ =>
match (instr, optimize_instructions_i64_const(func, dead, rhs_id)) {
(@lib.Instruction::I64Mul, Some(@lib.I64(raw))) =>
match optimize_instructions_i64_power_of_two_shift(raw) {
Some(shift) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_shl(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(shift))],
)
true
}
None => false
}
_ => false
}
}
}
///|
fn optimize_instructions_try_rewrite_div_rem_power_of_two(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match (instr, optimize_instructions_i32_const(func, dead, rhs_id)) {
(@lib.Instruction::I32DivU, Some(@lib.I32(raw))) =>
match optimize_instructions_i32_power_of_two_shift(raw) {
Some(shift) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_shr_u(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(shift))],
)
true
}
None => false
}
(@lib.Instruction::I32RemU, Some(@lib.I32(raw))) =>
match optimize_instructions_i32_power_of_two_shift(raw) {
Some(_) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i32_and(),
[lhs_id, @ir.hot_build_const_i32(func, @lib.I32(raw - 1))],
)
true
}
None => false
}
_ =>
match (instr, optimize_instructions_i64_const(func, dead, rhs_id)) {
(@lib.Instruction::I64DivU, Some(@lib.I64(raw))) =>
match optimize_instructions_i64_power_of_two_shift(raw) {
Some(shift) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_shr_u(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(shift))],
)
true
}
None => false
}
(@lib.Instruction::I64RemU, Some(@lib.I64(raw))) =>
match optimize_instructions_i64_power_of_two_shift(raw) {
Some(_) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
@lib.Instruction::i64_and(),
[lhs_id, @ir.hot_build_const_i64(func, @lib.I64(raw - 1L))],
)
true
}
None => false
}
_ => false
}
}
}
///|
fn optimize_instructions_required_sign_ext_bits(
instr : @lib.Instruction,
) -> Int32 {
match instr {
@lib.Instruction::I32Extend8S | @lib.Instruction::I64Extend8S => 8
@lib.Instruction::I32Extend16S | @lib.Instruction::I64Extend16S => 16
@lib.Instruction::I64Extend32S => 32
_ => 0
}
}
///|
fn optimize_instructions_try_remove_redundant_sign_ext(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
operand_id : Int32,
) -> Bool {
let needed_bits = optimize_instructions_required_sign_ext_bits(instr)
if needed_bits == 0 {
return false
}
// Binaryen v130 keeps direct literal sign-extension shells. Restrict this
// general redundancy rule to producer/local facts so exact literal knowledge
// does not accidentally widen the pass beyond the oracle's visitUnary shape.
match @ir.hot_node_get(func, operand_id).op {
@ir.HotOp::Const => return false
_ => ()
}
let known_bits = optimize_instructions_sign_ext_bits_for_value(
func, dead, scratch, operand_id,
)
if known_bits != 0 && known_bits <= needed_bits {
optimize_instructions_replace_with_existing_node(ctx, func, node_id)
true
} else {
false
}
}
///|
fn optimize_instructions_try_rewrite_sign_ext_idiom(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
let (left_shift_instr, rewrite_instr, shift_amount) = match instr {
@lib.Instruction::I32ShrS =>
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(24)) =>
(@lib.Instruction::i32_shl(), @lib.Instruction::i32_extend8s(), 24)
Some(@lib.I32(16)) =>
(@lib.Instruction::i32_shl(), @lib.Instruction::i32_extend16s(), 16)
_ => return false
}
@lib.Instruction::I64ShrS =>
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(56L)) =>
(@lib.Instruction::i64_shl(), @lib.Instruction::i64_extend8s(), 56)
Some(@lib.I64(48L)) =>
(@lib.Instruction::i64_shl(), @lib.Instruction::i64_extend16s(), 48)
Some(@lib.I64(32L)) =>
(@lib.Instruction::i64_shl(), @lib.Instruction::i64_extend32s(), 32)
_ => return false
}
_ => return false
}
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_id) {
return false
}
let lhs = @ir.hot_node_get(func, lhs_id)
match lhs.op {
@ir.HotOp::Binary => ()
_ => return false
}
if lhs.child_count != 2 {
return false
}
if @ir.hot_node_exact_instr(func, lhs_id) != Some(left_shift_instr) {
return false
}
let lhs_rhs = func.children[lhs.child_base + 1]
let same_amount = match shift_amount {
16 | 24 =>
match optimize_instructions_i32_const(func, dead, lhs_rhs) {
Some(@lib.I32(raw)) => raw == shift_amount
_ => false
}
_ =>
match optimize_instructions_i64_const(func, dead, lhs_rhs) {
Some(@lib.I64(raw)) => raw == shift_amount.to_int64()
_ => false
}
}
if !same_amount {
return false
}
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
rewrite_instr,
func.children[lhs.child_base],
)
true
}
///|
fn optimize_instructions_try_rewrite_shift(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
let mask_bits = match optimize_instructions_shift_mask_bits(instr) {
Some(bits) => bits
None => return false
}
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => {
let masked = raw & mask_bits
if masked == raw {
return false
}
if masked == 0 {
optimize_instructions_replace_with_existing_node(ctx, func, node_id)
return true
}
let replacement_rhs = @ir.hot_build_const_i32(func, @lib.I32(masked))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
instr,
[lhs_id, replacement_rhs],
)
return true
}
None => ()
}
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) => {
let mask64 = mask_bits.to_int64()
let masked = raw & mask64
if masked == raw {
return false
}
if masked == 0 {
optimize_instructions_replace_with_existing_node(ctx, func, node_id)
return true
}
let replacement_rhs = @ir.hot_build_const_i64(func, @lib.I64(masked))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
instr,
[lhs_id, replacement_rhs],
)
return true
}
None => ()
}
if !OiLiveNodeFact::proves_node_live(func, dead, rhs_id) {
return false
}
let rhs = if rhs_id < func.nodes.length() {
func.nodes[rhs_id]
} else {
@ir.hot_node_get(func, rhs_id)
}
let mask_operand_id = match rhs.op {
@ir.HotOp::Binary =>
match @ir.hot_node_exact_instr(func, rhs_id) {
Some(@lib.Instruction::I32And) =>
match
optimize_instructions_i32_const(
func,
dead,
func.children[rhs.child_base + 1],
) {
Some(@lib.I32(raw)) if (raw & mask_bits) == mask_bits =>
Some(func.children[rhs.child_base])
_ => None
}
Some(@lib.Instruction::I64And) =>
match
optimize_instructions_i64_const(
func,
dead,
func.children[rhs.child_base + 1],
) {
Some(@lib.I64(raw)) if (raw & mask_bits.to_int64()) ==
mask_bits.to_int64() => Some(func.children[rhs.child_base])
_ => None
}
_ => None
}
_ => None
}
match mask_operand_id {
Some(masked_operand_id) => {
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
instr,
[lhs_id, masked_operand_id],
)
true
}
None => false
}
}
///|
fn optimize_instructions_try_combine_nested_const_shift(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
let mask_bits = match optimize_instructions_shift_mask_bits(instr) {
Some(bits) => bits
None => return false
}
if !OiLiveNodeFact::proves_node_live(func, dead, lhs_id) {
return false
}
let inner = @ir.hot_node_get(func, lhs_id)
match inner.op {
@ir.HotOp::Binary => ()
_ => return false
}
if inner.child_count != 2 {
return false
}
if @ir.hot_node_exact_instr(func, lhs_id) != Some(instr) {
return false
}
let inner_lhs = func.children[inner.child_base]
let inner_rhs = func.children[inner.child_base + 1]
match instr {
@lib.Instruction::I32Shl
| @lib.Instruction::I32ShrS
| @lib.Instruction::I32ShrU
| @lib.Instruction::I32Rotl
| @lib.Instruction::I32Rotr => {
let outer_amount = match
optimize_instructions_shift_rhs_i32_amount(
func, dead, rhs_id, mask_bits,
) {
Some(amount) => amount
None => return false
}
let inner_amount = match
optimize_instructions_shift_rhs_i32_amount(
func, dead, inner_rhs, mask_bits,
) {
Some(amount) => amount
None => return false
}
let sum = outer_amount + inner_amount
let combined = match instr {
@lib.Instruction::I32Rotl | @lib.Instruction::I32Rotr => sum & mask_bits
@lib.Instruction::I32ShrS =>
if sum > mask_bits {
mask_bits
} else {
sum
}
_ => {
if sum > mask_bits {
// Sequential logical shifts discard every input bit. Evaluate
// effectful operands once, in their original order, before zero.
OiDroppedChildrenReplacementFact::from_child_ids(ctx, func, [
inner_lhs, inner_rhs, rhs_id,
]).replace_with_const_i32(ctx, func, node_id, @lib.I32(0))
return true
}
sum
}
}
match instr {
@lib.Instruction::I32ShrU =>
match
OiCombinedShiftZeroBitsFact::from_i32_shr_u(
func, dead, inner_lhs, combined,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
_ => ()
}
let replacement_rhs = @ir.hot_build_const_i32(func, @lib.I32(combined))
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
instr,
[inner_lhs, replacement_rhs],
)
true
}
@lib.Instruction::I64Shl
| @lib.Instruction::I64ShrS
| @lib.Instruction::I64ShrU
| @lib.Instruction::I64Rotl
| @lib.Instruction::I64Rotr => {
let outer_amount = match
optimize_instructions_shift_rhs_i64_amount(
func, dead, rhs_id, mask_bits,
) {
Some(amount) => amount
None => return false
}
let inner_amount = match
optimize_instructions_shift_rhs_i64_amount(
func, dead, inner_rhs, mask_bits,
) {
Some(amount) => amount
None => return false
}
let sum = outer_amount + inner_amount
let combined = match instr {
@lib.Instruction::I64Rotl | @lib.Instruction::I64Rotr => sum & mask_bits
@lib.Instruction::I64ShrS =>
if sum > mask_bits {
mask_bits
} else {
sum
}
_ => {
if sum > mask_bits {
// Sequential logical shifts discard every input bit. Evaluate
// effectful operands once, in their original order, before zero.
OiDroppedChildrenReplacementFact::from_child_ids(ctx, func, [
inner_lhs, inner_rhs, rhs_id,
]).replace_with_const_i64(ctx, func, node_id, @lib.I64(0L))
return true
}
sum
}
}
match instr {
@lib.Instruction::I64ShrU =>
match
OiCombinedShiftZeroBitsFact::from_i64_shr_u(
func, dead, inner_lhs, combined,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
_ => ()
}
let replacement_rhs = @ir.hot_build_const_i64(
func,
@lib.I64(combined.to_int64()),
)
optimize_instructions_rewrite_exact_with_children(
ctx,
func,
node_id,
instr,
[inner_lhs, replacement_rhs],
)
true
}
_ => false
}
}
///|
fn optimize_instructions_try_fold_compare_zero(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
if !OiTriviallyPureOperandFact::proves_live_node_pure(func, dead, lhs_id) {
return false
}
match instr {
@lib.Instruction::I32GeU =>
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) if raw == 0 => {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(1),
)
true
}
_ => false
}
@lib.Instruction::I32LtU =>
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) if raw == 0 => {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
true
}
_ => false
}
@lib.Instruction::I64GeU =>
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) if raw == 0L => {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(1),
)
true
}
_ => false
}
@lib.Instruction::I64LtU =>
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) if raw == 0L => {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
true
}
_ => false
}
_ => false
}
}
///|
priv struct OiRemainderInstructionFact {
live : Bool
exact_instr : @lib.Instruction?
}
///|
fn OiRemainderInstructionFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiRemainderInstructionFact {
// Binaryen keeps eq-zero wrappers around integer remainder because remainder
// may trap. Record Starshine's represented direct/local.tee remainder shape as
// fact data so compare-to-zero cleanup does not own a bespoke recursive scan.
let live = OiLiveNodeFact::proves_node_live(func, dead, node_id)
if !live {
return { live, exact_instr: None, }
}
let exact_id = match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalTee if @ir.hot_child_count(func, node_id) == 1 =>
@ir.hot_child_get(func, node_id, 0)
_ => node_id
}
{ live, exact_instr: @ir.hot_node_exact_instr(func, exact_id), }
}
///|
fn OiRemainderInstructionFact::proves_remainder(
self : OiRemainderInstructionFact,
) -> Bool {
if !self.live {
return false
}
match self.exact_instr {
Some(
@lib.Instruction::I32RemS
| @lib.Instruction::I32RemU
| @lib.Instruction::I64RemS
| @lib.Instruction::I64RemU
) => true
_ => false
}
}
///|
fn OiRemainderInstructionFact::proves_node_remainder(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
OiRemainderInstructionFact::from_node(func, dead, node_id).proves_remainder()
}
///|
fn optimize_instructions_should_keep_i64_eq_zero_if_condition(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match instr {
@lib.Instruction::I64Eq => ()
_ => return false
}
optimize_instructions_mark_if_condition_nodes(func, dead, scratch)
if node_id < 0 ||
node_id >= scratch.if_condition_nodes.length() ||
!scratch.if_condition_nodes[node_id] {
return false
}
let operand_id = if optimize_instructions_i64_const(func, dead, lhs_id) ==
Some(@lib.I64(0L)) {
rhs_id
} else if optimize_instructions_i64_const(func, dead, rhs_id) ==
Some(@lib.I64(0L)) {
lhs_id
} else {
return false
}
// The condition-shape guard must not suppress the existing constant
// fallthrough fold. The subsequent eqz rule preserves the complete wrapper
// evaluation, including local.tee writes and effectful block prefixes.
match
OiBooleanEqzFallthroughChainFact::from_operand(
func,
operand_id,
@lib.ValType::i64(),
scratch.local_facts,
) {
Some(fact) =>
optimize_instructions_i64_const(func, dead, fact.value_id) is None
None => true
}
}
///|
fn optimize_instructions_replace_ref_cast_with_ref_as_non_null(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
operand_id : Int32,
) -> Bool {
let result_types = @ir.hot_type_results(
func,
@ir.hot_node_type(func, node_id),
)
let temp = @ir.hot_build_heap(
func,
result_types,
@lib.Instruction::ref_as_non_null(),
[operand_id],
)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_replace_ref_cast_with_nullref_cast(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
operand_id : Int32,
) -> Bool {
let none_heap = @lib.HeapType::abs(@lib.AbsHeapType::none())
let nullref = @lib.ValType::ref_null(none_heap)
let temp = @ir.hot_build_heap(
func,
[nullref],
@lib.Instruction::ref_cast_type(@lib.RefType::new(true, none_heap)),
[operand_id],
)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_narrow_null_cast_boundary_block(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Block => ()
_ => return false
}
let label_id = @ir.hot_control_node_label(func, node_id)
if label_id < 0 || @ir.hot_label_branch_arity(func, label_id) != 1 {
return false
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[@lib.ValType::RefTypeValType(_)] => ()
_ => return false
}
let body = @ir.hot_region_body_ref(func, node_id)
if @ir.hot_region_root_count(func, body) != 1 {
return false
}
let branch_id = @ir.hot_region_get(func, body, 0)
let branch = @ir.hot_node_get(func, branch_id)
if branch.imm0 != label_id || @ir.hot_child_count(func, branch_id) != 1 {
return false
}
let is_cast_branch = match branch.op {
@ir.HotOp::BrOnCast | @ir.HotOp::BrOnCastFail => true
_ => false
}
if !is_cast_branch {
return false
}
let guard_id = @ir.hot_child_get(func, branch_id, 0)
match @ir.hot_node_get(func, guard_id).op {
@ir.HotOp::RefNull => ()
_ => return false
}
let none_heap = @lib.HeapType::abs(@lib.AbsHeapType::none())
let nullref = @lib.ValType::ref_null(none_heap)
let nullref_value_ty = @ir.hot_type_intern_value(func, nullref)
let nullref_block_ty = @ir.hot_type_intern_block_result(
func,
@lib.BlockType::val_type(nullref),
)
let target_nullable = match @ir.hot_node_exact_instr(func, branch_id) {
Some(
@lib.Instruction::BrOnCast(_, @lib.CastOp(_, target_nullable, _, _), _, _)
) => target_nullable
Some(
@lib.Instruction::BrOnCastFail(
_,
@lib.CastOp(_, target_nullable, _, _),
_,
_
)
) => target_nullable
_ => return false
}
let new_instr = match branch.op {
@ir.HotOp::BrOnCast =>
@lib.Instruction::br_on_cast(
@lib.LabelIdx::new(0U),
true,
none_heap,
target_nullable,
none_heap,
)
@ir.HotOp::BrOnCastFail =>
@lib.Instruction::br_on_cast_fail(
@lib.LabelIdx::new(0U),
true,
none_heap,
target_nullable,
none_heap,
)
_ => return false
}
let narrowed_guard = @ir.hot_build_ref_null(
func,
@lib.RefType::new(true, none_heap),
)
let payload_id = @ir.hot_alloc_instr_payload(
func,
@ir.HotInstrPayload::new(new_instr),
)
let replacement_branch = @ir.hot_build_node(
func,
branch.op,
nullref_value_ty,
imm0=label_id,
imm1=payload_id,
children=[narrowed_guard],
)
@ir.hot_replace_node(
func,
branch_id,
@ir.hot_node_get(func, replacement_branch),
)
@ir.hot_delete_node(func, replacement_branch)
@ir.hot_control_result_type_set(func, node_id, nullref_block_ty)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_replace_unreachable_guard_cast_branch(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::BrOnCast | @ir.HotOp::BrOnCastFail => ()
_ => return false
}
if @ir.hot_child_count(func, node_id) != 1 {
return false
}
let guard_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_get(func, guard_id).op {
@ir.HotOp::Unreachable => ()
_ => return false
}
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
}
///|
fn optimize_instructions_try_collapse_dropped_unreachable_block(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Drop => ()
_ => return false
}
if @ir.hot_child_count(func, node_id) != 1 {
return false
}
let block_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_get(func, block_id).op {
@ir.HotOp::Block => ()
_ => return false
}
let body = @ir.hot_region_body_ref(func, block_id)
let root_count = @ir.hot_region_root_count(func, body)
if root_count == 0 {
return false
}
let mut saw_unreachable = false
for idx = 0; idx < root_count; idx = idx + 1 {
let root = @ir.hot_region_get(func, body, idx)
match @ir.hot_node_get(func, root).op {
@ir.HotOp::Unreachable => {
saw_unreachable = true
break
}
_ =>
if !optimize_instructions_node_effects_are_pure(ctx, func, root) {
return false
}
}
}
if !saw_unreachable {
return false
}
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
}
///|
fn optimize_instructions_try_fold_ref_test_null(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let test_fact = match OiRefTestFact::from_node(func, node_id) {
Some(fact) => fact
None => return false
}
match OiRefTestRewritePlanFact::from_test(ctx, func, test_fact) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_fold_ref_cast_null(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let cast_fact = match OiRefCastFact::from_node(func, node_id) {
Some(fact) => fact
None => return false
}
match OiRefCastRewritePlanFact::from_cast(ctx, func, cast_fact) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_skip_ref_cast_desc_eq_exact_success_traps_never_happen(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let success_fact = match
OiDescriptorExactSuccessFact::from_node(ctx, func, node_id) {
Some(fact) => fact
None => return false
}
match
OiDescriptorExactSuccessRewritePlanFact::from_success(ctx, success_fact) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_skip_ref_cast_desc_eq_traps_never_happen(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let skip_fact = match
OiDescriptorTnhSkipFact::from_node(ctx, func, dead, node_id) {
Some(fact) => fact
None => return false
}
match OiDescriptorTnhSkipRewritePlanFact::from_skip(ctx, func, skip_fact) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_remove_ref_get_desc_child_null_check_traps_never_happen(
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
) -> Bool {
let get_desc_fact = match OiRefGetDescFact::from_node(func, descriptor_id) {
Some(fact) => fact
None => return false
}
match
OiRefGetDescNullCheckMoveFact::from_get_desc(
func,
get_desc_fact,
RefGetDescTrapsNeverHappenNullCheckMove,
) {
Some(move_fact) => move_fact.apply(ctx, func, descriptor_id)
None => false
}
}
///|
fn optimize_instructions_try_replace_ref_cast_desc_eq_trapping_descriptor_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let null_fact = match
OiDescriptorNullCastFact::from_node(ctx, func, dead, node_id) {
Some(fact) => fact
None => return false
}
match
OiDescriptorNullCastRewritePlanFact::from_null_cast(
null_fact,
OiDescriptorNullCastRewriteScopeFact::descriptor_trap_scope(),
) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv enum OiDescriptorNullableIfNoEffectMoveKind {
DescriptorNullableIfNoEffectMoveNullCheck
DescriptorNullableIfNoEffectMoveBlocked
}
///|
priv struct OiDescriptorNullableIfNoEffectPayloadFact {
condition_side_effect_free : Bool
has_else : Bool
single_root_arms : Bool
then_side_effect_free : Bool
else_side_effect_free : Bool
}
///|
priv struct OiDescriptorNullableIfNoEffectMoveFact {
if_root : Bool
has_three_children : Bool
payload : OiDescriptorNullableIfNoEffectPayloadFact?
kind : OiDescriptorNullableIfNoEffectMoveKind
}
///|
fn OiDescriptorNullableIfNoEffectMoveFact::from_if(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDescriptorNullableIfNoEffectMoveFact {
// TNH descriptor operand erasure can drop a nullable `if` child when the
// condition and both single-result arms are side-effect-free. This preserves
// the old represented shape as a fact rather than a helper-owned recursive
// side-effect-free scan. The explicit payload records the source-ordered
// condition/arm proof consumed by the result kind.
let node = @ir.hot_node_get(func, node_id)
let if_root = match node.op {
@ir.HotOp::If => true
_ => false
}
if !if_root {
return {
if_root,
has_three_children: false,
payload: None,
kind: DescriptorNullableIfNoEffectMoveBlocked,
}
}
let has_three_children = node.child_count == 3
if !has_three_children {
return {
if_root,
has_three_children,
payload: None,
kind: DescriptorNullableIfNoEffectMoveBlocked,
}
}
let condition_side_effect_free = OiSideEffectFreeExpressionFact::proves_node_free(
func,
dead,
@ir.hot_child_get(func, node_id, 0),
)
let then_region = @ir.hot_region_then_ref(func, node_id)
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None =>
return {
if_root,
has_three_children,
payload: Some({
condition_side_effect_free,
has_else: false,
single_root_arms: false,
then_side_effect_free: false,
else_side_effect_free: false,
}),
kind: DescriptorNullableIfNoEffectMoveBlocked,
}
}
let single_root_arms = @ir.hot_region_root_count(func, then_region) == 1 &&
@ir.hot_region_root_count(func, else_region) == 1
let then_side_effect_free = single_root_arms &&
OiSideEffectFreeExpressionFact::proves_node_free(
func,
dead,
@ir.hot_region_get(func, then_region, 0),
)
let else_side_effect_free = single_root_arms &&
OiSideEffectFreeExpressionFact::proves_node_free(
func,
dead,
@ir.hot_region_get(func, else_region, 0),
)
let payload : OiDescriptorNullableIfNoEffectPayloadFact = {
condition_side_effect_free,
has_else: true,
single_root_arms,
then_side_effect_free,
else_side_effect_free,
}
{
if_root,
has_three_children,
payload: Some(payload),
kind: if has_three_children &&
payload.condition_side_effect_free &&
payload.single_root_arms &&
payload.then_side_effect_free &&
payload.else_side_effect_free {
DescriptorNullableIfNoEffectMoveNullCheck
} else {
DescriptorNullableIfNoEffectMoveBlocked
},
}
}
///|
fn OiDescriptorNullableIfNoEffectMoveFact::proves_nullable_if_no_effect_move_allowed(
self : OiDescriptorNullableIfNoEffectMoveFact,
) -> Bool {
match (self.kind, self.payload) {
(DescriptorNullableIfNoEffectMoveNullCheck, Some(payload)) =>
self.if_root &&
self.has_three_children &&
payload.condition_side_effect_free &&
payload.has_else &&
payload.single_root_arms &&
payload.then_side_effect_free &&
payload.else_side_effect_free
_ => false
}
}
///|
priv enum OiDescriptorNullableIfControlMoveKind {
DescriptorNullableIfControlMoveNullCheck
DescriptorNullableIfControlMoveBlocked
}
///|
priv struct OiDescriptorNullableIfControlPayloadFact {
condition_no_escaping_branch_control : Bool
has_else : Bool
single_root_arms : Bool
then_no_escaping_branch_control : Bool
else_no_escaping_branch_control : Bool
}
///|
priv struct OiDescriptorNullableIfControlMoveFact {
if_root : Bool
has_three_children : Bool
payload : OiDescriptorNullableIfControlPayloadFact?
kind : OiDescriptorNullableIfControlMoveKind
}
///|
fn OiDescriptorNullableIfControlMoveFact::from_if(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorNullableIfControlMoveFact {
// Standalone ref.get_desc null-check movement may preserve effectful arms, but
// only while proving that the condition and both arms have no escaping branch
// control. Store that Binaryen ChildLocalizer-adjacent boundary explicitly as
// a payload-backed result kind instead of a parallel `can_move` boolean.
let node = @ir.hot_node_get(func, node_id)
let if_root = match node.op {
@ir.HotOp::If => true
_ => false
}
if !if_root {
return {
if_root,
has_three_children: false,
payload: None,
kind: DescriptorNullableIfControlMoveBlocked,
}
}
let has_three_children = node.child_count == 3
if !has_three_children {
return {
if_root,
has_three_children,
payload: None,
kind: DescriptorNullableIfControlMoveBlocked,
}
}
let condition_id = @ir.hot_child_get(func, node_id, 0)
let condition_no_escaping_branch_control = !OiDescriptorBranchControlFact::from_node(
func, condition_id, condition_id,
).proves_has_escaping_branch_control()
let then_region = @ir.hot_region_then_ref(func, node_id)
let else_region = match @ir.hot_region_else_ref(func, node_id) {
Some(region) => region
None =>
return {
if_root,
has_three_children,
payload: Some({
condition_no_escaping_branch_control,
has_else: false,
single_root_arms: false,
then_no_escaping_branch_control: false,
else_no_escaping_branch_control: false,
}),
kind: DescriptorNullableIfControlMoveBlocked,
}
}
let single_root_arms = @ir.hot_region_root_count(func, then_region) == 1 &&
@ir.hot_region_root_count(func, else_region) == 1
let then_no_escaping_branch_control = single_root_arms &&
!OiDescriptorBranchControlRegionFact::from_region(
func, node_id, then_region,
).proves_has_escaping_branch_control()
let else_no_escaping_branch_control = single_root_arms &&
!OiDescriptorBranchControlRegionFact::from_region(
func, node_id, else_region,
).proves_has_escaping_branch_control()
let payload : OiDescriptorNullableIfControlPayloadFact = {
condition_no_escaping_branch_control,
has_else: true,
single_root_arms,
then_no_escaping_branch_control,
else_no_escaping_branch_control,
}
{
if_root,
has_three_children,
payload: Some(payload),
kind: if has_three_children &&
payload.condition_no_escaping_branch_control &&
payload.single_root_arms &&
payload.then_no_escaping_branch_control &&
payload.else_no_escaping_branch_control {
DescriptorNullableIfControlMoveNullCheck
} else {
DescriptorNullableIfControlMoveBlocked
},
}
}
///|
fn OiDescriptorNullableIfControlMoveFact::proves_nullable_if_control_move_allowed(
self : OiDescriptorNullableIfControlMoveFact,
) -> Bool {
match (self.kind, self.payload) {
(DescriptorNullableIfControlMoveNullCheck, Some(payload)) =>
self.if_root &&
self.has_three_children &&
payload.condition_no_escaping_branch_control &&
payload.has_else &&
payload.single_root_arms &&
payload.then_no_escaping_branch_control &&
payload.else_no_escaping_branch_control
_ => false
}
}
///|
priv enum OiDescriptorBranchControlSummaryKind {
DescriptorBranchControlAbsent
DescriptorBranchControlInternal
DescriptorBranchControlEscaping
}
///|
priv struct OiDescriptorBranchControlSummaryFact {
kind : OiDescriptorBranchControlSummaryKind
}
///|
priv struct OiDescriptorBranchControlFact {
live : Bool
direct_control_fact : OiDescriptorBranchControlSummaryFact
nested_control_fact : OiDescriptorBranchControlSummaryFact
}
///|
priv struct OiDescriptorBranchControlRegionFact {
root_count : Int32
control_fact : OiDescriptorBranchControlSummaryFact
}
///|
priv enum OiDescriptorSkipOrderEffectKind {
DescriptorSkipOrderDirectEffect
DescriptorSkipOrderRegionEffect
DescriptorSkipOrderChildEffect
}
///|
priv struct OiDescriptorSkipOrderEffectFact {
kind : OiDescriptorSkipOrderEffectKind
}
///|
priv struct OiDescriptorSkipOrderFact {
live : Bool
effect_fact : OiDescriptorSkipOrderEffectFact?
}
///|
priv struct OiDescriptorSkipOrderRegionFact {
root_count : Int32
effect_fact : OiDescriptorSkipOrderEffectFact?
}
///|
priv enum OiDescriptorInternalControlEffectFreeKind {
DescriptorInternalControlDirectSideEffectFree
DescriptorInternalControlSupportedShape
DescriptorInternalControlUnsupported
}
///|
priv enum OiDescriptorInternalControlPayloadEffectKind {
DescriptorInternalControlPayloadEffectFree
DescriptorInternalControlPayloadEffectful
}
///|
priv struct OiDescriptorInternalControlBlockRegionFact {
kind : OiDescriptorInternalControlPayloadEffectKind
}
///|
priv struct OiDescriptorInternalControlChildEffectFact {
kind : OiDescriptorInternalControlPayloadEffectKind
}
///|
priv struct OiDescriptorInternalControlEffectFreeFact {
kind : OiDescriptorInternalControlEffectFreeKind
block_region_fact : OiDescriptorInternalControlBlockRegionFact?
child_effect_fact : OiDescriptorInternalControlChildEffectFact?
}
///|
priv struct OiDescriptorInternalControlEffectFreeRegionFact {
root_count : Int32
effect_fact : OiDescriptorInternalControlBlockRegionFact
}
///|
priv enum OiDescriptorContainmentKind {
DescriptorContainmentAbsent
DescriptorContainmentRootTarget
DescriptorContainmentChildTarget
DescriptorContainmentRegionTarget
}
///|
priv struct OiDescriptorContainmentFact {
kind : OiDescriptorContainmentKind
}
///|
priv struct OiDescriptorContainmentRegionTargetFact {
region_root_count : Int32
}
///|
priv struct OiDescriptorContainmentRegionFact {
root_count : Int32
target_fact : OiDescriptorContainmentRegionTargetFact?
}
///|
priv enum OiDescriptorLabelValidityKind {
DescriptorLabelValid
DescriptorLabelInvalid
}
///|
priv struct OiDescriptorLabelValidityFact {
kind : OiDescriptorLabelValidityKind
}
///|
priv struct OiDescriptorInternalLabelContainmentFact {
owner_fact : OiDescriptorContainmentFact
}
///|
priv struct OiDescriptorInternalLabelFact {
validity_fact : OiDescriptorLabelValidityFact
containment_fact : OiDescriptorInternalLabelContainmentFact?
}
///|
fn OiDescriptorContainmentFact::empty() -> OiDescriptorContainmentFact {
{ kind: DescriptorContainmentAbsent, }
}
///|
fn OiDescriptorContainmentFact::from_node(
func : @ir.HotFunc,
root_id : Int32,
target_id : Int32,
) -> OiDescriptorContainmentFact {
// Descriptor and standalone ref.get_desc control gates need to know whether a
// branch target's label owner is contained inside the expression being
// localized. Keep that subtree proof as selected root/child/region facts
// rather than a helper recursively scanning child and control regions.
if root_id == target_id {
return { kind: DescriptorContainmentRootTarget, }
}
if root_id < 0 || !@ir.hot_node_is_live(func, root_id) {
return OiDescriptorContainmentFact::empty()
}
let node = @ir.hot_node_get(func, root_id)
let mut child_contains_target = false
for slot = 0; slot < node.child_count; slot = slot + 1 {
child_contains_target = child_contains_target ||
OiDescriptorContainmentFact::from_node(
func,
@ir.hot_child_get(func, root_id, slot),
target_id,
).proves_contains_target()
}
if child_contains_target {
return { kind: DescriptorContainmentChildTarget, }
}
let region_contains_target = match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop | @ir.HotOp::Try | @ir.HotOp::TryTable =>
OiDescriptorContainmentRegionFact::from_region(
func,
@ir.hot_region_body_ref(func, root_id),
target_id,
).proves_contains_target()
@ir.HotOp::If => {
let then_contains = OiDescriptorContainmentRegionFact::from_region(
func,
@ir.hot_region_then_ref(func, root_id),
target_id,
).proves_contains_target()
let else_contains = match @ir.hot_region_else_ref(func, root_id) {
Some(region_ref) =>
OiDescriptorContainmentRegionFact::from_region(
func, region_ref, target_id,
).proves_contains_target()
None => false
}
then_contains || else_contains
}
_ => false
}
if region_contains_target {
{ kind: DescriptorContainmentRegionTarget, }
} else {
OiDescriptorContainmentFact::empty()
}
}
///|
fn OiDescriptorContainmentFact::proves_contains_target(
self : OiDescriptorContainmentFact,
) -> Bool {
match self.kind {
DescriptorContainmentRootTarget
| DescriptorContainmentChildTarget
| DescriptorContainmentRegionTarget => true
DescriptorContainmentAbsent => false
}
}
///|
fn OiDescriptorContainmentRegionFact::from_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
target_id : Int32,
) -> OiDescriptorContainmentRegionFact {
let root_count = @ir.hot_region_root_count(func, region_ref)
let mut contains_target = false
for slot = 0; slot < root_count; slot = slot + 1 {
contains_target = contains_target ||
OiDescriptorContainmentFact::from_node(
func,
@ir.hot_region_get(func, region_ref, slot),
target_id,
).proves_contains_target()
}
{
root_count,
target_fact: if contains_target {
Some({ region_root_count: root_count, })
} else {
None
},
}
}
///|
fn OiDescriptorContainmentRegionTargetFact::proves_contains_target(
self : OiDescriptorContainmentRegionTargetFact,
) -> Bool {
self.region_root_count > 0
}
///|
fn OiDescriptorContainmentRegionFact::proves_contains_target(
self : OiDescriptorContainmentRegionFact,
) -> Bool {
self.root_count > 0 &&
(match self.target_fact {
Some(fact) => fact.proves_contains_target()
None => false
})
}
///|
fn OiDescriptorLabelValidityFact::from_label_id(
func : @ir.HotFunc,
label_id : Int32,
) -> OiDescriptorLabelValidityFact {
if label_id < 0 || label_id >= func.labels.length() {
{ kind: DescriptorLabelInvalid, }
} else {
{ kind: DescriptorLabelValid, }
}
}
///|
fn OiDescriptorLabelValidityFact::proves_valid_label(
self : OiDescriptorLabelValidityFact,
) -> Bool {
match self.kind {
DescriptorLabelValid => true
DescriptorLabelInvalid => false
}
}
///|
fn OiDescriptorInternalLabelContainmentFact::from_label_owner(
func : @ir.HotFunc,
root_id : Int32,
label_id : Int32,
) -> OiDescriptorInternalLabelContainmentFact {
{
owner_fact: OiDescriptorContainmentFact::from_node(
func,
root_id,
@ir.hot_label_owner(func, label_id),
),
}
}
///|
fn OiDescriptorInternalLabelContainmentFact::proves_owner_contained(
self : OiDescriptorInternalLabelContainmentFact,
) -> Bool {
self.owner_fact.proves_contains_target()
}
///|
fn OiDescriptorInternalLabelFact::from_label(
func : @ir.HotFunc,
root_id : Int32,
label_id : Int32,
) -> OiDescriptorInternalLabelFact {
// Descriptor branch/control cleanup first proves that a branch label exists,
// then separately proves that the label owner remains inside the descriptor or
// ref.get_desc expression being localized. Keep those as nested source facts
// instead of parallel validity/containment booleans.
let validity_fact = OiDescriptorLabelValidityFact::from_label_id(
func, label_id,
)
{
validity_fact,
containment_fact: if validity_fact.proves_valid_label() {
Some(
OiDescriptorInternalLabelContainmentFact::from_label_owner(
func, root_id, label_id,
),
)
} else {
None
},
}
}
///|
fn OiDescriptorInternalLabelFact::proves_internal(
self : OiDescriptorInternalLabelFact,
) -> Bool {
self.validity_fact.proves_valid_label() &&
(match self.containment_fact {
Some(fact) => fact.proves_owner_contained()
None => false
})
}
///|
fn OiDescriptorBranchControlSummaryFact::from_flags(
has_branch_control : Bool,
has_escaping_branch_control : Bool,
) -> OiDescriptorBranchControlSummaryFact {
let kind = if has_escaping_branch_control {
DescriptorBranchControlEscaping
} else if has_branch_control {
DescriptorBranchControlInternal
} else {
DescriptorBranchControlAbsent
}
{ kind, }
}
///|
fn OiDescriptorBranchControlSummaryFact::empty() -> OiDescriptorBranchControlSummaryFact {
{ kind: DescriptorBranchControlAbsent, }
}
///|
fn OiDescriptorBranchControlSummaryFact::proves_has_branch_control(
self : OiDescriptorBranchControlSummaryFact,
) -> Bool {
match self.kind {
DescriptorBranchControlAbsent => false
DescriptorBranchControlInternal | DescriptorBranchControlEscaping => true
}
}
///|
fn OiDescriptorBranchControlSummaryFact::proves_has_escaping_branch_control(
self : OiDescriptorBranchControlSummaryFact,
) -> Bool {
match self.kind {
DescriptorBranchControlEscaping => true
DescriptorBranchControlAbsent | DescriptorBranchControlInternal => false
}
}
///|
fn OiDescriptorBranchControlFact::empty() -> OiDescriptorBranchControlFact {
{
live: false,
direct_control_fact: OiDescriptorBranchControlSummaryFact::empty(),
nested_control_fact: OiDescriptorBranchControlSummaryFact::empty(),
}
}
///|
fn OiDescriptorBranchControlFact::from_node(
func : @ir.HotFunc,
owner_id : Int32,
node_id : Int32,
) -> OiDescriptorBranchControlFact {
// Binaryen's descriptor/ref-get-desc cleanup uses branch-control and escaping
// branch-control predicates to decide whether a nullable child can be moved or
// stripped without swallowing a control transfer. Keep Starshine's represented
// subtree summary as one fact with explicit direct-branch, nested-branch,
// direct-escape, and nested-escape fields instead of separate recursive
// descriptor-control scanners.
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiDescriptorBranchControlFact::empty()
}
let node = @ir.hot_node_get(func, node_id)
let direct_branch_control = match node.op {
@ir.HotOp::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail => true
_ => false
}
let direct_escaping_branch_control = match node.op {
@ir.HotOp::Br | @ir.HotOp::BrIf =>
!OiDescriptorInternalLabelFact::from_label(func, owner_id, node.imm0).proves_internal()
@ir.HotOp::BrTable =>
if !OiDescriptorInternalLabelFact::from_label(
func,
owner_id,
@ir.hot_branch_table_default_target(func, node.imm0),
).proves_internal() {
true
} else {
let mut escaping = false
for label_id in @ir.hot_branch_table_targets(func, node.imm0) {
if !OiDescriptorInternalLabelFact::from_label(
func, owner_id, label_id,
).proves_internal() {
escaping = true
}
}
escaping
}
@ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail =>
!OiDescriptorInternalLabelFact::from_label(func, owner_id, node.imm0).proves_internal()
_ => false
}
let mut nested_branch_control = false
let mut nested_escaping_branch_control = false
if !direct_branch_control {
match node.op {
@ir.HotOp::Block
| @ir.HotOp::Loop
| @ir.HotOp::Try
| @ir.HotOp::TryTable => {
let body_fact = OiDescriptorBranchControlRegionFact::from_region(
func,
owner_id,
@ir.hot_region_body_ref(func, node_id),
)
nested_branch_control = nested_branch_control ||
body_fact.proves_has_branch_control()
nested_escaping_branch_control = nested_escaping_branch_control ||
body_fact.proves_has_escaping_branch_control()
}
@ir.HotOp::If => {
let condition_fact = OiDescriptorBranchControlFact::from_node(
func,
owner_id,
@ir.hot_child_get(func, node_id, 0),
)
nested_branch_control = nested_branch_control ||
condition_fact.proves_has_branch_control()
nested_escaping_branch_control = nested_escaping_branch_control ||
condition_fact.proves_has_escaping_branch_control()
let then_fact = OiDescriptorBranchControlRegionFact::from_region(
func,
owner_id,
@ir.hot_region_then_ref(func, node_id),
)
nested_branch_control = nested_branch_control ||
then_fact.proves_has_branch_control()
nested_escaping_branch_control = nested_escaping_branch_control ||
then_fact.proves_has_escaping_branch_control()
match @ir.hot_region_else_ref(func, node_id) {
Some(region_ref) => {
let else_fact = OiDescriptorBranchControlRegionFact::from_region(
func, owner_id, region_ref,
)
nested_branch_control = nested_branch_control ||
else_fact.proves_has_branch_control()
nested_escaping_branch_control = nested_escaping_branch_control ||
else_fact.proves_has_escaping_branch_control()
}
None => ()
}
}
_ => ()
}
}
if !direct_escaping_branch_control {
for slot = 0; slot < node.child_count; slot = slot + 1 {
let child_fact = OiDescriptorBranchControlFact::from_node(
func,
owner_id,
@ir.hot_child_get(func, node_id, slot),
)
nested_branch_control = nested_branch_control ||
child_fact.proves_has_branch_control()
nested_escaping_branch_control = nested_escaping_branch_control ||
child_fact.proves_has_escaping_branch_control()
}
}
{
live: true,
direct_control_fact: OiDescriptorBranchControlSummaryFact::from_flags(
direct_branch_control, direct_escaping_branch_control,
),
nested_control_fact: OiDescriptorBranchControlSummaryFact::from_flags(
nested_branch_control, nested_escaping_branch_control,
),
}
}
///|
fn OiDescriptorBranchControlFact::proves_has_branch_control(
self : OiDescriptorBranchControlFact,
) -> Bool {
self.live &&
(
self.direct_control_fact.proves_has_branch_control() ||
self.nested_control_fact.proves_has_branch_control()
)
}
///|
fn OiDescriptorBranchControlFact::proves_has_escaping_branch_control(
self : OiDescriptorBranchControlFact,
) -> Bool {
self.live &&
(
self.direct_control_fact.proves_has_escaping_branch_control() ||
self.nested_control_fact.proves_has_escaping_branch_control()
)
}
///|
fn OiDescriptorBranchControlRegionFact::from_region(
func : @ir.HotFunc,
owner_id : Int32,
region_ref : @ir.HotRegionRef,
) -> OiDescriptorBranchControlRegionFact {
OiDescriptorBranchControlRegionFact::from_region_range(
func,
owner_id,
region_ref,
0,
@ir.hot_region_root_count(func, region_ref),
)
}
///|
fn OiDescriptorBranchControlRegionFact::from_region_prefix_excluding_tail(
func : @ir.HotFunc,
owner_id : Int32,
region_ref : @ir.HotRegionRef,
) -> OiDescriptorBranchControlRegionFact {
let root_count = @ir.hot_region_root_count(func, region_ref)
if root_count <= 1 {
return OiDescriptorBranchControlRegionFact::empty()
}
OiDescriptorBranchControlRegionFact::from_region_range(
func,
owner_id,
region_ref,
0,
root_count - 1,
)
}
///|
fn OiDescriptorBranchControlRegionFact::from_region_range(
func : @ir.HotFunc,
owner_id : Int32,
region_ref : @ir.HotRegionRef,
start_slot : Int32,
end_slot : Int32,
) -> OiDescriptorBranchControlRegionFact {
let mut has_branch_control = false
let mut has_escaping_branch_control = false
let region_root_count = @ir.hot_region_root_count(func, region_ref)
let end = if end_slot < region_root_count {
end_slot
} else {
region_root_count
}
for slot = start_slot; slot < end; slot = slot + 1 {
let root_fact = OiDescriptorBranchControlFact::from_node(
func,
owner_id,
@ir.hot_region_get(func, region_ref, slot),
)
has_branch_control = has_branch_control ||
root_fact.proves_has_branch_control()
has_escaping_branch_control = has_escaping_branch_control ||
root_fact.proves_has_escaping_branch_control()
}
{
root_count: end - start_slot,
control_fact: OiDescriptorBranchControlSummaryFact::from_flags(
has_branch_control, has_escaping_branch_control,
),
}
}
///|
fn OiDescriptorBranchControlRegionFact::empty() -> OiDescriptorBranchControlRegionFact {
{
root_count: 0,
control_fact: OiDescriptorBranchControlSummaryFact::empty(),
}
}
///|
fn OiDescriptorBranchControlRegionFact::proves_has_branch_control(
self : OiDescriptorBranchControlRegionFact,
) -> Bool {
self.root_count > 0 && self.control_fact.proves_has_branch_control()
}
///|
fn OiDescriptorBranchControlRegionFact::proves_has_escaping_branch_control(
self : OiDescriptorBranchControlRegionFact,
) -> Bool {
self.root_count > 0 && self.control_fact.proves_has_escaping_branch_control()
}
///|
fn OiDescriptorSkipOrderFact::empty() -> OiDescriptorSkipOrderFact {
{ live: false, effect_fact: None, }
}
///|
fn OiDescriptorSkipOrderEffectFact::proves_order_sensitive_effect(
self : OiDescriptorSkipOrderEffectFact,
) -> Bool {
match self.kind {
DescriptorSkipOrderDirectEffect
| DescriptorSkipOrderRegionEffect
| DescriptorSkipOrderChildEffect => true
}
}
///|
fn OiDescriptorSkipOrderFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorSkipOrderFact {
// Binaryen's descriptor-cast TNH skip can erase the descriptor immediately
// only when doing so does not reorder calls, writes, atomics, EH, or other
// unremovable effects before the reference operand. Keep Starshine's
// represented order-sensitive subtree summary as direct/region/child effect
// facts instead of a recursive helper-local opcode scan.
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiDescriptorSkipOrderFact::empty()
}
let node = @ir.hot_node_get(func, node_id)
let direct_order_sensitive_effect = match node.op {
@ir.HotOp::LocalSet
| @ir.HotOp::LocalTee
| @ir.HotOp::GlobalSet
| @ir.HotOp::Store
| @ir.HotOp::MemoryGrow
| @ir.HotOp::MemoryCopy
| @ir.HotOp::MemoryFill
| @ir.HotOp::MemoryInit
| @ir.HotOp::DataDrop
| @ir.HotOp::TableSet
| @ir.HotOp::TableGrow
| @ir.HotOp::TableFill
| @ir.HotOp::TableCopy
| @ir.HotOp::TableInit
| @ir.HotOp::ElemDrop
| @ir.HotOp::Call
| @ir.HotOp::CallIndirect
| @ir.HotOp::CallRef
| @ir.HotOp::Atomic
| @ir.HotOp::Throw
| @ir.HotOp::ThrowRef => true
_ => false
}
if direct_order_sensitive_effect {
return {
live: true,
effect_fact: Some({ kind: DescriptorSkipOrderDirectEffect, }),
}
}
let region_order_sensitive_effect = match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop | @ir.HotOp::Try | @ir.HotOp::TryTable =>
OiDescriptorSkipOrderRegionFact::from_region(
func,
@ir.hot_region_body_ref(func, node_id),
).proves_has_order_sensitive_effect()
@ir.HotOp::If => {
let then_effect = OiDescriptorSkipOrderRegionFact::from_region(
func,
@ir.hot_region_then_ref(func, node_id),
).proves_has_order_sensitive_effect()
let else_effect = match @ir.hot_region_else_ref(func, node_id) {
Some(region_ref) =>
OiDescriptorSkipOrderRegionFact::from_region(func, region_ref).proves_has_order_sensitive_effect()
None => false
}
then_effect || else_effect
}
_ => false
}
if region_order_sensitive_effect {
return {
live: true,
effect_fact: Some({ kind: DescriptorSkipOrderRegionEffect, }),
}
}
let mut child_order_sensitive_effect = false
for slot = 0; slot < node.child_count; slot = slot + 1 {
child_order_sensitive_effect = child_order_sensitive_effect ||
OiDescriptorSkipOrderFact::from_node(
func,
@ir.hot_child_get(func, node_id, slot),
).proves_must_preserve_operand_order()
}
{
live: true,
effect_fact: if child_order_sensitive_effect {
Some({ kind: DescriptorSkipOrderChildEffect, })
} else {
None
},
}
}
///|
fn OiDescriptorSkipOrderFact::proves_must_preserve_operand_order(
self : OiDescriptorSkipOrderFact,
) -> Bool {
self.live &&
(match self.effect_fact {
Some(fact) => fact.proves_order_sensitive_effect()
None => false
})
}
///|
fn OiDescriptorSkipOrderRegionFact::from_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> OiDescriptorSkipOrderRegionFact {
let root_count = @ir.hot_region_root_count(func, region_ref)
let mut has_order_sensitive_effect = false
for slot = 0; slot < root_count; slot = slot + 1 {
has_order_sensitive_effect = has_order_sensitive_effect ||
OiDescriptorSkipOrderFact::from_node(
func,
@ir.hot_region_get(func, region_ref, slot),
).proves_must_preserve_operand_order()
}
{
root_count,
effect_fact: if has_order_sensitive_effect {
Some({ kind: DescriptorSkipOrderRegionEffect, })
} else {
None
},
}
}
///|
fn OiDescriptorSkipOrderRegionFact::proves_has_order_sensitive_effect(
self : OiDescriptorSkipOrderRegionFact,
) -> Bool {
self.root_count > 0 &&
(match self.effect_fact {
Some(fact) => fact.proves_order_sensitive_effect()
None => false
})
}
///|
fn OiDescriptorInternalControlBlockRegionFact::from_bool(
effect_free : Bool,
) -> OiDescriptorInternalControlBlockRegionFact {
{
kind: if effect_free {
DescriptorInternalControlPayloadEffectFree
} else {
DescriptorInternalControlPayloadEffectful
},
}
}
///|
fn OiDescriptorInternalControlBlockRegionFact::proves_effect_free(
self : OiDescriptorInternalControlBlockRegionFact,
) -> Bool {
match self.kind {
DescriptorInternalControlPayloadEffectFree => true
DescriptorInternalControlPayloadEffectful => false
}
}
///|
fn OiDescriptorInternalControlChildEffectFact::from_bool(
effect_free : Bool,
) -> OiDescriptorInternalControlChildEffectFact {
{
kind: if effect_free {
DescriptorInternalControlPayloadEffectFree
} else {
DescriptorInternalControlPayloadEffectful
},
}
}
///|
fn OiDescriptorInternalControlChildEffectFact::proves_effect_free(
self : OiDescriptorInternalControlChildEffectFact,
) -> Bool {
match self.kind {
DescriptorInternalControlPayloadEffectFree => true
DescriptorInternalControlPayloadEffectful => false
}
}
///|
fn OiDescriptorInternalControlEffectFreeFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
owner_id : Int32,
node_id : Int32,
) -> OiDescriptorInternalControlEffectFreeFact {
// TNH descriptor-null-check removal can ignore internal branch/control edges
// when all reachable payload work is otherwise effect-free. Preserve that
// represented Binaryen `trapOnNull` subset as data: direct side-effect-free
// proof, an allowed internal-control shell, block-region effect facts, and
// recursively effect-free child facts.
let direct_side_effect_free = OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, node_id,
)
if direct_side_effect_free {
return {
kind: DescriptorInternalControlDirectSideEffectFree,
block_region_fact: None,
child_effect_fact: None,
}
}
let node = @ir.hot_node_get(func, node_id)
let (supported_internal_control_shape, block_region_effect_free) = match
node.op {
@ir.HotOp::Br | @ir.HotOp::BrIf =>
(
OiDescriptorInternalLabelFact::from_label(func, owner_id, node.imm0).proves_internal(),
true,
)
@ir.HotOp::BrTable => {
let mut labels_internal = OiDescriptorInternalLabelFact::from_label(
func,
owner_id,
@ir.hot_branch_table_default_target(func, node.imm0),
).proves_internal()
if labels_internal {
for label_id in @ir.hot_branch_table_targets(func, node.imm0) {
labels_internal = labels_internal &&
OiDescriptorInternalLabelFact::from_label(func, owner_id, label_id).proves_internal()
}
}
(labels_internal, true)
}
@ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail =>
(
OiDescriptorInternalLabelFact::from_label(func, owner_id, node.imm0).proves_internal(),
true,
)
@ir.HotOp::Block =>
(
true,
OiDescriptorInternalControlEffectFreeRegionFact::from_region(
func,
dead,
owner_id,
@ir.hot_region_body_ref(func, node_id),
).proves_effect_free(),
)
@ir.HotOp::Drop => (node.child_count == 1, true)
_ => (false, false)
}
if !supported_internal_control_shape || !block_region_effect_free {
return {
kind: DescriptorInternalControlUnsupported,
block_region_fact: Some(
OiDescriptorInternalControlBlockRegionFact::from_bool(
block_region_effect_free,
),
),
child_effect_fact: None,
}
}
let mut child_effect_free = true
for slot = 0; slot < node.child_count; slot = slot + 1 {
child_effect_free = child_effect_free &&
OiDescriptorInternalControlEffectFreeFact::from_node(
func,
dead,
owner_id,
@ir.hot_child_get(func, node_id, slot),
).proves_effect_free_allowing_internal_control()
}
{
kind: DescriptorInternalControlSupportedShape,
block_region_fact: Some(
OiDescriptorInternalControlBlockRegionFact::from_bool(
block_region_effect_free,
),
),
child_effect_fact: Some(
OiDescriptorInternalControlChildEffectFact::from_bool(child_effect_free),
),
}
}
///|
fn OiDescriptorInternalControlEffectFreeFact::proves_effect_free_allowing_internal_control(
self : OiDescriptorInternalControlEffectFreeFact,
) -> Bool {
match self.kind {
DescriptorInternalControlDirectSideEffectFree => true
DescriptorInternalControlSupportedShape =>
match (self.block_region_fact, self.child_effect_fact) {
(Some(block_fact), Some(child_fact)) =>
block_fact.proves_effect_free() && child_fact.proves_effect_free()
_ => false
}
DescriptorInternalControlUnsupported => false
}
}
///|
fn OiDescriptorInternalControlEffectFreeRegionFact::from_region(
func : @ir.HotFunc,
dead : @bitset.BitSet,
owner_id : Int32,
region_ref : @ir.HotRegionRef,
) -> OiDescriptorInternalControlEffectFreeRegionFact {
let root_count = @ir.hot_region_root_count(func, region_ref)
let mut effect_free = true
for slot = 0; slot < root_count; slot = slot + 1 {
effect_free = effect_free &&
OiDescriptorInternalControlEffectFreeFact::from_node(
func,
dead,
owner_id,
@ir.hot_region_get(func, region_ref, slot),
).proves_effect_free_allowing_internal_control()
}
{
root_count,
effect_fact: OiDescriptorInternalControlBlockRegionFact::from_bool(
effect_free,
),
}
}
///|
fn OiDescriptorInternalControlEffectFreeRegionFact::proves_effect_free(
self : OiDescriptorInternalControlEffectFreeRegionFact,
) -> Bool {
ignore(self.root_count)
self.effect_fact.proves_effect_free()
}
///|
fn optimize_instructions_try_simplify_ref_cast_desc_eq_descriptor_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let simplification_fact = match
OiDescriptorOperandSimplificationFact::from_node(func, node_id) {
Some(fact) => fact
None => return false
}
OiDescriptorOperandSimplificationRewritePlanFact::from_fact(
simplification_fact,
).apply(ctx, func, dead, node_id)
}
///|
priv struct OiResultShapeFact {
live : Bool
no_results : Bool
single_ref_result : Bool
single_nullable_ref_result : Bool
multivalue_result : Bool
}
///|
fn OiResultShapeFact::empty() -> OiResultShapeFact {
{
live: false,
no_results: false,
single_ref_result: false,
single_nullable_ref_result: false,
multivalue_result: false,
}
}
///|
fn OiResultShapeFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiResultShapeFact {
// Binaryen's OI descriptor and standalone ref.get_desc rewrites frequently
// gate on result arity/nullability before localizing children. Keep those
// gates as a reusable fact rather than scattered scanner helpers so the
// represented ChildLocalizer subset can compose with the descriptor facts.
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiResultShapeFact::empty()
}
let results = @ir.hot_type_results(func, @ir.hot_node_type(func, node_id))
let no_results = results.length() == 0
let single_ref_result = match results {
[@lib.ValType::RefTypeValType(_)] => true
_ => false
}
let single_nullable_ref_result = match results {
[@lib.ValType::RefTypeValType(ref_type)] => ref_type.is_nullable()
_ => false
}
{
live: true,
no_results,
single_ref_result,
single_nullable_ref_result,
multivalue_result: results.length() > 1,
}
}
///|
fn OiResultShapeFact::proves_no_results(self : OiResultShapeFact) -> Bool {
self.live && self.no_results
}
///|
fn OiResultShapeFact::proves_single_ref_result(
self : OiResultShapeFact,
) -> Bool {
self.live && self.single_ref_result
}
///|
fn OiResultShapeFact::proves_single_nullable_ref_result(
self : OiResultShapeFact,
) -> Bool {
self.live && self.single_nullable_ref_result
}
///|
fn OiResultShapeFact::proves_multivalue_result(
self : OiResultShapeFact,
) -> Bool {
self.live && self.multivalue_result
}
///|
priv struct OiStandaloneRefGetDescKnownNullLeafFact {
node_id : Int32
}
///|
fn OiStandaloneRefGetDescKnownNullLeafFact::new(
node_id : Int32,
) -> OiStandaloneRefGetDescKnownNullLeafFact {
{ node_id, }
}
///|
priv struct OiStandaloneRefGetDescNullableLeafMoveFact {
node_id : Int32
}
///|
fn OiStandaloneRefGetDescNullableLeafMoveFact::new(
node_id : Int32,
) -> OiStandaloneRefGetDescNullableLeafMoveFact {
{ node_id, }
}
///|
priv struct OiStandaloneRefGetDescNontrappingOperandLeafFact {
node_id : Int32
}
///|
fn OiStandaloneRefGetDescNontrappingOperandLeafFact::new(
node_id : Int32,
) -> OiStandaloneRefGetDescNontrappingOperandLeafFact {
{ node_id, }
}
///|
priv struct OiStandaloneRefGetDescLeafFact {
known_null_leaf : OiStandaloneRefGetDescKnownNullLeafFact?
nullable_leaf_move : OiStandaloneRefGetDescNullableLeafMoveFact?
nontrapping_operand_leaf : OiStandaloneRefGetDescNontrappingOperandLeafFact?
}
///|
fn OiStandaloneRefGetDescLeafFact::empty() -> OiStandaloneRefGetDescLeafFact {
{
known_null_leaf: None,
nullable_leaf_move: None,
nontrapping_operand_leaf: None,
}
}
///|
fn OiStandaloneRefGetDescLeafFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiStandaloneRefGetDescLeafFact {
// Binaryen v130's ref.get_desc path delegates to skipNonNullCast and
// trapOnNull, so Starshine keeps the represented leaf proofs explicit here:
// ref.null known-null children, nullable local/global leaves that carry a
// moved null check, and the nontrapping leaf operands allowed under the
// current narrow call-child boundary are separate payload facts rather than
// parallel leaf booleans.
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiStandaloneRefGetDescLeafFact::empty()
}
let node = @ir.hot_node_get(func, node_id)
let no_children = @ir.hot_child_count(func, node_id) == 0
let single_nullable_ref_result = OiResultShapeFact::from_node(func, node_id).proves_single_nullable_ref_result()
let known_null_leaf = match node.op {
@ir.HotOp::RefNull =>
if no_children && single_nullable_ref_result {
Some(OiStandaloneRefGetDescKnownNullLeafFact::new(node_id))
} else {
None
}
_ => None
}
let nullable_leaf_move = match node.op {
@ir.HotOp::LocalGet | @ir.HotOp::GlobalGet =>
if no_children && single_nullable_ref_result {
Some(OiStandaloneRefGetDescNullableLeafMoveFact::new(node_id))
} else {
None
}
_ => None
}
let nontrapping_operand_leaf = match node.op {
@ir.HotOp::LocalGet
| @ir.HotOp::GlobalGet
| @ir.HotOp::Const
| @ir.HotOp::RefNull
| @ir.HotOp::RefFunc =>
if no_children {
Some(OiStandaloneRefGetDescNontrappingOperandLeafFact::new(node_id))
} else {
None
}
_ => None
}
{ known_null_leaf, nullable_leaf_move, nontrapping_operand_leaf, }
}
///|
fn OiStandaloneRefGetDescLeafFact::proves_known_null_ref_null_leaf(
self : OiStandaloneRefGetDescLeafFact,
) -> Bool {
match self.known_null_leaf {
Some(fact) => {
ignore(fact.node_id)
true
}
None => false
}
}
///|
fn OiStandaloneRefGetDescLeafFact::proves_nullable_ref_leaf_move_allowed(
self : OiStandaloneRefGetDescLeafFact,
) -> Bool {
match self.nullable_leaf_move {
Some(fact) => {
ignore(fact.node_id)
true
}
None => false
}
}
///|
fn OiStandaloneRefGetDescLeafFact::proves_nontrapping_call_operand_leaf(
self : OiStandaloneRefGetDescLeafFact,
) -> Bool {
match self.nontrapping_operand_leaf {
Some(fact) => {
ignore(fact.node_id)
true
}
None => false
}
}
///|
priv struct OiStandaloneRefGetDescLocalTeeChildFact {
live : Bool
leaf_fact : OiStandaloneRefGetDescLeafFact
}
///|
fn OiStandaloneRefGetDescLocalTeeChildFact::empty() -> OiStandaloneRefGetDescLocalTeeChildFact {
{ live: false, leaf_fact: OiStandaloneRefGetDescLeafFact::empty(), }
}
///|
fn OiStandaloneRefGetDescLocalTeeChildFact::from_tee(
func : @ir.HotFunc,
tee_id : Int32,
) -> OiStandaloneRefGetDescLocalTeeChildFact {
// Binaryen v130 standalone ref.get_desc movement only looks through a
// one-child local.tee when that child is itself one of the represented leaf
// facts. Keep the one-child proof and child leaf payload together instead of
// copying leaf outcomes into parallel local.tee booleans.
if @ir.hot_child_count(func, tee_id) != 1 {
return OiStandaloneRefGetDescLocalTeeChildFact::empty()
}
{
live: true,
leaf_fact: OiStandaloneRefGetDescLeafFact::from_node(
func,
@ir.hot_child_get(func, tee_id, 0),
),
}
}
///|
fn OiStandaloneRefGetDescLocalTeeChildFact::proves_known_null_ref_null_leaf(
self : OiStandaloneRefGetDescLocalTeeChildFact,
) -> Bool {
self.live && self.leaf_fact.proves_known_null_ref_null_leaf()
}
///|
fn OiStandaloneRefGetDescLocalTeeChildFact::proves_nullable_ref_leaf_move_allowed(
self : OiStandaloneRefGetDescLocalTeeChildFact,
) -> Bool {
self.live && self.leaf_fact.proves_nullable_ref_leaf_move_allowed()
}
///|
priv struct OiStandaloneRefGetDescLocalTeeFact {
live : Bool
single_nullable_ref_result : Bool
child_fact : OiStandaloneRefGetDescLocalTeeChildFact
}
///|
fn OiStandaloneRefGetDescLocalTeeFact::empty() -> OiStandaloneRefGetDescLocalTeeFact {
{
live: false,
single_nullable_ref_result: false,
child_fact: OiStandaloneRefGetDescLocalTeeChildFact::empty(),
}
}
///|
fn OiStandaloneRefGetDescLocalTeeFact::from_node(
func : @ir.HotFunc,
tee_id : Int32,
) -> OiStandaloneRefGetDescLocalTeeFact {
// Represent the two local.tee ref.get_desc boundaries as data: local.tee of a
// known-null ref.null can be preserved before the trap, while local.tee of a
// nullable local/global leaf can carry the moved null check.
if tee_id < 0 || !@ir.hot_node_is_live(func, tee_id) {
return OiStandaloneRefGetDescLocalTeeFact::empty()
}
match @ir.hot_node_get(func, tee_id).op {
@ir.HotOp::LocalTee => ()
_ => return OiStandaloneRefGetDescLocalTeeFact::empty()
}
let single_nullable_ref_result = OiResultShapeFact::from_node(func, tee_id).proves_single_nullable_ref_result()
{
live: true,
single_nullable_ref_result,
child_fact: OiStandaloneRefGetDescLocalTeeChildFact::from_tee(func, tee_id),
}
}
///|
fn OiStandaloneRefGetDescLocalTeeFact::proves_known_null_child(
self : OiStandaloneRefGetDescLocalTeeFact,
) -> Bool {
self.live &&
self.single_nullable_ref_result &&
self.child_fact.proves_known_null_ref_null_leaf()
}
///|
fn OiStandaloneRefGetDescLocalTeeFact::proves_nullable_ref_leaf_move_allowed(
self : OiStandaloneRefGetDescLocalTeeFact,
) -> Bool {
self.live &&
self.single_nullable_ref_result &&
self.child_fact.proves_nullable_ref_leaf_move_allowed()
}
///|
priv struct OiStandaloneRefGetDescControlShapeFact {
live : Bool
multivalue_result : Bool
direct_disallowed_control : Bool
nested_disallowed_control_or_multivalue : Bool
}
///|
fn OiStandaloneRefGetDescControlShapeFact::empty() -> OiStandaloneRefGetDescControlShapeFact {
{
live: false,
multivalue_result: false,
direct_disallowed_control: false,
nested_disallowed_control_or_multivalue: false,
}
}
///|
fn OiStandaloneRefGetDescControlShapeFact::from_node(
func : @ir.HotFunc,
owner_id : Int32,
node_id : Int32,
) -> OiStandaloneRefGetDescControlShapeFact {
// Standalone ref.get_desc null-check movement can only preserve a small
// self-contained control subset. Summarize that represented shape in a fact:
// multivalue children, disallowed direct control roots, and nested disallowed
// children. This keeps the compatibility helper from owning a recursive
// scanner while preserving the previous fail-closed control/EH boundary.
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return OiStandaloneRefGetDescControlShapeFact::empty()
}
let multivalue_result = OiResultShapeFact::from_node(func, node_id).proves_multivalue_result()
if multivalue_result {
return {
live: true,
multivalue_result,
direct_disallowed_control: false,
nested_disallowed_control_or_multivalue: false,
}
}
let node = @ir.hot_node_get(func, node_id)
let direct_disallowed_control = match node.op {
@ir.HotOp::Br | @ir.HotOp::BrIf =>
!OiDescriptorInternalLabelFact::from_label(func, owner_id, node.imm0).proves_internal()
@ir.HotOp::BrTable => {
let mut disallowed = !OiDescriptorInternalLabelFact::from_label(
func,
owner_id,
@ir.hot_branch_table_default_target(func, node.imm0),
).proves_internal()
if !disallowed {
for label_id in @ir.hot_branch_table_targets(func, node.imm0) {
disallowed = disallowed ||
!OiDescriptorInternalLabelFact::from_label(func, owner_id, label_id).proves_internal()
}
}
disallowed
}
@ir.HotOp::BrOnNonNull =>
!OiDescriptorInternalLabelFact::from_label(func, owner_id, node.imm0).proves_internal()
@ir.HotOp::Block
| @ir.HotOp::Loop
| @ir.HotOp::If
| @ir.HotOp::Try
| @ir.HotOp::TryTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail
| @ir.HotOp::Return
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate
| @ir.HotOp::Catch
| @ir.HotOp::CatchAll => true
_ => false
}
if direct_disallowed_control {
return {
live: true,
multivalue_result,
direct_disallowed_control,
nested_disallowed_control_or_multivalue: false,
}
}
let mut nested_disallowed_control_or_multivalue = false
for slot = 0; slot < node.child_count; slot = slot + 1 {
nested_disallowed_control_or_multivalue = nested_disallowed_control_or_multivalue ||
OiStandaloneRefGetDescControlShapeFact::from_node(
func,
owner_id,
@ir.hot_child_get(func, node_id, slot),
).proves_disallowed_control_or_multivalue()
}
{
live: true,
multivalue_result,
direct_disallowed_control,
nested_disallowed_control_or_multivalue,
}
}
///|
fn OiStandaloneRefGetDescControlShapeFact::proves_disallowed_control_or_multivalue(
self : OiStandaloneRefGetDescControlShapeFact,
) -> Bool {
self.live &&
(
self.multivalue_result ||
self.direct_disallowed_control ||
self.nested_disallowed_control_or_multivalue
)
}
///|
priv struct OiStandaloneRefGetDescLabelOwnerFact {
valid_label : Bool
owner_matches : Bool
}
///|
fn OiStandaloneRefGetDescLabelOwnerFact::from_label(
func : @ir.HotFunc,
label_id : Int32,
owner_id : Int32,
) -> OiStandaloneRefGetDescLabelOwnerFact {
// Binaryen-style standalone ref.get_desc movement needs to distinguish
// branches that stay inside the moved block from escaping branches. Keep that
// label-owner proof as data beside the root-shape facts instead of a helper
// predicate.
let valid_label = label_id >= 0 && label_id < func.labels.length()
let owner_matches = valid_label &&
@ir.hot_label_owner(func, label_id) == owner_id
{ valid_label, owner_matches, }
}
///|
fn OiStandaloneRefGetDescLabelOwnerFact::proves_owner(
self : OiStandaloneRefGetDescLabelOwnerFact,
) -> Bool {
self.valid_label && self.owner_matches
}
///|
fn OiStandaloneRefGetDescLabelOwnerFact::proves_escape(
self : OiStandaloneRefGetDescLabelOwnerFact,
) -> Bool {
self.valid_label && !self.owner_matches
}
///|
priv struct OiStandaloneRefGetDescRootShapeFact {
live : Bool
no_results : Bool
disallowed_control_or_multivalue : Bool
escaping_plain_branch_root : Bool
internal_br_on_non_null_root : Bool
escaping_br_root : Bool
escaping_br_if_root : Bool
escaping_br_table_root : Bool
internal_br_on_cast_root : Bool
br_on_null_local_root : Bool
internal_br_on_null_trampoline_root : Bool
}
///|
fn OiStandaloneRefGetDescRootShapeFact::empty() -> OiStandaloneRefGetDescRootShapeFact {
{
live: false,
no_results: false,
disallowed_control_or_multivalue: false,
escaping_plain_branch_root: false,
internal_br_on_non_null_root: false,
escaping_br_root: false,
escaping_br_if_root: false,
escaping_br_table_root: false,
internal_br_on_cast_root: false,
br_on_null_local_root: false,
internal_br_on_null_trampoline_root: false,
}
}
///|
fn OiStandaloneRefGetDescRootShapeFact::from_root(
func : @ir.HotFunc,
block_id : Int32,
root_id : Int32,
) -> OiStandaloneRefGetDescRootShapeFact {
// Standalone ref.get_desc null-check movement accepts only a represented
// block-root subset: internal br_on_non_null, escaping br_if with safe
// children, internal br_on_cast/fail over a nullable local, br_on_null local
// roots, and a tiny br_on_null trampoline block. Keep those root predicates in
// a fact so callers no longer own parallel root-shape scanners.
if root_id < 0 || !@ir.hot_node_is_live(func, root_id) {
return OiStandaloneRefGetDescRootShapeFact::empty()
}
let root = @ir.hot_node_get(func, root_id)
let no_results = OiResultShapeFact::from_node(func, root_id).proves_no_results()
let disallowed_control_or_multivalue = OiStandaloneRefGetDescControlShapeFact::from_node(
func, block_id, root_id,
).proves_disallowed_control_or_multivalue()
let escaping_plain_branch_root = match root.op {
@ir.HotOp::Br => {
let label = OiStandaloneRefGetDescLabelOwnerFact::from_label(
func,
root.imm0,
block_id,
)
label.valid_label &&
!label.owner_matches &&
@ir.hot_label_branch_arity(func, root.imm0) == 1 &&
root.child_count == 1 &&
@ir.hot_node_get(func, @ir.hot_child_get(func, root_id, 0)).op
is @ir.HotOp::LocalGet
}
@ir.HotOp::BrTable => {
let default_label = @ir.hot_branch_table_default_target(func, root.imm0)
let label = OiStandaloneRefGetDescLabelOwnerFact::from_label(
func, default_label, block_id,
)
let mut same_outer_target = label.valid_label &&
!label.owner_matches &&
@ir.hot_label_branch_arity(func, default_label) == 1
for target in @ir.hot_branch_table_targets(func, root.imm0) {
same_outer_target = same_outer_target && target == default_label
}
same_outer_target &&
root.child_count == 2 &&
@ir.hot_node_get(func, @ir.hot_child_get(func, root_id, 0)).op
is @ir.HotOp::LocalGet &&
@ir.hot_node_get(func, @ir.hot_child_get(func, root_id, 1)).op
is @ir.HotOp::LocalGet
}
_ => false
}
let internal_br_on_non_null_root = match root.op {
@ir.HotOp::BrOnNonNull =>
OiDescriptorInternalLabelFact::from_label(func, block_id, root.imm0).proves_internal()
_ => false
}
let mut child_disallowed = false
for slot = 0; slot < root.child_count; slot = slot + 1 {
child_disallowed = child_disallowed ||
OiStandaloneRefGetDescControlShapeFact::from_node(
func,
block_id,
@ir.hot_child_get(func, root_id, slot),
).proves_disallowed_control_or_multivalue()
}
let escaping_br_root = match root.op {
@ir.HotOp::Br =>
no_results &&
!child_disallowed &&
OiStandaloneRefGetDescLabelOwnerFact::from_label(
func,
root.imm0,
block_id,
).proves_escape()
_ => false
}
let escaping_br_if_root = match root.op {
@ir.HotOp::BrIf =>
!OiDescriptorInternalLabelFact::from_label(func, block_id, root.imm0).proves_internal() &&
no_results &&
!child_disallowed
_ => false
}
let escaping_br_table_root = match root.op {
@ir.HotOp::BrTable => {
let mut all_escape = OiStandaloneRefGetDescLabelOwnerFact::from_label(
func,
@ir.hot_branch_table_default_target(func, root.imm0),
block_id,
).proves_escape()
for target in @ir.hot_branch_table_targets(func, root.imm0) {
all_escape = all_escape &&
OiStandaloneRefGetDescLabelOwnerFact::from_label(
func, target, block_id,
).proves_escape()
}
no_results && !child_disallowed && all_escape
}
_ => false
}
let internal_br_on_cast_root = match root.op {
@ir.HotOp::BrOnCast | @ir.HotOp::BrOnCastFail =>
if !OiDescriptorInternalLabelFact::from_label(func, block_id, root.imm0).proves_internal() ||
@ir.hot_child_count(func, root_id) != 1 {
false
} else {
let cast_operands = match @ir.hot_node_exact_instr(func, root_id) {
Some(
@lib.Instruction::BrOnCast(
_,
@lib.CastOp(src_null, dst_null, src_exact, dst_exact),
src_heap,
dst_heap
)
) =>
Some((src_null, dst_null, src_exact, dst_exact, src_heap, dst_heap))
Some(
@lib.Instruction::BrOnCastFail(
_,
@lib.CastOp(src_null, dst_null, src_exact, dst_exact),
src_heap,
dst_heap
)
) =>
Some((src_null, dst_null, src_exact, dst_exact, src_heap, dst_heap))
_ => None
}
match cast_operands {
Some(
(
source_nullable,
target_nullable,
source_exact,
target_exact,
source_heap,
target_heap,
)
) =>
if !source_nullable ||
target_nullable ||
source_exact != target_exact ||
source_heap != target_heap {
false
} else {
let nullable_ref = @ir.hot_child_get(func, root_id, 0)
match @ir.hot_node_get(func, nullable_ref).op {
@ir.HotOp::LocalGet =>
match
@ir.hot_type_results(
func,
@ir.hot_node_type(func, nullable_ref),
) {
[@lib.ValType::RefTypeValType(ref_type)] =>
ref_type.is_nullable() &&
ref_type.is_exact() == source_exact &&
ref_type.get_heap_type() == source_heap
_ => false
}
_ => false
}
}
None => false
}
}
_ => false
}
let br_on_null_local_root = match root.op {
@ir.HotOp::BrOnNull =>
OiStandaloneRefGetDescLabelOwnerFact::from_label(
func,
root.imm0,
block_id,
).proves_owner() &&
@ir.hot_child_count(func, root_id) == 1 &&
(match @ir.hot_node_get(func, @ir.hot_child_get(func, root_id, 0)).op {
@ir.HotOp::LocalGet =>
OiResultShapeFact::from_node(
func,
@ir.hot_child_get(func, root_id, 0),
).proves_single_nullable_ref_result()
_ => false
})
_ => false
}
let internal_br_on_null_trampoline_root = match root.op {
@ir.HotOp::Block =>
if !no_results {
false
} else {
let inner_label = @ir.hot_control_node_label(func, root_id)
if !OiStandaloneRefGetDescLabelOwnerFact::from_label(
func, inner_label, root_id,
).proves_owner() ||
@ir.hot_label_branch_arity(func, inner_label) != 0 {
false
} else {
let body = @ir.hot_region_body_ref(func, root_id)
let root_count = @ir.hot_region_root_count(func, body)
if root_count == 1 {
let branch_id = @ir.hot_region_get(func, body, 0)
let branch = @ir.hot_node_get(func, branch_id)
match branch.op {
@ir.HotOp::Br =>
OiStandaloneRefGetDescLabelOwnerFact::from_label(
func,
branch.imm0,
block_id,
).proves_owner() &&
@ir.hot_child_count(func, branch_id) == 1 &&
OiStandaloneRefGetDescRootShapeFact::from_root(
func,
root_id,
@ir.hot_child_get(func, branch_id, 0),
).proves_br_on_null_local_root()
_ => false
}
} else if root_count == 2 {
let null_branch = @ir.hot_region_get(func, body, 0)
let value_branch = @ir.hot_region_get(func, body, 1)
if !OiStandaloneRefGetDescRootShapeFact::from_root(
func, root_id, null_branch,
).proves_br_on_null_local_root() {
false
} else {
let branch = @ir.hot_node_get(func, value_branch)
match branch.op {
@ir.HotOp::Br =>
OiStandaloneRefGetDescLabelOwnerFact::from_label(
func,
branch.imm0,
block_id,
).proves_owner() &&
@ir.hot_child_count(func, value_branch) == 0
_ => false
}
}
} else {
false
}
}
}
_ => false
}
{
live: true,
no_results,
disallowed_control_or_multivalue,
escaping_plain_branch_root,
internal_br_on_non_null_root,
escaping_br_root,
escaping_br_if_root,
escaping_br_table_root,
internal_br_on_cast_root,
br_on_null_local_root,
internal_br_on_null_trampoline_root,
}
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_escaping_plain_branch_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.no_results && self.escaping_plain_branch_root
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_internal_br_on_non_null_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.internal_br_on_non_null_root
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_escaping_br_if_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.escaping_br_if_root
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_escaping_br_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.escaping_br_root
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_escaping_br_table_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.escaping_br_table_root
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_internal_br_on_cast_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.internal_br_on_cast_root
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_br_on_null_local_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.br_on_null_local_root
}
///|
fn OiStandaloneRefGetDescRootShapeFact::proves_internal_br_on_null_trampoline_root(
self : OiStandaloneRefGetDescRootShapeFact,
) -> Bool {
self.live && self.internal_br_on_null_trampoline_root
}
///|
priv struct OiStandaloneRefGetDescBlockMovePrefixPayloadFact {
roots_allowed : Bool
saw_internal_ref_branch : Bool
}
///|
fn OiStandaloneRefGetDescBlockMovePrefixPayloadFact::empty() -> OiStandaloneRefGetDescBlockMovePrefixPayloadFact {
{ roots_allowed: false, saw_internal_ref_branch: false, }
}
///|
fn OiStandaloneRefGetDescBlockMovePrefixPayloadFact::from_region_prefix(
func : @ir.HotFunc,
block_id : Int32,
body : @ir.HotRegionRef,
root_count : Int32,
) -> OiStandaloneRefGetDescBlockMovePrefixPayloadFact {
// Source-order prefix fact for the represented standalone ref.get_desc block
// movement path: every non-final root must be void and either ordinary
// control-safe debris or one of the internal ref-branch handoff roots that can
// feed a final `ref.null` fallback.
let mut roots_allowed = true
let mut saw_internal_ref_branch = false
for slot = 0; slot < root_count - 1; slot = slot + 1 {
let root_id = @ir.hot_region_get(func, body, slot)
let root_fact = OiStandaloneRefGetDescRootShapeFact::from_root(
func, block_id, root_id,
)
if !root_fact.no_results {
roots_allowed = false
} else if root_fact.proves_internal_br_on_null_trampoline_root() {
saw_internal_ref_branch = true
} else if root_fact.proves_escaping_br_if_root() {
()
} else if root_fact.proves_escaping_plain_branch_root() ||
root_fact.proves_escaping_br_root() ||
root_fact.proves_escaping_br_table_root() {
()
} else if root_fact.disallowed_control_or_multivalue {
roots_allowed = false
} else if root_fact.proves_internal_br_on_non_null_root() {
saw_internal_ref_branch = true
}
}
{ roots_allowed, saw_internal_ref_branch, }
}
///|
priv struct OiStandaloneRefGetDescBlockMoveFinalPayloadFact {
root_allowed : Bool
}
///|
fn OiStandaloneRefGetDescBlockMoveFinalPayloadFact::empty() -> OiStandaloneRefGetDescBlockMoveFinalPayloadFact {
{ root_allowed: false, }
}
///|
fn OiStandaloneRefGetDescBlockMoveFinalPayloadFact::from_final_root(
func : @ir.HotFunc,
block_id : Int32,
final_id : Int32,
prefix_payload : OiStandaloneRefGetDescBlockMovePrefixPayloadFact,
) -> OiStandaloneRefGetDescBlockMoveFinalPayloadFact {
// The final root is the source-ordered nullable/control handoff: either the
// surviving nullable value, the represented ref.null fallback after an
// internal ref branch, or a terminating branch/cast root.
let final_fact = OiStandaloneRefGetDescRootShapeFact::from_root(
func, block_id, final_id,
)
let root_allowed = match @ir.hot_node_get(func, final_id).op {
@ir.HotOp::LocalGet =>
OiResultShapeFact::from_node(func, final_id).proves_single_nullable_ref_result() &&
!final_fact.disallowed_control_or_multivalue
@ir.HotOp::RefNull =>
prefix_payload.saw_internal_ref_branch &&
OiResultShapeFact::from_node(func, final_id).proves_single_nullable_ref_result() &&
!final_fact.disallowed_control_or_multivalue
@ir.HotOp::Br | @ir.HotOp::BrTable =>
final_fact.proves_escaping_br_root() ||
final_fact.proves_escaping_br_table_root() ||
(
final_fact.no_results &&
(
!final_fact.disallowed_control_or_multivalue ||
final_fact.proves_escaping_plain_branch_root()
)
)
@ir.HotOp::BrOnCast | @ir.HotOp::BrOnCastFail =>
final_fact.proves_internal_br_on_cast_root()
_ => false
}
{ root_allowed, }
}
///|
priv struct OiStandaloneRefGetDescBlockMoveFact {
live : Bool
single_nullable_ref_result : Bool
nonempty_region : Bool
prefix_payload : OiStandaloneRefGetDescBlockMovePrefixPayloadFact
final_payload : OiStandaloneRefGetDescBlockMoveFinalPayloadFact
}
///|
fn OiStandaloneRefGetDescBlockMoveFact::from_block(
func : @ir.HotFunc,
block_id : Int32,
) -> OiStandaloneRefGetDescBlockMoveFact {
// This fact is the represented standalone ref.get_desc block-child proof: all
// prefix roots must be void, free of unsupported nested control/multivalue,
// or one of the supported branch roots, and the final root must preserve the
// nullable reference/control handoff shape. Prefix and final payload facts
// keep those two source-order checks explicit instead of open movement flags.
let empty : OiStandaloneRefGetDescBlockMoveFact = {
live: false,
single_nullable_ref_result: false,
nonempty_region: false,
prefix_payload: OiStandaloneRefGetDescBlockMovePrefixPayloadFact::empty(),
final_payload: OiStandaloneRefGetDescBlockMoveFinalPayloadFact::empty(),
}
if block_id < 0 || !@ir.hot_node_is_live(func, block_id) {
return empty
}
match @ir.hot_node_get(func, block_id).op {
@ir.HotOp::Block => ()
_ => return empty
}
let single_nullable_ref_result = OiResultShapeFact::from_node(func, block_id).proves_single_nullable_ref_result()
if !single_nullable_ref_result {
return { ..empty, live: true, }
}
let body = @ir.hot_region_body_ref(func, block_id)
let root_count = @ir.hot_region_root_count(func, body)
if root_count == 0 {
return { ..empty, live: true, single_nullable_ref_result, }
}
let prefix_payload = OiStandaloneRefGetDescBlockMovePrefixPayloadFact::from_region_prefix(
func, block_id, body, root_count,
)
let final_id = @ir.hot_region_get(func, body, root_count - 1)
let final_payload = OiStandaloneRefGetDescBlockMoveFinalPayloadFact::from_final_root(
func, block_id, final_id, prefix_payload,
)
{
live: true,
single_nullable_ref_result,
nonempty_region: true,
prefix_payload,
final_payload,
}
}
///|
fn OiStandaloneRefGetDescBlockMoveFact::proves_nullable_block_move_allowed(
self : OiStandaloneRefGetDescBlockMoveFact,
) -> Bool {
self.live &&
self.single_nullable_ref_result &&
self.nonempty_region &&
self.prefix_payload.roots_allowed &&
self.final_payload.root_allowed
}
///|
priv struct OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact {
branch_free_roots : Bool
roots_void_control_allowed : Bool
}
///|
fn OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact::empty() -> OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact {
{ branch_free_roots: false, roots_void_control_allowed: false, }
}
///|
fn OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact::from_region_prefix(
func : @ir.HotFunc,
block_id : Int32,
body : @ir.HotRegionRef,
root_count : Int32,
) -> OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact {
// Source-order prefix fact for the represented known-null standalone
// ref.get_desc block path: every non-final root must be void/control-safe and
// must not be a branch root, matching the previous narrow Binaryen-style
// skipNonNullCast operand movement boundary without an open whitelist flag.
let mut branch_free_roots = true
let mut roots_void_control_allowed = true
for slot = 0; slot < root_count - 1; slot = slot + 1 {
let root_id = @ir.hot_region_get(func, body, slot)
match @ir.hot_node_get(func, root_id).op {
@ir.HotOp::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail => branch_free_roots = false
_ => ()
}
let control_fact = OiStandaloneRefGetDescControlShapeFact::from_node(
func, block_id, root_id,
)
if !OiResultShapeFact::from_node(func, root_id).proves_no_results() ||
control_fact.proves_disallowed_control_or_multivalue() {
roots_void_control_allowed = false
}
}
{ branch_free_roots, roots_void_control_allowed, }
}
///|
priv struct OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact {
known_null_ref_null_leaf : Bool
control_allowed : Bool
}
///|
fn OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact::empty() -> OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact {
{ known_null_ref_null_leaf: false, control_allowed: false, }
}
///|
fn OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact::from_final_root(
func : @ir.HotFunc,
block_id : Int32,
final_id : Int32,
) -> OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact {
// Final-root payload for the known-null standalone ref.get_desc block path:
// the result root must be the represented ref.null leaf and must not carry
// unsupported nested control/multivalue effects.
let final_control_fact = OiStandaloneRefGetDescControlShapeFact::from_node(
func, block_id, final_id,
)
{
known_null_ref_null_leaf: OiStandaloneRefGetDescLeafFact::from_node(
func, final_id,
).proves_known_null_ref_null_leaf(),
control_allowed: !final_control_fact.proves_disallowed_control_or_multivalue(),
}
}
///|
priv struct OiStandaloneRefGetDescKnownNullBlockFact {
live : Bool
single_nullable_ref_result : Bool
nonempty_region : Bool
prefix_payload : OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact
final_payload : OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact
}
///|
fn OiStandaloneRefGetDescKnownNullBlockFact::from_block(
func : @ir.HotFunc,
block_id : Int32,
) -> OiStandaloneRefGetDescKnownNullBlockFact {
// Known-null standalone ref.get_desc block operands are the previous narrow
// branch-free block subset expressed as a fact: void/control-safe prefix
// roots, no branch roots, and a final ref.null leaf with no nested unsupported
// control or multivalue result.
let empty : OiStandaloneRefGetDescKnownNullBlockFact = {
live: false,
single_nullable_ref_result: false,
nonempty_region: false,
prefix_payload: OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact::empty(),
final_payload: OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact::empty(),
}
if block_id < 0 || !@ir.hot_node_is_live(func, block_id) {
return empty
}
match @ir.hot_node_get(func, block_id).op {
@ir.HotOp::Block => ()
_ => return empty
}
let single_nullable_ref_result = OiResultShapeFact::from_node(func, block_id).proves_single_nullable_ref_result()
if !single_nullable_ref_result {
return { ..empty, live: true, }
}
let body = @ir.hot_region_body_ref(func, block_id)
let root_count = @ir.hot_region_root_count(func, body)
if root_count == 0 {
return { ..empty, live: true, single_nullable_ref_result, }
}
let prefix_payload = OiStandaloneRefGetDescKnownNullBlockPrefixPayloadFact::from_region_prefix(
func, block_id, body, root_count,
)
let final_id = @ir.hot_region_get(func, body, root_count - 1)
let final_payload = OiStandaloneRefGetDescKnownNullBlockFinalPayloadFact::from_final_root(
func, block_id, final_id,
)
{
live: true,
single_nullable_ref_result,
nonempty_region: true,
prefix_payload,
final_payload,
}
}
///|
fn OiStandaloneRefGetDescKnownNullBlockFact::proves_known_null_child(
self : OiStandaloneRefGetDescKnownNullBlockFact,
) -> Bool {
self.live &&
self.single_nullable_ref_result &&
self.nonempty_region &&
self.prefix_payload.branch_free_roots &&
self.prefix_payload.roots_void_control_allowed &&
self.final_payload.known_null_ref_null_leaf &&
self.final_payload.control_allowed
}
///|
priv struct OiStandaloneRefGetDescCallOperandLeafFact {
all_nontrapping_leaves : Bool
}
///|
fn OiStandaloneRefGetDescCallOperandLeafFact::empty() -> OiStandaloneRefGetDescCallOperandLeafFact {
{ all_nontrapping_leaves: false, }
}
///|
fn OiStandaloneRefGetDescCallOperandLeafFact::from_call(
func : @ir.HotFunc,
call_id : Int32,
) -> OiStandaloneRefGetDescCallOperandLeafFact {
// Standalone ref.get_desc call-child movement keeps Binaryen's skipNonNullCast
// null-check motion bounded to calls whose operands are represented simple
// nontrapping leaves, so the operand traversal is an explicit leaf fact rather
// than an aggregate boolean on the call movement fact.
let mut all_nontrapping_leaves = true
for slot = 0; slot < @ir.hot_child_count(func, call_id); slot = slot + 1 {
all_nontrapping_leaves = all_nontrapping_leaves &&
OiStandaloneRefGetDescLeafFact::from_node(
func,
@ir.hot_child_get(func, call_id, slot),
).proves_nontrapping_call_operand_leaf()
}
{ all_nontrapping_leaves, }
}
///|
fn OiStandaloneRefGetDescCallOperandLeafFact::proves_all_nontrapping_leaves(
self : OiStandaloneRefGetDescCallOperandLeafFact,
) -> Bool {
self.all_nontrapping_leaves
}
///|
priv struct OiStandaloneRefGetDescCallMoveFact {
live : Bool
single_ref_result : Bool
operand_leaf_fact : OiStandaloneRefGetDescCallOperandLeafFact
}
///|
fn OiStandaloneRefGetDescCallMoveFact::from_call(
func : @ir.HotFunc,
call_id : Int32,
) -> OiStandaloneRefGetDescCallMoveFact {
// The current represented call-child movement subset keeps Binaryen's
// skipNonNullCast idea but only for calls whose operands are proven by the
// nested leaf fact to avoid observable traps or effects before the moved
// ref.as_non_null check.
let empty = {
live: false,
single_ref_result: false,
operand_leaf_fact: OiStandaloneRefGetDescCallOperandLeafFact::empty(),
}
if call_id < 0 || !@ir.hot_node_is_live(func, call_id) {
return empty
}
match @ir.hot_node_get(func, call_id).op {
@ir.HotOp::Call | @ir.HotOp::CallIndirect => ()
_ => return empty
}
let single_ref_result = OiResultShapeFact::from_node(func, call_id).proves_single_ref_result()
if !single_ref_result {
return { ..empty, live: true, }
}
{
live: true,
single_ref_result,
operand_leaf_fact: OiStandaloneRefGetDescCallOperandLeafFact::from_call(
func, call_id,
),
}
}
///|
fn OiStandaloneRefGetDescCallMoveFact::proves_nullable_ref_call_move_allowed(
self : OiStandaloneRefGetDescCallMoveFact,
) -> Bool {
self.live &&
self.single_ref_result &&
self.operand_leaf_fact.proves_all_nontrapping_leaves()
}
///|
fn optimize_instructions_try_trap_standalone_ref_get_desc_known_null_child(
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
) -> Bool {
let get_desc_fact = match OiRefGetDescFact::from_node(func, descriptor_id) {
Some(fact) => fact
None => return false
}
let null_trap_fact = OiNullTrapFact::for_operand(
func,
get_desc_fact.child_id,
caller_scope_traps=true,
)
match OiNullTrapRewriteFact::from_trap_fact(null_trap_fact).kind {
NullTrapReplaceParentWithUnreachable => {
optimize_instructions_replace_with_unreachable(ctx, func, descriptor_id)
return true
}
_ => ()
}
match OiRefGetDescKnownNullChildFact::from_get_desc(func, get_desc_fact) {
Some(null_child_fact) => null_child_fact.apply(ctx, func, descriptor_id)
None => false
}
}
///|
fn optimize_instructions_try_move_standalone_ref_get_desc_null_check(
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
) -> Bool {
let get_desc_fact = match OiRefGetDescFact::from_node(func, descriptor_id) {
Some(fact) => fact
None => return false
}
// The standalone rewrite admits a small escaping-branch block family. A
// descriptor cast must use the stricter descriptor-operand proof instead:
// moving its null check can let the cast exactify across an escaping branch.
for parent_id = 0; parent_id < func.nodes.length(); parent_id = parent_id + 1 {
if @ir.hot_node_is_live(func, parent_id) &&
(
@ir.hot_node_get(func, parent_id).op is @ir.HotOp::RefCastDescEq ||
@ir.hot_node_get(func, parent_id).op is @ir.HotOp::RefTestDesc
) &&
@ir.hot_child_count(func, parent_id) > 1 &&
@ir.hot_child_get(func, parent_id, 1) == descriptor_id {
return false
}
}
if ctx.traps_never_happen {
let mut is_standalone_root = false
for root_idx = 0
root_idx < @ir.hot_root_count(func)
root_idx = root_idx + 1 {
let root_id = @ir.hot_root_get(func, root_idx)
if root_id == descriptor_id ||
(
@ir.hot_node_get(func, root_id).op is @ir.HotOp::LocalSet &&
@ir.hot_child_count(func, root_id) == 1 &&
@ir.hot_child_get(func, root_id, 0) == descriptor_id
) {
// Lifetime capture stores a standalone result in a scratch local.
// That carrier does not change the descriptor's own null trap.
is_standalone_root = true
break
}
}
if !is_standalone_root {
return false
}
}
match
OiRefGetDescNullCheckMoveFact::from_get_desc(
func,
get_desc_fact,
RefGetDescStandaloneNullCheckMove,
) {
Some(move_fact) => move_fact.apply(ctx, func, descriptor_id)
None => false
}
}
///|
fn optimize_instructions_try_move_ref_get_desc_child_null_check(
ctx : HotPassContext,
func : @ir.HotFunc,
descriptor_id : Int32,
) -> Bool {
let get_desc_fact = match OiRefGetDescFact::from_node(func, descriptor_id) {
Some(fact) => fact
None => return false
}
match
OiRefGetDescNullCheckMoveFact::from_get_desc(
func,
get_desc_fact,
RefGetDescDescriptorOperandNullCheckMove,
) {
Some(move_fact) => move_fact.apply(ctx, func, descriptor_id)
None => false
}
}
///|
fn optimize_instructions_try_fold_ref_cast_desc_eq_exact_descriptor_miss(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let miss_fact = match
OiDescriptorExactMissFact::from_node(ctx, func, dead, node_id) {
Some(fact) => fact
None => return false
}
match OiDescriptorExactMissRewritePlanFact::from_miss(ctx, miss_fact) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
fn optimize_instructions_try_narrow_ref_cast_desc_eq_result_type(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let cast_fact = match OiDescriptorCastFact::from_node(func, node_id) {
Some(fact) => fact
None => return false
}
let narrowing_fact = OiDescriptorResultNarrowingFact::from_cast(
ctx, func, cast_fact, false,
)
match
OiDescriptorResultNarrowingRewritePlanFact::from_cast_root(narrowing_fact) {
Some(plan_fact) => return plan_fact.apply(ctx, func, node_id)
None => ()
}
match OiDescriptorCastTighteningFact::from_cast(ctx, func, cast_fact) {
Some(tightening_fact) =>
match
OiDescriptorCastTighteningRewritePlanFact::from_fact(tightening_fact) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
None => false
}
}
///|
fn optimize_instructions_try_fold_ref_cast_desc_eq_null(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
let null_fact = match
OiDescriptorNullCastFact::from_node(ctx, func, dead, node_id) {
Some(fact) => fact
None => return false
}
match
OiDescriptorNullCastRewritePlanFact::from_null_cast(
null_fact,
OiDescriptorNullCastRewriteScopeFact::full_known_null_scope(),
) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv enum OiDescriptorAllocationEraseKind {
DescriptorAllocationMustPreserve
DescriptorAllocationEraseStructNewDefault
}
///|
priv struct OiDescriptorAllocationEraseFact {
live : Bool
struct_new_default : Bool
kind : OiDescriptorAllocationEraseKind
}
///|
fn OiDescriptorAllocationEraseFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiDescriptorAllocationEraseFact {
// Binaryen v130 can drop allocation-only descriptor operands while replacing
// descriptor casts/tests with statically-known results. Keep Starshine's
// represented allocation subset as an explicit result kind instead of hiding
// it as a raw instruction whitelist or parallel `can_erase` predicate in the
// descriptor erasure fact.
let struct_new_default = match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::StructNewDefault(_)) => true
_ => false
}
let kind = if struct_new_default {
DescriptorAllocationEraseStructNewDefault
} else {
DescriptorAllocationMustPreserve
}
{ live: true, struct_new_default, kind, }
}
///|
fn OiDescriptorAllocationEraseFact::proves_can_erase(
self : OiDescriptorAllocationEraseFact,
) -> Bool {
if !self.live {
return false
}
match self.kind {
DescriptorAllocationEraseStructNewDefault => self.struct_new_default
DescriptorAllocationMustPreserve => false
}
}
///|
priv enum OiDescriptorNullableChildEraseTnhKind {
DescriptorNullableChildMustPreserve
DescriptorNullableChildEraseSideEffectFree
DescriptorNullableChildEraseBlockInternalControl
DescriptorNullableChildEraseIfNullCheckMove
}
///|
priv struct OiDescriptorNullableChildEraseTnhFact {
live : Bool
side_effect_free : Bool
block_root : Bool
block_nonempty : Bool
block_no_escaping_branch_control : Bool
block_children_effect_free_internal_control : Bool
if_no_effect_move_fact : OiDescriptorNullableIfNoEffectMoveFact
kind : OiDescriptorNullableChildEraseTnhKind
}
///|
fn OiDescriptorNullableChildEraseTnhFact::from_child(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDescriptorNullableChildEraseTnhFact {
// Binaryen v130's traps-never-happen mode may erase a descriptor-side null
// check when the nullable child can be discarded or localized safely. This is
// the represented Starshine subset: already side-effect-free children, blocks
// whose roots are internally-control-safe, and the existing nullable-if
// movement proof. It is intentionally narrower than full ChildLocalizer and
// EffectAnalyzer parity.
let side_effect_free = OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, node_id,
)
let node = @ir.hot_node_get(func, node_id)
let mut block_root = false
let mut block_nonempty = false
let mut block_no_escaping_branch_control = false
let mut block_children_effect_free_internal_control = false
let if_no_effect_move_fact = OiDescriptorNullableIfNoEffectMoveFact::from_if(
func, dead, node_id,
)
match node.op {
@ir.HotOp::Block => {
block_root = true
let body = @ir.hot_region_body_ref(func, node_id)
block_nonempty = @ir.hot_region_root_count(func, body) > 0
block_no_escaping_branch_control = !OiDescriptorBranchControlFact::from_node(
func, node_id, node_id,
).proves_has_escaping_branch_control()
block_children_effect_free_internal_control = block_nonempty &&
block_no_escaping_branch_control
if block_children_effect_free_internal_control {
for slot = 0
slot < @ir.hot_region_root_count(func, body)
slot = slot + 1 {
if !OiDescriptorInternalControlEffectFreeFact::from_node(
func,
dead,
node_id,
@ir.hot_region_get(func, body, slot),
).proves_effect_free_allowing_internal_control() {
block_children_effect_free_internal_control = false
}
}
}
}
_ => ()
}
let kind = if side_effect_free {
DescriptorNullableChildEraseSideEffectFree
} else if block_root &&
block_nonempty &&
block_no_escaping_branch_control &&
block_children_effect_free_internal_control {
DescriptorNullableChildEraseBlockInternalControl
} else if if_no_effect_move_fact.proves_nullable_if_no_effect_move_allowed() {
DescriptorNullableChildEraseIfNullCheckMove
} else {
DescriptorNullableChildMustPreserve
}
{
live: true,
side_effect_free,
block_root,
block_nonempty,
block_no_escaping_branch_control,
block_children_effect_free_internal_control,
if_no_effect_move_fact,
kind,
}
}
///|
fn OiDescriptorNullableChildEraseTnhFact::proves_can_erase(
self : OiDescriptorNullableChildEraseTnhFact,
) -> Bool {
if !self.live {
return false
}
match self.kind {
DescriptorNullableChildEraseSideEffectFree => self.side_effect_free
DescriptorNullableChildEraseBlockInternalControl =>
self.block_root &&
self.block_nonempty &&
self.block_no_escaping_branch_control &&
self.block_children_effect_free_internal_control
DescriptorNullableChildEraseIfNullCheckMove =>
self.if_no_effect_move_fact.proves_nullable_if_no_effect_move_allowed()
DescriptorNullableChildMustPreserve => false
}
}
///|
priv enum OiRefGetDescOperandEraseKind {
RefGetDescOperandMustPreserve
RefGetDescOperandEraseDefault
RefGetDescOperandEraseTrapsNeverHappen
}
///|
priv struct OiRefGetDescOperandEraseFact {
live : Bool
ref_get_desc : Bool
child_non_nullable_type : Bool
child_known_non_null : Bool
child_side_effect_free : Bool
nullable_child_erase_tnh_fact : OiDescriptorNullableChildEraseTnhFact?
kind : OiRefGetDescOperandEraseKind
}
///|
fn OiRefGetDescOperandEraseFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiRefGetDescOperandEraseFact {
// Binaryen v130 descriptor rewrites may drop `ref.get_desc` when the operand
// is already known non-null and side-effect-free, or when TNH lets the
// descriptor-side null check be erased. Store that ordered result as a fact
// kind so callers do not rescan the wrapper and nullable child independently
// or consume parallel default/TNH booleans.
let get_desc_fact = match OiRefGetDescFact::from_node(func, node_id) {
Some(fact) => fact
None =>
return {
live: true,
ref_get_desc: false,
child_non_nullable_type: false,
child_known_non_null: false,
child_side_effect_free: false,
nullable_child_erase_tnh_fact: None,
kind: RefGetDescOperandMustPreserve,
}
}
let child_ref_fact = OiRefFact::from_node(func, get_desc_fact.child_id)
let child_non_nullable_type = child_ref_fact.proves_type_non_nullable()
let child_known_non_null = child_ref_fact.proves_known_non_null()
let child_side_effect_free = OiSideEffectFreeExpressionFact::proves_node_free(
func,
dead,
get_desc_fact.child_id,
)
let default_can_erase = (child_non_nullable_type || child_known_non_null) &&
child_side_effect_free
let nullable_child_erase_tnh_fact = match
get_desc_fact.explicit_nullable_child() {
Some(nullable_child) =>
Some(
OiDescriptorNullableChildEraseTnhFact::from_child(
func, dead, nullable_child,
),
)
None => None
}
let kind = if default_can_erase {
RefGetDescOperandEraseDefault
} else {
match nullable_child_erase_tnh_fact {
Some(fact) if fact.proves_can_erase() =>
RefGetDescOperandEraseTrapsNeverHappen
_ => RefGetDescOperandMustPreserve
}
}
{
live: true,
ref_get_desc: true,
child_non_nullable_type,
child_known_non_null,
child_side_effect_free,
nullable_child_erase_tnh_fact,
kind,
}
}
///|
fn OiRefGetDescOperandEraseFact::proves_default_can_erase(
self : OiRefGetDescOperandEraseFact,
) -> Bool {
self.live &&
self.ref_get_desc &&
(match self.kind {
RefGetDescOperandEraseDefault =>
(self.child_non_nullable_type || self.child_known_non_null) &&
self.child_side_effect_free
RefGetDescOperandEraseTrapsNeverHappen | RefGetDescOperandMustPreserve =>
false
})
}
///|
fn OiRefGetDescOperandEraseFact::proves_tnh_can_erase(
self : OiRefGetDescOperandEraseFact,
) -> Bool {
self.live &&
self.ref_get_desc &&
(match self.kind {
RefGetDescOperandEraseTrapsNeverHappen =>
match self.nullable_child_erase_tnh_fact {
Some(fact) => fact.proves_can_erase()
None => false
}
RefGetDescOperandEraseDefault | RefGetDescOperandMustPreserve => false
})
}
///|
priv enum OiRefAsNonNullDescriptorOperandEraseTnhKind {
RefAsNonNullOperandMustPreserve
RefAsNonNullOperandEraseNullableChildTnh
}
///|
priv struct OiRefAsNonNullDescriptorOperandEraseTnhFact {
live : Bool
ref_as_non_null : Bool
single_child : Bool
nullable_child_erase_tnh_fact : OiDescriptorNullableChildEraseTnhFact?
kind : OiRefAsNonNullDescriptorOperandEraseTnhKind
}
///|
fn OiRefAsNonNullDescriptorOperandEraseTnhFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiRefAsNonNullDescriptorOperandEraseTnhFact {
// Under TNH, Binaryen's descriptor operand cleanup can erase an explicit
// `ref.as_non_null` wrapper if the nullable child itself can be discarded or
// localized. Keep the wrapper arity, child-erasure evidence, and final
// preserve/erase decision separate from the top-level descriptor operand
// predicate.
let ref_as_non_null = match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull) => true
_ => false
}
let single_child = ref_as_non_null && @ir.hot_child_count(func, node_id) == 1
let nullable_child_erase_tnh_fact = if single_child {
Some(
OiDescriptorNullableChildEraseTnhFact::from_child(
func,
dead,
@ir.hot_child_get(func, node_id, 0),
),
)
} else {
None
}
let kind = match nullable_child_erase_tnh_fact {
Some(fact) if fact.proves_can_erase() =>
RefAsNonNullOperandEraseNullableChildTnh
_ => RefAsNonNullOperandMustPreserve
}
{
live: true,
ref_as_non_null,
single_child,
nullable_child_erase_tnh_fact,
kind,
}
}
///|
fn OiRefAsNonNullDescriptorOperandEraseTnhFact::proves_can_erase(
self : OiRefAsNonNullDescriptorOperandEraseTnhFact,
) -> Bool {
self.live &&
self.ref_as_non_null &&
self.single_child &&
(match self.kind {
RefAsNonNullOperandEraseNullableChildTnh =>
match self.nullable_child_erase_tnh_fact {
Some(fact) => fact.proves_can_erase()
None => false
}
RefAsNonNullOperandMustPreserve => false
})
}
///|
priv enum OiDescriptorOperandEraseKind {
DescriptorOperandMustPreserve
DescriptorOperandEraseSideEffectFree
DescriptorOperandErasePureNodeEffects
DescriptorOperandEraseAllocation
DescriptorOperandEraseRefGetDesc
DescriptorOperandEraseRefAsNonNullTrapsNeverHappen
DescriptorOperandEraseRefGetDescTrapsNeverHappen
}
///|
priv struct OiDescriptorOperandEraseFact {
live : Bool
side_effect_free : Bool
node_effects_pure : Bool
allocation_fact : OiDescriptorAllocationEraseFact
ref_get_desc_fact : OiRefGetDescOperandEraseFact
ref_as_non_null_tnh_fact : OiRefAsNonNullDescriptorOperandEraseTnhFact
traps_never_happen : Bool
kind : OiDescriptorOperandEraseKind
}
///|
fn OiDescriptorOperandEraseFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiDescriptorOperandEraseFact {
// Binaryen v130's descriptor-cast/test rewrites use `getDroppedChildren...`,
// trap-on-null, skipNonNullCast, ChildLocalizer, and EffectAnalyzer to decide
// whether descriptor operands may disappear. This fact records Starshine's
// current represented erasure reasons in one source-backed place and lowers
// them to an ordered result kind instead of a helper-local cascade of raw
// purity, allocation, ref.get_desc, and TNH checks.
let side_effect_free = OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, node_id,
)
let node_effects_pure = optimize_instructions_node_effects_are_pure(
ctx, func, node_id,
)
let allocation_fact = OiDescriptorAllocationEraseFact::from_node(
func, node_id,
)
let ref_get_desc_fact = OiRefGetDescOperandEraseFact::from_node(
func, dead, node_id,
)
let ref_as_non_null_tnh_fact = OiRefAsNonNullDescriptorOperandEraseTnhFact::from_node(
func, dead, node_id,
)
let kind = if side_effect_free {
DescriptorOperandEraseSideEffectFree
} else if node_effects_pure {
DescriptorOperandErasePureNodeEffects
} else if allocation_fact.proves_can_erase() {
DescriptorOperandEraseAllocation
} else if ref_get_desc_fact.proves_default_can_erase() {
DescriptorOperandEraseRefGetDesc
} else if ctx.traps_never_happen &&
ref_as_non_null_tnh_fact.proves_can_erase() {
DescriptorOperandEraseRefAsNonNullTrapsNeverHappen
} else if ctx.traps_never_happen && ref_get_desc_fact.proves_tnh_can_erase() {
DescriptorOperandEraseRefGetDescTrapsNeverHappen
} else {
DescriptorOperandMustPreserve
}
{
live: true,
side_effect_free,
node_effects_pure,
allocation_fact,
ref_get_desc_fact,
ref_as_non_null_tnh_fact,
traps_never_happen: ctx.traps_never_happen,
kind,
}
}
///|
fn OiDescriptorOperandEraseFact::proves_erases_without_effects(
self : OiDescriptorOperandEraseFact,
) -> Bool {
if !self.live {
return false
}
match self.kind {
DescriptorOperandEraseSideEffectFree => self.side_effect_free
DescriptorOperandErasePureNodeEffects => self.node_effects_pure
DescriptorOperandEraseAllocation => self.allocation_fact.proves_can_erase()
DescriptorOperandEraseRefGetDesc =>
self.ref_get_desc_fact.proves_default_can_erase()
DescriptorOperandEraseRefAsNonNullTrapsNeverHappen =>
self.traps_never_happen &&
self.ref_as_non_null_tnh_fact.proves_can_erase()
DescriptorOperandEraseRefGetDescTrapsNeverHappen =>
self.traps_never_happen && self.ref_get_desc_fact.proves_tnh_can_erase()
DescriptorOperandMustPreserve => false
}
}
///|
fn optimize_instructions_defaultable_scratch_type(
val_type : @lib.ValType,
) -> @lib.ValType? {
if @lib.has_default(val_type) {
return Some(val_type)
}
match val_type {
@lib.RefTypeValType(ref_type) =>
Some(
@lib.ValType::ref_type(
@lib.RefType::new(
true,
ref_type.get_heap_type(),
exact=ref_type.is_exact(),
),
),
)
_ => None
}
}
///|
fn optimize_instructions_defaultable_descriptor_scratch_type(
val_type : @lib.ValType,
) -> @lib.ValType? {
match val_type {
@lib.RefTypeValType(ref_type) =>
Some(
@lib.ValType::ref_type(
@lib.RefType::new(true, ref_type.get_heap_type()),
),
)
_ => if @lib.has_default(val_type) { Some(val_type) } else { None }
}
}
///|
fn optimize_instructions_try_replace_ref_get_desc_unreachable_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let get_desc_fact = match OiRefGetDescFact::from_node(func, node_id) {
Some(fact) => fact
None => return false
}
if get_desc_fact.child_is_unreachable(func) {
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
} else {
false
}
}
///|
fn optimize_instructions_try_replace_ref_cast_unreachable_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if @ir.hot_child_count(func, node_id) != 1 {
return false
}
let operand_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_get(func, operand_id).op {
@ir.HotOp::Unreachable => {
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
}
_ => false
}
}
///|
fn optimize_instructions_try_remove_ref_as_non_null_local_write_trap_check(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if !ctx.traps_never_happen && !ctx.ignore_implicit_traps {
return false
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => ()
_ => return false
}
if node.child_count != 1 {
return false
}
let local_type = match @ir.hot_local_type(func, node.imm0) {
Some(ty) => ty
None => return false
}
let non_null_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_exact_instr(func, non_null_id) {
Some(@lib.Instruction::RefAsNonNull) => ()
_ => return false
}
if @ir.hot_child_count(func, non_null_id) != 1 {
return false
}
let operand_id = @ir.hot_child_get(func, non_null_id, 0)
match @ir.hot_type_results(func, @ir.hot_node_type(func, operand_id)) {
[ty] if ty == local_type => ()
_ => return false
}
@ir.hot_child_set(func, node_id, 0, operand_id)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_fold_ref_as_non_null(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if @ir.hot_child_count(func, node_id) != 1 {
return false
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull) => ()
_ => return false
}
let operand_id = @ir.hot_child_get(func, node_id, 0)
if OiDescriptorBranchControlFact::from_node(func, operand_id, operand_id).proves_has_branch_control() {
return false
}
if optimize_instructions_try_move_ref_as_non_null_into_ref_cast_desc_eq(
ctx, func, node_id, operand_id,
) {
return true
}
let null_trap_fact = OiNullTrapFact::for_operand(
func,
operand_id,
caller_scope_traps=true,
)
match OiNullTrapRewriteFact::from_trap_fact(null_trap_fact).kind {
NullTrapReplaceParentWithUnreachable => {
optimize_instructions_replace_with_unreachable(ctx, func, node_id)
true
}
NullTrapSkipExplicitNonNullCheck => {
optimize_instructions_replace_with_node_id(ctx, func, node_id, operand_id)
true
}
_ => false
}
}
///|
fn optimize_instructions_try_move_ref_as_non_null_into_ref_cast_desc_eq(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
cast_id : Int32,
) -> Bool {
let cast_fact = match OiDescriptorCastFact::from_node(func, cast_id) {
Some(fact) => fact
None => return false
}
if !cast_fact.nullable {
optimize_instructions_replace_with_node_id(ctx, func, node_id, cast_id)
return true
}
let narrowing_fact = OiDescriptorResultNarrowingFact::from_cast(
ctx, func, cast_fact, true,
)
match
OiDescriptorResultNarrowingRewritePlanFact::from_ref_as_non_null_root(
narrowing_fact,
) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv struct OiRefEqNonNullLocalFact {
local_index : Int32?
trap_id : Int32?
}
///|
priv struct OiRefEqI31SourceFact {
local_index : Int32?
const_value : @lib.I32?
}
///|
priv enum OiRefEqI31NonNullKind {
RefEqI31UnknownNonNull
RefEqI31KnownNonNullLocal
RefEqI31KnownNonNullLiteral
}
///|
priv struct OiRefEqI31NonNullFact {
kind : OiRefEqI31NonNullKind
}
///|
priv enum OiRefEqI31DisjointTargetKind {
RefEqI31TargetMayMatch
RefEqI31TargetCannotMatch
}
///|
priv struct OiRefEqI31DisjointTargetFact {
kind : OiRefEqI31DisjointTargetKind
}
///|
priv struct OiRefEqI31Fact {
source_fact : OiRefEqI31SourceFact
non_null_fact : OiRefEqI31NonNullFact
disjoint_target_fact : OiRefEqI31DisjointTargetFact
}
///|
priv enum OiRefEqNullabilityKind {
RefEqUnknownNullability
RefEqKnownNull
RefEqKnownNonNullLocalType
RefEqKnownNonNullLiteral
}
///|
priv struct OiRefEqNullabilityFact {
kind : OiRefEqNullabilityKind
}
///|
priv struct OiRefEqSameLocalFact {
local_index : Int32?
}
///|
priv struct OiRefEqLocalTypeFact {
ref_type : @lib.RefType?
}
///|
priv struct OiRefEqOperandFact {
same_local_fact : OiRefEqSameLocalFact
same_non_null_fact : OiRefEqNonNullLocalFact
i31_fact : OiRefEqI31Fact
nullability_fact : OiRefEqNullabilityFact
local_type_fact : OiRefEqLocalTypeFact
}
///|
fn OiRefEqOperandFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiRefEqOperandFact {
// Binaryen v130 `visitRefEq` and nearby null/equality rewrites make their
// decisions from operand shape, local nullability, i31 facts, and disjoint
// reference heap facts. Keep those facts in one source-backed operand summary
// instead of spreading same-local, ref.i31, null, and local-heap scanners
// through the visitor.
let local_type_fact = OiRefEqLocalTypeFact::from_node(func, node_id)
let same_local_fact = OiRefEqSameLocalFact::from_node(ctx, func, node_id)
let same_non_null_fact = OiRefEqNonNullLocalFact::from_node(func, node_id)
let literal_fact = OiRefLiteralFact::from_node(func, node_id)
let i31_fact = OiRefEqI31Fact::from_node(
func,
dead,
node_id,
local_type_fact.ref_type,
literal_fact,
)
let nullability_fact = OiRefEqNullabilityFact::from_node(
func,
node_id,
local_type_fact.ref_type,
literal_fact,
)
{
same_local_fact,
same_non_null_fact,
i31_fact,
nullability_fact,
local_type_fact,
}
}
///|
fn OiRefEqSameLocalFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefEqSameLocalFact {
// Binaryen v130 `visitRefEq` first recognizes operands that are provably the
// same local value, including the represented nullable `ref.cast(local.get)`
// shell when removing that shell cannot add an implicit trap. Keep that local
// identity as its own fact instead of a parallel optional field on the wider
// operand summary.
let local_index = match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalGet => Some(@ir.hot_node_get(func, node_id).imm0)
@ir.HotOp::RefCast =>
OiRefEqSameLocalFact::from_nullable_ref_cast(ctx, func, node_id)
_ => None
}
{ local_index, }
}
///|
fn OiRefEqSameLocalFact::from_nullable_ref_cast(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Int32? {
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefCast(true, _, target)) => {
let child_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_get(func, child_id).op {
@ir.HotOp::LocalGet => {
let local_idx = @ir.hot_node_get(func, child_id).imm0
match @ir.hot_local_type(func, local_idx) {
Some(@lib.ValType::RefTypeValType(ref_type)) =>
if ctx.ignore_implicit_traps || ctx.traps_never_happen {
Some(local_idx)
} else if OiHeapRelationFact::from_heaps(
ctx,
ref_type.get_heap_type(),
target,
).known_match_without_full_subtyping() {
Some(local_idx)
} else {
None
}
_ => None
}
}
_ => None
}
}
_ => None
}
}
///|
fn OiRefEqLocalTypeFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefEqLocalTypeFact {
// Keep the direct-local static ref type as a named `visitRefEq` fact. It feeds
// only the represented local-type nullability, i31-disjointness, and local
// heap-disjoint equality decisions; fallthrough/result-type non-null proofs
// intentionally remain outside this no-widening ref.eq operand slice.
let ref_type = match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalGet =>
match @ir.hot_local_type(func, @ir.hot_node_get(func, node_id).imm0) {
Some(@lib.ValType::RefTypeValType(ref_type)) => Some(ref_type)
_ => None
}
_ => None
}
{ ref_type, }
}
///|
fn OiRefEqNonNullLocalFact::none() -> OiRefEqNonNullLocalFact {
{ local_index: None, trap_id: None, }
}
///|
fn OiRefEqNonNullLocalFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefEqNonNullLocalFact {
// Binaryen v130 can fold `ref.eq` of the same local through `ref.as_non_null`,
// but the wrapper may still be the operation that traps for nullable locals.
// Keep the local identity and optional trap-carrying wrapper as one fact rather
// than pairing the local with a sentinel trap id in the relation decision.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalGet =>
{
local_index: Some(@ir.hot_node_get(func, node_id).imm0),
trap_id: None,
}
_ =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull) => {
if @ir.hot_child_count(func, node_id) != 1 {
return OiRefEqNonNullLocalFact::none()
}
let child_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_get(func, child_id).op {
@ir.HotOp::LocalGet => {
let local_idx = @ir.hot_node_get(func, child_id).imm0
match @ir.hot_local_type(func, local_idx) {
Some(@lib.ValType::RefTypeValType(ref_type)) =>
{
local_index: Some(local_idx),
trap_id: if ref_type.is_non_nullable() {
None
} else {
Some(node_id)
},
}
_ => OiRefEqNonNullLocalFact::none()
}
}
_ => OiRefEqNonNullLocalFact::none()
}
}
_ => OiRefEqNonNullLocalFact::none()
}
}
}
///|
fn OiRefEqNullabilityFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
local_ref_type : @lib.RefType?,
literal_fact : OiRefLiteralFact,
) -> OiRefEqNullabilityFact {
// Binaryen v130 `visitRefEq` folds direct null-vs-null and null-vs-non-null
// pairs from explicit operand nullability. Keep Starshine's intentionally
// represented subset source-shaped: direct `ref.null`, non-null local types,
// and direct non-null literals, without broadening to every non-null result
// type that the shared `OiRefFact` can prove for other visitors.
let kind = match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::RefNull => RefEqKnownNull
@ir.HotOp::LocalGet =>
match local_ref_type {
Some(ref_type) if ref_type.is_non_nullable() =>
RefEqKnownNonNullLocalType
_ => RefEqUnknownNullability
}
_ =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefNull(_)) => RefEqKnownNull
_ =>
match literal_fact.kind {
I31RefLiteral | FuncRefLiteral => RefEqKnownNonNullLiteral
NoRefLiteral => RefEqUnknownNullability
}
}
}
{ kind, }
}
///|
fn OiRefEqNullabilityFact::proves_known_null(
self : OiRefEqNullabilityFact,
) -> Bool {
match self.kind {
RefEqKnownNull => true
RefEqUnknownNullability
| RefEqKnownNonNullLocalType
| RefEqKnownNonNullLiteral => false
}
}
///|
fn OiRefEqNullabilityFact::proves_known_non_null(
self : OiRefEqNullabilityFact,
) -> Bool {
match self.kind {
RefEqKnownNonNullLocalType | RefEqKnownNonNullLiteral => true
RefEqUnknownNullability | RefEqKnownNull => false
}
}
///|
fn OiRefEqI31SourceFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiRefEqI31SourceFact {
// Binaryen v130 `visitRefEq` has direct `ref.i31` equality paths for local
// identity and constant payload equality. Keep that direct-source proof nested
// under the wider i31 operand fact so later non-null/disjoint proofs do not
// duplicate child-shape parsing.
if @ir.hot_child_count(func, node_id) != 1 {
return { local_index: None, const_value: None, }
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefI31) => {
let child_id = @ir.hot_child_get(func, node_id, 0)
let local_index = match @ir.hot_node_get(func, child_id).op {
@ir.HotOp::LocalGet => Some(@ir.hot_node_get(func, child_id).imm0)
_ => None
}
{
local_index,
const_value: optimize_instructions_i32_const(func, dead, child_id),
}
}
_ => { local_index: None, const_value: None, }
}
}
///|
fn OiRefEqI31Fact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
local_ref_type : @lib.RefType?,
literal_fact : OiRefLiteralFact,
) -> OiRefEqI31Fact {
// Binaryen v130 `visitRefEq` has an i31-specific equality/disjointness path:
// direct `ref.i31(local.get)` operands compare equal by local identity, direct
// `ref.i31(const)` operands fold by payload equality, and known-non-null i31
// operands are disjoint from locals whose static heap cannot be i31. Keep
// those i31 proofs together instead of parallel operand-summary booleans.
let source_fact = OiRefEqI31SourceFact::from_node(func, dead, node_id)
let non_null_fact = OiRefEqI31NonNullFact::from_node(
func, node_id, local_ref_type, literal_fact,
)
let disjoint_target_fact = OiRefEqI31DisjointTargetFact::from_ref_type(
local_ref_type,
)
{ source_fact, non_null_fact, disjoint_target_fact, }
}
///|
fn OiRefEqI31NonNullFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
local_ref_type : @lib.RefType?,
literal_fact : OiRefLiteralFact,
) -> OiRefEqI31NonNullFact {
let kind = match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalGet =>
match local_ref_type {
Some(ref_type) if ref_type.is_non_nullable() &&
ref_type.get_heap_type() ==
@lib.HeapType::abs(@lib.AbsHeapType::i31()) =>
RefEqI31KnownNonNullLocal
_ => RefEqI31UnknownNonNull
}
_ =>
match literal_fact.kind {
I31RefLiteral => RefEqI31KnownNonNullLiteral
NoRefLiteral | FuncRefLiteral => RefEqI31UnknownNonNull
}
}
{ kind, }
}
///|
fn OiRefEqI31NonNullFact::proves_known_non_null(
self : OiRefEqI31NonNullFact,
) -> Bool {
match self.kind {
RefEqI31KnownNonNullLocal | RefEqI31KnownNonNullLiteral => true
RefEqI31UnknownNonNull => false
}
}
///|
fn OiRefEqI31DisjointTargetFact::from_ref_type(
local_ref_type : @lib.RefType?,
) -> OiRefEqI31DisjointTargetFact {
let kind = match local_ref_type {
Some(ref_type) if OiRefLiteralTargetFact::from_literal(
OiRefLiteralFact::i31(),
ref_type.get_heap_type(),
).proves_i31_miss_target() => RefEqI31TargetCannotMatch
_ => RefEqI31TargetMayMatch
}
{ kind, }
}
///|
fn OiRefEqI31DisjointTargetFact::proves_cannot_match(
self : OiRefEqI31DisjointTargetFact,
) -> Bool {
match self.kind {
RefEqI31TargetCannotMatch => true
RefEqI31TargetMayMatch => false
}
}
///|
fn OiRefEqI31Fact::proves_known_non_null(self : OiRefEqI31Fact) -> Bool {
self.non_null_fact.proves_known_non_null()
}
///|
fn OiRefEqI31Fact::proves_cannot_match_target(self : OiRefEqI31Fact) -> Bool {
self.disjoint_target_fact.proves_cannot_match()
}
///|
priv enum OiRefEqSkipCastShellKind {
RefEqSkipCastRefCast
RefEqSkipCastRefAsNonNull
}
///|
priv struct OiRefEqSkipCastShellFact {
child_id : Int32
kind : OiRefEqSkipCastShellKind
}
///|
fn OiRefEqSkipCastShellFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefEqSkipCastShellFact? {
// Binaryen v130 `visitRefEq` reuses the GC skip-cast shell set for nullable-eq
// operands: ordinary `ref.cast` and `ref.as_non_null` wrappers are removable
// only after a separate child-type fact proves the child already satisfies the
// required `(ref null eq)` target. Keep the wrapper classification as a fact
// instead of returning a local `-1` sentinel for unsupported opcodes.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::RefCast if @ir.hot_child_count(func, node_id) == 1 =>
Some({
child_id: @ir.hot_child_get(func, node_id, 0),
kind: RefEqSkipCastRefCast,
})
@ir.HotOp::Heap if @ir.hot_child_count(func, node_id) == 1 =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull) =>
Some({
child_id: @ir.hot_child_get(func, node_id, 0),
kind: RefEqSkipCastRefAsNonNull,
})
_ => None
}
_ => None
}
}
///|
fn OiRefEqSkipCastShellFact::replacement_child_id(
self : OiRefEqSkipCastShellFact,
) -> Int32 {
match self.kind {
RefEqSkipCastRefCast | RefEqSkipCastRefAsNonNull => self.child_id
}
}
///|
priv struct OiRefEqNullableEqRequirementFact {
child_id : Int32
}
///|
fn OiRefEqNullableEqRequirementFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefEqNullableEqRequirementFact? {
// The cast wrapper is removable only when the child heap is a known match for
// nullable eqref, matching Binaryen v130 `skipCast(input, (ref null eq))` and
// preserving validation after the wrapper is erased.
let ref_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[@lib.ValType::RefTypeValType(ref_type)] => ref_type
_ => return None
}
if OiHeapRelationFact::from_heaps(
ctx,
ref_type.get_heap_type(),
@lib.HeapType::abs(@lib.AbsHeapType::eq()),
).known_match_target() {
Some({ child_id: node_id, })
} else {
None
}
}
///|
priv struct OiRefEqSkipCastStepFact {
shell_fact : OiRefEqSkipCastShellFact
requirement_fact : OiRefEqNullableEqRequirementFact
}
///|
fn OiRefEqSkipCastStepFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefEqSkipCastStepFact? {
let shell_fact = match OiRefEqSkipCastShellFact::from_node(func, node_id) {
Some(fact) => fact
None => return None
}
let child_id = shell_fact.replacement_child_id()
let requirement_fact = match
OiRefEqNullableEqRequirementFact::from_node(ctx, func, child_id) {
Some(fact) => fact
None => return None
}
Some({ shell_fact, requirement_fact, })
}
///|
fn OiRefEqSkipCastStepFact::replacement_child_id(
self : OiRefEqSkipCastStepFact,
) -> Int32 {
ignore(self.requirement_fact.child_id)
self.shell_fact.replacement_child_id()
}
///|
priv struct OiRefEqSkipCastOperandFact {
replacement_id : Int32
changed : Bool
}
///|
fn OiRefEqSkipCastOperandFact::from_node(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> OiRefEqSkipCastOperandFact {
// Binaryen v130 `visitRefEq` calls `skipCast(input, (ref null eq))` only
// under traps-never-happen. The operand fact now walks explicit skip-cast
// shell and nullable-eq child-requirement facts so unsupported wrappers simply
// stop the chain instead of flowing through a sentinel child id.
if !ctx.traps_never_happen {
return { replacement_id: node_id, changed: false, }
}
let mut current_id = node_id
let mut changed = false
while current_id >= 0 && @ir.hot_node_is_live(func, current_id) {
let step_fact = match
OiRefEqSkipCastStepFact::from_node(ctx, func, current_id) {
Some(fact) => fact
None => break
}
current_id = step_fact.replacement_child_id()
changed = true
}
{ replacement_id: current_id, changed, }
}
///|
priv struct OiRefEqSkipCastFact {
lhs_replacement_id : Int32
rhs_replacement_id : Int32
changed : Bool
}
///|
fn OiRefEqSkipCastFact::from_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
lhs_id : Int32,
rhs_id : Int32,
) -> OiRefEqSkipCastFact {
let lhs = OiRefEqSkipCastOperandFact::from_node(ctx, func, lhs_id)
let rhs = OiRefEqSkipCastOperandFact::from_node(ctx, func, rhs_id)
{
lhs_replacement_id: lhs.replacement_id,
rhs_replacement_id: rhs.replacement_id,
changed: lhs.changed || rhs.changed,
}
}
///|
fn OiRefEqSkipCastFact::apply(
self : OiRefEqSkipCastFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if !self.changed {
return false
}
@ir.hot_child_set(func, node_id, 0, self.lhs_replacement_id)
@ir.hot_child_set(func, node_id, 1, self.rhs_replacement_id)
pass_mark_mutated(ctx, func)
true
}
///|
priv struct OiRefEqRelationFact {
lhs : OiRefEqOperandFact
rhs : OiRefEqOperandFact
}
///|
fn OiRefEqRelationFact::from_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiRefEqRelationFact {
{
lhs: OiRefEqOperandFact::from_node(ctx, func, dead, lhs_id),
rhs: OiRefEqOperandFact::from_node(ctx, func, dead, rhs_id),
}
}
///|
priv enum OiRefEqEqualityKind {
RefEqSameLocalEqual
RefEqSameI31LocalEqual
RefEqSameLocalNonNullTrapEqual
RefEqI31ConstEqual
RefEqI31ConstNotEqual
}
///|
priv struct OiRefEqEqualityFact {
kind : OiRefEqEqualityKind
trap_id : Int32?
result : @lib.I32
}
///|
fn OiRefEqEqualityFact::from_relation(
relation : OiRefEqRelationFact,
) -> OiRefEqEqualityFact? {
// Binaryen v130 `visitRefEq` first handles represented same-reference and
// i31-payload equalities, then the trap-preserving same-local
// `ref.as_non_null` case, then constant i31 payload comparisons. Keep that
// ordered relation result as a fact so the visitor does not own equality
// shape probes directly.
match
(
relation.lhs.same_local_fact.local_index,
relation.rhs.same_local_fact.local_index,
) {
(Some(lhs), Some(rhs)) if lhs == rhs =>
return Some({
kind: RefEqSameLocalEqual,
trap_id: None,
result: @lib.I32(1),
})
_ => ()
}
match
(
relation.lhs.i31_fact.source_fact.local_index,
relation.rhs.i31_fact.source_fact.local_index,
) {
(Some(lhs), Some(rhs)) if lhs == rhs =>
return Some({
kind: RefEqSameI31LocalEqual,
trap_id: None,
result: @lib.I32(1),
})
_ => ()
}
match
(
relation.lhs.same_non_null_fact.local_index,
relation.rhs.same_non_null_fact.local_index,
) {
(Some(lhs), Some(rhs)) if lhs == rhs =>
match relation.lhs.same_non_null_fact.trap_id {
Some(trap_id) =>
return Some({
kind: RefEqSameLocalNonNullTrapEqual,
trap_id: Some(trap_id),
result: @lib.I32(1),
})
None =>
match relation.rhs.same_non_null_fact.trap_id {
Some(trap_id) =>
return Some({
kind: RefEqSameLocalNonNullTrapEqual,
trap_id: Some(trap_id),
result: @lib.I32(1),
})
None => ()
}
}
_ => ()
}
match
(
relation.lhs.i31_fact.source_fact.const_value,
relation.rhs.i31_fact.source_fact.const_value,
) {
(Some(@lib.I32(lhs)), Some(@lib.I32(rhs))) => {
// ref.i31 discards the source integer's high bit.
let equal = (lhs & 0x7fffffff) == (rhs & 0x7fffffff)
Some({
kind: if equal {
RefEqI31ConstEqual
} else {
RefEqI31ConstNotEqual
},
trap_id: None,
result: if equal {
@lib.I32(1)
} else {
@lib.I32(0)
},
})
}
_ => None
}
}
///|
fn OiRefEqEqualityFact::apply(
self : OiRefEqEqualityFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.kind {
RefEqSameLocalNonNullTrapEqual =>
match self.trap_id {
Some(trap_id) =>
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
trap_id,
self.result,
)
None =>
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.result,
)
}
RefEqSameLocalEqual
| RefEqSameI31LocalEqual
| RefEqI31ConstEqual
| RefEqI31ConstNotEqual =>
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.result,
)
}
}
///|
priv enum OiRefEqImpossibleKind {
RefEqI31AgainstDisjointHeap
RefEqDisjointLocalHeapTypes
}
///|
priv struct OiRefEqImpossibleFact {
kind : OiRefEqImpossibleKind
}
///|
fn OiRefEqImpossibleFact::apply(
self : OiRefEqImpossibleFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
) -> Unit {
match self.kind {
RefEqI31AgainstDisjointHeap | RefEqDisjointLocalHeapTypes =>
optimize_instructions_replace_ref_eq_impossible_with_const_i32(
ctx, func, node_id, lhs_id, rhs_id,
)
}
}
///|
fn OiRefEqImpossibleFact::from_relation(
ctx : HotPassContext,
relation : OiRefEqRelationFact,
) -> OiRefEqImpossibleFact? {
// Binaryen v130 `visitRefEq` can fold impossible equality for the represented
// non-null-i31 versus disjoint-heap case and for local ref types with
// abstract struct/array disjoint heaps. Keep the miss reason explicit before
// the effect-preserving false replacement is selected.
if (
relation.lhs.i31_fact.proves_known_non_null() &&
relation.rhs.i31_fact.proves_cannot_match_target()
) ||
(
relation.rhs.i31_fact.proves_known_non_null() &&
relation.lhs.i31_fact.proves_cannot_match_target()
) {
return Some({ kind: RefEqI31AgainstDisjointHeap, })
}
match
(
relation.lhs.local_type_fact.ref_type,
relation.rhs.local_type_fact.ref_type,
) {
(Some(lhs), Some(rhs)) if (lhs.is_non_nullable() || rhs.is_non_nullable()) &&
OiHeapRelationFact::from_heaps(
ctx,
lhs.get_heap_type(),
rhs.get_heap_type(),
).known_abs_struct_array_disjoint() =>
Some({ kind: RefEqDisjointLocalHeapTypes, })
_ => None
}
}
///|
fn optimize_instructions_replace_with_drop_then_const_i32(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
drop_id : Int32,
value : @lib.I32,
) -> Unit {
let temp = @ir.hot_build_block(
func,
@lib.BlockType::val_type(@lib.ValType::i32()),
[@ir.hot_build_drop(func, drop_id), @ir.hot_build_const_i32(func, value)],
)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_drop_drop_then_const_i32(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
first_drop_id : Int32,
second_drop_id : Int32,
value : @lib.I32,
) -> Unit {
let temp = @ir.hot_build_block(
func,
@lib.BlockType::val_type(@lib.ValType::i32()),
[
@ir.hot_build_drop(func, first_drop_id),
@ir.hot_build_drop(func, second_drop_id),
@ir.hot_build_const_i32(func, value),
],
)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_drop_then_const_i64(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
drop_id : Int32,
value : @lib.I64,
) -> Unit {
let temp = @ir.hot_build_block(
func,
@lib.BlockType::val_type(@lib.ValType::i64()),
[@ir.hot_build_drop(func, drop_id), @ir.hot_build_const_i64(func, value)],
)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
}
///|
fn optimize_instructions_replace_with_drop_then_node_id(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
drop_id : Int32,
value_id : Int32,
) -> Bool {
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[] => @lib.BlockType::void_()
[val_type] => @lib.BlockType::val_type(val_type)
_ => return false
}
let temp = @ir.hot_build_block(func, block_type, [
@ir.hot_build_drop(func, drop_id),
value_id,
])
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_replace_with_local_set_then_node_id(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
localize_id : Int32,
value_id : Int32,
) -> Bool {
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[] => @lib.BlockType::void_()
[val_type] => @lib.BlockType::val_type(val_type)
_ => return false
}
let local_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, localize_id)) {
[val_type] =>
match
optimize_instructions_defaultable_descriptor_scratch_type(val_type) {
Some(local_type) => local_type
None => return false
}
_ => return false
}
let local_id = @ir.hot_append_body_local(func, local_type)
let temp = @ir.hot_build_block(func, block_type, [
@ir.hot_build_local_set(func, local_id, localize_id),
value_id,
])
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_restore_localized_result(
func : @ir.HotFunc,
local_get : Int32,
local_type : @lib.ValType,
result_type : @lib.ValType,
) -> Int32? {
if local_type == result_type {
return Some(local_get)
}
match (local_type, result_type) {
(
@lib.ValType::RefTypeValType(local_ref),
@lib.ValType::RefTypeValType(result_ref),
) if local_ref.is_nullable() &&
result_ref.is_non_nullable() &&
local_ref.get_heap_type() == result_ref.get_heap_type() &&
local_ref.is_exact() == result_ref.is_exact() =>
Some(
@ir.hot_build_heap(
func,
[result_type],
@lib.Instruction::ref_as_non_null(),
[local_get],
),
)
_ => None
}
}
///|
fn optimize_instructions_replace_with_localized_node_then_descriptor_then_local_get(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
value_id : Int32,
descriptor_id : Int32,
) -> Bool {
let result_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[val_type] => val_type
_ => return false
}
let value_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, value_id)) {
[val_type] => val_type
_ => return false
}
let local_type = match
optimize_instructions_defaultable_scratch_type(value_type) {
Some(local_type) => local_type
None => return false
}
let local_id = @ir.hot_append_body_local(func, local_type)
let local_get = @ir.hot_build_local_get(func, local_id)
let result_id = match
optimize_instructions_restore_localized_result(
func, local_get, local_type, result_type,
) {
Some(result_id) => result_id
None => return false
}
let descriptor_eval = if OiDescriptorOperandSkipLocalizationFact::from_operand(
func, descriptor_id,
).proves_should_localize_when_skipped() {
let descriptor_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, descriptor_id)) {
[val_type] =>
match
optimize_instructions_defaultable_descriptor_scratch_type(val_type) {
Some(local_type) => local_type
None => return false
}
_ => return false
}
let descriptor_local_id = @ir.hot_append_body_local(func, descriptor_type)
@ir.hot_build_local_set(func, descriptor_local_id, descriptor_id)
} else {
@ir.hot_build_drop(func, descriptor_id)
}
let temp = @ir.hot_build_block(func, @lib.BlockType::val_type(result_type), [
@ir.hot_build_local_set(func, local_id, value_id),
descriptor_eval,
result_id,
])
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_replace_with_drop_then_unreachable(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
drop_id : Int32,
) -> Bool {
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[] => @lib.BlockType::void_()
[val_type] => @lib.BlockType::val_type(val_type)
_ => return false
}
let temp = @ir.hot_build_block(func, block_type, [
@ir.hot_build_drop(func, drop_id),
@ir.hot_build_unreachable(func),
])
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_replace_with_drop_drop_then_unreachable(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
first_drop_id : Int32,
second_drop_id : Int32,
) -> Bool {
let block_type = match
@ir.hot_type_results(func, @ir.hot_node_type(func, node_id)) {
[] => @lib.BlockType::void_()
[val_type] => @lib.BlockType::val_type(val_type)
_ => return false
}
let temp = @ir.hot_build_block(func, block_type, [
@ir.hot_build_drop(func, first_drop_id),
@ir.hot_build_drop(func, second_drop_id),
@ir.hot_build_unreachable(func),
])
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_node_effects_are_pure(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
let effects = pass_require_effects(ctx, func)
OiNodeEffectPurityFact::from_node_masks(effects.node_masks, node_id).proves_pure()
}
///|
fn optimize_instructions_replace_ref_eq_impossible_with_const_i32(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
lhs_id : Int32,
rhs_id : Int32,
) -> Unit {
let lhs_pure = optimize_instructions_node_effects_are_pure(ctx, func, lhs_id)
let rhs_pure = optimize_instructions_node_effects_are_pure(ctx, func, rhs_id)
match (lhs_pure, rhs_pure) {
(true, true) =>
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
(false, true) =>
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
lhs_id,
@lib.I32(0),
)
(true, false) =>
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
rhs_id,
@lib.I32(0),
)
(false, false) => {
let temp = @ir.hot_build_block(
func,
@lib.BlockType::val_type(@lib.ValType::i32()),
[
@ir.hot_build_drop(func, lhs_id),
@ir.hot_build_drop(func, rhs_id),
@ir.hot_build_const_i32(func, @lib.I32(0)),
],
)
let replacement = @ir.hot_node_get(func, temp)
@ir.hot_replace_node(func, node_id, replacement)
@ir.hot_label_owner_set(
func,
@ir.hot_control_node_label(func, node_id),
node_id,
)
@ir.hot_delete_node(func, temp)
pass_mark_mutated(ctx, func)
}
}
}
///|
priv struct OiStructGetNonNullCheckFact {
nullable_operand_id : Int32
}
///|
fn OiStructGetNonNullCheckFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiStructGetNonNullCheckFact? {
// Binaryen v130 visitStructGet applies skipNonNullCast before trapOnNull. A
// direct ref.as_non_null is redundant because struct.get itself traps at the
// same receiver point. Keep the exact consumer contract and selected nullable
// operand in one typed fact; other GC consumers remain fail-closed.
if @ir.hot_child_count(func, node_id) != 1 {
return None
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::StructGet(_, _)) => ()
_ => return None
}
let checked_operand_id = @ir.hot_child_get(func, node_id, 0)
let checked_operand = @ir.hot_node_get(func, checked_operand_id)
match checked_operand.op {
@ir.HotOp::Heap if checked_operand.child_count == 1 => ()
_ => return None
}
match @ir.hot_node_exact_instr(func, checked_operand_id) {
Some(@lib.Instruction::RefAsNonNull) => ()
_ => return None
}
let nullable_operand_id = @ir.hot_child_get(func, checked_operand_id, 0)
match OiRefFact::from_node(func, nullable_operand_id).ref_type {
Some(ref_type) if ref_type.is_nullable() => Some({ nullable_operand_id, })
_ => None
}
}
///|
fn OiStructGetNonNullCheckFact::apply(
self : OiStructGetNonNullCheckFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
@ir.hot_child_set(func, node_id, 0, self.nullable_operand_id)
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_try_strip_struct_get_non_null_check(
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
match OiStructGetNonNullCheckFact::from_node(func, node_id) {
Some(fact) => fact.apply(ctx, func, node_id)
None => false
}
}
///|
priv enum OiRefEqOperandErasabilityKind {
RefEqOperandErasable
RefEqOperandPreserveReferenceTrap
RefEqOperandNotFoldable
}
///|
priv struct OiRefEqOperandErasabilityFact {
kind : OiRefEqOperandErasabilityKind
foldability_fact : OiConsecutiveInputFoldabilityFact
}
///|
fn OiRefEqOperandErasabilityFact::from_operand(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
operand_id : Int32,
) -> OiRefEqOperandErasabilityFact {
let foldability_fact = OiConsecutiveInputFoldabilityFact::from_right(
ctx, func, dead, operand_id,
)
match @ir.hot_node_exact_instr(func, operand_id) {
Some(@lib.Instruction::RefAsNonNull)
| Some(@lib.Instruction::RefCast(_, _, _)) => {
if !ctx.traps_never_happen && !ctx.ignore_implicit_traps {
return { kind: RefEqOperandPreserveReferenceTrap, foldability_fact, }
}
if @ir.hot_child_count(func, operand_id) == 1 &&
OiSideEffectFreeExpressionFact::proves_node_free(
func,
dead,
@ir.hot_child_get(func, operand_id, 0),
) {
return { kind: RefEqOperandErasable, foldability_fact, }
}
}
_ => ()
}
let kind = if foldability_fact.proves_foldable() {
RefEqOperandErasable
} else {
RefEqOperandNotFoldable
}
{ kind, foldability_fact, }
}
///|
fn OiRefEqOperandErasabilityFact::proves_erasable(
self : OiRefEqOperandErasabilityFact,
) -> Bool {
ignore(self.foldability_fact)
match self.kind {
RefEqOperandErasable => true
RefEqOperandPreserveReferenceTrap | RefEqOperandNotFoldable => false
}
}
///|
priv struct OiRefEqConsecutiveEqualityFact {
lhs_id : Int32
rhs_id : Int32
equality_fact : OiConsecutiveInputEqualityFact
lhs_erasability_fact : OiRefEqOperandErasabilityFact
rhs_erasability_fact : OiRefEqOperandErasabilityFact
}
///|
fn OiRefEqConsecutiveEqualityFact::from_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
lhs_id : Int32,
rhs_id : Int32,
) -> OiRefEqConsecutiveEqualityFact? {
// Binaryen v131 applies the shared consecutive-input equality machinery to
// ref.eq. A foldable right operand means only the left evaluation survives;
// otherwise both equal evaluations must be preserved before producing true.
let equality_fact = OiConsecutiveInputEqualityFact::from_adjacent_operands(
ctx, func, lhs_id, rhs_id,
)
if !equality_fact.proves_equal(func) {
return None
}
Some({
lhs_id,
rhs_id,
equality_fact,
lhs_erasability_fact: OiRefEqOperandErasabilityFact::from_operand(
ctx, func, dead, lhs_id,
),
rhs_erasability_fact: OiRefEqOperandErasabilityFact::from_operand(
ctx, func, dead, rhs_id,
),
})
}
///|
fn OiRefEqConsecutiveEqualityFact::apply(
self : OiRefEqConsecutiveEqualityFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if !self.equality_fact.proves_equal(func) {
return false
}
let lhs_foldable = self.lhs_erasability_fact.proves_erasable()
let rhs_foldable = self.rhs_erasability_fact.proves_erasable()
match (lhs_foldable, rhs_foldable) {
(true, false) =>
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
self.rhs_id,
@lib.I32(1),
)
(_, true) =>
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
self.lhs_id,
@lib.I32(1),
)
(false, false) =>
optimize_instructions_replace_with_drop_drop_then_const_i32(
ctx,
func,
node_id,
self.lhs_id,
self.rhs_id,
@lib.I32(1),
)
}
true
}
///|
fn optimize_instructions_try_rewrite_ref_eq_null(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> Bool {
if @ir.hot_child_count(func, node_id) != 2 {
return false
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefEq) => ()
_ => return false
}
let mut lhs_id = @ir.hot_child_get(func, node_id, 0)
let mut rhs_id = @ir.hot_child_get(func, node_id, 1)
match
OiRefEqConsecutiveEqualityFact::from_operands(
ctx, func, dead, lhs_id, rhs_id,
) {
Some(fact) => return fact.apply(ctx, func, node_id)
None => ()
}
let mut relation_fact = OiRefEqRelationFact::from_operands(
ctx, func, dead, lhs_id, rhs_id,
)
match OiRefEqEqualityFact::from_relation(relation_fact) {
Some(equality_fact) => {
equality_fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match OiRefEqImpossibleFact::from_relation(ctx, relation_fact) {
Some(impossible_fact) => {
impossible_fact.apply(ctx, func, node_id, lhs_id, rhs_id)
return true
}
None => ()
}
let skip_cast_fact = OiRefEqSkipCastFact::from_operands(
ctx, func, lhs_id, rhs_id,
)
let skipped_cast = skip_cast_fact.apply(ctx, func, node_id)
if skipped_cast {
lhs_id = skip_cast_fact.lhs_replacement_id
rhs_id = skip_cast_fact.rhs_replacement_id
relation_fact = OiRefEqRelationFact::from_operands(
ctx, func, dead, lhs_id, rhs_id,
)
match OiRefEqEqualityFact::from_relation(relation_fact) {
Some(equality_fact) => {
equality_fact.apply(ctx, func, node_id)
return true
}
None => ()
}
}
if relation_fact.lhs.nullability_fact.proves_known_null() &&
relation_fact.rhs.nullability_fact.proves_known_null() {
// Binaryen v131's consecutive-input path preserves one completed operand
// evaluation even when both references are statically null.
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
lhs_id,
@lib.I32(1),
)
return true
}
if relation_fact.lhs.nullability_fact.proves_known_null() {
if relation_fact.rhs.nullability_fact.proves_known_non_null() {
if optimize_instructions_node_effects_are_pure(ctx, func, rhs_id) {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
} else {
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
rhs_id,
@lib.I32(0),
)
}
} else {
ignore(
OiSynthesizedRefIsNullFact::from_operand(rhs_id).apply(
ctx, func, node_id,
),
)
}
return true
}
if relation_fact.rhs.nullability_fact.proves_known_null() {
if relation_fact.lhs.nullability_fact.proves_known_non_null() {
if optimize_instructions_node_effects_are_pure(ctx, func, lhs_id) {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
} else {
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
lhs_id,
@lib.I32(0),
)
}
} else {
ignore(
OiSynthesizedRefIsNullFact::from_operand(lhs_id).apply(
ctx, func, node_id,
),
)
}
return true
}
skipped_cast
}
///|
priv struct OiSameLocalIntegerCompareFact {
result : @lib.I32
}
///|
fn OiSameLocalIntegerCompareFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiSameLocalIntegerCompareFact? {
// Binaryen v130 folds integer relational compares of the same local value to
// constants in `visitBinary`. Keep the local-equality proof and per-opcode
// truth table as one fact so the visitor does not own a compare whitelist.
match
(
OiLocalGetLeafFact::from_node(func, dead, lhs_id).local_id,
OiLocalGetLeafFact::from_node(func, dead, rhs_id).local_id,
) {
(Some(lhs_local), Some(rhs_local)) if lhs_local == rhs_local => ()
_ => return None
}
let result = match instr {
@lib.Instruction::I32Eq | @lib.Instruction::I64Eq => @lib.I32(1)
@lib.Instruction::I32Ne
| @lib.Instruction::I64Ne
| @lib.Instruction::I32LtS
| @lib.Instruction::I32LtU
| @lib.Instruction::I64LtS
| @lib.Instruction::I64LtU
| @lib.Instruction::I32GtS
| @lib.Instruction::I32GtU
| @lib.Instruction::I64GtS
| @lib.Instruction::I64GtU => @lib.I32(0)
@lib.Instruction::I32LeS
| @lib.Instruction::I32LeU
| @lib.Instruction::I64LeS
| @lib.Instruction::I64LeU
| @lib.Instruction::I32GeS
| @lib.Instruction::I32GeU
| @lib.Instruction::I64GeS
| @lib.Instruction::I64GeU => @lib.I32(1)
_ => return None
}
Some({ result, })
}
///|
fn OiSameLocalIntegerCompareFact::apply(
self : OiSameLocalIntegerCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
optimize_instructions_replace_with_const_i32(ctx, func, node_id, self.result)
}
///|
fn optimize_instructions_bool_i32(value : Bool) -> @lib.I32 {
if value {
@lib.I32(1)
} else {
@lib.I32(0)
}
}
///|
fn optimize_instructions_try_fold_integer_const_eq_ne_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match instr {
@lib.Instruction::I32Eq
| @lib.Instruction::I32Ne
| @lib.Instruction::I32LtU
| @lib.Instruction::I32LeU
| @lib.Instruction::I32GtU
| @lib.Instruction::I32GeU =>
match
(
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(Some(@lib.I32(lhs_raw)), Some(@lib.I32(rhs_raw))) => {
match instr {
@lib.Instruction::I32Eq | @lib.Instruction::I32Ne if lhs_raw >
rhs_raw => return false
_ => ()
}
let result = match instr {
@lib.Instruction::I32Eq => lhs_raw == rhs_raw
@lib.Instruction::I32Ne => lhs_raw != rhs_raw
@lib.Instruction::I32LtU =>
lhs_raw.reinterpret_as_uint() < rhs_raw.reinterpret_as_uint()
@lib.Instruction::I32LeU =>
lhs_raw.reinterpret_as_uint() <= rhs_raw.reinterpret_as_uint()
@lib.Instruction::I32GtU =>
lhs_raw.reinterpret_as_uint() > rhs_raw.reinterpret_as_uint()
@lib.Instruction::I32GeU =>
lhs_raw.reinterpret_as_uint() >= rhs_raw.reinterpret_as_uint()
_ => false
}
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
optimize_instructions_bool_i32(result),
)
true
}
_ => false
}
@lib.Instruction::I64Eq
| @lib.Instruction::I64Ne
| @lib.Instruction::I64LtU
| @lib.Instruction::I64LeU
| @lib.Instruction::I64GtU
| @lib.Instruction::I64GeU =>
match
(
optimize_instructions_i64_const(func, dead, lhs_id),
optimize_instructions_i64_const(func, dead, rhs_id),
) {
(Some(@lib.I64(lhs_raw)), Some(@lib.I64(rhs_raw))) => {
match instr {
@lib.Instruction::I64Eq | @lib.Instruction::I64Ne if lhs_raw >
rhs_raw => return false
_ => ()
}
let result = match instr {
@lib.Instruction::I64Eq => lhs_raw == rhs_raw
@lib.Instruction::I64Ne => lhs_raw != rhs_raw
@lib.Instruction::I64LtU =>
lhs_raw.reinterpret_as_uint64() < rhs_raw.reinterpret_as_uint64()
@lib.Instruction::I64LeU =>
lhs_raw.reinterpret_as_uint64() <= rhs_raw.reinterpret_as_uint64()
@lib.Instruction::I64GtU =>
lhs_raw.reinterpret_as_uint64() > rhs_raw.reinterpret_as_uint64()
@lib.Instruction::I64GeU =>
lhs_raw.reinterpret_as_uint64() >= rhs_raw.reinterpret_as_uint64()
_ => false
}
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
optimize_instructions_bool_i32(result),
)
true
}
_ => false
}
_ => false
}
}
///|
priv enum OiDomainEdgeKind {
DomainLowerEdge
DomainUpperEdge
}
///|
priv struct OiSignedDomainEdgeCompareFact {
edge : OiDomainEdgeKind
result : @lib.I32
}
///|
priv struct OiUnsignedDomainEdgeCompareFact {
edge : OiDomainEdgeKind
result : @lib.I32
lhs_id : Int32
preserve_lhs : Bool
}
///|
fn OiSignedDomainEdgeCompareFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiSignedDomainEdgeCompareFact? {
// Binaryen v130 folds signed integer comparisons at the unreachable domain
// endpoints only when the left operand can be erased without preserving
// effects. Keep the endpoint proof and boolean result as a fact instead of a
// helper-level compare/result table.
if !OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id) {
return None
}
let (rhs_is_signed_min, rhs_is_signed_max) = match instr {
@lib.Instruction::I32LtS
| @lib.Instruction::I32LeS
| @lib.Instruction::I32GtS
| @lib.Instruction::I32GeS =>
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => (raw == -2147483648, raw == 2147483647)
_ => return None
}
@lib.Instruction::I64LtS
| @lib.Instruction::I64LeS
| @lib.Instruction::I64GtS
| @lib.Instruction::I64GeS =>
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) =>
(raw == -9223372036854775808L, raw == 9223372036854775807L)
_ => return None
}
_ => return None
}
if rhs_is_signed_min {
match instr {
@lib.Instruction::I32LtS | @lib.Instruction::I64LtS =>
Some({ edge: DomainLowerEdge, result: @lib.I32(0), })
@lib.Instruction::I32GeS | @lib.Instruction::I64GeS =>
Some({ edge: DomainLowerEdge, result: @lib.I32(1), })
_ => None
}
} else if rhs_is_signed_max {
match instr {
@lib.Instruction::I32GtS | @lib.Instruction::I64GtS =>
Some({ edge: DomainUpperEdge, result: @lib.I32(0), })
@lib.Instruction::I32LeS | @lib.Instruction::I64LeS =>
Some({ edge: DomainUpperEdge, result: @lib.I32(1), })
_ => None
}
} else {
None
}
}
///|
fn OiUnsignedDomainEdgeCompareFact::from_operands(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiUnsignedDomainEdgeCompareFact? {
// Binaryen v130 folds unsigned integer comparisons at the impossible domain
// endpoints and preserves left-operand effects by dropping the operand before
// the replacement constant. Carry that effect-preservation decision in the
// fact so the visitor does not own a separate unsigned endpoint whitelist.
let (rhs_is_zero, rhs_is_unsigned_max) = match instr {
@lib.Instruction::I32LtU
| @lib.Instruction::I32LeU
| @lib.Instruction::I32GtU
| @lib.Instruction::I32GeU =>
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => (raw == 0, raw == -1)
_ => return None
}
@lib.Instruction::I64LtU
| @lib.Instruction::I64LeU
| @lib.Instruction::I64GtU
| @lib.Instruction::I64GeU =>
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) => (raw == 0L, raw == -1L)
_ => return None
}
_ => return None
}
let (edge, result) = if rhs_is_zero {
match instr {
@lib.Instruction::I32LtU | @lib.Instruction::I64LtU =>
(DomainLowerEdge, @lib.I32(0))
@lib.Instruction::I32GeU | @lib.Instruction::I64GeU =>
(DomainLowerEdge, @lib.I32(1))
_ => return None
}
} else if rhs_is_unsigned_max {
match instr {
@lib.Instruction::I32GtU | @lib.Instruction::I64GtU =>
(DomainUpperEdge, @lib.I32(0))
@lib.Instruction::I32LeU | @lib.Instruction::I64LeU =>
(DomainUpperEdge, @lib.I32(1))
_ => return None
}
} else {
return None
}
Some({
edge,
result,
lhs_id,
preserve_lhs: OiSideEffectFreeExpressionFact::proves_node_free(
func, dead, lhs_id,
) ||
optimize_instructions_node_effects_are_pure(ctx, func, lhs_id),
})
}
///|
fn OiSignedDomainEdgeCompareFact::apply(
self : OiSignedDomainEdgeCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.edge {
DomainLowerEdge | DomainUpperEdge => ()
}
optimize_instructions_replace_with_const_i32(ctx, func, node_id, self.result)
}
///|
fn OiUnsignedDomainEdgeCompareFact::apply(
self : OiUnsignedDomainEdgeCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.edge {
DomainLowerEdge | DomainUpperEdge => ()
}
if self.preserve_lhs {
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
self.result,
)
} else {
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
node_id,
self.lhs_id,
self.result,
)
}
}
///|
fn optimize_instructions_try_fold_signed_domain_edge_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiSignedDomainEdgeCompareFact::from_operands(
func, dead, instr, lhs_id, rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
fn optimize_instructions_try_fold_unsigned_domain_edge_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiUnsignedDomainEdgeCompareFact::from_operands(
ctx, func, dead, instr, lhs_id, rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
fn optimize_instructions_try_fold_same_local_integer_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiSameLocalIntegerCompareFact::from_operands(
func, dead, instr, lhs_id, rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
priv enum OiSameLocalIntegerBinaryRewriteKind {
SameLocalBinaryConstI32Zero
SameLocalBinaryConstI64Zero
SameLocalBinaryRetainLeft
}
///|
priv struct OiSameLocalIntegerBinaryFact {
kind : OiSameLocalIntegerBinaryRewriteKind
retained_id : Int32
}
///|
fn OiSameLocalIntegerBinaryFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiSameLocalIntegerBinaryFact? {
// Binaryen v130 folds same-local integer `sub` / `xor` to typed zero and
// idempotent integer `and` / `or` to the local. Store the same-local proof,
// opcode family, and retained child as a fact instead of a visitor-local
// opcode whitelist.
match
(
OiLocalGetLeafFact::from_node(func, dead, lhs_id).local_id,
OiLocalGetLeafFact::from_node(func, dead, rhs_id).local_id,
) {
(Some(lhs_local), Some(rhs_local)) if lhs_local == rhs_local => ()
_ => return None
}
let kind = match instr {
@lib.Instruction::I32Sub | @lib.Instruction::I32Xor =>
OiSameLocalIntegerBinaryRewriteKind::SameLocalBinaryConstI32Zero
@lib.Instruction::I64Sub | @lib.Instruction::I64Xor =>
OiSameLocalIntegerBinaryRewriteKind::SameLocalBinaryConstI64Zero
@lib.Instruction::I32And
| @lib.Instruction::I32Or
| @lib.Instruction::I64And
| @lib.Instruction::I64Or =>
OiSameLocalIntegerBinaryRewriteKind::SameLocalBinaryRetainLeft
_ => return None
}
Some({ kind, retained_id: lhs_id, })
}
///|
fn OiSameLocalIntegerBinaryFact::apply(
self : OiSameLocalIntegerBinaryFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
match self.kind {
OiSameLocalIntegerBinaryRewriteKind::SameLocalBinaryConstI32Zero =>
optimize_instructions_replace_with_const_i32(
ctx,
func,
node_id,
@lib.I32(0),
)
OiSameLocalIntegerBinaryRewriteKind::SameLocalBinaryConstI64Zero =>
optimize_instructions_replace_with_const_i64(
ctx,
func,
node_id,
@lib.I64(0L),
)
OiSameLocalIntegerBinaryRewriteKind::SameLocalBinaryRetainLeft =>
optimize_instructions_replace_with_node_id(
ctx,
func,
node_id,
self.retained_id,
)
}
}
///|
fn optimize_instructions_try_fold_same_local_integer_binary(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiSameLocalIntegerBinaryFact::from_operands(
func, dead, instr, lhs_id, rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, node_id)
true
}
None => false
}
}
///|
priv struct OiI32SignExtensionRangeFact {
child_id : Int32
min_value : Int32
max_value : Int32
mask_value : Int32
}
///|
fn OiI32SignExtensionRangeFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiI32SignExtensionRangeFact? {
// Binaryen v130 recognizes the bounded signed result ranges for i32
// sign-extension opcodes before applying the equality and unsigned-top compare
// rewrites below. Store the range, child, and masking value together so those
// consumers do not reconstruct a local opcode/range scanner.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Unary if node.child_count == 1 => ()
_ => return None
}
let child_id = @ir.hot_child_get(func, node_id, 0)
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Extend8S) =>
Some({ child_id, min_value: -128, max_value: 127, mask_value: 255, })
Some(@lib.Instruction::I32Extend16S) =>
Some({ child_id, min_value: -32768, max_value: 32767, mask_value: 65535, })
_ => None
}
}
///|
priv struct OiI64SignExtensionNodeFact {
node_id : Int32
}
///|
fn OiI64SignExtensionNodeFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
) -> OiI64SignExtensionNodeFact? {
// Binaryen v130's unsigned-top i64 compare rewrite only needs proof that the
// left operand is a live i64 sign-extension unary. Keep that producer proof as
// an explicit fact instead of a boolean node-shape helper.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Unary if node.child_count == 1 => ()
_ => return None
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I64Extend8S)
| Some(@lib.Instruction::I64Extend16S)
| Some(@lib.Instruction::I64Extend32S) => Some({ node_id, })
_ => None
}
}
///|
priv struct OiI32SignExtensionEqualityFoldFact {
compare_id : Int32
sign_ext_fact : OiI32SignExtensionRangeFact
result : @lib.I32
}
///|
fn OiI32SignExtensionEqualityFoldFact::from_compare(
func : @ir.HotFunc,
dead : @bitset.BitSet,
compare_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32SignExtensionEqualityFoldFact? {
let sign_ext_fact = match
OiI32SignExtensionRangeFact::from_node(func, dead, lhs_id) {
Some(fact) => fact
None => return None
}
let rhs_value = match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => raw
_ => return None
}
if rhs_value >= sign_ext_fact.min_value &&
rhs_value <= sign_ext_fact.max_value {
return None
}
let result = match instr {
@lib.Instruction::I32Eq => @lib.I32(0)
@lib.Instruction::I32Ne => @lib.I32(1)
_ => return None
}
Some({ compare_id, sign_ext_fact, result, })
}
///|
fn OiI32SignExtensionEqualityFoldFact::apply(
self : OiI32SignExtensionEqualityFoldFact,
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
) -> Unit {
let drop_id = self.sign_ext_fact.child_id
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, drop_id) ||
optimize_instructions_node_effects_are_pure(ctx, func, drop_id) {
optimize_instructions_replace_with_const_i32(
ctx,
func,
self.compare_id,
self.result,
)
} else {
optimize_instructions_replace_with_drop_then_const_i32(
ctx,
func,
self.compare_id,
drop_id,
self.result,
)
}
}
///|
priv struct OiI32SignExtensionUnsignedTopCompareFact {
compare_id : Int32
sign_ext_fact : OiI32SignExtensionRangeFact
replacement_instr : @lib.Instruction
}
///|
fn OiI32SignExtensionUnsignedTopCompareFact::from_compare(
func : @ir.HotFunc,
dead : @bitset.BitSet,
compare_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI32SignExtensionUnsignedTopCompareFact? {
let sign_ext_fact = match
OiI32SignExtensionRangeFact::from_node(func, dead, lhs_id) {
Some(fact) => fact
None => return None
}
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) if raw == -1 => ()
_ => return None
}
let replacement_instr = match instr {
@lib.Instruction::I32LtU => @lib.Instruction::i32_ne()
@lib.Instruction::I32GeU => @lib.Instruction::i32_eq()
_ => return None
}
Some({ compare_id, sign_ext_fact, replacement_instr, })
}
///|
fn OiI32SignExtensionUnsignedTopCompareFact::apply(
self : OiI32SignExtensionUnsignedTopCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Unit {
let sign_ext_fact = self.sign_ext_fact
let mask_value = sign_ext_fact.mask_value
let masked = optimize_instructions_build_i32_binary(
func,
@lib.Instruction::i32_and(),
sign_ext_fact.child_id,
@ir.hot_build_const_i32(func, @lib.I32(mask_value)),
)
optimize_instructions_replace_with_exact_compare(
ctx,
func,
self.compare_id,
self.replacement_instr,
masked,
@ir.hot_build_const_i32(func, @lib.I32(mask_value)),
)
}
///|
priv struct OiI64SignExtensionUnsignedTopCompareFact {
compare_id : Int32
sign_ext_fact : OiI64SignExtensionNodeFact
replacement_instr : @lib.Instruction
}
///|
fn OiI64SignExtensionUnsignedTopCompareFact::from_compare(
func : @ir.HotFunc,
dead : @bitset.BitSet,
compare_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiI64SignExtensionUnsignedTopCompareFact? {
let sign_ext_fact = match
OiI64SignExtensionNodeFact::from_node(func, dead, lhs_id) {
Some(fact) => fact
None => return None
}
match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) if raw == -1L => ()
_ => return None
}
let replacement_instr = match instr {
@lib.Instruction::I64LtU => @lib.Instruction::i64_ne()
@lib.Instruction::I64GeU => @lib.Instruction::i64_eq()
_ => return None
}
Some({ compare_id, sign_ext_fact, replacement_instr, })
}
///|
fn OiI64SignExtensionUnsignedTopCompareFact::apply(
self : OiI64SignExtensionUnsignedTopCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Unit {
optimize_instructions_replace_with_exact_compare(
ctx,
func,
self.compare_id,
self.replacement_instr,
self.sign_ext_fact.node_id,
@ir.hot_build_const_i64(func, @lib.I64(-1L)),
)
}
///|
fn optimize_instructions_try_fold_i32_sign_extension_equality_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiI32SignExtensionEqualityFoldFact::from_compare(
func, dead, node_id, instr, lhs_id, rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func, dead)
true
}
None => false
}
}
///|
fn optimize_instructions_try_rewrite_i32_sign_extension_unsigned_top_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiI32SignExtensionUnsignedTopCompareFact::from_compare(
func, dead, node_id, instr, lhs_id, rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func)
true
}
None => false
}
}
///|
fn optimize_instructions_try_rewrite_i64_sign_extension_unsigned_top_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
match
OiI64SignExtensionUnsignedTopCompareFact::from_compare(
func, dead, node_id, instr, lhs_id, rhs_id,
) {
Some(fact) => {
fact.apply(ctx, func)
true
}
None => false
}
}
///|
fn optimize_instructions_i32_unsigned_max(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Int32? {
optimize_instructions_i32_unsigned_max_fact_for_node(
func, dead, scratch, node_id,
)
}
///|
fn optimize_instructions_i32_masked_unsigned_compare_result(
instr : @lib.Instruction,
max_value : Int32,
rhs : Int32,
) -> @lib.I32? {
let rhs_outside = rhs < 0 || rhs > max_value
if !rhs_outside {
return None
}
match instr {
@lib.Instruction::I32Eq
| @lib.Instruction::I32GeU
| @lib.Instruction::I32GtU => Some(@lib.I32(0))
@lib.Instruction::I32Ne
| @lib.Instruction::I32LtU
| @lib.Instruction::I32LeU => Some(@lib.I32(1))
@lib.Instruction::I32LtS =>
if rhs <= 0 {
Some(@lib.I32(0))
} else {
Some(@lib.I32(1))
}
@lib.Instruction::I32LeS =>
if rhs < 0 {
Some(@lib.I32(0))
} else {
Some(@lib.I32(1))
}
@lib.Instruction::I32GtS =>
if rhs < 0 {
Some(@lib.I32(1))
} else {
Some(@lib.I32(0))
}
@lib.Instruction::I32GeS =>
if rhs <= 0 {
Some(@lib.I32(1))
} else {
Some(@lib.I32(0))
}
_ => None
}
}
///|
fn optimize_instructions_i32_signed_to_unsigned_compare_instr(
instr : @lib.Instruction,
) -> @lib.Instruction? {
match instr {
@lib.Instruction::I32LtS => Some(@lib.Instruction::i32_lt_u())
@lib.Instruction::I32LeS => Some(@lib.Instruction::i32_le_u())
@lib.Instruction::I32GtS => Some(@lib.Instruction::i32_gt_u())
@lib.Instruction::I32GeS => Some(@lib.Instruction::i32_ge_u())
_ => None
}
}
///|
fn optimize_instructions_try_fold_i32_masked_unsigned_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
let max_value = match
optimize_instructions_i32_unsigned_max(func, dead, scratch, lhs_id) {
Some(value) => value
None => return false
}
let rhs = match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(raw)) => raw
_ => return false
}
match
optimize_instructions_i32_masked_unsigned_compare_result(
instr, max_value, rhs,
) {
Some(result) => {
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id) ||
optimize_instructions_node_effects_are_pure(ctx, func, lhs_id) {
optimize_instructions_replace_with_const_i32(ctx, func, node_id, result)
} else {
optimize_instructions_replace_with_drop_then_const_i32(
ctx, func, node_id, lhs_id, result,
)
}
true
}
None =>
match optimize_instructions_i32_signed_to_unsigned_compare_instr(instr) {
Some(replacement) if rhs >= 0 => {
optimize_instructions_replace_with_exact_compare(
ctx, func, node_id, replacement, lhs_id, rhs_id,
)
true
}
_ => false
}
}
}
///|
fn optimize_instructions_i64_unsigned_max(
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
) -> Int64? {
optimize_instructions_i64_unsigned_max_fact_for_node(
func, dead, scratch, node_id,
)
}
///|
fn optimize_instructions_i64_masked_unsigned_compare_result(
instr : @lib.Instruction,
max_value : Int64,
rhs : Int64,
) -> @lib.I32? {
let rhs_outside = rhs < 0L || rhs > max_value
if !rhs_outside {
return None
}
match instr {
@lib.Instruction::I64Eq
| @lib.Instruction::I64GeU
| @lib.Instruction::I64GtU => Some(@lib.I32(0))
@lib.Instruction::I64Ne
| @lib.Instruction::I64LtU
| @lib.Instruction::I64LeU => Some(@lib.I32(1))
@lib.Instruction::I64LtS =>
if rhs <= 0L {
Some(@lib.I32(0))
} else {
Some(@lib.I32(1))
}
@lib.Instruction::I64LeS =>
if rhs < 0L {
Some(@lib.I32(0))
} else {
Some(@lib.I32(1))
}
@lib.Instruction::I64GtS =>
if rhs < 0L {
Some(@lib.I32(1))
} else {
Some(@lib.I32(0))
}
@lib.Instruction::I64GeS =>
if rhs <= 0L {
Some(@lib.I32(1))
} else {
Some(@lib.I32(0))
}
_ => None
}
}
///|
fn optimize_instructions_i64_signed_to_unsigned_compare_instr(
instr : @lib.Instruction,
) -> @lib.Instruction? {
match instr {
@lib.Instruction::I64LtS => Some(@lib.Instruction::i64_lt_u())
@lib.Instruction::I64LeS => Some(@lib.Instruction::i64_le_u())
@lib.Instruction::I64GtS => Some(@lib.Instruction::i64_gt_u())
@lib.Instruction::I64GeS => Some(@lib.Instruction::i64_ge_u())
_ => None
}
}
///|
fn optimize_instructions_try_fold_i64_masked_unsigned_compare(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
let max_value = match
optimize_instructions_i64_unsigned_max(func, dead, scratch, lhs_id) {
Some(value) => value
None => return false
}
let rhs = match optimize_instructions_i64_const(func, dead, rhs_id) {
Some(@lib.I64(raw)) => raw
_ => return false
}
match
optimize_instructions_i64_masked_unsigned_compare_result(
instr, max_value, rhs,
) {
Some(result) => {
if OiSideEffectFreeExpressionFact::proves_node_free(func, dead, lhs_id) ||
optimize_instructions_node_effects_are_pure(ctx, func, lhs_id) {
optimize_instructions_replace_with_const_i32(ctx, func, node_id, result)
} else {
optimize_instructions_replace_with_drop_then_const_i32(
ctx, func, node_id, lhs_id, result,
)
}
true
}
None =>
match optimize_instructions_i64_signed_to_unsigned_compare_instr(instr) {
Some(replacement) if rhs >= 0L => {
optimize_instructions_replace_with_exact_compare(
ctx, func, node_id, replacement, lhs_id, rhs_id,
)
true
}
_ => false
}
}
}
///|
priv enum OiEqzBooleanCompareWidthKind {
EqzBooleanCompareI32
EqzBooleanCompareI64
}
///|
priv enum OiEqzBooleanCompareRewriteKind {
EqzBooleanCompareKeepEqz
EqzBooleanCompareInvertToNeZero
}
///|
priv struct OiEqzBooleanCompareOperandFact {
eqz_id : Int32
operand_id : Int32
const_value : Int32
width : OiEqzBooleanCompareWidthKind
}
///|
fn OiEqzBooleanCompareOperandFact::from_pair(
func : @ir.HotFunc,
dead : @bitset.BitSet,
eqz_id : Int32,
const_id : Int32,
) -> OiEqzBooleanCompareOperandFact? {
// Binaryen v130 `visitBinary` recognizes equality/inequality compares between
// a direct `i32.eqz` / `i64.eqz` boolean and exact i32 boolean constants. Keep
// the direct eqz shell, literal boolean, operand liveness, and eqz width as a
// source-backed fact instead of returning a `-1` sentinel pair to the visitor.
let width = match @ir.hot_node_exact_instr(func, eqz_id) {
Some(@lib.Instruction::I32Eqz) => EqzBooleanCompareI32
Some(@lib.Instruction::I64Eqz) => EqzBooleanCompareI64
_ => return None
}
let const_value = match
optimize_instructions_i32_const(func, dead, const_id) {
Some(@lib.I32(raw)) if raw == 0 || raw == 1 => raw
_ => return None
}
if @ir.hot_child_count(func, eqz_id) != 1 {
return None
}
let operand_id = @ir.hot_child_get(func, eqz_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, operand_id) {
return None
}
Some({ eqz_id, operand_id, const_value, width, })
}
///|
priv struct OiEqzBooleanCompareFact {
operand_fact : OiEqzBooleanCompareOperandFact
rewrite : OiEqzBooleanCompareRewriteKind
}
///|
fn OiEqzBooleanCompareFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> OiEqzBooleanCompareFact? {
match instr {
@lib.Instruction::I32Eq | @lib.Instruction::I32Ne => ()
_ => return None
}
let operand_fact = match
OiEqzBooleanCompareOperandFact::from_pair(func, dead, lhs_id, rhs_id) {
Some(fact) => fact
None =>
match
OiEqzBooleanCompareOperandFact::from_pair(func, dead, rhs_id, lhs_id) {
Some(fact) => fact
None => return None
}
}
let rewrite = match (instr, operand_fact.const_value) {
(@lib.Instruction::I32Eq, 1) | (@lib.Instruction::I32Ne, 0) =>
EqzBooleanCompareKeepEqz
_ => EqzBooleanCompareInvertToNeZero
}
Some({ operand_fact, rewrite, })
}
///|
fn OiEqzBooleanCompareFact::apply(
self : OiEqzBooleanCompareFact,
ctx : HotPassContext,
func : @ir.HotFunc,
node_id : Int32,
) -> Unit {
ignore(self.operand_fact.eqz_id)
let operand_id = self.operand_fact.operand_id
match (self.rewrite, self.operand_fact.width) {
(EqzBooleanCompareKeepEqz, EqzBooleanCompareI64) =>
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_eqz(),
operand_id,
)
(EqzBooleanCompareKeepEqz, EqzBooleanCompareI32) =>
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
operand_id,
)
(EqzBooleanCompareInvertToNeZero, EqzBooleanCompareI64) =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i64_ne(),
operand_id,
@ir.hot_build_const_i64(func, @lib.I64(0L)),
)
(EqzBooleanCompareInvertToNeZero, EqzBooleanCompareI32) =>
optimize_instructions_replace_with_exact_compare(
ctx,
func,
node_id,
@lib.Instruction::i32_ne(),
operand_id,
@ir.hot_build_const_i32(func, @lib.I32(0)),
)
}
}
///|
fn optimize_instructions_try_rewrite_compare_eqz(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
node_id : Int32,
instr : @lib.Instruction,
lhs_id : Int32,
rhs_id : Int32,
) -> Bool {
if optimize_instructions_should_keep_i64_eq_zero_if_condition(
func, dead, scratch, node_id, instr, lhs_id, rhs_id,
) {
return false
}
match
OiEqzBooleanCompareFact::from_operands(func, dead, instr, lhs_id, rhs_id) {
Some(fact) => {
fact.apply(ctx, func, node_id)
return true
}
None => ()
}
match
(
instr,
optimize_instructions_i32_const(func, dead, lhs_id),
optimize_instructions_i32_const(func, dead, rhs_id),
) {
(@lib.Instruction::I32Eq, Some(@lib.I32(0)), _) =>
if OiRemainderInstructionFact::proves_node_remainder(func, dead, rhs_id) {
()
} else {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
rhs_id,
)
return true
}
(@lib.Instruction::I32Eq, _, Some(@lib.I32(0))) =>
if OiRemainderInstructionFact::proves_node_remainder(func, dead, lhs_id) {
()
} else {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i32_eqz(),
lhs_id,
)
return true
}
_ => ()
}
match
(
instr,
optimize_instructions_i64_const(func, dead, lhs_id),
optimize_instructions_i64_const(func, dead, rhs_id),
) {
(@lib.Instruction::I64Eq, Some(@lib.I64(0L)), _) =>
if OiRemainderInstructionFact::proves_node_remainder(func, dead, rhs_id) {
false
} else {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_eqz(),
rhs_id,
)
true
}
(@lib.Instruction::I64Eq, _, Some(@lib.I64(0L))) =>
if OiRemainderInstructionFact::proves_node_remainder(func, dead, lhs_id) {
false
} else {
optimize_instructions_replace_with_exact_unary(
ctx,
func,
node_id,
@lib.Instruction::i64_eqz(),
lhs_id,
)
true
}
_ => false
}
}
///|
priv enum OiBooleanContextLeafRewriteKind {
BooleanContextLeafNoRewrite
BooleanContextLeafFoldEqzFallthroughConst
BooleanContextLeafForwardI32NeZero
BooleanContextLeafForwardI32ZeroMinus
BooleanContextLeafMaskI32SignedRemainder
}
///|
priv struct OiBooleanForwardReplacementFact {
replacement_id : Int32
}
///|
priv enum OiBooleanTryExpressionThrowKind {
BooleanTryOrdinaryExpressionProvenNonThrowing
BooleanTryBlockRegionProvenNonThrowing
}
///|
priv struct OiBooleanTryExpressionThrowFact {
kind : OiBooleanTryExpressionThrowKind
}
///|
fn OiBooleanTryExpressionThrowFact::proves_nonthrowing(
self : OiBooleanTryExpressionThrowFact,
) -> Bool {
match self.kind {
BooleanTryOrdinaryExpressionProvenNonThrowing
| BooleanTryBlockRegionProvenNonThrowing => true
}
}
///|
priv enum OiBooleanTryRegionThrowKind {
BooleanTryRegionProvenNonThrowing
}
///|
priv struct OiBooleanTryRegionThrowFact {
kind : OiBooleanTryRegionThrowKind
}
///|
fn OiBooleanTryRegionThrowFact::proves_nonthrowing(
self : OiBooleanTryRegionThrowFact,
) -> Bool {
match self.kind {
BooleanTryRegionProvenNonThrowing => true
}
}
///|
fn OiBooleanTryRegionThrowFact::from_region(
func : @ir.HotFunc,
region : @ir.HotRegionRef,
states : @hashmap.HashMap[Int32, Int32],
depth : Int32,
) -> OiBooleanTryRegionThrowFact? {
if depth >= 64 {
return None
}
let root_count = @ir.hot_region_root_count(func, region)
for slot = 0; slot < root_count; slot = slot + 1 {
match
OiBooleanTryExpressionThrowFact::from_node(
func,
@ir.hot_region_get(func, region, slot),
states,
depth + 1,
) {
Some(fact) if fact.proves_nonthrowing() => ()
_ => return None
}
}
Some({ kind: BooleanTryRegionProvenNonThrowing, })
}
///|
fn OiBooleanTryExpressionThrowFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
states : @hashmap.HashMap[Int32, Int32],
depth : Int32,
) -> OiBooleanTryExpressionThrowFact? {
// Binaryen v130 admits a Try fallthrough only when
// `EffectAnalyzer(..., try.body).throws()` is false. Prove that narrow
// property recursively rather than treating the direct node flag as a
// subtree summary. Ordinary expression children and nested block regions are
// explicit facts; other structured controls remain fail-closed until their
// owned regions are audited.
if depth >= 64 || node_id < 0 || node_id >= @ir.hot_node_count(func) {
return None
}
match states.get(node_id) {
Some(2) =>
return Some({ kind: BooleanTryOrdinaryExpressionProvenNonThrowing, })
Some(1) => return None
_ => states.set(node_id, 1)
}
if @ir.hot_node_may_throw_exception(func, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
let kind = match node.op {
@ir.HotOp::Block =>
match
OiBooleanTryRegionThrowFact::from_region(
func,
@ir.hot_region_body_ref(func, node_id),
states,
depth + 1,
) {
Some(fact) if fact.proves_nonthrowing() =>
BooleanTryBlockRegionProvenNonThrowing
_ => return None
}
_ => {
if @ir.hot_is_control_node(func, node_id) {
return None
}
for slot = 0; slot < node.child_count; slot = slot + 1 {
match
OiBooleanTryExpressionThrowFact::from_node(
func,
@ir.hot_child_get(func, node_id, slot),
states,
depth + 1,
) {
Some(fact) if fact.proves_nonthrowing() => ()
_ => return None
}
}
BooleanTryOrdinaryExpressionProvenNonThrowing
}
}
states.set(node_id, 2)
Some({ kind, })
}
///|
priv enum OiBooleanTryBodyThrowKind {
BooleanTrySingleRootBodyProvenNonThrowing
BooleanTryPrefixedBodyProvenNonThrowing
}
///|
priv struct OiBooleanTryBodyThrowFact {
kind : OiBooleanTryBodyThrowKind
value_id : Int32
}
///|
fn OiBooleanTryBodyThrowFact::proves_nonthrowing(
self : OiBooleanTryBodyThrowFact,
) -> Bool {
match self.kind {
BooleanTrySingleRootBodyProvenNonThrowing
| BooleanTryPrefixedBodyProvenNonThrowing => true
}
}
///|
fn OiBooleanTryBodyThrowFact::from_try_operand(
func : @ir.HotFunc,
try_id : Int32,
) -> OiBooleanTryBodyThrowFact? {
match @ir.hot_node_get(func, try_id).op {
@ir.HotOp::Try => ()
_ => return None
}
let body_ref = @ir.hot_region_body_ref(func, try_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count == 0 {
return None
}
let states : @hashmap.HashMap[Int32, Int32] = @hashmap.new()
for slot = 0; slot < root_count; slot = slot + 1 {
match
OiBooleanTryExpressionThrowFact::from_node(
func,
@ir.hot_region_get(func, body_ref, slot),
states,
0,
) {
Some(fact) if fact.proves_nonthrowing() => ()
_ => return None
}
}
Some({
kind: if root_count == 1 {
BooleanTrySingleRootBodyProvenNonThrowing
} else {
BooleanTryPrefixedBodyProvenNonThrowing
},
value_id: @ir.hot_region_get(func, body_ref, root_count - 1),
})
}
///|
priv enum OiBooleanEqzFallthroughValueKind {
BooleanEqzFallthroughSingleRootBlockTail
BooleanEqzFallthroughPrefixedBlockTail
BooleanEqzFallthroughLocalTeeChild
BooleanEqzFallthroughSingleRootLoopBody
BooleanEqzFallthroughPrefixedLoopBodyTail
BooleanEqzFallthroughIfThenArmTail
BooleanEqzFallthroughIfElseArmTail
BooleanEqzFallthroughSingleRootTryBody
BooleanEqzFallthroughPrefixedTryBodyTail
}
///|
priv struct OiBooleanEqzFallthroughValueFact {
kind : OiBooleanEqzFallthroughValueKind
value_id : Int32
}
///|
fn OiBooleanEqzFallthroughValueFact::from_operand(
func : @ir.HotFunc,
operand_id : Int32,
expected_type : @lib.ValType,
local_facts : OptimizeInstructionsLocalFacts,
) -> OiBooleanEqzFallthroughValueFact? {
// Binaryen v130 `Properties::getImmediateFallthrough` looks through unnamed
// nonempty blocks, direct local.tee values, loop bodies, the live arm of an
// if whose sibling arm is unreachable, and proven-nonthrowing try bodies.
// Keep the represented wrapper and selected value together so boolean eqz
// folding does not grow another opcode whitelist.
match @ir.hot_type_results(func, @ir.hot_node_type(func, operand_id)) {
[result] if result == expected_type => ()
_ => return None
}
let operand = @ir.hot_node_get(func, operand_id)
match operand.op {
@ir.HotOp::Block | @ir.HotOp::If | @ir.HotOp::Try => {
// A branch to this label supplies another result, independent of the
// lexical tail. OI only copies existing branch targets (the nested-if
// rewrite does so too); it never introduces a new target. A pass-local
// conservative inventory therefore stays sound after target removal or
// holder movement, without a whole-function scan after each rewrite.
let labels = match local_facts.boolean_exit_labels {
Some(labels) => labels
None => {
let labels = pass_compute_label_used(func)
local_facts.boolean_exit_labels = Some(labels)
labels
}
}
if pass_label_is_used(
@ir.hot_control_node_label(func, operand_id),
labels,
) {
return None
}
}
_ => ()
}
match operand.op {
@ir.HotOp::Block => {
let body_ref = @ir.hot_region_body_ref(func, operand_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count == 0 {
return None
}
Some({
kind: if root_count == 1 {
BooleanEqzFallthroughSingleRootBlockTail
} else {
BooleanEqzFallthroughPrefixedBlockTail
},
value_id: @ir.hot_region_get(func, body_ref, root_count - 1),
})
}
@ir.HotOp::LocalTee if operand.child_count == 1 =>
Some({
kind: BooleanEqzFallthroughLocalTeeChild,
value_id: @ir.hot_child_get(func, operand_id, 0),
})
@ir.HotOp::Loop => {
let body_ref = @ir.hot_region_body_ref(func, operand_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count == 0 {
return None
}
Some({
kind: if root_count == 1 {
BooleanEqzFallthroughSingleRootLoopBody
} else {
BooleanEqzFallthroughPrefixedLoopBodyTail
},
value_id: @ir.hot_region_get(func, body_ref, root_count - 1),
})
}
@ir.HotOp::If => {
let else_ref = match @ir.hot_region_else_ref(func, operand_id) {
Some(region) => region
None => return None
}
let one_sided = OiIfOneSidedFallthroughRegionFact::from_regions(
func,
@ir.hot_region_then_ref(func, operand_id),
else_ref,
)
let fallthrough_region = match one_sided.proven_fallthrough_region() {
Some(region) => region
None => return None
}
let value_id = match
optimize_instructions_region_tail_id(func, fallthrough_region) {
Some(tail_id) => tail_id
None => return None
}
let kind = match one_sided.kind {
IfThenStopsElseFallsThroughRegion => BooleanEqzFallthroughIfElseArmTail
IfElseStopsThenFallsThroughRegion => BooleanEqzFallthroughIfThenArmTail
IfNoOneSidedFallthroughRegion => return None
}
Some({ kind, value_id, })
}
@ir.HotOp::Try =>
match OiBooleanTryBodyThrowFact::from_try_operand(func, operand_id) {
Some(fact) if fact.proves_nonthrowing() =>
Some({
kind: match fact.kind {
BooleanTrySingleRootBodyProvenNonThrowing =>
BooleanEqzFallthroughSingleRootTryBody
BooleanTryPrefixedBodyProvenNonThrowing =>
BooleanEqzFallthroughPrefixedTryBodyTail
},
value_id: fact.value_id,
})
_ => None
}
_ => None
}
}
///|
fn OiBooleanEqzFallthroughValueFact::proves_supported_wrapper(
self : OiBooleanEqzFallthroughValueFact,
) -> Bool {
match self.kind {
BooleanEqzFallthroughSingleRootBlockTail
| BooleanEqzFallthroughPrefixedBlockTail
| BooleanEqzFallthroughLocalTeeChild
| BooleanEqzFallthroughSingleRootLoopBody
| BooleanEqzFallthroughPrefixedLoopBodyTail
| BooleanEqzFallthroughIfThenArmTail
| BooleanEqzFallthroughIfElseArmTail
| BooleanEqzFallthroughSingleRootTryBody
| BooleanEqzFallthroughPrefixedTryBodyTail => true
}
}
///|
priv enum OiBooleanEqzOperandPreservationKind {
BooleanEqzDropOriginalOperand
BooleanEqzErasePureWrapperChain
}
///|
fn OiBooleanEqzFallthroughValueFact::operand_preservation(
self : OiBooleanEqzFallthroughValueFact,
) -> OiBooleanEqzOperandPreservationKind {
match self.kind {
BooleanEqzFallthroughSingleRootBlockTail
| BooleanEqzFallthroughSingleRootLoopBody
| BooleanEqzFallthroughSingleRootTryBody => BooleanEqzErasePureWrapperChain
BooleanEqzFallthroughPrefixedBlockTail
| BooleanEqzFallthroughLocalTeeChild
| BooleanEqzFallthroughPrefixedLoopBodyTail
| BooleanEqzFallthroughPrefixedTryBodyTail
| BooleanEqzFallthroughIfThenArmTail
| BooleanEqzFallthroughIfElseArmTail => BooleanEqzDropOriginalOperand
}
}
///|
priv struct OiBooleanEqzFallthroughChainFact {
value_id : Int32
preservation : OiBooleanEqzOperandPreservationKind
}
///|
fn OiBooleanEqzFallthroughChainFact::from_operand(
func : @ir.HotFunc,
operand_id : Int32,
expected_type : @lib.ValType,
local_facts : OptimizeInstructionsLocalFacts,
) -> OiBooleanEqzFallthroughChainFact? {
// Binaryen v130 `Properties::getFallthrough` repeatedly applies immediate
// fallthrough selection. Accumulate the selected value and whether any
// represented wrapper requires preserving the complete original operand.
let mut value_id = operand_id
let mut preservation = BooleanEqzErasePureWrapperChain
let mut saw_wrapper = false
for depth = 0; depth < 16; depth = depth + 1 {
let step = match
OiBooleanEqzFallthroughValueFact::from_operand(
func, value_id, expected_type, local_facts,
) {
Some(fact) if fact.proves_supported_wrapper() => fact
_ => break
}
saw_wrapper = true
match step.operand_preservation() {
BooleanEqzDropOriginalOperand =>
preservation = BooleanEqzDropOriginalOperand
BooleanEqzErasePureWrapperChain => ()
}
if step.value_id == value_id {
return None
}
value_id = step.value_id
}
if !saw_wrapper {
return None
}
match
OiBooleanEqzFallthroughValueFact::from_operand(
func, value_id, expected_type, local_facts,
) {
Some(_) => None
None => Some({ value_id, preservation, })
}
}
///|
priv struct OiBooleanEqzFallthroughConstFact {
operand_id : Int32
folded_value : Int32
preservation : OiBooleanEqzOperandPreservationKind
}
///|
fn OiBooleanEqzFallthroughConstFact::from_i32_eqz(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
local_facts : OptimizeInstructionsLocalFacts,
) -> OiBooleanEqzFallthroughConstFact? {
// Binaryen v130 `optimizeBoolean` asks `Properties::getFallthrough` for an
// i32.eqz operand before folding a constant fallthrough. Represent nonempty
// single-i32-result block tails, direct local.tee children, loop-body tails,
// one-sided-if live-arm tails, and proven-nonthrowing try body tails,
// preserving or erasing the operand according to the selected source-backed
// wrapper outcome from `getDroppedChildrenAndAppend`.
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Eqz) if @ir.hot_child_count(func, node_id) == 1 =>
()
_ => return None
}
let operand_id = @ir.hot_child_get(func, node_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, operand_id) {
return None
}
let fallthrough = match
OiBooleanEqzFallthroughChainFact::from_operand(
func,
operand_id,
@lib.ValType::i32(),
local_facts,
) {
Some(fact) => fact
None => return None
}
let folded_value = match
optimize_instructions_i32_const(func, dead, fallthrough.value_id) {
Some(@lib.I32(0)) => 1
Some(@lib.I32(_)) => 0
None => return None
}
Some({ operand_id, folded_value, preservation: fallthrough.preservation, })
}
///|
fn OiBooleanEqzFallthroughConstFact::from_i64_eqz(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
local_facts : OptimizeInstructionsLocalFacts,
) -> OiBooleanEqzFallthroughConstFact? {
// Binaryen's sibling EqZInt64 path has the same i32 result and preservation
// contract, including proven-nonthrowing try body tails; only the fallthrough
// value and final literal width differ.
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I64Eqz) if @ir.hot_child_count(func, node_id) == 1 =>
()
_ => return None
}
let operand_id = @ir.hot_child_get(func, node_id, 0)
if !OiLiveNodeFact::proves_node_live(func, dead, operand_id) {
return None
}
let fallthrough = match
OiBooleanEqzFallthroughChainFact::from_operand(
func,
operand_id,
@lib.ValType::i64(),
local_facts,
) {
Some(fact) => fact
None => return None
}
let folded_value = match
optimize_instructions_i64_const(func, dead, fallthrough.value_id) {
Some(@lib.I64(0L)) => 1
Some(@lib.I64(_)) => 0
None => return None
}
Some({ operand_id, folded_value, preservation: fallthrough.preservation, })
}
///|
priv struct OiBooleanSignedRemainderMaskFact {
value_id : Int32
mask_value : Int32
}
///|
fn OiBooleanSignedRemainderMaskFact::from_operands(
func : @ir.HotFunc,
dead : @bitset.BitSet,
value_id : Int32,
divisor_id : Int32,
) -> OiBooleanSignedRemainderMaskFact? {
// Binaryen v130 accepts signed-min and positive or negative powers of two.
// Avoid taking abs(INT_MIN) directly; that case selects signed-max.
if !OiI32ValueFact::proves_node_i32_value(func, value_id) {
return None
}
let divisor = match optimize_instructions_i32_const(func, dead, divisor_id) {
Some(@lib.I32(raw)) => raw
None => return None
}
if divisor == -2147483648 {
return Some({ value_id, mask_value: 2147483647, })
}
let magnitude = if divisor < 0 { -divisor } else { divisor }
match optimize_instructions_i32_power_of_two_shift(magnitude) {
Some(_) => Some({ value_id, mask_value: magnitude - 1, })
None => None
}
}
///|
priv struct OiBooleanContextLeafRewriteFact {
kind : OiBooleanContextLeafRewriteKind
node_id : Int32
eqz_fallthrough_const_fact : OiBooleanEqzFallthroughConstFact?
forward_fact : OiBooleanForwardReplacementFact?
remainder_mask_fact : OiBooleanSignedRemainderMaskFact?
}
///|
fn OiBooleanContextLeafRewriteFact::from_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
local_facts : OptimizeInstructionsLocalFacts,
) -> OiBooleanContextLeafRewriteFact {
// Binaryen v130 `optimizeBoolean` forwards exact `i32.ne(x, 0)` and
// `i32.sub(0, x)` shells, and rewrites supported signed remainder shells to
// i32.and masks. Keep forwarding and instruction-rebuild payloads separate so
// direct and recursively selected boolean locations share one bounded plan.
let no_rewrite = {
kind: BooleanContextLeafNoRewrite,
node_id,
eqz_fallthrough_const_fact: None,
forward_fact: None,
remainder_mask_fact: None,
}
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return no_rewrite
}
match
OiBooleanEqzFallthroughConstFact::from_i32_eqz(
func, dead, node_id, local_facts,
) {
Some(eqz_fallthrough_const_fact) =>
return {
kind: BooleanContextLeafFoldEqzFallthroughConst,
node_id,
eqz_fallthrough_const_fact: Some(eqz_fallthrough_const_fact),
forward_fact: None,
remainder_mask_fact: None,
}
None => ()
}
match
OiBooleanEqzFallthroughConstFact::from_i64_eqz(
func, dead, node_id, local_facts,
) {
Some(eqz_fallthrough_const_fact) =>
return {
kind: BooleanContextLeafFoldEqzFallthroughConst,
node_id,
eqz_fallthrough_const_fact: Some(eqz_fallthrough_const_fact),
forward_fact: None,
remainder_mask_fact: None,
}
None => ()
}
if @ir.hot_child_count(func, node_id) != 2 {
return no_rewrite
}
let lhs_id = @ir.hot_child_get(func, node_id, 0)
let rhs_id = @ir.hot_child_get(func, node_id, 1)
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Ne) =>
match optimize_instructions_i32_const(func, dead, rhs_id) {
Some(@lib.I32(0)) if OiI32ValueFact::proves_node_i32_value(func, lhs_id) =>
{
kind: BooleanContextLeafForwardI32NeZero,
node_id,
eqz_fallthrough_const_fact: None,
forward_fact: Some({ replacement_id: lhs_id, }),
remainder_mask_fact: None,
}
_ => no_rewrite
}
Some(@lib.Instruction::I32Sub) =>
match optimize_instructions_i32_const(func, dead, lhs_id) {
Some(@lib.I32(0)) if OiI32ValueFact::proves_node_i32_value(func, rhs_id) =>
{
kind: BooleanContextLeafForwardI32ZeroMinus,
node_id,
eqz_fallthrough_const_fact: None,
forward_fact: Some({ replacement_id: rhs_id, }),
remainder_mask_fact: None,
}
_ => no_rewrite
}
Some(@lib.Instruction::I32RemS) =>
match
OiBooleanSignedRemainderMaskFact::from_operands(
func, dead, lhs_id, rhs_id,
) {
Some(mask_fact) =>
{
kind: BooleanContextLeafMaskI32SignedRemainder,
node_id,
eqz_fallthrough_const_fact: None,
forward_fact: None,
remainder_mask_fact: Some(mask_fact),
}
None => no_rewrite
}
_ => no_rewrite
}
}
///|
fn OiBooleanContextLeafRewriteFact::apply_selected_rewrite(
self : OiBooleanContextLeafRewriteFact,
func : @ir.HotFunc,
) -> Int32? {
match self.kind {
BooleanContextLeafFoldEqzFallthroughConst =>
match self.eqz_fallthrough_const_fact {
Some(fact) if self.node_id >= 0 && fact.operand_id >= 0 => {
let folded = @ir.hot_build_const_i32(
func,
@lib.I32(fact.folded_value),
)
match fact.preservation {
BooleanEqzErasePureWrapperChain => Some(folded)
BooleanEqzDropOriginalOperand =>
Some(
@ir.hot_build_block(
func,
@lib.BlockType::val_type(@lib.ValType::i32()),
[@ir.hot_build_drop(func, fact.operand_id), folded],
),
)
}
}
_ => None
}
BooleanContextLeafForwardI32NeZero
| BooleanContextLeafForwardI32ZeroMinus =>
match self.forward_fact {
Some(fact) if self.node_id >= 0 && fact.replacement_id >= 0 =>
Some(fact.replacement_id)
_ => None
}
BooleanContextLeafMaskI32SignedRemainder =>
match self.remainder_mask_fact {
Some(fact) if self.node_id >= 0 && fact.value_id >= 0 => {
@ir.hot_node_exact_instr_set(
func,
self.node_id,
@lib.Instruction::i32_and(),
)
@ir.hot_child_set(func, self.node_id, 0, fact.value_id)
@ir.hot_child_set(
func,
self.node_id,
1,
@ir.hot_build_const_i32(func, @lib.I32(fact.mask_value)),
)
Some(self.node_id)
}
_ => None
}
BooleanContextLeafNoRewrite => None
}
}
///|
priv struct OiBooleanOrOperandsContextFact {
or_id : Int32
left_id : Int32
right_id : Int32
}
///|
fn OiBooleanOrOperandsContextFact::from_node(
func : @ir.HotFunc,
node_id : Int32,
) -> OiBooleanOrOperandsContextFact? {
// Binaryen v130 `optimizeBoolean` gives only direct i32.or operands boolean
// context, and visits both sides independently before continuing.
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::I32Or) if @ir.hot_child_count(func, node_id) == 2 =>
Some({
or_id: node_id,
left_id: @ir.hot_child_get(func, node_id, 0),
right_id: @ir.hot_child_get(func, node_id, 1),
})
_ => None
}
}
///|
priv struct OiBooleanBlockTailContextFact {
block_id : Int32
body_ref : @ir.HotRegionRef
tail_slot : Int32
tail_id : Int32
}
///|
fn OiBooleanBlockTailContextFact::from_condition(
func : @ir.HotFunc,
condition_id : Int32,
) -> OiBooleanBlockTailContextFact? {
// Binaryen v130 `optimizeBoolean` recurses only into the final expression of
// a nonempty i32 block. HOT stores that expression as the final region root.
match @ir.hot_node_get(func, condition_id).op {
@ir.HotOp::Block => ()
_ => return None
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, condition_id)) {
[result] if result == @lib.ValType::i32() => ()
_ => return None
}
let body_ref = @ir.hot_region_body_ref(func, condition_id)
let root_count = @ir.hot_region_root_count(func, body_ref)
if root_count == 0 {
return None
}
let tail_slot = root_count - 1
Some({
block_id: condition_id,
body_ref,
tail_slot,
tail_id: @ir.hot_region_get(func, body_ref, tail_slot),
})
}
///|
priv struct OiBooleanSelectArmsContextFact {
select_id : Int32
true_id : Int32
false_id : Int32
}
///|
fn OiBooleanSelectArmsContextFact::from_condition(
func : @ir.HotFunc,
condition_id : Int32,
) -> OiBooleanSelectArmsContextFact? {
// Binaryen v130 `optimizeBoolean` recursively optimizes both select value
// arms without changing the selector.
let node = @ir.hot_node_get(func, condition_id)
match node.op {
@ir.HotOp::Select if node.child_count == 3 => ()
_ => return None
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, condition_id)) {
[result] if result == @lib.ValType::i32() => ()
_ => return None
}
Some({
select_id: condition_id,
true_id: @ir.hot_child_get(func, condition_id, 0),
false_id: @ir.hot_child_get(func, condition_id, 1),
})
}
///|
priv struct OiBooleanIfArmContextFact {
region_ref : @ir.HotRegionRef
tail_slot : Int32
tail_id : Int32
}
///|
fn OiBooleanIfArmContextFact::from_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> OiBooleanIfArmContextFact? {
let root_count = @ir.hot_region_root_count(func, region_ref)
if root_count == 0 {
return None
}
let tail_slot = root_count - 1
Some({
region_ref,
tail_slot,
tail_id: @ir.hot_region_get(func, region_ref, tail_slot),
})
}
///|
priv struct OiBooleanIfArmsContextFact {
if_id : Int32
then_arm : OiBooleanIfArmContextFact
else_arm : OiBooleanIfArmContextFact
}
///|
fn OiBooleanIfArmsContextFact::from_condition(
func : @ir.HotFunc,
condition_id : Int32,
) -> OiBooleanIfArmsContextFact? {
// Binaryen v130 `optimizeBoolean` recursively optimizes both arms only when
// the if itself has i32 type. HOT stores each arm expression as a region, so
// preserve any arm prefixes and carry each final root independently.
match @ir.hot_node_get(func, condition_id).op {
@ir.HotOp::If => ()
_ => return None
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, condition_id)) {
[result] if result == @lib.ValType::i32() => ()
_ => return None
}
let then_arm = match
OiBooleanIfArmContextFact::from_region(
func,
@ir.hot_region_then_ref(func, condition_id),
) {
Some(fact) => fact
None => return None
}
let else_region = match @ir.hot_region_else_ref(func, condition_id) {
Some(region) => region
None => return None
}
let else_arm = match
OiBooleanIfArmContextFact::from_region(func, else_region) {
Some(fact) => fact
None => return None
}
Some({ if_id: condition_id, then_arm, else_arm, })
}
///|
priv struct OiBooleanTryContextFact {
try_id : Int32
body_arm : OiBooleanIfArmContextFact
catch_arm : OiBooleanIfArmContextFact?
}
///|
fn OiBooleanTryContextFact::from_condition(
func : @ir.HotFunc,
condition_id : Int32,
) -> OiBooleanTryContextFact? {
// Binaryen v130 `optimizeBoolean` recursively optimizes an i32 try body and
// every catch body independently. HOT's represented legacy Try has one body
// region plus at most one catch region, each with a final root and optional
// preserved prefix roots.
match @ir.hot_node_get(func, condition_id).op {
@ir.HotOp::Try => ()
_ => return None
}
match @ir.hot_type_results(func, @ir.hot_node_type(func, condition_id)) {
[result] if result == @lib.ValType::i32() => ()
_ => return None
}
let body_arm = match
OiBooleanIfArmContextFact::from_region(
func,
@ir.hot_region_body_ref(func, condition_id),
) {
Some(fact) => fact
None => return None
}
let catch_arm = match @ir.hot_region_catch_ref(func, condition_id) {
Some(region) => OiBooleanIfArmContextFact::from_region(func, region)
None => None
}
Some({ try_id: condition_id, body_arm, catch_arm, })
}
///|
priv struct OiBooleanContextRewriteOutcome {
result_id : Int32
changed : Bool
}
///|
fn OiBooleanContextRewriteOutcome::unchanged(
node_id : Int32,
) -> OiBooleanContextRewriteOutcome {
{ result_id: node_id, changed: false, }
}
///|
fn optimize_instructions_rewrite_boolean_context_node(
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
local_facts : OptimizeInstructionsLocalFacts,
) -> OiBooleanContextRewriteOutcome {
// Follow Binaryen v130 `optimizeBoolean` source order: direct unary/binary
// rewrites first, then recursive i32.or, block, if, select, and try contexts.
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return OiBooleanContextRewriteOutcome::unchanged(node_id)
}
match
OiBooleanContextLeafRewriteFact::from_node(func, dead, node_id, local_facts).apply_selected_rewrite(
func,
) {
Some(replacement_id) => return { result_id: replacement_id, changed: true, }
None => ()
}
match OiBooleanOrOperandsContextFact::from_node(func, node_id) {
Some(fact) => {
let left = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.left_id,
local_facts,
)
let right = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.right_id,
local_facts,
)
if left.result_id != fact.left_id {
@ir.hot_child_set(func, fact.or_id, 0, left.result_id)
}
if right.result_id != fact.right_id {
@ir.hot_child_set(func, fact.or_id, 1, right.result_id)
}
return { result_id: fact.or_id, changed: left.changed || right.changed, }
}
None => ()
}
match OiBooleanBlockTailContextFact::from_condition(func, node_id) {
Some(fact) => {
let tail = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.tail_id,
local_facts,
)
if tail.result_id != fact.tail_id {
ignore(
@ir.hot_region_set(
func,
fact.body_ref,
fact.tail_slot,
tail.result_id,
),
)
}
return { result_id: fact.block_id, changed: tail.changed, }
}
None => ()
}
match OiBooleanIfArmsContextFact::from_condition(func, node_id) {
Some(fact) => {
let then_result = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.then_arm.tail_id,
local_facts,
)
let else_result = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.else_arm.tail_id,
local_facts,
)
if then_result.result_id != fact.then_arm.tail_id {
ignore(
@ir.hot_region_set(
func,
fact.then_arm.region_ref,
fact.then_arm.tail_slot,
then_result.result_id,
),
)
}
if else_result.result_id != fact.else_arm.tail_id {
ignore(
@ir.hot_region_set(
func,
fact.else_arm.region_ref,
fact.else_arm.tail_slot,
else_result.result_id,
),
)
}
return {
result_id: fact.if_id,
changed: then_result.changed || else_result.changed,
}
}
None => ()
}
match OiBooleanSelectArmsContextFact::from_condition(func, node_id) {
Some(fact) => {
let true_result = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.true_id,
local_facts,
)
let false_result = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.false_id,
local_facts,
)
if true_result.result_id != fact.true_id {
@ir.hot_child_set(func, fact.select_id, 0, true_result.result_id)
}
if false_result.result_id != fact.false_id {
@ir.hot_child_set(func, fact.select_id, 1, false_result.result_id)
}
return {
result_id: fact.select_id,
changed: true_result.changed || false_result.changed,
}
}
None => ()
}
match OiBooleanTryContextFact::from_condition(func, node_id) {
Some(fact) => {
let body_result = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
fact.body_arm.tail_id,
local_facts,
)
if body_result.result_id != fact.body_arm.tail_id {
ignore(
@ir.hot_region_set(
func,
fact.body_arm.region_ref,
fact.body_arm.tail_slot,
body_result.result_id,
),
)
}
let catch_result = match fact.catch_arm {
Some(catch_arm) => {
let result = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
catch_arm.tail_id,
local_facts,
)
if result.result_id != catch_arm.tail_id {
ignore(
@ir.hot_region_set(
func,
catch_arm.region_ref,
catch_arm.tail_slot,
result.result_id,
),
)
}
Some(result)
}
None => None
}
let catch_changed = match catch_result {
Some(result) => result.changed
None => false
}
return {
result_id: fact.try_id,
changed: body_result.changed || catch_changed,
}
}
None => OiBooleanContextRewriteOutcome::unchanged(node_id)
}
}
///|
fn optimize_instructions_try_optimize_boolean_condition_context(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
scratch : OptimizeInstructionsScratch,
) -> Bool {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If if node.child_count > 0 => ()
_ => return false
}
let condition_id = @ir.hot_child_get(func, node_id, 0)
let outcome = optimize_instructions_rewrite_boolean_context_node(
func,
dead,
condition_id,
scratch.local_facts,
)
if !outcome.changed {
return false
}
if outcome.result_id != condition_id {
@ir.hot_child_set(func, node_id, 0, outcome.result_id)
}
pass_mark_mutated(ctx, func)
true
}
///|
fn optimize_instructions_visit_region(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
region : @ir.HotRegionRef,
seen : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
) -> Bool {
let mut changed = false
for idx = 0; idx < @ir.hot_region_root_count(func, region); idx = idx + 1 {
let root_id = @ir.hot_region_get(func, region, idx)
if root_id < 0 {
continue
}
if OiLiveNodeFact::proves_node_live(func, dead, root_id) {
changed = optimize_instructions_visit_node(
ctx, func, dead, root_id, seen, scratch,
) ||
changed
} else {
changed = match
OiDeadShellRewritePlanFact::from_node(ctx, func, dead, root_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, root_id) || changed
None => changed
}
}
}
changed
}
///|
fn optimize_instructions_visit_node(
ctx : HotPassContext,
func : @ir.HotFunc,
dead : @bitset.BitSet,
node_id : Int32,
seen : @bitset.BitSet,
scratch : OptimizeInstructionsScratch,
) -> Bool {
if !OiLiveNodeFact::proves_node_live(func, dead, node_id) {
return false
}
if seen[node_id] {
return false
}
seen[node_id] = true
match ctx.perf {
Some(perf) => perf_count_traversal_visit(perf)
None => ()
}
let node = func.nodes[node_id]
let mut changed = false
for slot = 0; slot < node.child_count; slot = slot + 1 {
let child_id = func.children[node.child_base + slot]
if !OiLiveNodeFact::proves_node_live(func, dead, child_id) {
continue
}
changed = optimize_instructions_visit_node(
ctx, func, dead, child_id, seen, scratch,
) ||
changed
}
changed = match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
optimize_instructions_visit_region(
ctx,
func,
dead,
@ir.hot_region_body_ref(func, node_id),
seen,
scratch,
) ||
changed
@ir.HotOp::If => {
let then_changed = optimize_instructions_visit_region(
ctx,
func,
dead,
@ir.hot_region_then_ref(func, node_id),
seen,
scratch,
)
let else_changed = match @ir.hot_region_else_ref(func, node_id) {
Some(else_region) =>
optimize_instructions_visit_region(
ctx, func, dead, else_region, seen, scratch,
)
None => false
}
then_changed || else_changed || changed
}
_ => changed
}
let local_changed = match node.op {
@ir.HotOp::Unary =>
if node.child_count != 1 {
false
} else {
optimize_instructions_try_remove_redundant_sign_ext(
ctx,
func,
dead,
scratch,
node_id,
func.instr_payloads[node.imm0].instr,
func.children[node.child_base],
) ||
optimize_instructions_try_fold_eqz(
ctx,
func,
dead,
scratch,
node_id,
func.instr_payloads[node.imm0].instr,
func.children[node.child_base],
) ||
optimize_instructions_try_replace_zero_bits_with_zero(
ctx, func, dead, scratch, node_id,
)
}
@ir.HotOp::Binary =>
if node.child_count != 2 {
false
} else {
let initial_instr = func.instr_payloads[node.imm0].instr
let initial_lhs = func.children[node.child_base]
let initial_rhs = func.children[node.child_base + 1]
let changed = optimize_instructions_try_fold_same_local_integer_binary(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_apply_demorgan_and_eqz(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_fold_identity_binary(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_fold_absorbing_binary(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_remove_redundant_unsigned_mask(
ctx, func, dead, scratch, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_canonicalize_signed_const_unsigned(
ctx, func, dead, scratch, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_boolean_maxbits_binary(
ctx, func, dead, scratch, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_fold_exact_const_binary(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_add_sub(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_mul_negation(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_float_binary(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_mul_shift(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_div_rem_power_of_two(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_sign_ext_idiom(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_combine_nested_const_shift(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_rewrite_shift(
ctx, func, dead, node_id, initial_instr, initial_lhs, initial_rhs,
) ||
optimize_instructions_try_replace_zero_bits_with_zero(
ctx, func, dead, scratch, node_id,
)
let current = func.nodes[node_id]
let canonicalized = match current.op {
@ir.HotOp::Binary => {
let current_instr = func.instr_payloads[current.imm0].instr
if !OiCommutativeInstructionFact::proves_instr_commutative(
current_instr,
) {
false
} else {
let current_lhs = func.children[current.child_base]
let current_rhs = func.children[current.child_base + 1]
optimize_instructions_try_canonicalize_commutative(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
scratch,
)
}
}
_ => false
}
if canonicalized {
let current = func.nodes[node_id]
let current_lhs = func.children[current.child_base]
let current_rhs = func.children[current.child_base + 1]
let current_instr = func.instr_payloads[current.imm0].instr
optimize_instructions_try_fold_same_local_integer_binary(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_apply_demorgan_and_eqz(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_fold_identity_binary(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_fold_absorbing_binary(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_remove_redundant_unsigned_mask(
ctx, func, dead, scratch, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_boolean_maxbits_binary(
ctx, func, dead, scratch, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_fold_exact_const_binary(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_add_sub(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_mul_negation(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_float_binary(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_mul_shift(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_div_rem_power_of_two(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_sign_ext_idiom(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_combine_nested_const_shift(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_shift(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_replace_zero_bits_with_zero(
ctx, func, dead, scratch, node_id,
) ||
true
} else {
changed
}
}
@ir.HotOp::Convert =>
if node.child_count != 1 {
false
} else {
optimize_instructions_try_rewrite_reinterpret_load_result(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_rewrite_i64_extend_i32_load_result(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_fold_convert(
ctx,
func,
dead,
scratch,
node_id,
func.instr_payloads[node.imm0].instr,
func.children[node.child_base],
) ||
optimize_instructions_try_replace_zero_bits_with_zero(
ctx, func, dead, scratch, node_id,
)
}
@ir.HotOp::Compare =>
if node.child_count != 2 {
false
} else {
let mut current_instr = func.instr_payloads[node.imm0].instr
let mut current_lhs = func.children[node.child_base]
let mut current_rhs = func.children[node.child_base + 1]
let mut changed = false
if optimize_instructions_try_fold_integer_const_eq_ne_compare(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_fold_signed_domain_edge_compare(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_fold_unsigned_domain_edge_compare(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_fold_same_local_integer_compare(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_boolean_maxbits_binary(
ctx, func, dead, scratch, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_compare_eqz(
ctx, func, dead, scratch, node_id, current_instr, current_lhs, current_rhs,
) {
true
} else {
if optimize_instructions_try_canonicalize_signed_const_unsigned(
ctx, func, dead, scratch, node_id, current_instr, current_lhs, current_rhs,
) {
changed = true
let current = func.nodes[node_id]
current_instr = func.instr_payloads[current.imm0].instr
current_lhs = func.children[current.child_base]
current_rhs = func.children[current.child_base + 1]
}
if optimize_instructions_try_canonicalize_relational_const(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) {
changed = true
let current = func.nodes[node_id]
current_instr = func.instr_payloads[current.imm0].instr
current_lhs = func.children[current.child_base]
current_rhs = func.children[current.child_base + 1]
}
let relational_operands_changed = optimize_instructions_try_canonicalize_relational_operands(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs, scratch,
)
if relational_operands_changed {
changed = true
let current = func.nodes[node_id]
current_instr = func.instr_payloads[current.imm0].instr
current_lhs = func.children[current.child_base]
current_rhs = func.children[current.child_base + 1]
}
if optimize_instructions_try_canonicalize_compare_const(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) {
changed = true
let current = func.nodes[node_id]
current_instr = func.instr_payloads[current.imm0].instr
current_lhs = func.children[current.child_base]
current_rhs = func.children[current.child_base + 1]
}
let range_folded = optimize_instructions_try_fold_i32_sign_extension_equality_compare(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
)
let signext_top_rewritten = if range_folded {
false
} else {
optimize_instructions_try_rewrite_i32_sign_extension_unsigned_top_compare(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_rewrite_i64_sign_extension_unsigned_top_compare(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
)
}
let masked_unsigned_folded = if range_folded || signext_top_rewritten {
false
} else {
optimize_instructions_try_fold_i32_masked_unsigned_compare(
ctx, func, dead, scratch, node_id, current_instr, current_lhs, current_rhs,
) ||
optimize_instructions_try_fold_i64_masked_unsigned_compare(
ctx, func, dead, scratch, node_id, current_instr, current_lhs, current_rhs,
)
}
changed = range_folded ||
signext_top_rewritten ||
masked_unsigned_folded ||
changed
let zero_folded = if masked_unsigned_folded ||
signext_top_rewritten ||
range_folded {
false
} else {
optimize_instructions_try_fold_compare_zero(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
)
}
changed = zero_folded || changed
if zero_folded || masked_unsigned_folded || range_folded {
changed
} else {
let canonicalized = if OiCommutativeInstructionFact::proves_instr_commutative(
current_instr,
) {
optimize_instructions_try_canonicalize_commutative(
ctx, func, dead, node_id, current_instr, current_lhs, current_rhs,
scratch,
)
} else {
false
}
if canonicalized {
let current = func.nodes[node_id]
let next_instr = func.instr_payloads[current.imm0].instr
let next_lhs = func.children[current.child_base]
let next_rhs = func.children[current.child_base + 1]
optimize_instructions_try_canonicalize_compare_const(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_fold_integer_const_eq_ne_compare(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_fold_signed_domain_edge_compare(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_fold_unsigned_domain_edge_compare(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_fold_i32_sign_extension_equality_compare(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_rewrite_i32_sign_extension_unsigned_top_compare(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_rewrite_i64_sign_extension_unsigned_top_compare(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_fold_i32_masked_unsigned_compare(
ctx, func, dead, scratch, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_fold_i64_masked_unsigned_compare(
ctx, func, dead, scratch, node_id, next_instr, next_lhs, next_rhs,
) ||
optimize_instructions_try_fold_compare_zero(
ctx, func, dead, node_id, next_instr, next_lhs, next_rhs,
) ||
true
} else {
changed
}
}
}
}
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
optimize_instructions_try_remove_ref_as_non_null_local_write_trap_check(
ctx, func, node_id,
)
@ir.HotOp::Select =>
optimize_instructions_try_fold_const_select(
ctx, func, dead, scratch, node_id,
) ||
optimize_instructions_try_hoist_eqz_boolean_select(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_hoist_matching_reference_select(
ctx, func, node_id,
) ||
optimize_instructions_try_hoist_matching_unary_select(ctx, func, node_id)
@ir.HotOp::Drop =>
optimize_instructions_try_collapse_dropped_unreachable_block(
ctx, func, node_id,
)
@ir.HotOp::TupleExtract =>
optimize_instructions_try_fold_tuple_extract_tuple_make(
ctx, func, dead, node_id,
)
@ir.HotOp::RefIsNull =>
match OiRefIsNullRewritePlanFact::from_node(ctx, func, node_id) {
Some(plan_fact) => plan_fact.apply(ctx, func, node_id)
None => false
}
@ir.HotOp::RefTest | @ir.HotOp::RefTestDesc =>
optimize_instructions_try_fold_ref_test_null(ctx, func, node_id)
@ir.HotOp::RefCast =>
optimize_instructions_try_fold_ref_cast_null(ctx, func, node_id) ||
optimize_instructions_try_replace_ref_cast_unreachable_operand(
ctx, func, node_id,
)
@ir.HotOp::RefCastDescEq =>
optimize_instructions_try_replace_ref_cast_desc_eq_trapping_descriptor_operand(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_skip_ref_cast_desc_eq_exact_success_traps_never_happen(
ctx, func, node_id,
) ||
optimize_instructions_try_skip_ref_cast_desc_eq_traps_never_happen(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_fold_ref_cast_desc_eq_null(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_fold_ref_cast_desc_eq_exact_descriptor_miss(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_simplify_ref_cast_desc_eq_descriptor_operand(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_narrow_ref_cast_desc_eq_result_type(
ctx, func, node_id,
)
@ir.HotOp::Heap =>
optimize_instructions_try_strip_struct_get_non_null_check(
ctx, func, node_id,
) ||
optimize_instructions_try_replace_ref_get_desc_unreachable_operand(
ctx, func, node_id,
) ||
optimize_instructions_try_trap_standalone_ref_get_desc_known_null_child(
ctx, func, node_id,
) ||
optimize_instructions_try_move_standalone_ref_get_desc_null_check(
ctx, func, node_id,
) ||
optimize_instructions_try_fold_ref_as_non_null(ctx, func, node_id) ||
optimize_instructions_try_rewrite_ref_eq_null(ctx, func, dead, node_id) ||
optimize_instructions_try_rewrite_array_new_default_init(
ctx, func, node_id,
) ||
optimize_instructions_try_rewrite_array_new_single_fixed(
ctx, func, node_id,
) ||
optimize_instructions_try_rewrite_array_new_fixed_repeated_values(
ctx, func, node_id,
) ||
false
@ir.HotOp::CallRef | @ir.HotOp::ReturnCallRef =>
optimize_instructions_try_directize_ref_func_call_ref(
ctx, func, dead, node_id,
)
@ir.HotOp::BrOnCast | @ir.HotOp::BrOnCastFail =>
optimize_instructions_try_replace_unreachable_guard_cast_branch(
ctx, func, node_id,
)
@ir.HotOp::Load =>
optimize_instructions_try_fold_const_memory_access_offset(
ctx, func, dead, node_id,
)
@ir.HotOp::Atomic =>
optimize_instructions_try_rewrite_aggregate_atomic(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_fold_const_memory_access_offset(
ctx, func, dead, node_id,
)
@ir.HotOp::Store =>
optimize_instructions_try_fold_const_memory_access_offset(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_truncate_narrow_store_const_value(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_drop_narrow_store_value_mask(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_rewrite_reinterpret_store_value(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_widen_store_of_wrap_i64(
ctx, func, dead, node_id,
)
@ir.HotOp::MemoryCopy =>
optimize_instructions_try_expand_tiny_memory_copy(
ctx, func, dead, node_id,
)
@ir.HotOp::MemoryFill =>
optimize_instructions_try_expand_tiny_memory_fill(
ctx, func, dead, node_id,
)
@ir.HotOp::Block | @ir.HotOp::Loop =>
optimize_instructions_try_narrow_null_cast_boundary_block(
ctx, func, node_id,
) ||
optimize_instructions_try_collapse_dead_region_suffix(
ctx,
func,
dead,
@ir.hot_region_body_ref(func, node_id),
@ir.hot_control_node_label(func, node_id),
)
@ir.HotOp::If =>
optimize_instructions_try_hoist_matching_unary_if(ctx, func, node_id) ||
optimize_instructions_try_hoist_matching_reference_if(ctx, func, node_id) ||
optimize_instructions_try_sink_same_global_set_if(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_sink_same_local_set_if_then_revisit_condition(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_fold_constant_if_condition(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_optimize_boolean_condition_context(
ctx, func, dead, node_id, scratch,
) ||
optimize_instructions_try_optimize_if_condition(ctx, func, dead, node_id) ||
optimize_instructions_try_wrap_boolean_if_value_in_eqz(
ctx, func, dead, node_id,
) ||
optimize_instructions_try_collapse_dead_region_suffix(
ctx,
func,
dead,
@ir.hot_region_then_ref(func, node_id),
@ir.hot_control_node_label(func, node_id),
) ||
(match @ir.hot_region_else_ref(func, node_id) {
Some(else_region) =>
optimize_instructions_try_collapse_dead_region_suffix(
ctx,
func,
dead,
else_region,
@ir.hot_control_node_label(func, node_id),
)
None => false
})
_ => false
}
local_changed || changed
}
///|
priv struct OiNoRewriteI32LiteralRevisionFact {
revision : Int32
root_id : Int32
root_count : Int32
node_count : Int32
}
///|
fn OiNoRewriteI32LiteralRevisionFact::from_func(
func : @ir.HotFunc,
) -> OiNoRewriteI32LiteralRevisionFact? {
if func.roots.length() != 1 {
return None
}
let root_id = func.roots[0]
if !@ir.hot_node_is_live(func, root_id) ||
@ir.hot_child_count(func, root_id) != 0 {
return None
}
let root = @ir.hot_node_get(func, root_id)
match root.op {
@ir.HotOp::Const =>
match @ir.hot_const_get(func, root.imm0) {
@ir.HotConstPayload::I32Const(_) =>
Some({
revision: @ir.hot_revision_current(func),
root_id,
root_count: 1,
node_count: @ir.hot_node_count(func),
})
_ => None
}
_ => None
}
}
///|
fn OiNoRewriteI32LiteralRevisionFact::proves_no_rewrite_candidate(
self : OiNoRewriteI32LiteralRevisionFact,
func : @ir.HotFunc,
) -> Bool {
self.revision == @ir.hot_revision_current(func) &&
self.root_id == func.roots[0] &&
self.root_count == 1 &&
self.node_count == @ir.hot_node_count(func)
}
///|
fn optimize_instructions_run(
ctx : HotPassContext,
func : @ir.HotFunc,
) -> HotPassResult {
match OiNoRewriteI32LiteralRevisionFact::from_func(func) {
Some(fact) if fact.proves_no_rewrite_candidate(func) =>
return HotPassResult::unchanged()
_ => ()
}
let mut changed = false
let node_count = @ir.hot_node_count(func)
let scratch_init_start_us = perf_start_timer_opt(
ctx.perf,
"detail:optimize-instructions:scratch-init",
)
let seen = @bitset.BitSet::new(node_count)
let dead = @bitset.BitSet::new(node_count)
let scratch = OptimizeInstructionsScratch::new(
node_count,
@ir.hot_local_count(func),
)
perf_stop_timer_opt(
ctx.perf,
"detail:optimize-instructions:scratch-init",
scratch_init_start_us,
)
let free_node_init_start_us = perf_start_timer_opt(
ctx.perf,
"detail:optimize-instructions:free-node-init",
)
for free_id in func.free_nodes {
if free_id >= 0 && free_id < dead.length() {
dead[free_id] = true
}
}
perf_stop_timer_opt(
ctx.perf,
"detail:optimize-instructions:free-node-init",
free_node_init_start_us,
)
let local_facts_start_us = perf_start_timer_opt(
ctx.perf,
"detail:optimize-instructions:collect-local-facts",
)
optimize_instructions_collect_local_facts(ctx, func, dead, scratch)
perf_stop_timer_opt(
ctx.perf,
"detail:optimize-instructions:collect-local-facts",
local_facts_start_us,
)
let visit_start_us = perf_start_timer_opt(
ctx.perf,
"detail:optimize-instructions:visit-roots",
)
for slot = 0; slot < func.roots.length(); slot = slot + 1 {
changed = optimize_instructions_visit_node(
ctx,
func,
dead,
func.roots[slot],
seen,
scratch,
) ||
changed
}
perf_stop_timer_opt(
ctx.perf,
"detail:optimize-instructions:visit-roots",
visit_start_us,
)
let dead_suffix_start_us = perf_start_timer_opt(
ctx.perf,
"detail:optimize-instructions:collapse-root-dead-suffix",
)
changed = optimize_instructions_try_collapse_dead_region_suffix(
ctx,
func,
dead,
@ir.HotRegionRef::root(),
-1,
) ||
changed
perf_stop_timer_opt(
ctx.perf,
"detail:optimize-instructions:collapse-root-dead-suffix",
dead_suffix_start_us,
)
if changed {
HotPassResult::changed()
} else {
HotPassResult::unchanged()
}
}