// Licensing scope: see ../../LICENSE and this directory's LICENSE and NOTICE.
// 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 ssa_nomerge_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("ssa-nomerge", 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 ssa_nomerge_summary() -> String {
"Freshen single-source locals and materialize defaults while preserving merge traffic."
}
///|
/// Planned local-write action for Binaryen-style `ssa-nomerge` rewriting.
pub(all) enum SsaNoMergeWriteDecision {
Freshen(@ir.LocalId)
KeepCanonical
} derive(Eq, Debug)
///|
/// Planned local-get action for Binaryen-style `ssa-nomerge` rewriting.
pub(all) enum SsaNoMergeGetDecision {
Retarget(@ir.LocalId)
MaterializeDefault
KeepCanonical
} derive(Eq, Debug)
///|
/// Analysis-only write rewrite entry. This does not mutate the function.
pub(all) struct SsaNoMergeWriteRewrite {
write_id : @ir.NodeId
original_local : @ir.LocalId
influenced_gets : Array[@ir.NodeId]
decision : SsaNoMergeWriteDecision
} derive(Eq, Debug)
///|
/// Analysis-only get rewrite entry. This does not mutate the function.
pub(all) struct SsaNoMergeGetRewrite {
get_id : @ir.NodeId
original_local : @ir.LocalId
decision : SsaNoMergeGetDecision
} derive(Eq, Debug)
///|
/// Analysis-only no-merge rewrite plan built from `HotLocalGraph` facts.
pub(all) struct SsaNoMergeRewritePlan {
write_rewrites : Array[SsaNoMergeWriteRewrite]
get_rewrites : Array[SsaNoMergeGetRewrite]
} derive(Eq, Debug)
///|
fn ssa_nomerge_fresh_local_for_write(
fresh_by_write : Array[Int32],
write_id : @ir.NodeId,
) -> @ir.LocalId? {
if write_id >= 0 &&
write_id < fresh_by_write.length() &&
fresh_by_write[write_id] >= 0 {
Some(fresh_by_write[write_id])
} else {
None
}
}
///|
fn ssa_nomerge_plan_get_decision(
graph : @ir.HotLocalGraph,
fresh_by_write : Array[Int32],
get_id : @ir.NodeId,
) -> SsaNoMergeGetDecision {
if @ir.local_graph_get_is_merge(graph, get_id) {
return SsaNoMergeGetDecision::KeepCanonical
}
let sources = @ir.local_graph_reaching_sources_for_get(graph, get_id)
if sources.length() != 1 {
return SsaNoMergeGetDecision::KeepCanonical
}
match sources[0] {
@ir.HotLocalGraphSource::SetSource(write_id) =>
match ssa_nomerge_fresh_local_for_write(fresh_by_write, write_id) {
Some(local_id) => SsaNoMergeGetDecision::Retarget(local_id)
None => SsaNoMergeGetDecision::KeepCanonical
}
@ir.HotLocalGraphSource::EntrySource(local_id) =>
if @ir.local_graph_entry_source_has_legal_default(graph, local_id) {
SsaNoMergeGetDecision::MaterializeDefault
} else {
SsaNoMergeGetDecision::KeepCanonical
}
}
}
///|
fn ssa_nomerge_note_loop_local_access(
first_accesses : Array[Int32],
local_id : @ir.LocalId,
access : Int32,
) -> Unit {
if local_id >= 0 &&
local_id < first_accesses.length() &&
first_accesses[local_id] == 0 {
first_accesses[local_id] = access
}
}
///|
fn ssa_nomerge_collect_loop_body_accesses_in_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
writes : @bitset.BitSet,
first_accesses : Array[Int32],
visited : @bitset.BitSet,
) -> Unit {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
ssa_nomerge_collect_loop_body_accesses_in_node(
func,
@ir.hot_region_get(func, region_ref, idx),
writes,
first_accesses,
visited,
)
}
}
///|
fn ssa_nomerge_collect_loop_body_accesses_in_node(
func : @ir.HotFunc,
node_id : @ir.NodeId,
writes : @bitset.BitSet,
first_accesses : Array[Int32],
visited : @bitset.BitSet,
) -> Unit {
if node_id < 0 ||
!@ir.hot_node_is_live(func, node_id) ||
(node_id < visited.length() && visited[node_id]) {
return
}
if node_id < visited.length() {
visited[node_id] = true
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet =>
ssa_nomerge_note_loop_local_access(first_accesses, node.imm0, 1)
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
ssa_nomerge_collect_loop_body_accesses_in_node(
func,
@ir.hot_child_get(func, node_id, slot),
writes,
first_accesses,
visited,
)
}
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
ssa_nomerge_collect_loop_body_accesses_in_region(
func,
@ir.hot_region_body_ref(func, node_id),
writes,
first_accesses,
visited,
)
@ir.HotOp::If => {
ssa_nomerge_collect_loop_body_accesses_in_region(
func,
@ir.hot_region_then_ref(func, node_id),
writes,
first_accesses,
visited,
)
match @ir.hot_region_else_ref(func, node_id) {
Some(region_ref) =>
ssa_nomerge_collect_loop_body_accesses_in_region(
func, region_ref, writes, first_accesses, visited,
)
None => ()
}
}
_ => ()
}
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => {
if node.imm0 >= 0 && node.imm0 < writes.length() {
writes[node.imm0] = true
}
ssa_nomerge_note_loop_local_access(first_accesses, node.imm0, 2)
}
_ => ()
}
}
///|
fn ssa_nomerge_mark_loop_carried_locals_in_node(
func : @ir.HotFunc,
node_id : @ir.NodeId,
preserve_locals : @bitset.BitSet,
visited : @bitset.BitSet,
) -> Unit {
if node_id < 0 ||
!@ir.hot_node_is_live(func, node_id) ||
(node_id < visited.length() && visited[node_id]) {
return
}
if node_id < visited.length() {
visited[node_id] = true
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Loop => {
let local_count = @ir.hot_local_count(func)
let loop_writes = @bitset.BitSet::new(local_count)
let first_accesses = Array::make(local_count, 0)
ssa_nomerge_collect_loop_body_accesses_in_region(
func,
@ir.hot_region_body_ref(func, node_id),
loop_writes,
first_accesses,
@bitset.BitSet::new(@ir.hot_node_count(func)),
)
for local_id = 0; local_id < local_count; local_id = local_id + 1 {
if loop_writes[local_id] && first_accesses[local_id] == 1 {
preserve_locals[local_id] = true
}
}
}
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
ssa_nomerge_mark_loop_carried_locals_in_node(
func,
@ir.hot_child_get(func, node_id, slot),
preserve_locals,
visited,
)
}
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
ssa_nomerge_mark_loop_carried_locals_in_region(
func,
@ir.hot_region_body_ref(func, node_id),
preserve_locals,
visited,
)
@ir.HotOp::If => {
ssa_nomerge_mark_loop_carried_locals_in_region(
func,
@ir.hot_region_then_ref(func, node_id),
preserve_locals,
visited,
)
match @ir.hot_region_else_ref(func, node_id) {
Some(region_ref) =>
ssa_nomerge_mark_loop_carried_locals_in_region(
func, region_ref, preserve_locals, visited,
)
None => ()
}
}
_ => ()
}
}
///|
fn ssa_nomerge_mark_loop_carried_locals_in_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
preserve_locals : @bitset.BitSet,
visited : @bitset.BitSet,
) -> Unit {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
ssa_nomerge_mark_loop_carried_locals_in_node(
func,
@ir.hot_region_get(func, region_ref, idx),
preserve_locals,
visited,
)
}
}
///|
pub fn ssa_nomerge_loop_carried_canonical_writes(
func : @ir.HotFunc,
) -> @bitset.BitSet {
let preserve_writes = @bitset.BitSet::new(@ir.hot_node_count(func))
let preserve_locals = @bitset.BitSet::new(@ir.hot_local_count(func))
let visited = @bitset.BitSet::new(@ir.hot_node_count(func))
for root_idx = 0; root_idx < @ir.hot_root_count(func); root_idx = root_idx + 1 {
ssa_nomerge_mark_loop_carried_locals_in_node(
func,
@ir.hot_root_get(func, root_idx),
preserve_locals,
visited,
)
}
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if @ir.hot_node_is_live(func, node_id) {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
if node.imm0 >= 0 &&
node.imm0 < preserve_locals.length() &&
preserve_locals[node.imm0] {
preserve_writes[node_id] = true
}
_ => ()
}
}
}
preserve_writes
}
///|
priv struct SsaNoMergeParamTeeFacts {
has_plain_br : Bool
has_local_get : Array[Bool]
}
///|
fn ssa_nomerge_param_tee_facts(func : @ir.HotFunc) -> SsaNoMergeParamTeeFacts {
let mut has_plain_br = false
let has_local_get = Array::make(@ir.hot_local_count(func), false)
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if @ir.hot_node_is_live(func, node_id) {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Br => has_plain_br = true
@ir.HotOp::LocalGet =>
if node.imm0 >= 0 && node.imm0 < has_local_get.length() {
has_local_get[node.imm0] = true
}
_ => ()
}
}
}
SsaNoMergeParamTeeFacts::{ has_plain_br, has_local_get, }
}
///|
fn ssa_nomerge_write_is_branch_carried_param_tee(
func : @ir.HotFunc,
node_id : @ir.NodeId,
facts : SsaNoMergeParamTeeFacts,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalTee =>
node.imm0 >= 0 &&
node.imm0 < @ir.hot_param_count(func) &&
facts.has_plain_br &&
facts.has_local_get[node.imm0]
_ => false
}
}
///|
/// Build the analysis-only no-merge rewrite plan from LocalGraph facts.
pub fn ssa_nomerge_build_rewrite_plan(
func : @ir.HotFunc,
graph : @ir.HotLocalGraph,
) -> SsaNoMergeRewritePlan {
let writes : Array[SsaNoMergeWriteRewrite] = []
let fresh_by_write = Array::make(@ir.hot_node_count(func), -1)
let loop_carried_canonical_writes = ssa_nomerge_loop_carried_canonical_writes(
func,
)
let param_tee_facts = ssa_nomerge_param_tee_facts(func)
let mut next_fresh_local = @ir.hot_local_count(func)
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if !@ir.hot_node_is_live(func, node_id) ||
!@ir.local_graph_node_is_write(graph, node_id) {
continue
}
let decision = if ssa_nomerge_write_is_branch_carried_param_tee(
func, node_id, param_tee_facts,
) ||
(
node_id < loop_carried_canonical_writes.length() &&
loop_carried_canonical_writes[node_id] &&
@ir.local_graph_write_has_merge_influences(graph, node_id)
) {
SsaNoMergeWriteDecision::KeepCanonical
} else if @ir.local_graph_write_is_no_merge_freshenable(graph, node_id) {
let fresh_local = next_fresh_local
next_fresh_local = next_fresh_local + 1
SsaNoMergeWriteDecision::Freshen(fresh_local)
} else {
SsaNoMergeWriteDecision::KeepCanonical
}
writes.push({
write_id: node_id,
original_local: @ir.local_graph_write_local_id(graph, node_id),
influenced_gets: @ir.local_graph_influenced_gets_for_write(graph, node_id),
decision,
})
match decision {
SsaNoMergeWriteDecision::Freshen(local_id) =>
fresh_by_write[node_id] = local_id
SsaNoMergeWriteDecision::KeepCanonical => ()
}
}
let gets : Array[SsaNoMergeGetRewrite] = []
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if !@ir.hot_node_is_live(func, node_id) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet =>
gets.push({
get_id: node_id,
original_local: node.imm0,
decision: ssa_nomerge_plan_get_decision(
graph, fresh_by_write, node_id,
),
})
_ => ()
}
}
{ write_rewrites: writes, get_rewrites: gets, }
}
///|
/// Keep raw occurrence positions stable across omitted unreachable operations
/// and synthetic stack captures. Allocate only locals that the raw writer can
/// emit; HOT-only scratch writes must never consume a raw occurrence or index.
fn ssa_nomerge_build_raw_rewrite_plan(
func : @ir.HotFunc,
graph : @ir.HotLocalGraph,
) -> SsaNoMergeRewritePlan {
let accesses = @ir.hot_lift_local_accesses(func)
let hot_plan = ssa_nomerge_build_rewrite_plan(func, graph)
let decisions : Array[SsaNoMergeWriteDecision] = Array::make(
@ir.hot_node_count(func),
SsaNoMergeWriteDecision::KeepCanonical,
)
for write in hot_plan.write_rewrites {
decisions[write.write_id] = write.decision
}
let writes : Array[SsaNoMergeWriteRewrite] = []
let fresh_by_write = Array::make(@ir.hot_node_count(func), -1)
let seen_write_ids = Array::make(@ir.hot_node_count(func), false)
let mut next_fresh = accesses.local_count
for access in accesses.writes {
let (original_local, write_id) = access
let decision = if write_id >= 0 {
match decisions[write_id] {
SsaNoMergeWriteDecision::Freshen(_) => {
let fresh = next_fresh
next_fresh += 1
SsaNoMergeWriteDecision::Freshen(fresh)
}
SsaNoMergeWriteDecision::KeepCanonical =>
SsaNoMergeWriteDecision::KeepCanonical
}
} else {
SsaNoMergeWriteDecision::KeepCanonical
}
writes.push({
write_id,
original_local,
decision,
influenced_gets: if write_id >= 0 {
@ir.local_graph_influenced_gets_for_write(graph, write_id)
} else {
[]
},
})
if write_id >= 0 && !seen_write_ids[write_id] {
seen_write_ids[write_id] = true
match decision {
SsaNoMergeWriteDecision::Freshen(local_id) =>
fresh_by_write[write_id] = local_id
SsaNoMergeWriteDecision::KeepCanonical => ()
}
}
}
let gets : Array[SsaNoMergeGetRewrite] = []
for access in accesses.gets {
let (original_local, get_id) = access
gets.push({
get_id,
original_local,
decision: if get_id >= 0 {
ssa_nomerge_plan_get_decision(graph, fresh_by_write, get_id)
} else {
SsaNoMergeGetDecision::KeepCanonical
},
})
}
{ write_rewrites: writes, get_rewrites: gets, }
}
///|
fn ssa_nomerge_has_local_writes(func : @ir.HotFunc) -> Bool {
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if !@ir.hot_node_is_live(func, node_id) {
continue
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => return true
_ => ()
}
}
false
}
///|
fn ssa_nomerge_is_control_region_slot(op : @ir.HotOp, slot : Int32) -> Bool {
match op {
@ir.HotOp::Block | @ir.HotOp::Loop => true
@ir.HotOp::If => slot == 1 || slot == 2
@ir.HotOp::Try | @ir.HotOp::TryTable => slot == 0 || slot == 1
_ => false
}
}
///|
fn ssa_nomerge_subtree_has_local_or_terminator(
func : @ir.HotFunc,
node_id : @ir.NodeId,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
if @ir.hot_is_terminator_node(func, node_id) {
return true
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalGet | @ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
return true
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if ssa_nomerge_subtree_has_local_or_terminator(
func,
@ir.hot_child_get(func, node_id, slot),
) {
return true
}
}
false
}
///|
fn ssa_nomerge_is_simple_nested_value_block_operand(
func : @ir.HotFunc,
parent_id : @ir.NodeId,
slot : Int32,
child_id : @ir.NodeId,
) -> Bool {
@ir.ssa_simple_drop_value_block_allowed(func, parent_id, slot, child_id)
}
///|
fn ssa_nomerge_is_simple_nested_result_if_operand(
func : @ir.HotFunc,
parent_id : @ir.NodeId,
slot : Int32,
child_id : @ir.NodeId,
) -> Bool {
@ir.ssa_simple_drop_result_if_allowed(func, parent_id, slot, child_id)
}
///|
fn ssa_nomerge_supported_drop_result_ifs(
func : @ir.HotFunc,
visits~ : Array[Int32]? = None,
) -> Array[Bool] {
let supported = Array::make(@ir.hot_node_count(func), false)
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
match visits {
Some(count) => count[0] += 1
None => ()
}
if !@ir.hot_node_is_live(func, node_id) {
continue
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Drop if @ir.hot_child_count(func, node_id) == 1 => {
let if_id = @ir.hot_child_get(func, node_id, 0)
if if_id >= 0 &&
if_id < supported.length() &&
@ir.hot_node_is_live(func, if_id) &&
@ir.hot_node_get(func, if_id).op is @ir.HotOp::If &&
ssa_nomerge_is_simple_nested_result_if_operand(
func, node_id, 0, if_id,
) {
supported[if_id] = true
}
}
_ => ()
}
}
supported
}
///|
fn ssa_nomerge_is_supported_result_if_condition_value_block(
func : @ir.HotFunc,
supported_drop_result_ifs : Array[Bool],
parent_id : @ir.NodeId,
parent_slot : Int32,
block_id : @ir.NodeId,
) -> Bool {
if parent_slot != 0 ||
parent_id < 0 ||
block_id < 0 ||
!@ir.hot_node_is_live(func, parent_id) ||
!@ir.hot_node_is_live(func, block_id) {
return false
}
match @ir.hot_node_get(func, parent_id).op {
@ir.HotOp::If => ()
_ => return false
}
if @ir.hot_child_get(func, parent_id, 0) != block_id {
return false
}
@ir.ssa_simple_condition_value_block_allowed(func, block_id) &&
parent_id < supported_drop_result_ifs.length() &&
supported_drop_result_ifs[parent_id]
}
///|
fn ssa_nomerge_nested_cfg_node_is_fail_closed(
func : @ir.HotFunc,
supported_drop_result_ifs : Array[Bool],
parent_id : @ir.NodeId,
parent_slot : Int32,
node_id : @ir.NodeId,
) -> Bool {
if ssa_nomerge_is_simple_nested_value_block_operand(
func, parent_id, parent_slot, node_id,
) {
return false
}
if ssa_nomerge_is_simple_nested_result_if_operand(
func, parent_id, parent_slot, node_id,
) {
return false
}
if ssa_nomerge_is_supported_result_if_condition_value_block(
func, supported_drop_result_ifs, parent_id, parent_slot, node_id,
) {
return false
}
ssa_nomerge_subtree_has_local_or_terminator(func, node_id)
}
///|
fn ssa_nomerge_has_nested_cfg_control_node(
func : @ir.HotFunc,
supported_drop_result_ifs : Array[Bool],
node_id : @ir.NodeId,
is_region_root : Bool,
parent_id : @ir.NodeId,
parent_slot : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
if !is_region_root &&
(
@ir.hot_is_control_node(func, node_id) ||
@ir.hot_is_terminator_node(func, node_id)
) &&
ssa_nomerge_nested_cfg_node_is_fail_closed(
func, supported_drop_result_ifs, parent_id, parent_slot, node_id,
) {
return true
}
let node = @ir.hot_node_get(func, node_id)
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
let child_id = @ir.hot_child_get(func, node_id, slot)
if child_id < 0 || !@ir.hot_node_is_live(func, child_id) {
continue
}
let child_is_region_root = @ir.hot_is_control_node(func, node_id) &&
ssa_nomerge_is_control_region_slot(node.op, slot)
if ssa_nomerge_has_nested_cfg_control_node(
func, supported_drop_result_ifs, child_id, child_is_region_root, node_id,
slot,
) {
return true
}
}
false
}
///|
fn ssa_nomerge_has_nested_cfg_sensitive_control(func : @ir.HotFunc) -> Bool {
// Operand-nested straight-line `drop(block (result t) ...)` value blocks are
// allowed via `ssa_simple_drop_value_block_allowed(...)` and inline rename in
// `ssa_local_visit_node`. Operand-nested `drop(if (result t) ...)` with a
// single scalar numeric result, straight-line arms, and a straight-line
// condition (including condition-local set/tee or one simple `block (result i32)`
// when CFG-backed rename/use-def inline-visit the predecessor block before both
// arms) is allowed via `ssa_simple_drop_result_if_allowed(...)` and CFG-backed
// rename. Branches and nested control in the condition stay fail-closed. All other
// nested value-control regions can still
// contain local reads/writes and terminators outside SSA v1's region-root
// model; fail closed for those shapes.
let supported_drop_result_ifs = ssa_nomerge_supported_drop_result_ifs(func)
for root_idx = 0; root_idx < @ir.hot_root_count(func); root_idx = root_idx + 1 {
if ssa_nomerge_has_nested_cfg_control_node(
func,
supported_drop_result_ifs,
@ir.hot_root_get(func, root_idx),
true,
-1,
-1,
) {
return true
}
}
false
}
///|
fn ssa_nomerge_has_exceptional_flow(func : @ir.HotFunc) -> Bool {
// SSA v1 is normal-flow-only. The CFG records `ExceptionalEdge` for
// try/try_table and throw-family ops, while continuation handlers add their
// own non-local label exits. Liveness, dominators, phi placement, rename, and
// destroy ignore those predecessors. Fail closed here (not feature support):
// rewriting locals while skipping those edges can drop writes that must reach
// catch or continuation-handler destinations.
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if !@ir.hot_node_is_live(func, node_id) {
continue
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Try
| @ir.HotOp::TryTable
| @ir.HotOp::Throw
| @ir.HotOp::ThrowRef
| @ir.HotOp::Rethrow
| @ir.HotOp::Delegate
| @ir.HotOp::Catch
| @ir.HotOp::CatchAll
| @ir.HotOp::Continuation => return true
_ => ()
}
}
false
}
///|
fn ssa_nomerge_has_loop_param_control(func : @ir.HotFunc) -> Bool {
// Typed loop params/results are loop-carried control values on the wasm stack,
// not body locals. HOT represents params as the Loop node's param-prefix
// children and branch operands on backedges (`br` / `br_if` / `br_table`), with
// `HotLabelInfo.branch_arity` recording the payload count. Result loops also
// carry stack values across their branch/continuation ABI even when the loop has
// no explicit params.
//
// SSA v1's local overlay models only `LocalGet` / `LocalSet` / `LocalTee`
// reaching definitions. It does not treat loop-param/result value flow as local
// defs, and destruction only inserts predecessor copies for overlay phis on
// normal CFG edges — not wasm branch operands that re-enter typed loop headers.
//
// Rewriting locals around typed loops without loop-param/result-aware liveness,
// use-def, phi placement, rename, destroy, and lower can conflate a local SSA
// version with the loop-carried stack ABI or allocate scratch locals for values
// Binaryen keeps in the canonical local. Fail closed until that stack is proven
// together; regression tests lock the no-mutation contract.
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if !@ir.hot_node_is_live(func, node_id) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Loop =>
match @ir.hot_type_get(func, node.ty) {
@ir.HotTypeInfo::BlockResult(block_type) =>
if block_type != @lib.BlockType::void_() {
return true
}
@ir.HotTypeInfo::BlockResultResolved(_, params, results) =>
if params.length() > 0 || results.length() > 0 {
return true
}
_ => ()
}
_ => ()
}
}
false
}
///|
fn ssa_nomerge_has_branch_heavy_void_loop_exit_mesh(func : @ir.HotFunc) -> Bool {
// Branch-heavy void loop meshes like the sampled `moonbit.gc.free` artifact
// (`abs=57`) currently make SSA destruction allocate more scratch locals than
// Binaryen for multi-predecessor branch exits. This is not the typed loop-result
// ABI gap covered above: these are ordinary void loops with many local-carrying
// exits. Fail closed for this high-complexity shape until SSA destruction can
// model/reuse those predecessor copies without adding unproven extra locals.
if @ir.hot_local_count(func) < 17 {
return false
}
let mut loop_count = 0
let mut branch_count = 0
let mut local_write_count = 0
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if !@ir.hot_node_is_live(func, node_id) {
continue
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Loop => loop_count = loop_count + 1
@ir.HotOp::Br | @ir.HotOp::BrIf | @ir.HotOp::BrTable =>
branch_count = branch_count + 1
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
local_write_count = local_write_count + 1
_ => ()
}
}
loop_count >= 4 && branch_count >= 5 && local_write_count >= 18
}
///|
fn ssa_nomerge_needs_rewrite(func : @ir.HotFunc, ssa : @ir.HotLocalSsa) -> Bool {
if ssa.phis.length() != 0 {
return true
}
for origin in ssa.value_origins {
match origin {
@ir.HotSsaValueOrigin::LocalSetDef(_)
| @ir.HotSsaValueOrigin::LocalTeeDef(_) => return true
_ => ()
}
}
for node_id = 0
node_id < ssa.local_write_defs.length()
node_id = node_id + 1 {
if ssa.local_write_defs[node_id] >= 0 ||
!@ir.hot_node_is_live(func, node_id) {
continue
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => return true
_ => ()
}
}
false
}
///|
fn ssa_nomerge_queue_merge_operand_children(
func : @ir.HotFunc,
node_id : @ir.NodeId,
operands : @bitset.BitSet,
pending : Array[@ir.NodeId],
) -> Unit {
let node = @ir.hot_node_get(func, node_id)
let operand_count = match node.op {
@ir.HotOp::Block => node.imm1
@ir.HotOp::Loop => @ir.hot_label_branch_arity(func, node.imm0)
_ => @ir.hot_child_count(func, node_id)
}
for slot = 0; slot < operand_count; slot = slot + 1 {
match node.op {
@ir.HotOp::If => if slot == 1 || slot == 2 { continue }
@ir.HotOp::Try | @ir.HotOp::TryTable =>
if slot == 0 || slot == 1 {
continue
}
_ => ()
}
let child = @ir.hot_child_get(func, node_id, slot)
if child >= 0 && @ir.hot_node_is_live(func, child) && !operands[child] {
operands[child] = true
pending.push(child)
}
}
}
///|
fn ssa_nomerge_preserve_merge_operand_writes(
func : @ir.HotFunc,
cfg : @ir.HotCfg,
ssa : @ir.HotLocalSsa,
preserve_writes : @bitset.BitSet,
) -> Unit {
if ssa.phis.is_empty() {
return
}
// Predecessor copies precede the whole terminator root. Its operand writes
// run later, so an input defined there must remain in the phi's local.
// One shared worklist visits each operand once and excludes region bodies.
let operands = @bitset.BitSet::new(@ir.hot_node_count(func))
let pending : Array[@ir.NodeId] = []
for block in cfg.blocks {
match block.nodes.last() {
Some(last) =>
if @ir.hot_is_branch(func, last) ||
@ir.hot_is_control_node(func, last) ||
@ir.hot_is_terminator_node(func, last) {
ssa_nomerge_queue_merge_operand_children(
func, last, operands, pending,
)
}
None => ()
}
}
while !pending.is_empty() {
let node = pending.pop().unwrap()
ssa_nomerge_queue_merge_operand_children(func, node, operands, pending)
}
for inputs in ssa.phi_inputs {
for input in inputs {
match ssa.value_origins[input.value_id] {
@ir.HotSsaValueOrigin::LocalSetDef(write)
| @ir.HotSsaValueOrigin::LocalTeeDef(write) =>
if operands[write] && cfg.node_to_block[write] == input.predecessor {
preserve_writes[write] = true
}
_ => ()
}
}
}
}
///|
fn ssa_nomerge_run(ctx : HotPassContext, func : @ir.HotFunc) -> HotPassResult {
if !ssa_nomerge_has_local_writes(func) {
return HotPassResult::unchanged()
}
if pass_has_repeated_entry_evaluation(func) ||
ssa_nomerge_has_exceptional_flow(func) ||
ssa_nomerge_has_loop_param_control(func) ||
ssa_nomerge_has_branch_heavy_void_loop_exit_mesh(func) ||
ssa_nomerge_has_nested_cfg_sensitive_control(func) {
return HotPassResult::unchanged()
}
// Keep the graph policy local: the revision-only shared cache holds root
// graphs. All overlays here must use this same operand-expanded CFG.
let cfg = @ir.cfg_build(func, expand_operand_control=true)
let dom = @ir.dom_build(cfg)
let use_def = @ir.use_def_build(func, cfg=Some(cfg))
let live = @ir.liveness_build(cfg, use_def)
let ssa = @ir.ssa_build_local(func, cfg, dom, live, use_def)
if !ssa_nomerge_needs_rewrite(func, ssa) {
return HotPassResult::unchanged()
}
let before_revision = @ir.hot_revision_current(func)
let loop_carried_writes = ssa_nomerge_loop_carried_canonical_writes(func)
ssa_nomerge_preserve_merge_operand_writes(func, cfg, ssa, loop_carried_writes)
let param_tee_facts = ssa_nomerge_param_tee_facts(func)
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
if node_id < loop_carried_writes.length() &&
ssa_nomerge_write_is_branch_carried_param_tee(
func, node_id, param_tee_facts,
) {
loop_carried_writes[node_id] = true
}
}
ignore(
@ir.ssa_destroy_into_hot(
func,
cfg,
ssa,
preserve_write_nodes=loop_carried_writes,
),
)
if @ir.hot_revision_current(func) == before_revision {
return HotPassResult::unchanged()
}
pass_mark_mutated(ctx, func)
HotPassResult::changed()
}