// 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 2015 WebAssembly Community Group participants
// Counterpart: src/passes/SimplifyLocals.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 simplify_locals_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals", requires=[], 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 simplify_locals_summary() -> String {
"Sink local.sets into later gets, lift structured block/if/loop returns when safe, canonicalize equivalent copies, and remove dead local writes."
}
///|
pub fn simplify_locals_notee_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals-notee", requires=[], invalidates=[
@ir.HotAnalysis::cfg(),
@ir.HotAnalysis::dominance(),
@ir.HotAnalysis::liveness(),
@ir.HotAnalysis::use_def(),
@ir.HotAnalysis::effects(),
@ir.HotAnalysis::loop_info(),
@ir.HotAnalysis::ssa(),
])
}
///|
fn simplify_locals_notee_alias_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals-no-tee", requires=[], 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 simplify_locals_notee_summary() -> String {
"Run the structured no-tee SimplifyLocals variant: sink single-use local traffic, synthesize control results, canonicalize equivalent copies, and remove dead writes without creating sink tees."
}
///|
pub fn simplify_locals_nonesting_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals-nonesting", requires=[], invalidates=[
@ir.HotAnalysis::cfg(),
@ir.HotAnalysis::dominance(),
@ir.HotAnalysis::liveness(),
@ir.HotAnalysis::use_def(),
@ir.HotAnalysis::effects(),
@ir.HotAnalysis::loop_info(),
@ir.HotAnalysis::ssa(),
])
}
///|
fn simplify_locals_nonesting_alias_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals-no-nesting", requires=[], 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 simplify_locals_nonesting_summary() -> String {
"Run the flat SimplifyLocals variant: retarget copy traffic and clean dead locals without creating tees, control results, or ordinary new nesting."
}
///|
pub fn simplify_locals_nostructure_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals-nostructure", requires=[], invalidates=[
@ir.HotAnalysis::cfg(),
@ir.HotAnalysis::dominance(),
@ir.HotAnalysis::liveness(),
@ir.HotAnalysis::use_def(),
@ir.HotAnalysis::effects(),
@ir.HotAnalysis::loop_info(),
@ir.HotAnalysis::ssa(),
])
}
///|
fn simplify_locals_nostructure_alias_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals-no-structure", requires=[], 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 simplify_locals_notee_nostructure_descriptor() -> @ir.HotPassDescriptor {
@ir.HotPassDescriptor::new("simplify-locals-notee-nostructure", requires=[], 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 simplify_locals_nostructure_summary() -> String {
"Run the no-structure SimplifyLocals variant: sink local traffic and dead writes without synthesizing block/if/loop result rewrites."
}
///|
pub fn simplify_locals_notee_nostructure_summary() -> String {
"Run Binaryen's size-biased no-tee/no-structure SimplifyLocals spelling: sink local traffic and dead writes without fresh tees or structure-result rewrites."
}
///|
priv struct SimplifyLocalsPolicy {
allow_structure_rewrites : Bool
allow_sink_tee : Bool
allow_nesting : Bool
}
///|
fn SimplifyLocalsPolicy::new(
allow_structure_rewrites : Bool,
allow_sink_tee : Bool,
allow_nesting : Bool,
) -> SimplifyLocalsPolicy {
{ allow_structure_rewrites, allow_sink_tee, allow_nesting, }
}
///|
priv struct SimplifyLocalsEffects {
mask : @ir.EffectMask
local_reads : Array[Int32]
local_writes : Array[Int32]
global_reads : Array[Int32]
global_writes : Array[Int32]
}
///|
priv struct SimplifyLocalsSinkableInfo {
node_id : Int32
effects : SimplifyLocalsEffects
}
///|
priv struct SimplifyLocalsSinkables {
module_ctx : @ir.HotModuleContext?
entries : Array[SimplifyLocalsSinkableInfo?]
active_pos : Array[Int32]
active_locals : Array[Int32]
effect_bit_counts : Array[Int32]
local_read_counts : Array[Int32]
local_write_counts : Array[Int32]
scratch_local_read_stamps : Array[Int32]
scratch_local_write_stamps : Array[Int32]
mut next_scratch_stamp : Int32
source_gets : Array[Array[(Int32, Int32)]]
captures : PassSourceCaptures
mut effect_scan_workspace : SimplifyLocalsEffectScanWorkspace?
mut value_order_minimums : SimplifyLocalsValueOrderMinimums?
}
///|
priv struct SimplifyLocalsTailSet {
root_idx : Int32
node_id : Int32
local_id : Int32
value_id : Int32
}
///|
priv struct SimplifyLocalsBranchExit {
region_ref : @ir.HotRegionRef
set_idx : Int32
set_id : Int32
br_idx : Int32
br_id : Int32
value_id : Int32
}
///|
priv struct SimplifyLocalsStackedForwarderTail {
payload_if : Int32
rewrite_region : @ir.HotRegionRef
rewrite_idx : Int32
}
///|
fn simplify_locals_new_sinkables(
func : @ir.HotFunc,
module_ctx~ : @ir.HotModuleContext? = None,
source_gets~ : Array[Array[(Int32, Int32)]]? = None,
captures~ : PassSourceCaptures? = None,
) -> SimplifyLocalsSinkables {
let captures = match captures {
Some(captures) => captures
None => PassSourceCaptures::new(func)
}
let source_gets = match source_gets {
Some(reads) => reads
None => {
let reads : Array[Array[(Int32, Int32)]] = Array::makei(
@ir.hot_local_count(func),
fn(_) { [] },
)
for id = 0; id < @ir.hot_node_count(func); id = id + 1 {
if @ir.hot_node_is_live(func, id) {
let node = @ir.hot_node_get(func, id)
if node.op is @ir.HotOp::LocalGet &&
node.imm0 >= 0 &&
node.imm0 < reads.length() {
reads[node.imm0].push((node.order, id))
}
}
}
for local_reads in reads {
local_reads.sort_by(fn(a, b) { a.0.compare(b.0) })
}
reads
}
}
{
module_ctx,
entries: Array::make(@ir.hot_local_count(func), None),
active_pos: Array::make(@ir.hot_local_count(func), -1),
active_locals: [],
effect_bit_counts: Array::make(10, 0),
local_read_counts: Array::make(@ir.hot_local_count(func), 0),
local_write_counts: Array::make(@ir.hot_local_count(func), 0),
scratch_local_read_stamps: Array::make(@ir.hot_local_count(func), 0),
scratch_local_write_stamps: Array::make(@ir.hot_local_count(func), 0),
next_scratch_stamp: 1,
source_gets,
captures,
effect_scan_workspace: None,
value_order_minimums: None,
}
}
///|
fn simplify_locals_has_earlier_source_read(
func : @ir.HotFunc,
sinkables : SimplifyLocalsSinkables,
local_id : Int32,
definition_order : Int32,
current_order : Int32,
) -> Bool {
let reads = sinkables.source_gets[local_id]
let mut lo = 0
let mut hi = reads.length()
while lo < hi {
let mid = lo + (hi - lo) / 2
if reads[mid].0 <= definition_order {
lo = mid + 1
} else {
hi = mid
}
}
while lo < reads.length() && reads[lo].0 < current_order {
let id = reads[lo].1
if @ir.hot_node_is_live(func, id) {
let read = @ir.hot_node_get(func, id)
if read.op is @ir.HotOp::LocalGet &&
read.imm0 == local_id &&
read.order > definition_order &&
read.order < current_order {
return true
}
}
lo += 1
}
false
}
///|
fn simplify_locals_accumulate_effect_mask_counts(
counts : Array[Int32],
mask : @ir.EffectMask,
delta : Int32,
) -> Unit {
let mut bit = 1U
let mut idx = 0
while idx < counts.length() {
if (mask & bit) != 0U {
counts[idx] = counts[idx] + delta
}
bit = bit << 1
idx = idx + 1
}
}
///|
fn simplify_locals_accumulate_local_effect_counts(
counts : Array[Int32],
effects : Array[Int32],
delta : Int32,
) -> Unit {
for local_id in effects {
if local_id >= 0 && local_id < counts.length() {
counts[local_id] = counts[local_id] + delta
}
}
}
///|
fn simplify_locals_sinkables_add_effects(
sinkables : SimplifyLocalsSinkables,
effects : SimplifyLocalsEffects,
) -> Unit {
simplify_locals_accumulate_effect_mask_counts(
sinkables.effect_bit_counts,
effects.mask,
1,
)
simplify_locals_accumulate_local_effect_counts(
sinkables.local_read_counts,
effects.local_reads,
1,
)
simplify_locals_accumulate_local_effect_counts(
sinkables.local_write_counts,
effects.local_writes,
1,
)
}
///|
fn simplify_locals_sinkables_remove_effects(
sinkables : SimplifyLocalsSinkables,
effects : SimplifyLocalsEffects,
) -> Unit {
simplify_locals_accumulate_effect_mask_counts(
sinkables.effect_bit_counts,
effects.mask,
-1,
)
simplify_locals_accumulate_local_effect_counts(
sinkables.local_read_counts,
effects.local_reads,
-1,
)
simplify_locals_accumulate_local_effect_counts(
sinkables.local_write_counts,
effects.local_writes,
-1,
)
}
///|
fn simplify_locals_sinkables_have_effect(
sinkables : SimplifyLocalsSinkables,
bit : @ir.EffectMask,
) -> Bool {
let mut mask_bit = 1U
let mut idx = 0
while idx < sinkables.effect_bit_counts.length() {
if mask_bit == bit {
return sinkables.effect_bit_counts[idx] > 0
}
mask_bit = mask_bit << 1
idx = idx + 1
}
false
}
///|
fn simplify_locals_sinkables_may_conflict(
sinkables : SimplifyLocalsSinkables,
rhs_mask : @ir.EffectMask,
rhs_local_read : Int32,
rhs_local_write : Int32,
) -> Bool {
if sinkables.active_locals.is_empty() || rhs_mask == 0U {
return false
}
if simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_CALL) ||
simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_CONTROL | @ir.EFFECT_MASK_THROW | @ir.EFFECT_MASK_TRAP,
) {
return true
}
// Pending calls can observe any nonlocal state; pending control, throws,
// and traps can determine whether a later effect executes at all. Consult
// the exact ordering check even when the RHS has no matching effect bit.
if simplify_locals_sinkables_have_effect(sinkables, @ir.EFFECT_MASK_CALL) ||
simplify_locals_sinkables_have_effect(sinkables, @ir.EFFECT_MASK_CONTROL) ||
simplify_locals_sinkables_have_effect(sinkables, @ir.EFFECT_MASK_THROW) ||
simplify_locals_sinkables_have_effect(sinkables, @ir.EFFECT_MASK_TRAP) {
return true
}
if simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_LOCAL_STATE) {
if rhs_local_read >= 0 &&
rhs_local_read < sinkables.local_write_counts.length() &&
sinkables.local_write_counts[rhs_local_read] > 0 {
return true
}
if rhs_local_write >= 0 &&
rhs_local_write < sinkables.local_read_counts.length() &&
(
sinkables.local_read_counts[rhs_local_write] > 0 ||
sinkables.local_write_counts[rhs_local_write] > 0
) {
return true
}
}
if simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_GLOBAL_STATE) &&
simplify_locals_sinkables_have_effect(
sinkables,
@ir.EFFECT_MASK_GLOBAL_STATE,
) {
return true
}
let rhs_reads_memory = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_MEMORY_READ,
)
let rhs_writes_memory = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_MEMORY_WRITE,
)
if (
rhs_reads_memory &&
simplify_locals_sinkables_have_effect(
sinkables,
@ir.EFFECT_MASK_MEMORY_WRITE,
)
) ||
(
rhs_writes_memory &&
(
simplify_locals_sinkables_have_effect(
sinkables,
@ir.EFFECT_MASK_MEMORY_READ,
) ||
simplify_locals_sinkables_have_effect(
sinkables,
@ir.EFFECT_MASK_MEMORY_WRITE,
)
)
) {
return true
}
let rhs_reads_table = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_TABLE_READ,
)
let rhs_writes_table = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_TABLE_WRITE,
)
if (
rhs_reads_table &&
simplify_locals_sinkables_have_effect(
sinkables,
@ir.EFFECT_MASK_TABLE_WRITE,
)
) ||
(
rhs_writes_table &&
(
simplify_locals_sinkables_have_effect(
sinkables,
@ir.EFFECT_MASK_TABLE_READ,
) ||
simplify_locals_sinkables_have_effect(
sinkables,
@ir.EFFECT_MASK_TABLE_WRITE,
)
)
) {
return true
}
false
}
///|
fn simplify_locals_sinkables_get(
sinkables : SimplifyLocalsSinkables,
local_id : Int32,
) -> SimplifyLocalsSinkableInfo? {
if local_id < 0 || local_id >= sinkables.entries.length() {
return None
}
sinkables.entries[local_id]
}
///|
fn simplify_locals_sinkables_set(
sinkables : SimplifyLocalsSinkables,
local_id : Int32,
info : SimplifyLocalsSinkableInfo,
) -> Unit {
if local_id < 0 || local_id >= sinkables.entries.length() {
return
}
if sinkables.active_pos[local_id] < 0 {
sinkables.active_pos[local_id] = sinkables.active_locals.length()
sinkables.active_locals.push(local_id)
} else {
match sinkables.entries[local_id] {
Some(previous) =>
simplify_locals_sinkables_remove_effects(sinkables, previous.effects)
None => ()
}
}
sinkables.entries[local_id] = Some(info)
simplify_locals_sinkables_add_effects(sinkables, info.effects)
}
///|
fn simplify_locals_sinkables_clear_local(
sinkables : SimplifyLocalsSinkables,
local_id : Int32,
) -> Unit {
if local_id < 0 || local_id >= sinkables.entries.length() {
return
}
let pos = sinkables.active_pos[local_id]
if pos >= 0 {
match sinkables.entries[local_id] {
Some(info) =>
simplify_locals_sinkables_remove_effects(sinkables, info.effects)
None => ()
}
let last_local = sinkables.active_locals.pop().unwrap()
if last_local != local_id {
sinkables.active_locals[pos] = last_local
sinkables.active_pos[last_local] = pos
}
sinkables.active_pos[local_id] = -1
}
sinkables.entries[local_id] = None
}
///|
fn simplify_locals_sinkables_should_bulk_clear(
sinkables : SimplifyLocalsSinkables,
) -> Bool {
sinkables.active_locals.length() * 4 >= sinkables.local_read_counts.length()
}
///|
fn simplify_locals_sinkables_zero_aggregate_counts(
sinkables : SimplifyLocalsSinkables,
) -> Unit {
for idx = 0; idx < sinkables.effect_bit_counts.length(); idx = idx + 1 {
sinkables.effect_bit_counts[idx] = 0
}
for idx = 0; idx < sinkables.local_read_counts.length(); idx = idx + 1 {
sinkables.local_read_counts[idx] = 0
sinkables.local_write_counts[idx] = 0
}
}
///|
fn simplify_locals_clear_sinkables(sinkables : SimplifyLocalsSinkables) -> Unit {
if simplify_locals_sinkables_should_bulk_clear(sinkables) {
while !sinkables.active_locals.is_empty() {
let local_id = sinkables.active_locals.pop().unwrap()
sinkables.entries[local_id] = None
sinkables.active_pos[local_id] = -1
}
simplify_locals_sinkables_zero_aggregate_counts(sinkables)
return
}
while !sinkables.active_locals.is_empty() {
let local_id = sinkables.active_locals.pop().unwrap()
match sinkables.entries[local_id] {
Some(info) =>
simplify_locals_sinkables_remove_effects(sinkables, info.effects)
None => ()
}
sinkables.entries[local_id] = None
sinkables.active_pos[local_id] = -1
}
}
///|
fn simplify_locals_clear_throwing_sinkables(
sinkables : SimplifyLocalsSinkables,
) -> Unit {
let mut idx = 0
while idx < sinkables.active_locals.length() {
let local_id = sinkables.active_locals[idx]
match sinkables.entries[local_id] {
Some(info) if simplify_locals_effects_has(
info.effects.mask,
@ir.EFFECT_MASK_THROW,
) => {
simplify_locals_sinkables_clear_local(sinkables, local_id)
continue
}
_ => ()
}
idx = idx + 1
}
}
///|
fn simplify_locals_empty_effects() -> SimplifyLocalsEffects {
{
mask: 0U,
local_reads: [],
local_writes: [],
global_reads: [],
global_writes: [],
}
}
///|
fn simplify_locals_next_scratch_stamp(
sinkables : SimplifyLocalsSinkables,
) -> Int32 {
let stamp = sinkables.next_scratch_stamp
sinkables.next_scratch_stamp = stamp + 1
stamp
}
///|
fn simplify_locals_record_sparse_local_effect(
local_ids : Array[Int32],
stamps : Array[Int32],
stamp : Int32,
local_id : Int32,
) -> Unit {
if local_id < 0 || local_id >= stamps.length() {
return
}
if stamps[local_id] == stamp {
return
}
stamps[local_id] = stamp
local_ids.push(local_id)
}
///|
fn simplify_locals_record_exact_local_effects(
func : @ir.HotFunc,
node_id : Int32,
local_reads : Array[Int32],
local_writes : Array[Int32],
read_stamps : Array[Int32],
write_stamps : Array[Int32],
stamp : Int32,
) -> Unit {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet =>
simplify_locals_record_sparse_local_effect(
local_reads,
read_stamps,
stamp,
node.imm0,
)
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
simplify_locals_record_sparse_local_effect(
local_writes,
write_stamps,
stamp,
node.imm0,
)
_ => ()
}
}
///|
fn simplify_locals_record_exact_global_effects(
func : @ir.HotFunc,
node_id : Int32,
global_reads : Array[Int32],
global_writes : Array[Int32],
) -> Unit {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::GlobalGet =>
if node.imm0 >= 0 && !global_reads.contains(node.imm0) {
global_reads.push(node.imm0)
}
@ir.HotOp::GlobalSet =>
if node.imm0 >= 0 && !global_writes.contains(node.imm0) {
global_writes.push(node.imm0)
}
_ => ()
}
}
///|
fn simplify_locals_collect_region_local_effects(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
local_reads : Array[Int32],
local_writes : Array[Int32],
read_stamps : Array[Int32],
write_stamps : Array[Int32],
stamp : Int32,
) -> Unit {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
simplify_locals_collect_subtree_local_effects(
func,
@ir.hot_region_get(func, region_ref, idx),
local_reads,
local_writes,
read_stamps,
write_stamps,
stamp,
)
}
}
///|
fn simplify_locals_collect_subtree_local_effects(
func : @ir.HotFunc,
node_id : Int32,
local_reads : Array[Int32],
local_writes : Array[Int32],
read_stamps : Array[Int32],
write_stamps : Array[Int32],
stamp : Int32,
) -> Unit {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return
}
simplify_locals_record_exact_local_effects(
func, node_id, local_reads, local_writes, read_stamps, write_stamps, stamp,
)
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => {
simplify_locals_collect_subtree_local_effects(
func,
@ir.hot_child_get(func, node_id, 0),
local_reads,
local_writes,
read_stamps,
write_stamps,
stamp,
)
simplify_locals_collect_region_local_effects(
func,
@ir.hot_region_then_ref(func, node_id),
local_reads,
local_writes,
read_stamps,
write_stamps,
stamp,
)
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
simplify_locals_collect_region_local_effects(
func, else_ref, local_reads, local_writes, read_stamps, write_stamps,
stamp,
)
None => ()
}
return
}
@ir.HotOp::Try => {
simplify_locals_collect_region_local_effects(
func,
@ir.hot_region_body_ref(func, node_id),
local_reads,
local_writes,
read_stamps,
write_stamps,
stamp,
)
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
simplify_locals_collect_region_local_effects(
func, catch_ref, local_reads, local_writes, read_stamps, write_stamps,
stamp,
)
None => ()
}
return
}
@ir.HotOp::TryTable => {
simplify_locals_collect_region_local_effects(
func,
@ir.hot_region_body_ref(func, node_id),
local_reads,
local_writes,
read_stamps,
write_stamps,
stamp,
)
simplify_locals_collect_region_local_effects(
func,
@ir.hot_region_catch_list_ref(func, node_id),
local_reads,
local_writes,
read_stamps,
write_stamps,
stamp,
)
return
}
_ => ()
}
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)
simplify_locals_collect_subtree_local_effects(
func, child_id, local_reads, local_writes, read_stamps, write_stamps, stamp,
)
}
}
///|
fn simplify_locals_collect_region_global_effects(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
global_reads : Array[Int32],
global_writes : Array[Int32],
) -> Unit {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
simplify_locals_collect_subtree_global_effects(
func,
@ir.hot_region_get(func, region_ref, idx),
global_reads,
global_writes,
)
}
}
///|
fn simplify_locals_collect_subtree_global_effects(
func : @ir.HotFunc,
node_id : Int32,
global_reads : Array[Int32],
global_writes : Array[Int32],
) -> Unit {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return
}
simplify_locals_record_exact_global_effects(
func, node_id, global_reads, global_writes,
)
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => {
simplify_locals_collect_subtree_global_effects(
func,
@ir.hot_child_get(func, node_id, 0),
global_reads,
global_writes,
)
simplify_locals_collect_region_global_effects(
func,
@ir.hot_region_then_ref(func, node_id),
global_reads,
global_writes,
)
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
simplify_locals_collect_region_global_effects(
func, else_ref, global_reads, global_writes,
)
None => ()
}
return
}
@ir.HotOp::Try => {
simplify_locals_collect_region_global_effects(
func,
@ir.hot_region_body_ref(func, node_id),
global_reads,
global_writes,
)
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
simplify_locals_collect_region_global_effects(
func, catch_ref, global_reads, global_writes,
)
None => ()
}
return
}
@ir.HotOp::TryTable => {
simplify_locals_collect_region_global_effects(
func,
@ir.hot_region_body_ref(func, node_id),
global_reads,
global_writes,
)
simplify_locals_collect_region_global_effects(
func,
@ir.hot_region_catch_list_ref(func, node_id),
global_reads,
global_writes,
)
return
}
_ => ()
}
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)
simplify_locals_collect_subtree_global_effects(
func, child_id, global_reads, global_writes,
)
}
}
///|
fn simplify_locals_effects_mask_for_flags(flags : UInt32) -> @ir.EffectMask {
let mut mask = 0U
if (flags & @ir.HOT_FLAG_CONTROL) != 0U ||
(flags & @ir.HOT_FLAG_BRANCH) != 0U ||
(flags & @ir.HOT_FLAG_TERMINATOR) != 0U {
mask = mask | @ir.EFFECT_MASK_CONTROL
}
if (flags & @ir.HOT_FLAG_CALL) != 0U {
mask = mask | @ir.EFFECT_MASK_CALL | @ir.EFFECT_MASK_THROW
}
if (flags & @ir.HOT_FLAG_THROWS) != 0U ||
(flags & @ir.HOT_FLAG_HAS_EXCEPTIONAL_SUCC) != 0U {
mask = mask | @ir.EFFECT_MASK_THROW
}
if (flags & @ir.HOT_FLAG_MAY_TRAP) != 0U {
mask = mask | @ir.EFFECT_MASK_TRAP
}
mask
}
///|
fn simplify_locals_effects_mask_for_atomic_exact(
instr : @lib.Instruction?,
) -> @ir.EffectMask {
match instr {
Some(@lib.Instruction::MemoryAtomicNotify(_)) =>
@ir.EFFECT_MASK_MEMORY_WRITE | @ir.EFFECT_MASK_TRAP
Some(@lib.Instruction::MemoryAtomicWait32(_))
| Some(@lib.Instruction::MemoryAtomicWait64(_))
| Some(@lib.Instruction::I32AtomicLoad(_, _))
| Some(@lib.Instruction::I64AtomicLoad(_, _))
| Some(@lib.Instruction::I32AtomicLoad8U(_, _))
| Some(@lib.Instruction::I32AtomicLoad16U(_, _))
| Some(@lib.Instruction::I64AtomicLoad8U(_, _))
| Some(@lib.Instruction::I64AtomicLoad16U(_, _))
| Some(@lib.Instruction::I64AtomicLoad32U(_, _))
| Some(@lib.Instruction::StructAtomicGet(_, _, _))
| Some(@lib.Instruction::StructAtomicGetS(_, _, _))
| Some(@lib.Instruction::StructAtomicGetU(_, _, _)) =>
// Treat atomic reads as memory-ordering barriers for local sinking: they
// read memory, but ordinary read-only loads must not commute across them.
@ir.EFFECT_MASK_MEMORY_READ |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TRAP
Some(@lib.Instruction::I32AtomicStore(_, _))
| Some(@lib.Instruction::I64AtomicStore(_, _))
| Some(@lib.Instruction::I32AtomicStore8(_, _))
| Some(@lib.Instruction::I32AtomicStore16(_, _))
| Some(@lib.Instruction::I64AtomicStore8(_, _))
| Some(@lib.Instruction::I64AtomicStore16(_, _))
| Some(@lib.Instruction::I64AtomicStore32(_, _)) =>
@ir.EFFECT_MASK_MEMORY_WRITE | @ir.EFFECT_MASK_TRAP
Some(@lib.Instruction::AtomicRmw(_, _, _))
| Some(@lib.Instruction::AtomicCmpxchg(_, _, _)) =>
@ir.EFFECT_MASK_MEMORY_READ |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TRAP
_ =>
@ir.EFFECT_MASK_MEMORY_READ |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TRAP
}
}
///|
fn simplify_locals_effects_mask_for_heap_exact(
instr : @lib.Instruction?,
) -> @ir.EffectMask {
match instr {
Some(@lib.Instruction::StructGet(_, _))
| Some(@lib.Instruction::StructGetS(_, _))
| Some(@lib.Instruction::StructGetU(_, _))
| Some(@lib.Instruction::ArrayGet(_))
| Some(@lib.Instruction::ArrayGetS(_))
| Some(@lib.Instruction::ArrayGetU(_))
| Some(@lib.Instruction::ArrayLoad(_, _, _))
| Some(@lib.Instruction::ArrayLen)
| Some(@lib.Instruction::RefGetDesc(_)) =>
@ir.EFFECT_MASK_MEMORY_READ | @ir.EFFECT_MASK_TRAP
Some(@lib.Instruction::StructSet(_, _))
| Some(@lib.Instruction::ArraySet(_))
| Some(@lib.Instruction::ArrayStore(_, _, _))
| Some(@lib.Instruction::ArrayFill(_)) =>
@ir.EFFECT_MASK_MEMORY_WRITE | @ir.EFFECT_MASK_TRAP
Some(@lib.Instruction::StructNew(_))
| Some(@lib.Instruction::StructNewDefault(_))
| Some(@lib.Instruction::StructNewDesc(_))
| Some(@lib.Instruction::StructNewDefaultDesc(_))
| Some(@lib.Instruction::ArrayNew(_))
| Some(@lib.Instruction::ArrayNewDefault(_))
| Some(@lib.Instruction::ArrayNewFixed(_, _))
| Some(@lib.Instruction::ArrayNewData(_, _))
| Some(@lib.Instruction::ArrayNewElem(_, _))
| Some(@lib.Instruction::ArrayCopy(_, _))
| Some(@lib.Instruction::ArrayInitData(_, _))
| Some(@lib.Instruction::ArrayInitElem(_, _)) =>
@ir.EFFECT_MASK_MEMORY_READ |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TRAP
Some(@lib.Instruction::RefAsNonNull)
| Some(@lib.Instruction::I31GetS)
| Some(@lib.Instruction::I31GetU) => @ir.EFFECT_MASK_TRAP
Some(@lib.Instruction::RefEq)
| Some(@lib.Instruction::AnyConvertExtern)
| Some(@lib.Instruction::ExternConvertAny)
| Some(@lib.Instruction::RefI31) => 0U
_ => @ir.EFFECT_MASK_TRAP
}
}
///|
fn simplify_locals_effects_mask_for_exact_node(
func : @ir.HotFunc,
node_id : Int32,
) -> @ir.EffectMask {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return 0U
}
let node = @ir.hot_node_get(func, node_id)
let exact_mask = match node.op {
@ir.HotOp::LocalGet | @ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
@ir.EFFECT_MASK_LOCAL_STATE
@ir.HotOp::GlobalGet | @ir.HotOp::GlobalSet => @ir.EFFECT_MASK_GLOBAL_STATE
@ir.HotOp::Load => @ir.EFFECT_MASK_MEMORY_READ
@ir.HotOp::Store => @ir.EFFECT_MASK_MEMORY_WRITE
@ir.HotOp::MemorySize => @ir.EFFECT_MASK_MEMORY_READ
@ir.HotOp::MemoryGrow =>
@ir.EFFECT_MASK_MEMORY_READ | @ir.EFFECT_MASK_MEMORY_WRITE
@ir.HotOp::MemoryCopy | @ir.HotOp::MemoryInit =>
@ir.EFFECT_MASK_MEMORY_READ | @ir.EFFECT_MASK_MEMORY_WRITE
@ir.HotOp::MemoryFill | @ir.HotOp::DataDrop => @ir.EFFECT_MASK_MEMORY_WRITE
@ir.HotOp::TableGet | @ir.HotOp::TableSize => @ir.EFFECT_MASK_TABLE_READ
@ir.HotOp::TableSet
| @ir.HotOp::TableGrow
| @ir.HotOp::TableFill
| @ir.HotOp::ElemDrop => @ir.EFFECT_MASK_TABLE_WRITE
@ir.HotOp::TableCopy | @ir.HotOp::TableInit =>
@ir.EFFECT_MASK_TABLE_READ | @ir.EFFECT_MASK_TABLE_WRITE
@ir.HotOp::Binary | @ir.HotOp::Unary | @ir.HotOp::Convert =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.I32DivS)
| Some(@lib.I32DivU)
| Some(@lib.I32RemS)
| Some(@lib.I32RemU)
| Some(@lib.I64DivS)
| Some(@lib.I64DivU)
| Some(@lib.I64RemS)
| Some(@lib.I64RemU)
| Some(@lib.I32TruncF32S)
| Some(@lib.I32TruncF32U)
| Some(@lib.I32TruncF64S)
| Some(@lib.I32TruncF64U)
| Some(@lib.I64TruncF32S)
| Some(@lib.I64TruncF32U)
| Some(@lib.I64TruncF64S)
| Some(@lib.I64TruncF64U)
| Some(@lib.RefAsNonNull) => @ir.EFFECT_MASK_TRAP
_ => 0U
}
@ir.HotOp::StringOp =>
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.StringEncodeUtf8Array)
| Some(@lib.StringEncodeWtf16Array)
| Some(@lib.StringEncodeLossyUtf8Array)
| Some(@lib.StringEncodeWtf8Array) =>
@ir.EFFECT_MASK_MEMORY_WRITE | @ir.EFFECT_MASK_TRAP
Some(@lib.StringNewUtf8Array)
| Some(@lib.StringNewWtf16Array)
| Some(@lib.StringNewLossyUtf8Array)
| Some(@lib.StringNewWtf8Array) =>
@ir.EFFECT_MASK_MEMORY_READ | @ir.EFFECT_MASK_TRAP
Some(@lib.StringEq) => 0U
_ => @ir.EFFECT_MASK_TRAP
}
@ir.HotOp::Simd => {
let mut mask = 0U
if (node.flags & @ir.HOT_FLAG_LOAD) != 0U {
mask = mask | @ir.EFFECT_MASK_MEMORY_READ
}
if (node.flags & @ir.HOT_FLAG_STORE) != 0U {
mask = mask | @ir.EFFECT_MASK_MEMORY_WRITE
}
mask
}
@ir.HotOp::Atomic =>
simplify_locals_effects_mask_for_atomic_exact(
@ir.hot_node_exact_instr(func, node_id),
)
@ir.HotOp::Heap =>
simplify_locals_effects_mask_for_heap_exact(
@ir.hot_node_exact_instr(func, node_id),
)
_ => 0U
}
simplify_locals_effects_mask_for_flags(node.flags) | exact_mask
}
///|
fn simplify_locals_exact_local_read_id(node : @ir.HotNode) -> Int32 {
match node.op {
@ir.HotOp::LocalGet => node.imm0
_ => -1
}
}
///|
fn simplify_locals_exact_local_write_id(node : @ir.HotNode) -> Int32 {
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => node.imm0
_ => -1
}
}
///|
fn simplify_locals_exact_global_read_id(node : @ir.HotNode) -> Int32 {
match node.op {
@ir.HotOp::GlobalGet => node.imm0
_ => -1
}
}
///|
fn simplify_locals_exact_global_write_id(node : @ir.HotNode) -> Int32 {
match node.op {
@ir.HotOp::GlobalSet => node.imm0
_ => -1
}
}
///|
fn simplify_locals_effect_scan_push_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
pending : Array[Int32],
) -> Unit {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
pending.push(@ir.hot_region_get(func, region_ref, idx))
}
}
///|
fn simplify_locals_effects_mask_for_subtree_scan(
func : @ir.HotFunc,
node_id : Int32,
workspace~ : SimplifyLocalsEffectScanWorkspace? = None,
) -> (@ir.EffectMask, Int32) {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return (0U, 0)
}
if @ir.hot_child_count(func, node_id) == 0 &&
!@ir.hot_node_kind_is_control(func, node_id) {
return (simplify_locals_effects_mask_for_exact_node(func, node_id), 1)
}
let pending : Array[Int32] = [node_id]
let marks = match workspace {
Some(workspace) => workspace.begin(func)
None => Array::make(@ir.hot_node_count(func), 0)
}
let epoch = match workspace {
Some(workspace) => workspace.epoch
None => 1
}
let mut mask = 0U
let mut visited_nodes = 0
while !pending.is_empty() {
let current_id = pending.pop().unwrap()
if current_id < 0 ||
current_id >= marks.length() ||
marks[current_id] == epoch ||
!@ir.hot_node_is_live(func, current_id) {
continue
}
marks[current_id] = epoch
visited_nodes += 1
mask = mask | simplify_locals_effects_mask_for_exact_node(func, current_id)
let current = @ir.hot_node_get(func, current_id)
for slot = 0; slot < @ir.hot_child_count(func, current_id); slot = slot + 1 {
if !simplify_locals_is_region_child_slot(func, current_id, slot) {
pending.push(@ir.hot_child_get(func, current_id, slot))
}
}
match current.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
simplify_locals_effect_scan_push_region(
func,
@ir.hot_region_body_ref(func, current_id),
pending,
)
@ir.HotOp::If => {
simplify_locals_effect_scan_push_region(
func,
@ir.hot_region_then_ref(func, current_id),
pending,
)
match @ir.hot_region_else_ref(func, current_id) {
Some(else_ref) =>
simplify_locals_effect_scan_push_region(func, else_ref, pending)
None => ()
}
}
@ir.HotOp::Try => {
simplify_locals_effect_scan_push_region(
func,
@ir.hot_region_body_ref(func, current_id),
pending,
)
match @ir.hot_region_catch_ref(func, current_id) {
Some(catch_ref) =>
simplify_locals_effect_scan_push_region(func, catch_ref, pending)
None => ()
}
}
@ir.HotOp::TryTable => {
simplify_locals_effect_scan_push_region(
func,
@ir.hot_region_body_ref(func, current_id),
pending,
)
simplify_locals_effect_scan_push_region(
func,
@ir.hot_region_catch_list_ref(func, current_id),
pending,
)
}
_ => ()
}
}
(mask, visited_nodes)
}
///|
fn simplify_locals_effects_mask_for_subtree(
func : @ir.HotFunc,
node_id : Int32,
workspace~ : SimplifyLocalsEffectScanWorkspace? = None,
) -> @ir.EffectMask {
simplify_locals_effects_mask_for_subtree_scan(func, node_id, workspace~).0
}
///|
fn simplify_locals_effects_for_pending_local_set(
func : @ir.HotFunc,
node_id : Int32,
node : @ir.HotNode,
sinkables : SimplifyLocalsSinkables,
) -> SimplifyLocalsEffects {
let effects = simplify_locals_empty_effects()
let stamp = simplify_locals_next_scratch_stamp(sinkables)
let value_mask = if node.child_count != 0 {
let value = @ir.hot_child_get(func, node_id, 0)
let workspace = if @ir.hot_node_count(func) >= 32 &&
@ir.hot_child_count(func, value) > 0 {
match sinkables.effect_scan_workspace {
Some(workspace) => Some(workspace)
None => {
let workspace = SimplifyLocalsEffectScanWorkspace::new()
sinkables.effect_scan_workspace = Some(workspace)
Some(workspace)
}
}
} else {
None
}
simplify_locals_effects_mask_for_subtree(func, value, workspace~)
} else {
0U
}
if node.child_count != 0 &&
simplify_locals_effects_has(value_mask, @ir.EFFECT_MASK_LOCAL_STATE) {
simplify_locals_collect_subtree_local_effects(
func,
@ir.hot_child_get(func, node_id, 0),
effects.local_reads,
effects.local_writes,
sinkables.scratch_local_read_stamps,
sinkables.scratch_local_write_stamps,
stamp,
)
}
if node.child_count != 0 &&
simplify_locals_effects_has(value_mask, @ir.EFFECT_MASK_GLOBAL_STATE) {
simplify_locals_collect_subtree_global_effects(
func,
@ir.hot_child_get(func, node_id, 0),
effects.global_reads,
effects.global_writes,
)
}
simplify_locals_record_sparse_local_effect(
effects.local_writes,
sinkables.scratch_local_write_stamps,
stamp,
node.imm0,
)
{ ..effects, mask: value_mask | @ir.EFFECT_MASK_LOCAL_STATE, }
}
///|
fn simplify_locals_effects_has(
mask : @ir.EffectMask,
bit : @ir.EffectMask,
) -> Bool {
(mask & bit) != 0U
}
///|
fn simplify_locals_effects_has_local(
local_ids : Array[Int32],
local_id : Int32,
) -> Bool {
if local_id < 0 {
return false
}
for current_id in local_ids {
if current_id == local_id {
return true
}
}
false
}
///|
fn simplify_locals_effects_conflict_exact_local(
lhs : SimplifyLocalsEffects,
rhs_local_read : Int32,
rhs_local_write : Int32,
) -> Bool {
if simplify_locals_effects_has_local(lhs.local_writes, rhs_local_read) {
return true
}
if simplify_locals_effects_has_local(lhs.local_reads, rhs_local_write) ||
simplify_locals_effects_has_local(lhs.local_writes, rhs_local_write) {
return true
}
false
}
///|
fn simplify_locals_effects_conflict_exact_global(
module_ctx : @ir.HotModuleContext?,
lhs : SimplifyLocalsEffects,
rhs_global_read : Int32,
rhs_global_write : Int32,
) -> Bool {
if lhs.global_reads.is_empty() && lhs.global_writes.is_empty() {
return true
}
if rhs_global_read < 0 && rhs_global_write < 0 {
return true
}
if rhs_global_read >= 0 {
for write in lhs.global_writes {
if !globals_are_provably_disjoint(module_ctx, write, rhs_global_read) {
return true
}
}
}
if rhs_global_write >= 0 {
for read in lhs.global_reads {
if !globals_are_provably_disjoint(module_ctx, read, rhs_global_write) {
return true
}
}
for write in lhs.global_writes {
if !globals_are_provably_disjoint(module_ctx, write, rhs_global_write) {
return true
}
}
}
false
}
///|
fn simplify_locals_effects_readonly_trap_commutable(
mask : @ir.EffectMask,
) -> Bool {
(
mask &
(
@ir.EFFECT_MASK_CALL |
@ir.EFFECT_MASK_CONTROL |
@ir.EFFECT_MASK_THROW |
@ir.EFFECT_MASK_GLOBAL_STATE |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TABLE_WRITE
)
) ==
0U
}
///|
fn simplify_locals_effects_local_state_only(mask : @ir.EffectMask) -> Bool {
(
mask &
(
@ir.EFFECT_MASK_CALL |
@ir.EFFECT_MASK_CONTROL |
@ir.EFFECT_MASK_THROW |
@ir.EFFECT_MASK_TRAP |
@ir.EFFECT_MASK_GLOBAL_STATE |
@ir.EFFECT_MASK_MEMORY_READ |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TABLE_READ |
@ir.EFFECT_MASK_TABLE_WRITE
)
) ==
0U
}
///|
fn simplify_locals_effects_ordered_before(
lhs : SimplifyLocalsEffects,
rhs_mask : @ir.EffectMask,
rhs_local_read : Int32,
rhs_local_write : Int32,
rhs_global_read : Int32,
rhs_global_write : Int32,
module_ctx~ : @ir.HotModuleContext? = None,
) -> Bool {
// An unrelated local read neither traps nor changes observable state.
// It may precede a pending control/call value when that value cannot change
// the read local. Writes need the full ordering and exception checks below.
if rhs_mask == @ir.EFFECT_MASK_LOCAL_STATE &&
rhs_local_read >= 0 &&
rhs_local_write < 0 &&
!simplify_locals_effects_conflict_exact_local(
lhs, rhs_local_read, rhs_local_write,
) {
return false
}
let lhs_has_call = simplify_locals_effects_has(lhs.mask, @ir.EFFECT_MASK_CALL)
let rhs_has_call = simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_CALL)
if lhs_has_call || rhs_has_call {
let local_state_conflict = simplify_locals_effects_has(
lhs.mask,
@ir.EFFECT_MASK_LOCAL_STATE,
) &&
simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_LOCAL_STATE) &&
simplify_locals_effects_conflict_exact_local(
lhs, rhs_local_read, rhs_local_write,
)
if lhs_has_call &&
!rhs_has_call &&
simplify_locals_effects_local_state_only(rhs_mask) &&
!local_state_conflict {
()
} else if rhs_has_call &&
!lhs_has_call &&
simplify_locals_effects_local_state_only(lhs.mask) &&
!local_state_conflict {
()
} else {
return true
}
}
let lhs_non_linear = simplify_locals_effects_has(
lhs.mask,
@ir.EFFECT_MASK_CONTROL | @ir.EFFECT_MASK_THROW,
)
let rhs_non_linear = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_CONTROL | @ir.EFFECT_MASK_THROW,
)
if (lhs_non_linear || rhs_non_linear) && lhs.mask != 0U && rhs_mask != 0U {
return true
}
if simplify_locals_effects_has(lhs.mask, @ir.EFFECT_MASK_LOCAL_STATE) &&
simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_LOCAL_STATE) &&
simplify_locals_effects_conflict_exact_local(
lhs, rhs_local_read, rhs_local_write,
) {
return true
}
if simplify_locals_effects_has(lhs.mask, @ir.EFFECT_MASK_GLOBAL_STATE) &&
simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_GLOBAL_STATE) &&
simplify_locals_effects_conflict_exact_global(
module_ctx, lhs, rhs_global_read, rhs_global_write,
) {
return true
}
let lhs_reads_memory = simplify_locals_effects_has(
lhs.mask,
@ir.EFFECT_MASK_MEMORY_READ,
)
let lhs_writes_memory = simplify_locals_effects_has(
lhs.mask,
@ir.EFFECT_MASK_MEMORY_WRITE,
)
let rhs_reads_memory = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_MEMORY_READ,
)
let rhs_writes_memory = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_MEMORY_WRITE,
)
if lhs_writes_memory && (rhs_reads_memory || rhs_writes_memory) {
return true
}
if lhs_reads_memory && rhs_writes_memory {
return true
}
let lhs_reads_table = simplify_locals_effects_has(
lhs.mask,
@ir.EFFECT_MASK_TABLE_READ,
)
let lhs_writes_table = simplify_locals_effects_has(
lhs.mask,
@ir.EFFECT_MASK_TABLE_WRITE,
)
let rhs_reads_table = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_TABLE_READ,
)
let rhs_writes_table = simplify_locals_effects_has(
rhs_mask,
@ir.EFFECT_MASK_TABLE_WRITE,
)
if lhs_writes_table && (rhs_reads_table || rhs_writes_table) {
return true
}
if lhs_reads_table && rhs_writes_table {
return true
}
let lhs_traps = simplify_locals_effects_has(lhs.mask, @ir.EFFECT_MASK_TRAP)
let rhs_traps = simplify_locals_effects_has(rhs_mask, @ir.EFFECT_MASK_TRAP)
let readonly_trap_commutable = simplify_locals_effects_readonly_trap_commutable(
lhs.mask,
) &&
simplify_locals_effects_readonly_trap_commutable(rhs_mask)
let lhs_has_non_local_effects = simplify_locals_effects_has(
lhs.mask,
@ir.EFFECT_MASK_CALL |
@ir.EFFECT_MASK_CONTROL |
@ir.EFFECT_MASK_THROW |
@ir.EFFECT_MASK_TRAP |
@ir.EFFECT_MASK_GLOBAL_STATE |
@ir.EFFECT_MASK_MEMORY_READ |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TABLE_READ |
@ir.EFFECT_MASK_TABLE_WRITE,
)
if lhs_traps && rhs_mask != 0U && !readonly_trap_commutable {
return true
}
if rhs_traps && lhs_has_non_local_effects && !readonly_trap_commutable {
return true
}
false
}
///|
fn simplify_locals_invalidate_sinkables(
func : @ir.HotFunc,
node_id : Int32,
node : @ir.HotNode,
sinkables : SimplifyLocalsSinkables,
skip_local~ : Int32 = -1,
) -> Unit {
let effects_mask = simplify_locals_effects_mask_for_exact_node(func, node_id)
if effects_mask == 0U {
return
}
let effects_local_read = simplify_locals_exact_local_read_id(node)
let effects_local_write = simplify_locals_exact_local_write_id(node)
let effects_global_read = simplify_locals_exact_global_read_id(node)
let effects_global_write = simplify_locals_exact_global_write_id(node)
let clear_all = simplify_locals_effects_has(
effects_mask,
@ir.EFFECT_MASK_CALL,
) ||
simplify_locals_effects_has(
effects_mask,
@ir.EFFECT_MASK_CONTROL | @ir.EFFECT_MASK_THROW,
)
if clear_all && skip_local < 0 {
simplify_locals_clear_sinkables(sinkables)
return
}
if !simplify_locals_sinkables_may_conflict(
sinkables, effects_mask, effects_local_read, effects_local_write,
) {
return
}
let mut idx = 0
while idx < sinkables.active_locals.length() {
let local_id = sinkables.active_locals[idx]
if local_id == skip_local {
idx = idx + 1
continue
}
match sinkables.entries[local_id] {
Some(info) =>
if !@ir.hot_node_is_live(func, info.node_id) ||
simplify_locals_effects_ordered_before(
info.effects,
effects_mask,
effects_local_read,
effects_local_write,
effects_global_read,
effects_global_write,
module_ctx=sinkables.module_ctx,
) {
simplify_locals_sinkables_clear_local(sinkables, local_id)
continue
}
None => ()
}
idx = idx + 1
}
}
///|
// Factories pass their immediately allocated, never-attached wrapper.
fn simplify_locals_take_new_wrapper(
func : @ir.HotFunc,
replacement_id : Int32,
retirement_scans : Array[Int32]?,
) -> @ir.HotNode {
let replacement = @ir.hot_node_get(func, replacement_id)
// The builder allocated this node immediately before extraction, and no
// root or child reference has been attached to it. Keep ordinary checked
// deletion for all nodes whose detached ownership is not proved here.
if replacement_id == @ir.hot_node_count(func) - 1 {
@ir.hot_delete_detached_node(func, replacement_id)
} else {
match retirement_scans {
Some(scans) => scans[0] += 1
None => ()
}
@ir.hot_delete_node(func, replacement_id)
}
replacement
}
///|
fn simplify_locals_make_nop(
func : @ir.HotFunc,
retirement_scans~ : Array[Int32]? = None,
) -> @ir.HotNode {
let replacement_id = @ir.hot_build_nop(func)
simplify_locals_take_new_wrapper(func, replacement_id, retirement_scans)
}
///|
fn simplify_locals_make_drop(
func : @ir.HotFunc,
value_id : Int32,
retirement_scans~ : Array[Int32]? = None,
) -> @ir.HotNode {
let replacement_id = @ir.hot_build_drop(func, value_id)
simplify_locals_take_new_wrapper(func, replacement_id, retirement_scans)
}
///|
fn simplify_locals_make_local_get(
func : @ir.HotFunc,
local_id : Int32,
retirement_scans~ : Array[Int32]? = None,
) -> @ir.HotNode {
let replacement_id = @ir.hot_build_local_get(func, local_id)
simplify_locals_take_new_wrapper(func, replacement_id, retirement_scans)
}
///|
fn simplify_locals_make_local_tee(
func : @ir.HotFunc,
set_id : Int32,
retirement_scans~ : Array[Int32]? = None,
) -> @ir.HotNode {
let replacement_id = @ir.hot_build_local_tee_from_set(func, set_id)
simplify_locals_take_new_wrapper(func, replacement_id, retirement_scans)
}
///|
/// Keep the captured value at its source position when removing a carrier.
/// Fresh wrapper order can move dependent conditional reads before this write.
fn simplify_locals_make_local_set(
func : @ir.HotFunc,
local_id : Int32,
value_id : Int32,
retirement_scans~ : Array[Int32]? = None,
) -> @ir.HotNode {
let replacement_id = @ir.hot_build_local_set_from_value(
func, local_id, value_id,
)
simplify_locals_take_new_wrapper(func, replacement_id, retirement_scans)
}
///|
fn simplify_locals_child_use_count_capped_at_two(
func : @ir.HotFunc,
target_id : Int32,
) -> Int32 {
let mut count = 0
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
let node = func.nodes[node_id]
if node.child_count == 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
for slot = 0; slot < node.child_count; slot = slot + 1 {
if func.children[node.child_base + slot] == target_id {
count += 1
if count == 2 {
return 2
}
}
}
}
count
}
///|
fn simplify_locals_two_child_uses_are_unique(
func : @ir.HotFunc,
first_id : Int32,
second_id : Int32,
) -> Bool {
if first_id == second_id {
return simplify_locals_child_use_count_capped_at_two(func, first_id) == 1
}
let mut first_count = 0
let mut second_count = 0
for node_id = 0; node_id < @ir.hot_node_count(func); node_id = node_id + 1 {
let node = func.nodes[node_id]
if node.child_count == 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
for slot = 0; slot < node.child_count; slot = slot + 1 {
let child_id = func.children[node.child_base + slot]
if child_id == first_id {
first_count += 1
if first_count == 2 {
return false
}
} else if child_id == second_id {
second_count += 1
if second_count == 2 {
return false
}
}
}
}
first_count == 1 && second_count == 1
}
///|
fn simplify_locals_local_type_id(
func : @ir.HotFunc,
local_id : Int32,
fallback_id : Int32,
) -> Int32 {
match @ir.hot_local_type(func, local_id) {
Some(local_type) =>
@ir.hot_type_intern_block_result(
func,
@lib.BlockType::val_type(local_type),
)
None =>
match @ir.hot_type_results(func, @ir.hot_node_type(func, fallback_id)) {
[value_type] =>
@ir.hot_type_intern_block_result(
func,
@lib.BlockType::val_type(value_type),
)
_ => @ir.hot_type_void(func)
}
}
}
///|
fn simplify_locals_local_type_is_defaultable(
func : @ir.HotFunc,
local_id : Int32,
) -> Bool {
match @ir.hot_local_type(func, local_id) {
Some(@lib.RefTypeValType(ref_type)) =>
@lib.RefType::is_defaultable(ref_type)
Some(@lib.BotValType) => false
Some(_) => true
None => false
}
}
///|
fn simplify_locals_region_last_live_non_nop_root_idx(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> Int32? {
let mut idx = @ir.hot_region_root_count(func, region_ref) - 1
while idx >= 0 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id >= 0 && @ir.hot_node_is_live(func, node_id) {
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Nop => ()
_ => return Some(idx)
}
}
idx = idx - 1
}
None
}
///|
fn simplify_locals_region_next_live_non_nop_root_idx(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
after_idx : Int32,
) -> Int32? {
let mut idx = after_idx + 1
while idx < @ir.hot_region_root_count(func, region_ref) {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id >= 0 && @ir.hot_node_is_live(func, node_id) {
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Nop => ()
_ => return Some(idx)
}
}
idx = idx + 1
}
None
}
///|
fn simplify_locals_region_tail_local_set(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> SimplifyLocalsTailSet? {
let root_idx = match
simplify_locals_region_last_live_non_nop_root_idx(func, region_ref) {
Some(root_idx) => root_idx
None => return None
}
let node_id = @ir.hot_region_get(func, region_ref, root_idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet =>
Some({
root_idx,
node_id,
local_id: node.imm0,
value_id: @ir.hot_child_get(func, node_id, 0),
})
_ => None
}
}
///|
fn simplify_locals_region_root_index_of(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
node_id : Int32,
) -> Int32? {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
if @ir.hot_region_get(func, region_ref, idx) == node_id {
return Some(idx)
}
}
None
}
///|
fn simplify_locals_replace_tail_set_with_result_value(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
tail : SimplifyLocalsTailSet,
) -> Unit {
let root_count = @ir.hot_region_root_count(func, region_ref)
let trailing_nop_idx = if root_count > 0 {
let last_idx = root_count - 1
let last_id = @ir.hot_region_get(func, region_ref, last_idx)
if last_idx != tail.root_idx &&
last_id >= 0 &&
@ir.hot_node_is_live(func, last_id) &&
@ir.hot_node_get(func, last_id).op is @ir.HotOp::Nop {
Some(last_idx)
} else {
None
}
} else {
None
}
match simplify_locals_region_root_index_of(func, region_ref, tail.value_id) {
Some(value_idx) if value_idx != tail.root_idx => {
// A carried result already executes at this earlier root. Removing its
// consumer must not move the computation past intervening effects.
ignore(
@ir.hot_region_set(
func,
region_ref,
tail.root_idx,
@ir.hot_build_nop(func),
),
)
return
}
_ => ()
}
// Stack operands may have been evaluated before intervening void roots.
// Keep that source position when promoting the operand to a region result.
let source_orders = @ir.hot_source_first_effect_orders(func)
let value_order = @ir.hot_node_get(func, tail.value_id).order
for idx = 0; idx < tail.root_idx; idx = idx + 1 {
let root = @ir.hot_region_get(func, region_ref, idx)
if root >= 0 &&
root < source_orders.length() &&
source_orders[root] > value_order &&
source_orders[root] < @ir.hot_node_count(func) {
ignore(
@ir.hot_region_set(
func,
region_ref,
tail.root_idx,
@ir.hot_build_nop(func),
),
)
@ir.hot_region_insert(func, region_ref, idx, [tail.value_id])
return
}
}
// Binaryen first enlarges structured arms with a trailing nop, then nops the
// original local.set and installs its value in that result slot. Preserve
// that source-owned shape even when doing the rewrite immediately in Hot IR.
ignore(
@ir.hot_region_set(func, region_ref, tail.root_idx, @ir.hot_build_nop(func)),
)
match trailing_nop_idx {
Some(result_idx) =>
ignore(@ir.hot_region_set(func, region_ref, result_idx, tail.value_id))
None => @ir.hot_region_insert(func, region_ref, root_count, [tail.value_id])
}
}
///|
fn simplify_locals_build_one_armed_if_then_body(
func : @ir.HotFunc,
then_ref : @ir.HotRegionRef,
value_id : Int32,
) -> Array[Int32] {
let then_body : Array[Int32] = []
let mut value_already_present = false
for idx = 0; idx < @ir.hot_region_root_count(func, then_ref); idx = idx + 1 {
let root_id = @ir.hot_region_get(func, then_ref, idx)
if root_id < 0 || !@ir.hot_node_is_live(func, root_id) {
continue
}
if root_id == value_id {
value_already_present = true
}
then_body.push(root_id)
}
if !value_already_present {
then_body.push(value_id)
}
then_body
}
///|
fn simplify_locals_subtree_mentions_local(
func : @ir.HotFunc,
node_id : Int32,
local_id : Int32,
) -> 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::LocalGet | @ir.HotOp::LocalSet | @ir.HotOp::LocalTee =>
if node.imm0 == local_id {
return true
}
_ => ()
}
for child_idx = 0
child_idx < @ir.hot_child_count(func, node_id)
child_idx = child_idx + 1 {
if simplify_locals_subtree_mentions_local(
func,
@ir.hot_child_get(func, node_id, child_idx),
local_id,
) {
return true
}
}
false
}
///|
fn simplify_locals_subtree_can_exit_before_fallthrough(
func : @ir.HotFunc,
root_id : Int32,
) -> Bool {
let labels : Array[Int32] = []
fn visit(node_id : Int32) -> 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::Return
| @ir.HotOp::ReturnCall
| @ir.HotOp::ReturnCallIndirect
| @ir.HotOp::ReturnCallRef
| @ir.HotOp::Throw
| @ir.HotOp::ThrowRef
| @ir.HotOp::Rethrow
| @ir.HotOp::Delegate => return true
// A call can transfer to an enclosing exception handler, which can
// observe a local write made before the call.
@ir.HotOp::Call | @ir.HotOp::CallIndirect | @ir.HotOp::CallRef =>
return true
_ => ()
}
match @ir.hot_branch_target(func, node_id) {
Some(label) => if !labels.contains(label) { return true }
None => ()
}
for label in @ir.hot_branch_table_targets_for_node(func, node_id) {
if !labels.contains(label) {
return true
}
}
for label in @ir.hot_continuation_targets(func, node_id) {
if !labels.contains(label) {
return true
}
}
let label = if @ir.hot_node_kind_is_control(func, node_id) {
@ir.hot_control_node_label(func, node_id)
} else {
-1
}
if label >= 0 {
labels.push(label)
}
// Child spans include structured bodies. Internal label exits still
// reach the following write; only exits from this subtree block motion.
for child in @ir.hot_child_span(func, node_id) {
if visit(child) {
return true
}
}
if label >= 0 {
ignore(labels.pop())
}
false
}
visit(root_id)
}
///|
fn simplify_locals_root_allows_moving_local_set_value_after(
func : @ir.HotFunc,
node_id : Int32,
local_id : Int32,
target_label : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return true
}
!simplify_locals_subtree_mentions_local(func, node_id, local_id) &&
!simplify_locals_subtree_has_branch_to_label(func, node_id, target_label) &&
!simplify_locals_subtree_can_exit_before_fallthrough(func, node_id)
}
///|
fn simplify_locals_collect_block_return_candidates(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
target_label : Int32,
get_counts : Array[Int32],
) -> Array[SimplifyLocalsTailSet] {
let candidates : Array[SimplifyLocalsTailSet] = []
let trailing_roots : Array[Int32] = []
let mut idx = @ir.hot_region_root_count(func, region_ref) - 1
while idx >= 0 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id >= 0 && @ir.hot_node_is_live(func, node_id) {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Nop => ()
@ir.HotOp::LocalSet => {
let local_id = node.imm0
let value_id = @ir.hot_child_get(func, node_id, 0)
let mut allowed = local_id >= 0 &&
local_id < get_counts.length() &&
get_counts[local_id] != 0 &&
simplify_locals_value_is_pure(func, value_id) &&
!simplify_locals_subtree_has_branch_to_label(
func, value_id, target_label,
) &&
!simplify_locals_subtree_has_typed_control(func, value_id)
if allowed {
for trailing_id in trailing_roots {
if !simplify_locals_root_allows_moving_local_set_value_after(
func, trailing_id, local_id, target_label,
) {
allowed = false
break
}
}
}
if allowed {
candidates.push({ root_idx: idx, node_id, local_id, value_id, })
}
trailing_roots.push(node_id)
}
_ => trailing_roots.push(node_id)
}
}
idx = idx - 1
}
candidates
}
///|
fn simplify_locals_following_root_local_get_order(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
after_idx : Int32,
) -> Array[Int32] {
let order : Array[Int32] = []
let seen = Array::make(@ir.hot_local_count(func), false)
for idx = after_idx + 1
idx < @ir.hot_region_root_count(func, region_ref)
idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Nop => ()
@ir.HotOp::LocalGet =>
if node.imm0 >= 0 && node.imm0 < seen.length() && !seen[node.imm0] {
seen[node.imm0] = true
order.push(node.imm0)
}
_ => ()
}
}
order
}
///|
fn simplify_locals_find_branch_exit_set_for_local(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
br_idx : Int32,
target_label : Int32,
local_id : Int32,
) -> SimplifyLocalsBranchExit? {
let br_id = @ir.hot_region_get(func, region_ref, br_idx)
let mut idx = br_idx - 1
while idx >= 0 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
idx = idx - 1
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Nop => ()
@ir.HotOp::LocalSet if node.imm0 == local_id => {
let value_id = @ir.hot_child_get(func, node_id, 0)
if !simplify_locals_value_is_pure(func, value_id) ||
simplify_locals_subtree_has_branch_to_label(
func, value_id, target_label,
) ||
simplify_locals_subtree_has_typed_control(func, value_id) {
return None
}
return Some({
region_ref,
set_idx: idx,
set_id: node_id,
br_idx,
br_id,
value_id,
})
}
_ =>
if !simplify_locals_root_allows_moving_local_set_value_after(
func, node_id, local_id, target_label,
) {
return None
}
}
idx = idx - 1
}
None
}
///|
fn simplify_locals_node_is_nonfallthrough_exit(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Unreachable
| @ir.HotOp::Br
| @ir.HotOp::BrTable
| @ir.HotOp::Return
| @ir.HotOp::ReturnCall
| @ir.HotOp::ReturnCallIndirect
| @ir.HotOp::ReturnCallRef
| @ir.HotOp::Throw
| @ir.HotOp::Rethrow
| @ir.HotOp::ThrowRef
| @ir.HotOp::Delegate => true
_ => false
}
}
///|
fn simplify_locals_region_is_unreachableish(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> Bool {
match simplify_locals_region_last_live_non_nop_root_idx(func, region_ref) {
Some(root_idx) =>
simplify_locals_node_is_nonfallthrough_exit(
func,
@ir.hot_region_get(func, region_ref, root_idx),
)
None => false
}
}
///|
fn simplify_locals_control_has_typed_branch_payload(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
@ir.hot_label_branch_arity(func, @ir.hot_control_node_label(func, node_id)) !=
0
}
///|
fn SimplifyLocalsStackedForwarderTail::new(
payload_if : Int32,
rewrite_region : @ir.HotRegionRef,
rewrite_idx : Int32,
) -> SimplifyLocalsStackedForwarderTail {
{ payload_if, rewrite_region, rewrite_idx, }
}
///|
fn simplify_locals_branch_payload_count(
func : @ir.HotFunc,
node_id : Int32,
) -> Int32 {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Br | @ir.HotOp::Delegate => node.child_count
@ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail =>
if node.child_count == 0 {
0
} else {
node.child_count - 1
}
_ => 0
}
}
///|
fn simplify_locals_build_i32_eqz(func : @ir.HotFunc, operand : Int32) -> Int32 {
@ir.hot_build_select(
func,
@lib.ValType::i32(),
@ir.hot_build_const_i32(func, @lib.I32(0)),
@ir.hot_build_const_i32(func, @lib.I32(1)),
operand,
)
}
///|
fn simplify_locals_try_extract_stacked_forwarder_tail(
func : @ir.HotFunc,
candidate_node : Int32,
rewrite_region : @ir.HotRegionRef,
rewrite_idx : Int32,
) -> SimplifyLocalsStackedForwarderTail? {
if candidate_node < 0 ||
!@ir.hot_node_is_live(func, candidate_node) ||
@ir.hot_node_result_arity(func, candidate_node) == 0 {
return None
}
match @ir.hot_node_get(func, candidate_node).op {
@ir.HotOp::If =>
Some(
SimplifyLocalsStackedForwarderTail::new(
candidate_node, rewrite_region, rewrite_idx,
),
)
@ir.HotOp::Block => {
let wrapper_body = @ir.hot_region_body_ref(func, candidate_node)
let wrapper_root_count = @ir.hot_region_root_count(func, wrapper_body)
if wrapper_root_count == 0 || wrapper_root_count > 2 {
return None
}
let wrapper_prefix = @ir.hot_region_get(func, wrapper_body, 0)
if wrapper_prefix < 0 || !@ir.hot_node_is_live(func, wrapper_prefix) {
return None
}
match @ir.hot_node_get(func, wrapper_prefix).op {
@ir.HotOp::Block => ()
_ => return None
}
let wrapper_prefix_body = @ir.hot_region_body_ref(func, wrapper_prefix)
let wrapper_prefix_root_count = @ir.hot_region_root_count(
func, wrapper_prefix_body,
)
if wrapper_prefix_root_count < 2 {
return None
}
let wrapper_br_idx = wrapper_prefix_root_count - 1
let wrapper_br = @ir.hot_region_get(
func, wrapper_prefix_body, wrapper_br_idx,
)
if wrapper_br < 0 ||
!@ir.hot_node_is_live(func, wrapper_br) ||
@ir.hot_branch_target(func, wrapper_br) !=
Some(@ir.hot_control_node_label(func, candidate_node)) ||
simplify_locals_branch_payload_count(func, wrapper_br) != 0 {
return None
}
match @ir.hot_node_get(func, wrapper_br).op {
@ir.HotOp::Br => ()
_ => return None
}
simplify_locals_try_extract_stacked_forwarder_tail(
func,
@ir.hot_region_get(func, wrapper_prefix_body, wrapper_br_idx - 1),
wrapper_prefix_body,
wrapper_br_idx,
)
}
_ => None
}
}
///|
fn simplify_locals_try_rewrite_split_local_set_wrapper_forwarder(
func : @ir.HotFunc,
node_id : Int32,
detached : Array[Int32],
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => ()
_ => return false
}
if @ir.hot_child_count(func, node_id) == 0 {
return false
}
let value_id = @ir.hot_child_get(func, node_id, 0)
if value_id < 0 ||
!@ir.hot_node_is_live(func, value_id) ||
@ir.hot_node_result_arity(func, value_id) == 0 {
return false
}
match @ir.hot_node_get(func, value_id).op {
@ir.HotOp::Block => ()
_ => return false
}
let outer_body = @ir.hot_region_body_ref(func, value_id)
let outer_root_count = @ir.hot_region_root_count(func, outer_body)
if outer_root_count == 0 {
return false
}
let outer_wrapper = @ir.hot_region_get(func, outer_body, 0)
if outer_wrapper < 0 || !@ir.hot_node_is_live(func, outer_wrapper) {
return false
}
match @ir.hot_node_get(func, outer_wrapper).op {
@ir.HotOp::Block => ()
_ => return false
}
let outer_wrapper_body = @ir.hot_region_body_ref(func, outer_wrapper)
let outer_wrapper_root_count = @ir.hot_region_root_count(
func, outer_wrapper_body,
)
if outer_wrapper_root_count == 0 {
return false
}
let mut payload_block = -1
let mut outer_br = -1
if outer_wrapper_root_count == 1 {
outer_br = @ir.hot_region_get(func, outer_wrapper_body, 0)
if outer_br < 0 || !@ir.hot_node_is_live(func, outer_br) {
return false
}
match @ir.hot_node_get(func, outer_br).op {
@ir.HotOp::Br => ()
_ => return false
}
if @ir.hot_branch_target(func, outer_br) !=
Some(@ir.hot_control_node_label(func, value_id)) ||
simplify_locals_branch_payload_count(func, outer_br) != 1 {
return false
}
payload_block = @ir.hot_child_get(func, outer_br, 0)
} else {
payload_block = @ir.hot_region_get(
func,
outer_wrapper_body,
outer_wrapper_root_count - 2,
)
outer_br = @ir.hot_region_get(
func,
outer_wrapper_body,
outer_wrapper_root_count - 1,
)
if payload_block < 0 ||
outer_br < 0 ||
!@ir.hot_node_is_live(func, payload_block) ||
!@ir.hot_node_is_live(func, outer_br) ||
@ir.hot_branch_target(func, outer_br) !=
Some(@ir.hot_control_node_label(func, value_id)) ||
simplify_locals_branch_payload_count(func, outer_br) != 0 {
return false
}
match @ir.hot_node_get(func, outer_br).op {
@ir.HotOp::Br => ()
_ => return false
}
}
if payload_block < 0 ||
!@ir.hot_node_is_live(func, payload_block) ||
@ir.hot_node_result_arity(func, payload_block) == 0 {
return false
}
match @ir.hot_node_get(func, payload_block).op {
@ir.HotOp::Block => ()
_ => return false
}
let payload_body = @ir.hot_region_body_ref(func, payload_block)
let payload_root_count = @ir.hot_region_root_count(func, payload_body)
if payload_root_count == 0 || payload_root_count > 2 {
return false
}
let payload_forwarder = @ir.hot_region_get(func, payload_body, 0)
if payload_forwarder < 0 || !@ir.hot_node_is_live(func, payload_forwarder) {
return false
}
match @ir.hot_node_get(func, payload_forwarder).op {
@ir.HotOp::Block => ()
_ => return false
}
let payload_forwarder_body = @ir.hot_region_body_ref(func, payload_forwarder)
let payload_forwarder_root_count = @ir.hot_region_root_count(
func, payload_forwarder_body,
)
if payload_forwarder_root_count == 1 {
let direct_br = @ir.hot_region_get(func, payload_forwarder_body, 0)
if direct_br >= 0 && @ir.hot_node_is_live(func, direct_br) {
match @ir.hot_node_get(func, direct_br).op {
@ir.HotOp::Br =>
if simplify_locals_branch_payload_count(func, direct_br) == 1 {
let direct_if = @ir.hot_child_get(func, direct_br, 0)
if direct_if >= 0 &&
@ir.hot_node_is_live(func, direct_if) &&
@ir.hot_node_result_arity(func, direct_if) == 1 {
match @ir.hot_node_get(func, direct_if).op {
@ir.HotOp::If => {
let then_region = @ir.hot_region_then_ref(func, direct_if)
let else_region = match
@ir.hot_region_else_ref(func, direct_if) {
Some(else_region) => else_region
None => return false
}
let if_label = @ir.hot_control_node_label(func, direct_if)
let direct_target = match
@ir.hot_branch_target(func, direct_br) {
Some(target) => target
None => return false
}
let try_build = fn(
branch_ref : @ir.HotRegionRef,
body_ref : @ir.HotRegionRef,
negate_condition : Bool,
) -> Bool {
if @ir.hot_region_root_count(func, branch_ref) != 1 ||
@ir.hot_region_root_count(func, body_ref) == 0 {
return false
}
let branch_root = @ir.hot_region_get(func, branch_ref, 0)
if branch_root < 0 ||
!@ir.hot_node_is_live(func, branch_root) {
return false
}
let branch_node = @ir.hot_node_get(func, branch_root)
match branch_node.op {
@ir.HotOp::Br if branch_node.child_count == 0 => ()
_ => return false
}
if branch_node.imm0 == if_label {
return false
}
let body_tail_idx = @ir.hot_region_root_count(
func, body_ref,
) -
1
let body_payload = @ir.hot_region_get(
func, body_ref, body_tail_idx,
)
if body_payload < 0 ||
!@ir.hot_node_is_live(func, body_payload) ||
@ir.hot_node_result_arity(func, body_payload) != 1 {
return false
}
let body_prefix : Array[Int32] = []
for root_idx = 0
root_idx < body_tail_idx
root_idx = root_idx + 1 {
let root_id = @ir.hot_region_get(func, body_ref, root_idx)
if @ir.cfg_terminator_edge_kinds(
@ir.hot_node_get(func, root_id).op,
).length() >
0 {
return false
}
body_prefix.push(root_id)
}
let guard_condition = if negate_condition {
simplify_locals_build_i32_eqz(
func,
@ir.hot_child_get(func, direct_if, 0),
)
} else {
@ir.hot_child_get(func, direct_if, 0)
}
let replacement = [
@ir.hot_build_br_if(
func,
branch_node.imm0,
guard_condition,
),
] +
body_prefix +
[
@ir.hot_build_br(func, direct_target, values=[
body_payload,
]),
@ir.hot_build_unreachable(func),
]
for
removed_id in @ir.hot_region_splice(
func, payload_forwarder_body, 0, 1, replacement,
) {
detached.push(removed_id)
}
return true
}
if try_build(else_region, then_region, true) ||
try_build(then_region, else_region, false) {
return true
}
}
_ => ()
}
}
}
_ => ()
}
}
}
if payload_forwarder_root_count < 2 {
return false
}
let match_ = match
simplify_locals_try_extract_stacked_forwarder_tail(
func,
@ir.hot_region_get(
func,
payload_forwarder_body,
payload_forwarder_root_count - 2,
),
payload_forwarder_body,
payload_forwarder_root_count - 1,
) {
Some(match_) => match_
None => return false
}
let inner_br = @ir.hot_region_get(
func,
match_.rewrite_region,
match_.rewrite_idx,
)
if inner_br < 0 ||
!@ir.hot_node_is_live(func, inner_br) ||
@ir.hot_branch_target(func, inner_br) !=
Some(@ir.hot_control_node_label(func, payload_block)) {
return false
}
match @ir.hot_node_get(func, inner_br).op {
@ir.HotOp::Br => ()
_ => return false
}
let node_roots : Array[Int32] = []
for root_idx = 0; root_idx < payload_root_count; root_idx = root_idx + 1 {
node_roots.push(@ir.hot_region_get(func, payload_body, root_idx))
}
for root_idx = 1; root_idx < outer_root_count; root_idx = root_idx + 1 {
node_roots.push(@ir.hot_region_get(func, outer_body, root_idx))
}
@ir.hot_control_result_type_set(
func,
match_.payload_if,
@ir.hot_type_void(func),
)
let outer_br_replacement = @ir.hot_build_br(
func,
@ir.hot_control_node_label(func, payload_block),
)
@ir.hot_replace_node(
func,
outer_br,
@ir.hot_node_get(func, outer_br_replacement),
)
let inner_br_replacement = @ir.hot_build_br(
func,
@ir.hot_control_node_label(func, value_id),
)
@ir.hot_replace_node(
func,
inner_br,
@ir.hot_node_get(func, inner_br_replacement),
)
let wrapper_roots : Array[Int32] = []
let wrapper_prefix_count = if outer_wrapper_root_count == 1 {
0
} else {
outer_wrapper_root_count - 2
}
for root_idx = 0; root_idx < wrapper_prefix_count; root_idx = root_idx + 1 {
wrapper_roots.push(@ir.hot_region_get(func, outer_wrapper_body, root_idx))
}
wrapper_roots.push(value_id)
wrapper_roots.push(outer_br)
@ir.hot_region_replace_body(func, outer_wrapper_body, wrapper_roots)
@ir.hot_region_replace_body(
func,
@ir.hot_region_body_ref(func, value_id),
node_roots,
)
@ir.hot_region_replace_body(
func,
@ir.hot_region_body_ref(func, payload_block),
[outer_wrapper],
)
@ir.hot_child_set(func, node_id, 0, payload_block)
for
removed_id in @ir.hot_region_splice(
func,
match_.rewrite_region,
match_.rewrite_idx,
1,
[@ir.hot_build_unreachable(func)],
) {
detached.push(removed_id)
}
true
}
///|
fn simplify_locals_node_targets_label(
func : @ir.HotFunc,
node_id : Int32,
label_id : Int32,
) -> 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::Continuation =>
@ir.hot_continuation_targets(func, node_id).contains(label_id)
@ir.HotOp::BrTable =>
if @ir.hot_branch_table_default_target(func, node.imm0) == label_id {
true
} else {
let targets = @ir.hot_branch_table_targets_for_node(func, node_id)
for target in targets {
if target == label_id {
return true
}
}
false
}
_ => @ir.hot_branch_target(func, node_id) == Some(label_id)
}
}
///|
fn simplify_locals_is_discardable_default_gc_allocation(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::StructNewDefault(_)) => true
_ => false
}
}
///|
fn simplify_locals_value_can_clone_node(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return true
}
// A local consumes one lane of a shared multi-result producer. Copying
// that producer into the local's read repeats the entire operation and
// loses the lane selected by the original stack consumption.
if @ir.hot_node_result_arity(func, node_id) != 1 {
return false
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Load => false
_ => !@ir.hot_node_kind_is_control(func, node_id)
}
}
///|
fn simplify_locals_subtree_reads_only_params(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return true
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet => return @ir.hot_local_is_param(func, node.imm0)
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => return false
_ => ()
}
if @ir.hot_node_kind_is_control(func, node_id) {
return false
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if simplify_locals_is_region_child_slot(func, node_id, slot) {
return false
}
if !simplify_locals_subtree_reads_only_params(
func,
@ir.hot_child_get(func, node_id, slot),
) {
return false
}
}
true
}
///|
fn simplify_locals_value_can_move_one_use(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if simplify_locals_value_can_clone_node(func, node_id) {
return true
}
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return true
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Load =>
@ir.hot_node_result_arity(func, node_id) == 1 &&
@ir.hot_child_count(func, node_id) == 1 &&
simplify_locals_subtree_reads_only_params(
func,
@ir.hot_child_get(func, node_id, 0),
)
@ir.HotOp::If => {
if @ir.hot_node_result_arity(func, node_id) != 1 {
return false
}
if @ir.hot_child_count(func, node_id) <= 0 {
return false
}
let cond_id = @ir.hot_child_get(func, node_id, 0)
cond_id < 0 ||
!@ir.hot_node_is_live(func, cond_id) ||
(
!@ir.hot_node_kind_is_control(func, cond_id) &&
@ir.hot_node_result_arity(func, cond_id) == 1
)
}
_ => false
}
}
///|
fn simplify_locals_value_can_tee(func : @ir.HotFunc, node_id : Int32) -> Bool {
node_id < 0 ||
!@ir.hot_node_is_live(func, node_id) ||
@ir.hot_node_result_arity(func, node_id) == 1
}
///|
fn simplify_locals_try_consume_following_local_get(
func : @ir.HotFunc,
parent_region : @ir.HotRegionRef,
root_idx : Int32,
local_id : Int32,
detached : Array[Int32],
) -> Bool {
let next_idx = match
simplify_locals_region_next_live_non_nop_root_idx(
func, parent_region, root_idx,
) {
Some(next_idx) => next_idx
None => return false
}
let next_id = @ir.hot_region_get(func, parent_region, next_idx)
if next_id < 0 || !@ir.hot_node_is_live(func, next_id) {
return false
}
let next = @ir.hot_node_get(func, next_id)
match next.op {
@ir.HotOp::LocalGet if next.imm0 == local_id => {
let removed = @ir.hot_region_splice(func, parent_region, next_idx, 1, [])
for removed_id in removed {
detached.push(removed_id)
}
true
}
_ => false
}
}
///|
fn simplify_locals_inline_input_count(
func : @ir.HotFunc,
node_id : Int32,
) -> Int32 {
// A single syntactic local.get in a loop can execute many times and see
// writes from the backedge. Region roots are not expression operands.
// Leave them to the region scanner, which manages reaching definitions
// and effect barriers; only inline into inputs evaluated before entry.
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Block | @ir.HotOp::Try | @ir.HotOp::TryTable => 0
@ir.HotOp::Loop =>
@ir.hot_label_branch_arity(
func,
@ir.hot_control_node_label(func, node_id),
)
@ir.HotOp::If => 1
_ => @ir.hot_child_count(func, node_id)
}
}
///|
fn simplify_locals_try_inline_local_get_child(
func : @ir.HotFunc,
node_id : Int32,
local_id : Int32,
replacement_id : Int32,
allow_nesting : Bool,
detached : Array[Int32],
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
let node = @ir.hot_node_get(func, node_id)
let replacement_is_copy = match
simplify_locals_direct_local_get_value(func, replacement_id) {
Some(_) => true
None => false
}
let parent_is_local_set = match node.op {
@ir.HotOp::LocalSet => true
_ => false
}
let input_count = simplify_locals_inline_input_count(func, node_id)
for slot = 0; slot < input_count; 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 = @ir.hot_node_get(func, child_id)
match child.op {
@ir.HotOp::LocalGet if child.imm0 == local_id => {
if !allow_nesting && !replacement_is_copy && !parent_is_local_set {
return false
}
@ir.hot_child_set(func, node_id, slot, replacement_id)
detached.push(child_id)
return true
}
_ =>
if simplify_locals_try_inline_local_get_child(
func, child_id, local_id, replacement_id, allow_nesting, detached,
) {
return true
}
}
}
false
}
///|
fn simplify_locals_try_inline_leading_local_get_child(
func : @ir.HotFunc,
node_id : Int32,
local_id : Int32,
replacement_id : Int32,
allow_nesting : Bool,
detached : Array[Int32],
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
let node = @ir.hot_node_get(func, node_id)
let replacement_is_copy = match
simplify_locals_direct_local_get_value(func, replacement_id) {
Some(_) => true
None => false
}
let parent_is_local_set = match node.op {
@ir.HotOp::LocalSet => true
_ => false
}
let input_count = simplify_locals_inline_input_count(func, node_id)
for slot = 0; slot < input_count; 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 = @ir.hot_node_get(func, child_id)
match child.op {
@ir.HotOp::Nop => continue
@ir.HotOp::LocalGet if child.imm0 == local_id => {
if !allow_nesting && !replacement_is_copy && !parent_is_local_set {
return false
}
@ir.hot_child_set(func, node_id, slot, replacement_id)
detached.push(child_id)
return true
}
_ =>
return simplify_locals_try_inline_leading_local_get_child(
func, child_id, local_id, replacement_id, allow_nesting, detached,
)
}
}
false
}
///|
fn simplify_locals_try_move_control_to_leading_local_get_child(
func : @ir.HotFunc,
node_id : Int32,
local_id : Int32,
replacement_id : Int32,
detached : Array[Int32],
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
let input_count = simplify_locals_inline_input_count(func, node_id)
for slot = 0; slot < input_count; 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 = @ir.hot_node_get(func, child_id)
match child.op {
@ir.HotOp::Nop => continue
@ir.HotOp::LocalGet if child.imm0 == local_id => {
let label_id = @ir.hot_control_node_label(func, replacement_id)
@ir.hot_replace_node(
func,
child_id,
@ir.hot_node_get(func, replacement_id),
)
@ir.hot_label_owner_set(func, label_id, child_id)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func, replacement_id, detached,
)
return true
}
_ =>
return simplify_locals_try_move_control_to_leading_local_get_child(
func, child_id, local_id, replacement_id, detached,
)
}
}
false
}
///|
fn simplify_locals_try_inline_following_local_get(
func : @ir.HotFunc,
parent_region : @ir.HotRegionRef,
root_idx : Int32,
local_id : Int32,
replacement_id : Int32,
allow_nesting : Bool,
detached : Array[Int32],
) -> Bool {
if replacement_id < 0 ||
!@ir.hot_node_is_live(func, replacement_id) ||
@ir.hot_node_result_arity(func, replacement_id) != 1 {
return false
}
let next_idx = match
simplify_locals_region_next_live_non_nop_root_idx(
func, parent_region, root_idx,
) {
Some(next_idx) => next_idx
None => return false
}
let next_id = @ir.hot_region_get(func, parent_region, next_idx)
let replacement_is_control = replacement_id >= 0 &&
@ir.hot_node_is_live(func, replacement_id) &&
@ir.hot_node_kind_is_control(func, replacement_id)
if replacement_is_control &&
next_id >= 0 &&
@ir.hot_node_is_live(func, next_id) {
let next = @ir.hot_node_get(func, next_id)
match next.op {
@ir.HotOp::LocalGet if next.imm0 == local_id => {
let label_id = @ir.hot_control_node_label(func, replacement_id)
@ir.hot_replace_node(
func,
next_id,
@ir.hot_node_get(func, replacement_id),
)
@ir.hot_label_owner_set(func, label_id, next_id)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func, replacement_id, detached,
)
return true
}
_ =>
return simplify_locals_try_move_control_to_leading_local_get_child(
func, next_id, local_id, replacement_id, detached,
)
}
}
if !replacement_is_control &&
next_id >= 0 &&
@ir.hot_node_is_live(func, next_id) &&
replacement_id >= 0 &&
@ir.hot_node_is_live(func, replacement_id) &&
@ir.hot_node_result_arity(func, replacement_id) == 1 {
let next = @ir.hot_node_get(func, next_id)
match next.op {
@ir.HotOp::LocalGet if next.imm0 == local_id => {
if !allow_nesting &&
simplify_locals_direct_local_get_value(func, replacement_id) == None {
return false
}
let removed = @ir.hot_region_splice(func, parent_region, next_idx, 1, [
replacement_id,
])
for removed_id in removed {
detached.push(removed_id)
}
return true
}
_ => ()
}
}
if simplify_locals_value_is_child_inline_safe(func, replacement_id) {
return simplify_locals_try_inline_local_get_child(
func, next_id, local_id, replacement_id, allow_nesting, detached,
)
}
let replacement_is_copy = match
simplify_locals_direct_local_get_value(func, replacement_id) {
Some(_) => true
None => false
}
if next_id >= 0 && @ir.hot_node_is_live(func, next_id) {
let next = @ir.hot_node_get(func, next_id)
match next.op {
@ir.HotOp::LocalGet if next.imm0 == local_id &&
replacement_id >= 0 &&
@ir.hot_node_is_live(func, replacement_id) &&
@ir.hot_node_result_arity(func, replacement_id) == 1 => {
if !allow_nesting && !replacement_is_copy {
return false
}
@ir.hot_replace_node(
func,
next_id,
@ir.hot_node_get(func, replacement_id),
)
detached.push(replacement_id)
return true
}
_ => ()
}
}
simplify_locals_try_inline_leading_local_get_child(
func, next_id, local_id, replacement_id, allow_nesting, detached,
)
}
///|
fn simplify_locals_count_local_gets(func : @ir.HotFunc) -> Array[Int32] {
let counts = Array::make(@ir.hot_local_count(func), 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
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet =>
if node.imm0 >= 0 && node.imm0 < counts.length() {
counts[node.imm0] = counts[node.imm0] + 1
}
_ => ()
}
}
counts
}
///|
fn simplify_locals_discount_subtree_local_gets(
func : @ir.HotFunc,
node_id : Int32,
counts : Array[Int32],
) -> Unit {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet =>
if node.imm0 >= 0 && node.imm0 < counts.length() && counts[node.imm0] > 0 {
counts[node.imm0] = counts[node.imm0] - 1
}
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
simplify_locals_discount_subtree_local_gets(
func,
@ir.hot_child_get(func, node_id, slot),
counts,
)
}
}
///|
fn simplify_locals_discount_unreachable_root_tail_gets(
func : @ir.HotFunc,
counts : Array[Int32],
) -> Unit {
let root = @ir.HotRegionRef::root()
let mut terminated = false
for idx = 0; idx < @ir.hot_region_root_count(func, root); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, root, idx)
if terminated {
simplify_locals_discount_subtree_local_gets(func, node_id, counts)
} else if simplify_locals_node_is_nonfallthrough_exit(func, node_id) {
terminated = true
}
}
}
///|
fn simplify_locals_value_is_pure(func : @ir.HotFunc, node_id : Int32) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return true
}
let node = @ir.hot_node_get(func, node_id)
if (
simplify_locals_effects_mask_for_exact_node(func, node_id) &
(
@ir.EFFECT_MASK_TRAP |
@ir.EFFECT_MASK_MEMORY_WRITE |
@ir.EFFECT_MASK_TABLE_WRITE
)
) !=
0U {
return false
}
let op_is_pure = match node.op {
@ir.HotOp::Nop
| @ir.HotOp::Const
| @ir.HotOp::LocalGet
| @ir.HotOp::GlobalGet
| @ir.HotOp::Drop
| @ir.HotOp::Select
| @ir.HotOp::RefNull
| @ir.HotOp::RefIsNull
| @ir.HotOp::RefFunc
| @ir.HotOp::RefTest
| @ir.HotOp::RefTestDesc
| @ir.HotOp::Unary
| @ir.HotOp::Binary
| @ir.HotOp::Compare
| @ir.HotOp::TupleMake
| @ir.HotOp::TupleExtract
| @ir.HotOp::StringOp
| @ir.HotOp::MemorySize
| @ir.HotOp::TableSize
| @ir.HotOp::Simd => true
_ => false
}
if !op_is_pure {
return false
}
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) &&
!simplify_locals_value_is_pure(func, child_id) {
return false
}
}
true
}
///|
fn simplify_locals_value_is_child_inline_safe(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return true
}
if simplify_locals_effects_has(
simplify_locals_effects_mask_for_subtree(func, node_id),
@ir.EFFECT_MASK_GLOBAL_STATE,
) {
return false
}
if simplify_locals_value_is_pure(func, node_id) {
return true
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Block => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
for idx = 0
idx < @ir.hot_region_root_count(func, body_ref)
idx = idx + 1 {
if !simplify_locals_value_is_child_inline_safe(
func,
@ir.hot_region_get(func, body_ref, idx),
) {
return false
}
}
true
}
@ir.HotOp::If => {
if !simplify_locals_value_is_child_inline_safe(
func,
@ir.hot_child_get(func, node_id, 0),
) {
return false
}
let then_ref = @ir.hot_region_then_ref(func, node_id)
for idx = 0
idx < @ir.hot_region_root_count(func, then_ref)
idx = idx + 1 {
if !simplify_locals_value_is_child_inline_safe(
func,
@ir.hot_region_get(func, then_ref, idx),
) {
return false
}
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
for idx = 0
idx < @ir.hot_region_root_count(func, else_ref)
idx = idx + 1 {
if !simplify_locals_value_is_child_inline_safe(
func,
@ir.hot_region_get(func, else_ref, idx),
) {
return false
}
}
None => ()
}
true
}
_ => false
}
}
///|
fn simplify_locals_mark_reachable(
func : @ir.HotFunc,
node_id : Int32,
reachable : Array[Bool],
) -> Unit {
if node_id < 0 ||
node_id >= reachable.length() ||
!@ir.hot_node_is_live(func, node_id) ||
reachable[node_id] {
return
}
reachable[node_id] = true
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
simplify_locals_mark_reachable(
func,
@ir.hot_child_get(func, node_id, slot),
reachable,
)
}
}
///|
fn simplify_locals_build_reachable_nodes(func : @ir.HotFunc) -> Array[Bool] {
let reachable = Array::make(@ir.hot_node_count(func), false)
for root_idx = 0; root_idx < @ir.hot_root_count(func); root_idx = root_idx + 1 {
simplify_locals_mark_reachable(
func,
@ir.hot_root_get(func, root_idx),
reachable,
)
}
reachable
}
///|
fn simplify_locals_build_label_owner_nodes(func : @ir.HotFunc) -> Array[Bool] {
let label_owners = Array::make(@ir.hot_node_count(func), false)
for label_id = 0; label_id < func.labels.length(); label_id = label_id + 1 {
let owner = @ir.hot_label_owner(func, label_id)
if owner >= 0 &&
owner < label_owners.length() &&
@ir.hot_node_is_live(func, owner) {
label_owners[owner] = true
}
}
label_owners
}
///|
priv struct SimplifyLocalsDetachedControlOwner {
replacement_id : Int32
detached_id : Int32
}
///|
fn simplify_locals_rehome_detached_control_owners(
func : @ir.HotFunc,
owners : Array[SimplifyLocalsDetachedControlOwner],
) -> Unit {
for owner in owners {
if owner.replacement_id < 0 ||
!@ir.hot_node_is_live(func, owner.replacement_id) {
continue
}
if owner.detached_id < 0 || !@ir.hot_node_is_live(func, owner.detached_id) {
continue
}
let detached = @ir.hot_node_get(func, owner.detached_id)
let label_id = match detached.op {
@ir.HotOp::Block
| @ir.HotOp::Loop
| @ir.HotOp::If
| @ir.HotOp::Try
| @ir.HotOp::TryTable =>
@ir.hot_control_node_label(func, owner.detached_id)
_ => continue
}
@ir.hot_label_owner_set(func, label_id, owner.replacement_id)
}
}
///|
fn simplify_locals_queue_rehomed_wrapper_for_deletion(
func : @ir.HotFunc,
wrapper_id : Int32,
detached : Array[Int32],
) -> Unit {
if wrapper_id < 0 || !@ir.hot_node_is_live(func, wrapper_id) {
return
}
// The wrapper's value has already been installed at a new owner. Sever the
// old child edge before recursive detached-node deletion so that deleting the
// local.set/local.tee cannot also delete the rehomed value subtree.
@ir.hot_replace_node(func, wrapper_id, simplify_locals_make_nop(func))
detached.push(wrapper_id)
}
///|
fn simplify_locals_delete_detached_nodes(
func : @ir.HotFunc,
node_ids : Array[Int32],
) -> Unit {
if node_ids.is_empty() {
return
}
let reachable = simplify_locals_build_reachable_nodes(func)
let label_owners = simplify_locals_build_label_owner_nodes(func)
let seen = Array::make(@ir.hot_node_count(func), false)
for node_id in node_ids {
if node_id < 0 || node_id >= reachable.length() || seen[node_id] {
continue
}
seen[node_id] = true
if !@ir.hot_node_is_live(func, node_id) ||
reachable[node_id] ||
label_owners[node_id] {
continue
}
@ir.hot_delete_detached_node(func, node_id)
}
}
///|
fn simplify_locals_record_local_set(
func : @ir.HotFunc,
node_id : Int32,
node : @ir.HotNode,
sinkables : SimplifyLocalsSinkables,
first_cycle : Bool,
allow_sink_tee : Bool,
get_counts : Array[Int32],
deferred_sets : Array[Int32],
) -> Bool {
let local_id = node.imm0
let mut changed = false
match simplify_locals_sinkables_get(sinkables, local_id) {
Some(previous_info) =>
// A lifted block can revisit its existing pending writes. Replacing a
// write with a drop at that point removes the current definition too.
if previous_info.node_id != node_id &&
@ir.hot_node_is_live(func, previous_info.node_id) {
let previous_node = @ir.hot_node_get(func, previous_info.node_id)
match previous_node.op {
@ir.HotOp::LocalSet => {
let previous_value_id = @ir.hot_child_get(
func,
previous_info.node_id,
0,
)
// A later root can consume a read evaluated between these writes.
// Keep its reaching definition even though tree order has not yet
// visited the consumer containing that stacked read.
if !simplify_locals_has_earlier_source_read(
func,
sinkables,
local_id,
previous_node.order,
node.order,
) {
@ir.hot_replace_node(
func,
previous_info.node_id,
simplify_locals_make_drop(func, previous_value_id),
)
changed = true
}
}
_ => ()
}
}
None => ()
}
simplify_locals_sinkables_clear_local(sinkables, local_id)
if (first_cycle || !allow_sink_tee) && get_counts[local_id] > 1 {
deferred_sets.push(node_id)
return changed
}
simplify_locals_sinkables_set(sinkables, local_id, {
node_id,
effects: simplify_locals_effects_for_pending_local_set(
func, node_id, node, sinkables,
),
})
changed
}
///|
fn simplify_locals_source_order_inputs(
func : @ir.HotFunc,
node_id : Int32,
) -> Array[Int32] {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Block
| @ir.HotOp::Try
| @ir.HotOp::TryTable
| @ir.HotOp::Nop
| @ir.HotOp::Unreachable
| @ir.HotOp::Const
| @ir.HotOp::RefNull
| @ir.HotOp::LocalGet
| @ir.HotOp::GlobalGet
| @ir.HotOp::MemorySize
| @ir.HotOp::TableSize
| @ir.HotOp::DataDrop
| @ir.HotOp::ElemDrop
| @ir.HotOp::RefFunc => []
@ir.HotOp::Loop => {
let out : Array[Int32] = []
let input_count = @ir.hot_label_branch_arity(
func,
@ir.hot_control_node_label(func, node_id),
)
for idx = 0; idx < input_count; idx = idx + 1 {
out.push(@ir.hot_child_get(func, node_id, idx))
}
out
}
@ir.HotOp::If => [@ir.hot_child_get(func, node_id, 0)]
_ => {
let out : Array[Int32] = []
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 {
out.push(child_id)
}
}
out
}
}
}
///|
fn simplify_locals_collect_preceding_call_dependency(
func : @ir.HotFunc,
node_id : Int32,
before_node_id : Int32,
seen : Array[Bool],
out : Array[Int32],
) -> Unit {
if node_id < 0 ||
node_id >= seen.length() ||
seen[node_id] ||
!@ir.hot_node_is_live(func, node_id) {
return
}
seen[node_id] = true
if node_id < before_node_id &&
@ir.hot_node_result_arity(func, node_id) > 0 &&
!@ir.effects_is_pure(@ir.effects_for_node(func, node_id)) {
out.push(node_id)
return
}
for input_id in simplify_locals_source_order_inputs(func, node_id) {
simplify_locals_collect_preceding_call_dependency(
func, input_id, before_node_id, seen, out,
)
}
}
///|
fn simplify_locals_source_input_min_node_id(
func : @ir.HotFunc,
node_id : Int32,
memo : Array[Int32],
sentinel : Int32,
) -> Int32 {
if node_id < 0 ||
node_id >= memo.length() ||
!@ir.hot_node_is_live(func, node_id) {
return sentinel
}
if memo[node_id] >= 0 {
return memo[node_id]
}
let mut min_id = node_id
// Store a conservative bound before descending in case the graph shares a
// node through more than one input path.
memo[node_id] = node_id
for input_id in simplify_locals_source_order_inputs(func, node_id) {
let candidate = simplify_locals_source_input_min_node_id(
func, input_id, memo, sentinel,
)
if candidate < min_id {
min_id = candidate
}
}
memo[node_id] = min_id
min_id
}
///|
fn simplify_locals_future_input_suffix_min_node_ids(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> Array[Int32] {
let root_count = @ir.hot_region_root_count(func, region_ref)
let sentinel = @ir.hot_node_count(func)
let memo = Array::make(sentinel, -1)
let suffix_mins = Array::make(root_count + 1, sentinel)
let mut idx = root_count
while idx > 0 {
idx -= 1
let root_id = @ir.hot_region_get(func, region_ref, idx)
let mut min_id = suffix_mins[idx + 1]
if root_id >= 0 && @ir.hot_node_is_live(func, root_id) {
for input_id in simplify_locals_source_order_inputs(func, root_id) {
let candidate = simplify_locals_source_input_min_node_id(
func, input_id, memo, sentinel,
)
if candidate < min_id {
min_id = candidate
}
}
}
suffix_mins[idx] = min_id
}
suffix_mins
}
///|
fn simplify_locals_preceding_future_call_dependencies(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
root_idx : Int32,
current_root : Int32,
future_input_suffix_mins~ : Array[Int32]? = None,
future_roots_scanned~ : Array[Int32]? = None,
) -> Array[Int32] {
match @ir.hot_node_get(func, current_root).op {
@ir.HotOp::Call | @ir.HotOp::CallIndirect | @ir.HotOp::CallRef => ()
_ => return []
}
if @ir.hot_node_result_arity(func, current_root) != 0 {
return []
}
match future_input_suffix_mins {
Some(mins) if root_idx + 1 < mins.length() &&
mins[root_idx + 1] >= current_root => return []
_ => ()
}
let seen = Array::make(@ir.hot_node_count(func), false)
let out : Array[Int32] = []
for idx = root_idx + 1
idx < @ir.hot_region_root_count(func, region_ref)
idx = idx + 1 {
match future_roots_scanned {
Some(scanned) => scanned[0] += 1
None => ()
}
let future_root = @ir.hot_region_get(func, region_ref, idx)
for input_id in simplify_locals_source_order_inputs(func, future_root) {
simplify_locals_collect_preceding_call_dependency(
func, input_id, current_root, seen, out,
)
}
}
out.sort_by(fn(left, right) { left.compare(right) })
out
}
///|
fn simplify_locals_scan_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
sinkables : SimplifyLocalsSinkables,
first_cycle : Bool,
get_counts : Array[Int32],
detached : Array[Int32],
policy : SimplifyLocalsPolicy,
protect_control_embedded_local_tee : Bool,
inside_control : Bool,
deferred_sets : Array[Int32],
future_roots_scanned~ : Array[Int32]? = None,
) -> Bool {
if protect_control_embedded_local_tee &&
inside_control &&
simplify_locals_region_has_control_embedded_local_tee(
func, region_ref, true,
) {
return false
}
let capture_depth = sinkables.captures.touched.length()
let mut changed = false
let mut previous_root = -1
let mut lane = 0
let initial_revision = @ir.hot_revision_current(func)
let mut future_input_suffix_mins : Array[Int32]? = None
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
if @ir.hot_region_root_count(func, region_ref) >= 16 &&
@ir.hot_revision_current(func) == initial_revision &&
@ir.hot_node_result_arity(func, node_id) == 0 {
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Call | @ir.HotOp::CallIndirect | @ir.HotOp::CallRef =>
match future_input_suffix_mins {
None =>
future_input_suffix_mins = Some(
simplify_locals_future_input_suffix_min_node_ids(
func, region_ref,
),
)
Some(_) => ()
}
_ => ()
}
}
let current_suffix_mins = if @ir.hot_revision_current(func) ==
initial_revision {
future_input_suffix_mins
} else {
None
}
for
dependency_id in simplify_locals_preceding_future_call_dependencies(
func,
region_ref,
idx,
node_id,
future_input_suffix_mins=current_suffix_mins,
future_roots_scanned~,
) {
if simplify_locals_scan_node(
func,
dependency_id,
sinkables,
first_cycle,
get_counts,
detached,
policy,
None,
-1,
protect_control_embedded_local_tee,
inside_control,
false,
deferred_sets,
) {
changed = true
}
}
let flat_parent_is_local_set = if idx + 1 <
@ir.hot_region_root_count(func, region_ref) {
let next_id = @ir.hot_region_get(func, region_ref, idx + 1)
if next_id >= 0 && @ir.hot_node_is_live(func, next_id) {
match @ir.hot_node_get(func, next_id).op {
@ir.HotOp::LocalSet => true
_ => false
}
} else {
false
}
} else {
false
}
let arity = @ir.hot_node_result_arity(func, node_id)
lane = if previous_root == node_id { lane + 1 } else { 0 }
let force_eval = arity == 0 ||
(
sinkables.captures.is_region_capture(node_id, capture_depth) &&
(lane == 0 || lane % arity == 0)
)
previous_root = node_id
if simplify_locals_scan_node(
func,
node_id,
sinkables,
first_cycle,
get_counts,
detached,
policy,
Some(region_ref),
idx,
protect_control_embedded_local_tee,
inside_control,
flat_parent_is_local_set,
deferred_sets,
force_eval~,
) {
changed = true
}
sinkables.captures.mark(node_id)
}
sinkables.captures.rewind(capture_depth)
changed
}
///|
fn simplify_locals_node_has_any_branch(
func : @ir.HotFunc,
node_id : Int32,
) -> 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::Br
| @ir.HotOp::BrIf
| @ir.HotOp::BrTable
| @ir.HotOp::BrOnNull
| @ir.HotOp::BrOnNonNull
| @ir.HotOp::BrOnCast
| @ir.HotOp::BrOnCastFail
| @ir.HotOp::Delegate => return true
// An on-label handler can leave the current linear fallthrough before a
// later write, so facts learned after the resume do not dominate the exit.
@ir.HotOp::Continuation =>
if !@ir.hot_continuation_targets(func, node_id).is_empty() {
return true
}
@ir.HotOp::Block | @ir.HotOp::Loop =>
if simplify_locals_region_has_any_branch(
func,
@ir.hot_region_body_ref(func, node_id),
) {
return true
}
@ir.HotOp::If => {
if simplify_locals_node_has_any_branch(
func,
@ir.hot_child_get(func, node_id, 0),
) ||
simplify_locals_region_has_any_branch(
func,
@ir.hot_region_then_ref(func, node_id),
) {
return true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
if simplify_locals_region_has_any_branch(func, else_ref) {
return true
}
None => ()
}
}
@ir.HotOp::Try => {
if simplify_locals_region_has_any_branch(
func,
@ir.hot_region_body_ref(func, node_id),
) {
return true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
if simplify_locals_region_has_any_branch(func, catch_ref) {
return true
}
None => ()
}
}
@ir.HotOp::TryTable =>
if simplify_locals_region_has_any_branch(
func,
@ir.hot_region_body_ref(func, node_id),
) ||
simplify_locals_region_has_any_branch(
func,
@ir.hot_region_catch_list_ref(func, node_id),
) {
return true
}
_ =>
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if simplify_locals_node_has_any_branch(
func,
@ir.hot_child_get(func, node_id, slot),
) {
return true
}
}
}
false
}
///|
fn simplify_locals_region_has_any_branch(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
) -> Bool {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
if simplify_locals_node_has_any_branch(
func,
@ir.hot_region_get(func, region_ref, idx),
) {
return true
}
}
false
}
///|
fn simplify_locals_scan_node(
func : @ir.HotFunc,
node_id : Int32,
sinkables : SimplifyLocalsSinkables,
first_cycle : Bool,
get_counts : Array[Int32],
detached : Array[Int32],
policy : SimplifyLocalsPolicy,
parent_region : @ir.HotRegionRef?,
root_idx : Int32,
protect_control_embedded_local_tee : Bool,
inside_control : Bool,
flat_sink_parent_is_local_set : Bool,
deferred_sets : Array[Int32],
force_eval~ : Bool = false,
) -> Bool {
if !force_eval && sinkables.captures.contains(node_id) {
return false
}
let capture_depth = sinkables.captures.touched.length()
let changed = simplify_locals_scan_node_uncaptured(
func, node_id, sinkables, first_cycle, get_counts, detached, policy, parent_region,
root_idx, protect_control_embedded_local_tee, inside_control, flat_sink_parent_is_local_set,
deferred_sets,
)
sinkables.captures.rewind(capture_depth)
changed
}
///|
fn simplify_locals_scan_node_uncaptured(
func : @ir.HotFunc,
node_id : Int32,
sinkables : SimplifyLocalsSinkables,
first_cycle : Bool,
get_counts : Array[Int32],
detached : Array[Int32],
policy : SimplifyLocalsPolicy,
parent_region : @ir.HotRegionRef?,
root_idx : Int32,
protect_control_embedded_local_tee : Bool,
inside_control : Bool,
flat_sink_parent_is_local_set : Bool,
deferred_sets : Array[Int32],
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
if protect_control_embedded_local_tee &&
inside_control &&
simplify_locals_node_has_control_embedded_local_tee(func, node_id, true) {
return false
}
let mut changed = false
let node = @ir.hot_node_get(func, node_id)
let mut previous_input = -1
let mut lane = 0
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop | @ir.HotOp::If =>
for index = 0
index < pass_value_operand_count(func, node_id)
index = index + 1 {
let slot = pass_value_operand_slot(node, index)
let input = @ir.hot_child_get(func, node_id, slot)
let arity = @ir.hot_node_result_arity(func, input)
lane = if previous_input == input { lane + 1 } else { 0 }
let force_eval = arity == 0 || (lane > 0 && lane % arity == 0)
previous_input = input
if simplify_locals_scan_node(
func,
input,
sinkables,
first_cycle,
get_counts,
detached,
policy,
None,
-1,
protect_control_embedded_local_tee,
inside_control,
false,
deferred_sets,
force_eval~,
) {
changed = true
}
if !(node.op is @ir.HotOp::If && slot == 0) {
sinkables.captures.mark(input)
}
}
_ => ()
}
match node.op {
@ir.HotOp::Block => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
if simplify_locals_scan_region(
func, body_ref, sinkables, first_cycle, get_counts, detached, policy, protect_control_embedded_local_tee,
true, deferred_sets,
) {
changed = true
}
let keep_sinkables = !simplify_locals_region_has_any_branch(
func, body_ref,
) &&
!(protect_control_embedded_local_tee &&
simplify_locals_region_has_control_embedded_local_tee(
func, body_ref, true,
))
if !keep_sinkables {
simplify_locals_clear_sinkables(sinkables)
}
if policy.allow_structure_rewrites {
match parent_region {
Some(parent_region) =>
if simplify_locals_try_rewrite_block_return(
func,
parent_region,
root_idx,
node_id,
get_counts,
detached,
allow_multi_use=policy.allow_sink_tee,
) {
return true
}
None => ()
}
}
return changed
}
@ir.HotOp::Loop => {
let loop_sinkables = simplify_locals_new_sinkables(
func,
source_gets=Some(sinkables.source_gets),
captures=Some(sinkables.captures),
module_ctx=sinkables.module_ctx,
)
if simplify_locals_scan_region(
func,
@ir.hot_region_body_ref(func, node_id),
loop_sinkables,
first_cycle,
get_counts,
detached,
policy,
protect_control_embedded_local_tee,
true,
deferred_sets,
) {
changed = true
}
simplify_locals_clear_sinkables(sinkables)
if policy.allow_structure_rewrites {
match parent_region {
Some(parent_region) =>
if simplify_locals_try_rewrite_loop_return(
func,
parent_region,
root_idx,
node_id,
get_counts,
detached,
allow_multi_use=policy.allow_sink_tee,
) {
return true
}
None => ()
}
}
return changed
}
@ir.HotOp::If => {
simplify_locals_clear_sinkables(sinkables)
let then_sinkables = simplify_locals_new_sinkables(
func,
source_gets=Some(sinkables.source_gets),
captures=Some(sinkables.captures),
module_ctx=sinkables.module_ctx,
)
if simplify_locals_scan_region(
func,
@ir.hot_region_then_ref(func, node_id),
then_sinkables,
first_cycle,
get_counts,
detached,
policy,
protect_control_embedded_local_tee,
true,
deferred_sets,
) {
changed = true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) => {
let else_sinkables = simplify_locals_new_sinkables(
func,
source_gets=Some(sinkables.source_gets),
captures=Some(sinkables.captures),
module_ctx=sinkables.module_ctx,
)
if simplify_locals_scan_region(
func, else_ref, else_sinkables, first_cycle, get_counts, detached,
policy, protect_control_embedded_local_tee, true, deferred_sets,
) {
changed = true
}
}
None => ()
}
simplify_locals_clear_sinkables(sinkables)
if policy.allow_structure_rewrites {
match parent_region {
Some(parent_region) =>
if simplify_locals_try_rewrite_if_return(
func,
parent_region,
root_idx,
node_id,
get_counts,
detached,
allow_multi_use=policy.allow_sink_tee,
) {
return true
}
None => ()
}
}
return changed
}
@ir.HotOp::Try => {
// Binaryen only drops pending sinkables whose values may throw at a
// try boundary: moving those into the body would put the throw under a
// handler that previously could not catch it. Nonthrowing values may
// still sink into the body, but no pending value is kept after the try.
simplify_locals_clear_throwing_sinkables(sinkables)
if simplify_locals_scan_region(
func,
@ir.hot_region_body_ref(func, node_id),
sinkables,
first_cycle,
get_counts,
detached,
policy,
protect_control_embedded_local_tee,
true,
deferred_sets,
) {
changed = true
}
simplify_locals_clear_sinkables(sinkables)
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) => {
let catch_sinkables = simplify_locals_new_sinkables(
func,
source_gets=Some(sinkables.source_gets),
captures=Some(sinkables.captures),
module_ctx=sinkables.module_ctx,
)
if simplify_locals_scan_region(
func, catch_ref, catch_sinkables, first_cycle, get_counts, detached,
policy, protect_control_embedded_local_tee, true, deferred_sets,
) {
changed = true
}
}
None => ()
}
return changed
}
@ir.HotOp::TryTable => {
// Match the try boundary above for the try_table body. Catch-list roots
// are branch-target handlers, so they are still scanned with a fresh
// sinkable set.
simplify_locals_clear_throwing_sinkables(sinkables)
if simplify_locals_scan_region(
func,
@ir.hot_region_body_ref(func, node_id),
sinkables,
first_cycle,
get_counts,
detached,
policy,
protect_control_embedded_local_tee,
true,
deferred_sets,
) {
changed = true
}
simplify_locals_clear_sinkables(sinkables)
let catch_sinkables = simplify_locals_new_sinkables(
func,
source_gets=Some(sinkables.source_gets),
captures=Some(sinkables.captures),
module_ctx=sinkables.module_ctx,
)
if simplify_locals_scan_region(
func,
@ir.hot_region_catch_list_ref(func, node_id),
catch_sinkables,
first_cycle,
get_counts,
detached,
policy,
protect_control_embedded_local_tee,
true,
deferred_sets,
) {
changed = true
}
return changed
}
_ =>
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)
let scan_child_id = match node.op {
@ir.HotOp::Drop if slot == 0 &&
child_id >= 0 &&
@ir.hot_node_is_live(func, child_id) =>
match @ir.hot_node_get(func, child_id).op {
@ir.HotOp::LocalTee => {
let root_drop_tee = root_idx >= 0 &&
root_idx < @ir.hot_root_count(func) &&
node_id == @ir.hot_root_get(func, root_idx)
let tee_value = @ir.hot_child_get(func, child_id, 0)
if root_drop_tee &&
tee_value >= 0 &&
simplify_locals_child_use_count_capped_at_two(func, tee_value) ==
1 {
tee_value
} else {
-1
}
}
_ => child_id
}
_ => child_id
}
if scan_child_id < 0 || !@ir.hot_node_is_live(func, scan_child_id) {
continue
}
if simplify_locals_scan_node(
func,
scan_child_id,
sinkables,
first_cycle,
get_counts,
detached,
policy,
None,
-1,
protect_control_embedded_local_tee,
inside_control,
match node.op {
@ir.HotOp::LocalSet => true
_ => false
},
deferred_sets,
) {
changed = true
}
}
}
let mut current = @ir.hot_node_get(func, node_id)
match current.op {
@ir.HotOp::LocalGet => {
let local_id = current.imm0
match simplify_locals_sinkables_get(sinkables, local_id) {
Some(info) =>
if @ir.hot_node_is_live(func, info.node_id) {
let pending_node = @ir.hot_node_get(func, info.node_id)
match pending_node.op {
@ir.HotOp::LocalSet => {
// Tree visitation can reach an old stacked read after a later
// block write. That read cannot consume the pending definition.
// Replacing an existing write can give it a later factory
// allocation order without moving its execution position.
let pending_order = if pending_node.order < info.node_id {
pending_node.order
} else {
info.node_id
}
let read_order = if current.order < node_id {
current.order
} else {
node_id
}
if read_order < pending_order {
simplify_locals_sinkables_clear_local(sinkables, local_id)
simplify_locals_invalidate_sinkables(
func, node_id, current, sinkables,
)
return changed
}
// A previous cycle can place an older read in a later root's
// operand tree. Do not sink its definition into the first read
// visited by tree order if that would cross the older read.
if get_counts[local_id] > 1 &&
simplify_locals_has_earlier_source_read(
func,
sinkables,
local_id,
pending_node.order,
current.order,
) {
simplify_locals_sinkables_clear_local(sinkables, local_id)
simplify_locals_invalidate_sinkables(
func, node_id, current, sinkables,
)
return changed
}
let value_id = @ir.hot_child_get(func, info.node_id, 0)
let one_use = first_cycle || get_counts[local_id] == 1
let pending_copy_local = simplify_locals_direct_local_get_value(
func, value_id,
)
if !policy.allow_nesting &&
pending_copy_local == None &&
!flat_sink_parent_is_local_set {
simplify_locals_sinkables_clear_local(sinkables, local_id)
simplify_locals_invalidate_sinkables(
func, node_id, current, sinkables,
)
return changed
}
if !policy.allow_nesting && !one_use {
match pending_copy_local {
Some(source_local) => {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_local_get(func, source_local),
)
if source_local >= 0 && source_local < get_counts.length() {
get_counts[source_local] = get_counts[source_local] + 1
}
if local_id >= 0 &&
local_id < get_counts.length() &&
get_counts[local_id] > 0 {
get_counts[local_id] = get_counts[local_id] - 1
}
simplify_locals_sinkables_clear_local(sinkables, local_id)
simplify_locals_invalidate_sinkables(
func, node_id, current, sinkables,
)
return true
}
None => ()
}
}
if one_use &&
simplify_locals_value_can_move_one_use(func, value_id) {
@ir.hot_replace_node(
func,
node_id,
@ir.hot_node_get(func, value_id),
preserve_value_order=true,
)
detached.push(value_id)
} else if policy.allow_sink_tee &&
!one_use &&
simplify_locals_value_can_tee(func, value_id) {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_local_tee(func, info.node_id),
preserve_value_order=true,
)
} else {
simplify_locals_sinkables_clear_local(sinkables, local_id)
simplify_locals_invalidate_sinkables(
func, node_id, current, sinkables,
)
return changed
}
@ir.hot_replace_node(
func,
info.node_id,
simplify_locals_make_nop(func),
)
simplify_locals_sinkables_clear_local(sinkables, local_id)
changed = true
}
_ => simplify_locals_sinkables_clear_local(sinkables, local_id)
}
} else {
simplify_locals_sinkables_clear_local(sinkables, local_id)
}
None => ()
}
simplify_locals_invalidate_sinkables(func, node_id, current, sinkables)
return changed
}
_ => ()
}
match current.op {
@ir.HotOp::Drop => {
let child_id = @ir.hot_child_get(func, node_id, 0)
if child_id >= 0 && @ir.hot_node_is_live(func, child_id) {
let child = @ir.hot_node_get(func, child_id)
match child.op {
@ir.HotOp::Heap if simplify_locals_is_discardable_default_gc_allocation(
func, child_id,
) => {
@ir.hot_replace_node(func, node_id, simplify_locals_make_nop(func))
detached.push(child_id)
changed = true
}
@ir.HotOp::LocalTee => {
let value_id = @ir.hot_child_get(func, child_id, 0)
let root_drop_tee = root_idx >= 0 &&
root_idx < @ir.hot_root_count(func) &&
node_id == @ir.hot_root_get(func, root_idx)
if root_drop_tee &&
simplify_locals_child_use_count_capped_at_two(func, value_id) == 1 {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_local_set(func, child.imm0, value_id),
)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func, child_id, detached,
)
changed = true
}
}
_ => ()
}
}
}
_ => ()
}
current = @ir.hot_node_get(func, node_id)
match current.op {
@ir.HotOp::LocalSet => {
let value_id = @ir.hot_child_get(func, node_id, 0)
if root_idx >= 0 &&
get_counts[current.imm0] == 1 &&
simplify_locals_value_is_child_inline_safe(func, value_id) {
match parent_region {
Some(parent_region) =>
if simplify_locals_try_inline_following_local_get(
func,
parent_region,
root_idx,
current.imm0,
value_id,
policy.allow_nesting,
detached,
) {
simplify_locals_invalidate_sinkables(
func,
node_id,
current,
sinkables,
skip_local=current.imm0,
)
simplify_locals_sinkables_clear_local(sinkables, current.imm0)
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_nop(func),
)
return true
}
None => ()
}
}
simplify_locals_invalidate_sinkables(
func,
node_id,
current,
sinkables,
skip_local=current.imm0,
)
let crosses_older_stack_value = policy.allow_sink_tee &&
get_counts[current.imm0] > 1 &&
!@ir.hot_node_kind_is_control(func, value_id) &&
(match parent_region {
Some(parent_region) =>
simplify_locals_set_crosses_older_stack_value(
func,
parent_region,
root_idx,
node_id,
current.imm0,
minimums=simplify_locals_value_order_minimums_for_region(
sinkables, func, parent_region,
),
) ||
simplify_locals_set_first_use_crosses_intervening_local_write(
func,
parent_region,
root_idx,
current.imm0,
minimums=simplify_locals_value_order_minimums_for_region(
sinkables, func, parent_region,
),
)
None => false
})
if !crosses_older_stack_value {
if simplify_locals_record_local_set(
func,
node_id,
current,
sinkables,
first_cycle,
policy.allow_sink_tee,
get_counts,
deferred_sets,
) {
changed = true
}
} else {
simplify_locals_sinkables_clear_local(sinkables, current.imm0)
}
changed
}
_ => {
simplify_locals_invalidate_sinkables(func, node_id, current, sinkables)
changed
}
}
}
///|
fn simplify_locals_collect_block_branch_exits(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
target_label : Int32,
local_id : Int32,
exits : Array[SimplifyLocalsBranchExit],
) -> Bool {
let last_idx = simplify_locals_region_last_live_non_nop_root_idx(
func, region_ref,
)
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
if simplify_locals_node_targets_label(func, node_id, target_label) {
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Br => {
if node.child_count != 0 || last_idx != Some(idx) {
return false
}
match
simplify_locals_find_branch_exit_set_for_local(
func, region_ref, idx, target_label, local_id,
) {
Some(exit) => exits.push(exit)
None => return false
}
}
@ir.HotOp::BrIf => {
// An untyped br_if has only its condition child. The local write
// immediately dominating the conditional exit can become the new
// branch payload while preserving the condition as the final child.
if simplify_locals_branch_payload_count(func, node_id) != 0 {
return false
}
if node.child_count != 1 ||
simplify_locals_node_reads_local(
func,
@ir.hot_child_get(func, node_id, 0),
local_id,
) {
return false
}
match
simplify_locals_find_branch_exit_set_for_local(
func, region_ref, idx, target_label, local_id,
) {
Some(exit) => exits.push(exit)
None => return false
}
}
_ => return false
}
continue
}
let node = @ir.hot_node_get(func, node_id)
if !@ir.hot_node_kind_is_control(func, node_id) {
if simplify_locals_subtree_has_branch_to_label(
func, node_id, target_label,
) {
return false
}
continue
}
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
match node.op {
@ir.HotOp::Loop => {
let input_count = @ir.hot_label_branch_arity(
func,
@ir.hot_control_node_label(func, node_id),
)
for child_idx = 0
child_idx < input_count
child_idx = child_idx + 1 {
if simplify_locals_subtree_has_branch_to_label(
func,
@ir.hot_child_get(func, node_id, child_idx),
target_label,
) {
return false
}
}
}
_ => ()
}
if simplify_locals_control_has_typed_branch_payload(func, node_id) &&
simplify_locals_region_has_branch_to_label(
func, body_ref, target_label,
) {
return false
}
if !simplify_locals_collect_block_branch_exits(
func, body_ref, target_label, local_id, exits,
) {
return false
}
}
@ir.HotOp::If => {
if simplify_locals_subtree_has_branch_to_label(
func,
@ir.hot_child_get(func, node_id, 0),
target_label,
) {
return false
}
let then_ref = @ir.hot_region_then_ref(func, node_id)
if simplify_locals_control_has_typed_branch_payload(func, node_id) &&
simplify_locals_region_has_branch_to_label(
func, then_ref, target_label,
) {
return false
}
if !simplify_locals_collect_block_branch_exits(
func, then_ref, target_label, local_id, exits,
) {
return false
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) => {
if simplify_locals_control_has_typed_branch_payload(func, node_id) &&
simplify_locals_region_has_branch_to_label(
func, else_ref, target_label,
) {
return false
}
if !simplify_locals_collect_block_branch_exits(
func, else_ref, target_label, local_id, exits,
) {
return false
}
}
None => ()
}
}
@ir.HotOp::Try => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
if simplify_locals_control_has_typed_branch_payload(func, node_id) &&
simplify_locals_region_has_branch_to_label(
func, body_ref, target_label,
) {
return false
}
if !simplify_locals_collect_block_branch_exits(
func, body_ref, target_label, local_id, exits,
) {
return false
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) => {
if simplify_locals_control_has_typed_branch_payload(func, node_id) &&
simplify_locals_region_has_branch_to_label(
func, catch_ref, target_label,
) {
return false
}
if !simplify_locals_collect_block_branch_exits(
func, catch_ref, target_label, local_id, exits,
) {
return false
}
}
None => ()
}
}
@ir.HotOp::TryTable => {
let body_ref = @ir.hot_region_body_ref(func, node_id)
let catch_list_ref = @ir.hot_region_catch_list_ref(func, node_id)
if simplify_locals_control_has_typed_branch_payload(func, node_id) &&
(
simplify_locals_region_has_branch_to_label(
func, body_ref, target_label,
) ||
simplify_locals_region_has_branch_to_label(
func, catch_list_ref, target_label,
)
) {
return false
}
if !simplify_locals_collect_block_branch_exits(
func, body_ref, target_label, local_id, exits,
) ||
!simplify_locals_collect_block_branch_exits(
func, catch_list_ref, target_label, local_id, exits,
) {
return false
}
}
_ => ()
}
}
true
}
///|
fn simplify_locals_try_apply_block_return_candidate(
func : @ir.HotFunc,
node_id : Int32,
label_id : Int32,
candidate : SimplifyLocalsTailSet,
detached : Array[Int32],
) -> Bool {
let body_ref = @ir.hot_region_body_ref(func, node_id)
let exits : Array[SimplifyLocalsBranchExit] = []
if !simplify_locals_collect_block_branch_exits(
func,
body_ref,
label_id,
candidate.local_id,
exits,
) {
return false
}
simplify_locals_replace_tail_set_with_result_value(func, body_ref, candidate)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func,
candidate.node_id,
detached,
)
for exit in exits {
ignore(
@ir.hot_region_set(
func,
exit.region_ref,
exit.set_idx,
@ir.hot_build_nop(func),
),
)
let old_branch_children = @ir.hot_child_span(func, exit.br_id)
let new_branch_children : Array[Int32] = [exit.value_id]
for child_id in old_branch_children {
new_branch_children.push(child_id)
}
ignore(@ir.hot_replace_child_span(func, exit.br_id, new_branch_children))
// A conditional branch with a payload produces that payload when the
// branch is not taken. The original untyped br_if was a statement, so
// preserve that shape explicitly instead of leaving a typed region root
// for lowering to spill through fresh temporary locals.
match @ir.hot_node_get(func, exit.br_id).op {
@ir.HotOp::BrIf =>
ignore(
@ir.hot_region_set(
func,
exit.region_ref,
exit.br_idx,
@ir.hot_build_drop(func, exit.br_id),
),
)
_ => ()
}
simplify_locals_queue_rehomed_wrapper_for_deletion(
func,
exit.set_id,
detached,
)
}
@ir.hot_control_result_type_set(
func,
node_id,
simplify_locals_local_type_id(func, candidate.local_id, candidate.value_id),
)
true
}
///|
fn simplify_locals_region_has_branch_to_label(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
target_label : Int32,
) -> Bool {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
if simplify_locals_node_targets_label(func, node_id, target_label) {
return true
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
if simplify_locals_region_has_branch_to_label(
func,
@ir.hot_region_body_ref(func, node_id),
target_label,
) {
return true
}
@ir.HotOp::If => {
if simplify_locals_region_has_branch_to_label(
func,
@ir.hot_region_then_ref(func, node_id),
target_label,
) {
return true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
if simplify_locals_region_has_branch_to_label(
func, else_ref, target_label,
) {
return true
}
None => ()
}
}
@ir.HotOp::Try => {
if simplify_locals_region_has_branch_to_label(
func,
@ir.hot_region_body_ref(func, node_id),
target_label,
) {
return true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
if simplify_locals_region_has_branch_to_label(
func, catch_ref, target_label,
) {
return true
}
None => ()
}
}
@ir.HotOp::TryTable =>
if simplify_locals_region_has_branch_to_label(
func,
@ir.hot_region_body_ref(func, node_id),
target_label,
) ||
simplify_locals_region_has_branch_to_label(
func,
@ir.hot_region_catch_list_ref(func, node_id),
target_label,
) {
return true
}
_ => ()
}
}
false
}
///|
fn simplify_locals_subtree_has_branch_to_label(
func : @ir.HotFunc,
node_id : Int32,
target_label : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
if simplify_locals_node_targets_label(func, node_id, target_label) {
return true
}
for child_idx = 0
child_idx < @ir.hot_child_count(func, node_id)
child_idx = child_idx + 1 {
if simplify_locals_subtree_has_branch_to_label(
func,
@ir.hot_child_get(func, node_id, child_idx),
target_label,
) {
return true
}
}
false
}
///|
fn simplify_locals_subtree_has_typed_control(
func : @ir.HotFunc,
node_id : Int32,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
if @ir.hot_node_kind_is_control(func, node_id) &&
@ir.hot_node_result_arity(func, node_id) != 0 {
return true
}
for child_idx = 0
child_idx < @ir.hot_child_count(func, node_id)
child_idx = child_idx + 1 {
if simplify_locals_subtree_has_typed_control(
func,
@ir.hot_child_get(func, node_id, child_idx),
) {
return true
}
}
false
}
///|
fn simplify_locals_try_rewrite_block_return(
func : @ir.HotFunc,
parent_region : @ir.HotRegionRef,
root_idx : Int32,
node_id : Int32,
get_counts : Array[Int32],
detached : Array[Int32],
allow_multi_use~ : Bool = true,
) -> 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::Block => ()
_ => return false
}
if @ir.hot_node_result_arity(func, node_id) != 0 {
return false
}
let body_ref = @ir.hot_region_body_ref(func, node_id)
let label_id = @ir.hot_control_node_label(func, node_id)
let candidates = simplify_locals_collect_block_return_candidates(
func, body_ref, label_id, get_counts,
)
if candidates.is_empty() {
return false
}
let preferred_locals = simplify_locals_following_root_local_get_order(
func, parent_region, root_idx,
)
for preferred_local in preferred_locals {
for candidate in candidates {
if candidate.local_id != preferred_local ||
(!allow_multi_use && get_counts[candidate.local_id] > 1) {
continue
}
if simplify_locals_try_apply_block_return_candidate(
func, node_id, label_id, candidate, detached,
) {
if get_counts[candidate.local_id] == 1 &&
(
simplify_locals_try_consume_following_local_get(
func,
parent_region,
root_idx,
candidate.local_id,
detached,
) ||
simplify_locals_try_inline_following_local_get(
func,
parent_region,
root_idx,
candidate.local_id,
node_id,
true,
detached,
)
) {
return true
}
ignore(
@ir.hot_region_set(
func,
parent_region,
root_idx,
@ir.hot_build_local_set_from_value(
func,
candidate.local_id,
node_id,
),
),
)
return true
}
}
}
let mut candidate_idx = candidates.length() - 1
while candidate_idx >= 0 {
let candidate = candidates[candidate_idx]
if !allow_multi_use && get_counts[candidate.local_id] > 1 {
candidate_idx = candidate_idx - 1
continue
}
if simplify_locals_try_apply_block_return_candidate(
func, node_id, label_id, candidate, detached,
) {
if get_counts[candidate.local_id] == 1 &&
(
simplify_locals_try_consume_following_local_get(
func,
parent_region,
root_idx,
candidate.local_id,
detached,
) ||
simplify_locals_try_inline_following_local_get(
func,
parent_region,
root_idx,
candidate.local_id,
node_id,
true,
detached,
)
) {
return true
}
ignore(
@ir.hot_region_set(
func,
parent_region,
root_idx,
@ir.hot_build_local_set_from_value(func, candidate.local_id, node_id),
),
)
return true
}
candidate_idx = candidate_idx - 1
}
false
}
///|
fn simplify_locals_try_rewrite_if_return(
func : @ir.HotFunc,
parent_region : @ir.HotRegionRef,
root_idx : Int32,
node_id : Int32,
get_counts : Array[Int32],
detached : Array[Int32],
allow_multi_use~ : Bool = true,
) -> 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::If => ()
_ => return false
}
if @ir.hot_node_result_arity(func, node_id) != 0 {
return false
}
let then_ref = @ir.hot_region_then_ref(func, node_id)
let then_tail = simplify_locals_region_tail_local_set(func, then_ref)
let then_unreachable = simplify_locals_region_is_unreachableish(
func, then_ref,
)
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) => {
let else_tail = simplify_locals_region_tail_local_set(func, else_ref)
let else_unreachable = simplify_locals_region_is_unreachableish(
func, else_ref,
)
let chosen_local = if then_unreachable {
match else_tail {
Some(tail) => tail.local_id
None => return false
}
} else if else_unreachable {
match then_tail {
Some(tail) => tail.local_id
None => return false
}
} else {
match (then_tail, else_tail) {
(Some(then_tail), Some(else_tail)) if then_tail.local_id ==
else_tail.local_id => then_tail.local_id
_ => return false
}
}
if chosen_local < 0 ||
chosen_local >= get_counts.length() ||
get_counts[chosen_local] == 0 ||
(!allow_multi_use && get_counts[chosen_local] > 1) {
return false
}
let fallback_value = if then_unreachable {
else_tail.unwrap().value_id
} else {
then_tail.unwrap().value_id
}
if (
!then_unreachable &&
simplify_locals_subtree_has_typed_control(
func,
then_tail.unwrap().value_id,
)
) ||
(
!else_unreachable &&
simplify_locals_subtree_has_typed_control(
func,
else_tail.unwrap().value_id,
)
) {
return false
}
if !then_unreachable {
let tail = then_tail.unwrap()
simplify_locals_replace_tail_set_with_result_value(func, then_ref, tail)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func,
tail.node_id,
detached,
)
}
if !else_unreachable {
let tail = else_tail.unwrap()
simplify_locals_replace_tail_set_with_result_value(func, else_ref, tail)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func,
tail.node_id,
detached,
)
}
@ir.hot_control_result_type_set(
func,
node_id,
simplify_locals_local_type_id(func, chosen_local, fallback_value),
)
if get_counts[chosen_local] == 1 &&
simplify_locals_try_consume_following_local_get(
func, parent_region, root_idx, chosen_local, detached,
) {
return true
}
ignore(
@ir.hot_region_set(
func,
parent_region,
root_idx,
@ir.hot_build_local_set_from_value(func, chosen_local, node_id),
),
)
true
}
None => {
let then_tail = match then_tail {
Some(then_tail) => then_tail
None => return false
}
if then_tail.local_id < 0 ||
then_tail.local_id >= get_counts.length() ||
(!allow_multi_use && get_counts[then_tail.local_id] > 1) ||
!simplify_locals_local_type_is_defaultable(func, then_tail.local_id) {
return false
}
simplify_locals_replace_tail_set_with_result_value(
func, then_ref, then_tail,
)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func,
then_tail.node_id,
detached,
)
let result_block_type = match
@ir.hot_local_type(func, then_tail.local_id) {
Some(local_type) => @lib.BlockType::val_type(local_type)
None => return false
}
let replacement_then = simplify_locals_build_one_armed_if_then_body(
func,
then_ref,
then_tail.value_id,
)
if simplify_locals_region_has_branch_to_label(
func,
then_ref,
@ir.hot_control_node_label(func, node_id),
) {
// Existing void exits must skip the synthesized assignment. Keep their
// original label on an outer block around the new result conditional.
let result_if = @ir.hot_build_if(
func,
result_block_type,
@ir.hot_child_get(func, node_id, 0),
replacement_then,
else_body=Some([@ir.hot_build_local_get(func, then_tail.local_id)]),
)
let capture = @ir.hot_build_local_set_from_value(
func,
then_tail.local_id,
result_if,
)
@ir.hot_region_replace_body(func, then_ref, [])
@ir.hot_control_demote_if_to_block(func, node_id, [capture])
return true
}
let else_region = @ir.hot_build_region(
func,
[@ir.hot_build_local_get(func, then_tail.local_id)],
1,
)
@ir.hot_child_set(func, node_id, 2, else_region)
@ir.hot_region_replace_body(func, then_ref, replacement_then)
@ir.hot_control_result_type_set(
func,
node_id,
@ir.hot_type_intern_block_result(func, result_block_type),
)
if get_counts[then_tail.local_id] == 1 &&
simplify_locals_try_consume_following_local_get(
func,
parent_region,
root_idx,
then_tail.local_id,
detached,
) {
return true
}
ignore(
@ir.hot_region_set(
func,
parent_region,
root_idx,
@ir.hot_build_local_set_from_value(func, then_tail.local_id, node_id),
),
)
true
}
}
}
///|
fn simplify_locals_try_rewrite_loop_return(
func : @ir.HotFunc,
parent_region : @ir.HotRegionRef,
root_idx : Int32,
node_id : Int32,
get_counts : Array[Int32],
detached : Array[Int32],
allow_multi_use~ : Bool = true,
) -> 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::Loop => ()
_ => return false
}
let body_ref = @ir.hot_region_body_ref(func, node_id)
let tail = match simplify_locals_region_tail_local_set(func, body_ref) {
Some(tail) => tail
None => return false
}
if tail.local_id < 0 ||
tail.local_id >= get_counts.length() ||
get_counts[tail.local_id] == 0 ||
(!allow_multi_use && get_counts[tail.local_id] > 1) {
return false
}
if simplify_locals_region_has_branch_to_label(
func,
body_ref,
@ir.hot_control_node_label(func, node_id),
) {
return false
}
if simplify_locals_region_next_live_non_nop_root_idx(
func, parent_region, root_idx,
)
is None {
return false
}
// The tail value still executes inside the loop. Export the loop's result,
// rather than copying its producer into a following local.get.
simplify_locals_replace_tail_set_with_result_value(func, body_ref, tail)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func,
tail.node_id,
detached,
)
@ir.hot_control_result_type_set(
func,
node_id,
simplify_locals_local_type_id(func, tail.local_id, tail.value_id),
)
if get_counts[tail.local_id] == 1 &&
(
simplify_locals_try_inline_following_local_get(
func,
parent_region,
root_idx,
tail.local_id,
node_id,
true,
detached,
) ||
simplify_locals_try_consume_following_local_get(
func,
parent_region,
root_idx,
tail.local_id,
detached,
)
) {
return true
}
ignore(
@ir.hot_region_set(
func,
parent_region,
root_idx,
@ir.hot_build_local_set_from_value(func, tail.local_id, node_id),
),
)
true
}
///|
fn simplify_locals_try_rewrite_nested_one_armed_if_child(
func : @ir.HotFunc,
parent_id : Int32,
slot : Int32,
child_id : Int32,
get_counts : Array[Int32],
detached : Array[Int32],
allow_multi_use~ : Bool = true,
) -> Bool {
if child_id < 0 || !@ir.hot_node_is_live(func, child_id) {
return false
}
let child = @ir.hot_node_get(func, child_id)
match child.op {
@ir.HotOp::If => ()
_ => return false
}
if @ir.hot_node_result_arity(func, child_id) != 0 {
return false
}
match @ir.hot_region_else_ref(func, child_id) {
Some(_) => return false
None => ()
}
let then_ref = @ir.hot_region_then_ref(func, child_id)
let then_tail = match simplify_locals_region_tail_local_set(func, then_ref) {
Some(then_tail) => then_tail
None => return false
}
if then_tail.local_id < 0 ||
then_tail.local_id >= get_counts.length() ||
get_counts[then_tail.local_id] == 0 ||
(!allow_multi_use && get_counts[then_tail.local_id] > 1) ||
!simplify_locals_local_type_is_defaultable(func, then_tail.local_id) {
return false
}
simplify_locals_replace_tail_set_with_result_value(func, then_ref, then_tail)
simplify_locals_queue_rehomed_wrapper_for_deletion(
func,
then_tail.node_id,
detached,
)
let result_block_type = match @ir.hot_local_type(func, then_tail.local_id) {
Some(local_type) => @lib.BlockType::val_type(local_type)
None => return false
}
let replacement_then = simplify_locals_build_one_armed_if_then_body(
func,
then_ref,
then_tail.value_id,
)
if simplify_locals_region_has_branch_to_label(
func,
then_ref,
@ir.hot_control_node_label(func, child_id),
) {
let result_if = @ir.hot_build_if(
func,
result_block_type,
@ir.hot_child_get(func, child_id, 0),
replacement_then,
else_body=Some([@ir.hot_build_local_get(func, then_tail.local_id)]),
)
let capture = @ir.hot_build_local_set_from_value(
func,
then_tail.local_id,
result_if,
)
@ir.hot_region_replace_body(func, then_ref, [])
@ir.hot_control_demote_if_to_block(func, child_id, [capture])
return true
}
// Rewrite in place so the conditional's source position and label remain
// unchanged. A fresh conditional would place its write after old consumers.
let else_region = @ir.hot_build_region(
func,
[@ir.hot_build_local_get(func, then_tail.local_id)],
1,
)
@ir.hot_child_set(func, child_id, 2, else_region)
@ir.hot_region_replace_body(func, then_ref, replacement_then)
@ir.hot_control_result_type_set(
func,
child_id,
@ir.hot_type_intern_block_result(func, result_block_type),
)
@ir.hot_child_set(
func,
parent_id,
slot,
@ir.hot_build_local_set_from_value(func, then_tail.local_id, child_id),
)
true
}
///|
fn simplify_locals_try_rewrite_nested_one_armed_if_children(
func : @ir.HotFunc,
node_id : Int32,
get_counts : Array[Int32],
detached : Array[Int32],
allow_multi_use~ : Bool = true,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
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
}
if simplify_locals_try_rewrite_nested_one_armed_if_children(
func,
child_id,
get_counts,
detached,
allow_multi_use~,
) ||
simplify_locals_try_rewrite_nested_one_armed_if_child(
func,
node_id,
slot,
child_id,
get_counts,
detached,
allow_multi_use~,
) {
return true
}
}
false
}
///|
fn simplify_locals_try_structure_rewrite_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
get_counts : Array[Int32],
detached : Array[Int32],
allow_multi_use~ : Bool = true,
) -> Bool {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
if simplify_locals_try_rewrite_split_local_set_wrapper_forwarder(
func, node_id, detached,
) {
return true
}
if simplify_locals_try_rewrite_nested_one_armed_if_children(
func,
node_id,
get_counts,
detached,
allow_multi_use~,
) {
return true
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::Block =>
if simplify_locals_try_structure_rewrite_region(
func,
@ir.hot_region_body_ref(func, node_id),
get_counts,
detached,
allow_multi_use~,
) ||
simplify_locals_try_rewrite_block_return(
func,
region_ref,
idx,
node_id,
get_counts,
detached,
allow_multi_use~,
) {
return true
}
@ir.HotOp::If => {
if simplify_locals_try_structure_rewrite_region(
func,
@ir.hot_region_then_ref(func, node_id),
get_counts,
detached,
allow_multi_use~,
) {
return true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
if simplify_locals_try_structure_rewrite_region(
func,
else_ref,
get_counts,
detached,
allow_multi_use~,
) {
return true
}
None => ()
}
if simplify_locals_try_rewrite_if_return(
func,
region_ref,
idx,
node_id,
get_counts,
detached,
allow_multi_use~,
) {
return true
}
}
@ir.HotOp::Loop =>
if simplify_locals_try_structure_rewrite_region(
func,
@ir.hot_region_body_ref(func, node_id),
get_counts,
detached,
allow_multi_use~,
) ||
simplify_locals_try_rewrite_loop_return(
func,
region_ref,
idx,
node_id,
get_counts,
detached,
allow_multi_use~,
) {
return true
}
@ir.HotOp::Try => {
if simplify_locals_try_structure_rewrite_region(
func,
@ir.hot_region_body_ref(func, node_id),
get_counts,
detached,
allow_multi_use~,
) {
return true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
if simplify_locals_try_structure_rewrite_region(
func,
catch_ref,
get_counts,
detached,
allow_multi_use~,
) {
return true
}
None => ()
}
}
@ir.HotOp::TryTable =>
if simplify_locals_try_structure_rewrite_region(
func,
@ir.hot_region_body_ref(func, node_id),
get_counts,
detached,
allow_multi_use~,
) ||
simplify_locals_try_structure_rewrite_region(
func,
@ir.hot_region_catch_list_ref(func, node_id),
get_counts,
detached,
allow_multi_use~,
) {
return true
}
_ => ()
}
}
false
}
///|
fn simplify_locals_run_structure_rewrites(
func : @ir.HotFunc,
detached : Array[Int32],
allow_multi_use~ : Bool = true,
) -> Bool {
simplify_locals_try_structure_rewrite_region(
func,
@ir.HotRegionRef::root(),
simplify_locals_count_local_gets(func),
detached,
allow_multi_use~,
)
}
///|
priv struct SimplifyLocalsEquivalentState {
rep_of_local : Array[Int32]
members_of_rep : Array[Array[Int32]]
captures : PassSourceCaptures
local_versions : Array[Int32]
read_versions : Array[Int32]
next_version : Array[Int32]
}
///|
fn simplify_locals_new_equivalences(
func : @ir.HotFunc,
captures~ : PassSourceCaptures? = None,
read_versions~ : Array[Int32]? = None,
next_version~ : Array[Int32]? = None,
) -> SimplifyLocalsEquivalentState {
let captures = match captures {
Some(captures) => captures
None => PassSourceCaptures::new(func)
}
let read_versions = match read_versions {
Some(versions) => versions
None => Array::make(@ir.hot_node_count(func), -1)
}
let next_version = match next_version {
Some(next) => next
None => [0]
}
let local_versions = Array::makei(@ir.hot_local_count(func), fn(_) {
let version = next_version[0]
next_version[0] += 1
version
})
let rep_of_local = Array::make(@ir.hot_local_count(func), 0)
let members_of_rep : Array[Array[Int32]] = []
for local_id = 0
local_id < @ir.hot_local_count(func)
local_id = local_id + 1 {
rep_of_local[local_id] = local_id
members_of_rep.push([local_id])
}
{
rep_of_local,
members_of_rep,
captures,
local_versions,
read_versions,
next_version,
}
}
///|
fn simplify_locals_clone_equivalences(
equivalences : SimplifyLocalsEquivalentState,
) -> SimplifyLocalsEquivalentState {
let cloned_members : Array[Array[Int32]] = []
for members in equivalences.members_of_rep {
cloned_members.push(members.copy())
}
{
rep_of_local: equivalences.rep_of_local.copy(),
members_of_rep: cloned_members,
captures: equivalences.captures,
local_versions: equivalences.local_versions.copy(),
read_versions: equivalences.read_versions,
next_version: equivalences.next_version,
}
}
///|
fn simplify_locals_clear_equivalences(
equivalences : SimplifyLocalsEquivalentState,
) -> Unit {
for local_id = 0
local_id < equivalences.rep_of_local.length()
local_id = local_id + 1 {
equivalences.rep_of_local[local_id] = local_id
equivalences.local_versions[local_id] = equivalences.next_version[0]
equivalences.next_version[0] += 1
equivalences.members_of_rep[local_id] = [local_id]
}
}
///|
fn simplify_locals_reset_equivalent_local(
equivalences : SimplifyLocalsEquivalentState,
local_id : Int32,
) -> Unit {
if local_id < 0 || local_id >= equivalences.rep_of_local.length() {
return
}
let rep = equivalences.rep_of_local[local_id]
if rep < 0 || rep >= equivalences.members_of_rep.length() {
equivalences.rep_of_local[local_id] = local_id
equivalences.members_of_rep[local_id] = [local_id]
return
}
let remaining : Array[Int32] = []
for member_id in equivalences.members_of_rep[rep] {
if member_id != local_id {
remaining.push(member_id)
}
}
if rep == local_id && !remaining.is_empty() {
let new_rep = remaining[0]
equivalences.members_of_rep[new_rep] = remaining
for member_id in remaining {
equivalences.rep_of_local[member_id] = new_rep
}
} else if rep != local_id {
equivalences.members_of_rep[rep] = remaining
}
equivalences.rep_of_local[local_id] = local_id
equivalences.members_of_rep[local_id] = [local_id]
}
///|
fn simplify_locals_equivalent_check(
equivalences : SimplifyLocalsEquivalentState,
lhs : Int32,
rhs : Int32,
) -> Bool {
lhs >= 0 &&
rhs >= 0 &&
lhs < equivalences.rep_of_local.length() &&
rhs < equivalences.rep_of_local.length() &&
equivalences.rep_of_local[lhs] == equivalences.rep_of_local[rhs]
}
///|
fn simplify_locals_add_equivalence(
equivalences : SimplifyLocalsEquivalentState,
local_id : Int32,
other_local : Int32,
) -> Unit {
if local_id < 0 ||
other_local < 0 ||
local_id >= equivalences.rep_of_local.length() ||
other_local >= equivalences.rep_of_local.length() ||
local_id == other_local {
return
}
let other_rep = equivalences.rep_of_local[other_local]
if other_rep < 0 ||
other_rep >= equivalences.members_of_rep.length() ||
equivalences.rep_of_local[local_id] == other_rep {
return
}
simplify_locals_reset_equivalent_local(equivalences, local_id)
equivalences.rep_of_local[local_id] = other_rep
equivalences.members_of_rep[other_rep].push(local_id)
}
///|
fn simplify_locals_direct_local_get_value(
func : @ir.HotFunc,
node_id : Int32,
) -> Int32? {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet => Some(node.imm0)
_ => None
}
}
///|
fn simplify_locals_equivalent_local_get_value(
func : @ir.HotFunc,
node_id : Int32,
) -> Int32? {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return None
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet => Some(node.imm0)
_ =>
// Binaryen's Properties::getFallthrough sees through this refining
// wrapper. If it succeeds, the assigned local contains the same
// reference as the source; if it traps, no following read is reached.
match @ir.hot_node_exact_instr(func, node_id) {
Some(@lib.Instruction::RefAsNonNull) if node.child_count == 1 =>
simplify_locals_equivalent_local_get_value(
func,
@ir.hot_child_get(func, node_id, 0),
)
_ => None
}
}
}
///|
fn simplify_locals_equivalent_read_node(
func : @ir.HotFunc,
node_id : Int32,
) -> Int32 {
let mut current = node_id
while current >= 0 && @ir.hot_node_is_live(func, current) {
let node = @ir.hot_node_get(func, current)
if node.op is @ir.HotOp::LocalGet {
return current
}
match @ir.hot_node_exact_instr(func, current) {
Some(@lib.Instruction::RefAsNonNull) if node.child_count == 1 =>
current = @ir.hot_child_get(func, current, 0)
_ => return -1
}
}
-1
}
///|
fn simplify_locals_type_env(ctx : HotPassContext) -> @validate.Env? {
match ctx.module_ctx {
Some(module_ctx) =>
Some(@validate.Env::new().with_types(module_ctx.subtypes))
None => None
}
}
///|
fn simplify_locals_local_type_is_subtype(
env : @validate.Env?,
func : @ir.HotFunc,
lhs : Int32,
rhs : Int32,
) -> Bool {
let lhs_type = match @ir.hot_local_type(func, lhs) {
Some(lhs_type) => lhs_type
None => return false
}
let rhs_type = match @ir.hot_local_type(func, rhs) {
Some(rhs_type) => rhs_type
None => return false
}
if lhs_type == rhs_type {
return true
}
match env {
Some(env) => @validate.Match::matches(lhs_type, rhs_type, env)
None => false
}
}
///|
fn simplify_locals_get_count_ignoring_current(
get_counts : Array[Int32],
current_local : Int32,
local_id : Int32,
) -> Int32 {
let count = get_counts[local_id]
if local_id == current_local && count > 0 {
count - 1
} else {
count
}
}
///|
fn simplify_locals_pick_best_equivalent_local(
env : @validate.Env?,
func : @ir.HotFunc,
equivalences : SimplifyLocalsEquivalentState,
get_counts : Array[Int32],
local_id : Int32,
) -> Int32 {
if local_id < 0 || local_id >= equivalences.rep_of_local.length() {
return local_id
}
let mut best = local_id
let rep = equivalences.rep_of_local[local_id]
if rep < 0 || rep >= equivalences.members_of_rep.length() {
return best
}
for candidate in equivalences.members_of_rep[rep] {
if candidate == best ||
!simplify_locals_local_type_is_subtype(env, func, candidate, best) {
continue
}
let candidate_type = @ir.hot_local_type(func, candidate)
let best_type = @ir.hot_local_type(func, best)
let candidate_has_more_gets = simplify_locals_get_count_ignoring_current(
get_counts, local_id, candidate,
) >
simplify_locals_get_count_ignoring_current(get_counts, local_id, best)
if candidate_type != best_type || candidate_has_more_gets {
best = candidate
}
}
best
}
///|
fn simplify_locals_scan_equivalent_region(
env : @validate.Env?,
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
equivalences : SimplifyLocalsEquivalentState,
get_counts : Array[Int32],
tee_defined_locals : Array[Bool],
) -> Bool {
let capture_depth = equivalences.captures.touched.length()
let mut changed = false
let mut previous_root = -1
let mut lane = 0
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
let arity = @ir.hot_node_result_arity(func, node_id)
lane = if previous_root == node_id { lane + 1 } else { 0 }
let force_eval = arity == 0 ||
(
equivalences.captures.is_region_capture(node_id, capture_depth) &&
(lane == 0 || lane % arity == 0)
)
previous_root = node_id
if simplify_locals_scan_equivalent_node(
env,
func,
node_id,
equivalences,
get_counts,
tee_defined_locals,
false,
force_eval~,
) {
changed = true
}
equivalences.captures.mark(node_id)
}
equivalences.captures.rewind(capture_depth)
changed
}
///|
fn simplify_locals_scan_equivalent_current(
env : @validate.Env?,
func : @ir.HotFunc,
node_id : Int32,
equivalences : SimplifyLocalsEquivalentState,
get_counts : Array[Int32],
tee_defined_locals : Array[Bool],
protect_tee_defined_locals : Bool,
) -> 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::LocalSet | @ir.HotOp::LocalTee => {
let local_id = node.imm0
while equivalences.read_versions.length() <= node_id {
equivalences.read_versions.push(-1)
}
// A later lane can revisit a captured producer after another write to
// this local. Do not learn its old copy alias again. Still invalidate
// aliases on every write visit, including actual repeated evaluation.
let first_write_visit = equivalences.read_versions[node_id] < 0
let value_id = @ir.hot_child_get(func, node_id, 0)
let read_id = simplify_locals_equivalent_read_node(func, value_id)
let source_local = simplify_locals_equivalent_local_get_value(
func, value_id,
)
let source_is_current = first_write_visit &&
(match source_local {
Some(source) =>
read_id >= 0 &&
read_id < equivalences.read_versions.length() &&
equivalences.read_versions[read_id] ==
equivalences.local_versions[source]
None => false
})
equivalences.local_versions[local_id] = equivalences.next_version[0]
equivalences.next_version[0] += 1
equivalences.read_versions[node_id] = equivalences.local_versions[local_id]
match source_local {
Some(value_local) if source_is_current =>
if simplify_locals_equivalent_check(
equivalences, local_id, value_local,
) {
false
} else {
simplify_locals_reset_equivalent_local(equivalences, local_id)
simplify_locals_add_equivalence(equivalences, local_id, value_local)
false
}
_ => {
simplify_locals_reset_equivalent_local(equivalences, local_id)
false
}
}
}
@ir.HotOp::LocalGet => {
let local_id = node.imm0
while equivalences.read_versions.length() <= node_id {
equivalences.read_versions.push(-1)
}
// Two lane consumers can visit the same captured producer. Its read was
// evaluated before aliases learned inside that producer; rewriting it on
// the later visit can turn an initializing tee into a self-copy.
if equivalences.read_versions[node_id] >= 0 {
return false
}
equivalences.read_versions[node_id] = equivalences.local_versions[local_id]
let best = simplify_locals_pick_best_equivalent_local(
env, func, equivalences, get_counts, local_id,
)
if protect_tee_defined_locals &&
local_id >= 0 &&
local_id < tee_defined_locals.length() &&
tee_defined_locals[local_id] &&
best != local_id {
return false
}
if best == local_id ||
!simplify_locals_local_type_is_subtype(env, func, best, local_id) {
return false
}
let best_type = @ir.hot_local_type(func, best)
let old_type = @ir.hot_local_type(func, local_id)
let has_more_gets = simplify_locals_get_count_ignoring_current(
get_counts, local_id, best,
) >
simplify_locals_get_count_ignoring_current(
get_counts, local_id, local_id,
)
if !has_more_gets && best_type == old_type {
return false
}
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_local_get(func, best),
)
equivalences.read_versions[node_id] = equivalences.local_versions[best]
if best >= 0 && best < get_counts.length() {
get_counts[best] = get_counts[best] + 1
}
if local_id >= 0 &&
local_id < get_counts.length() &&
get_counts[local_id] > 0 {
get_counts[local_id] = get_counts[local_id] - 1
}
true
}
_ => false
}
}
///|
fn simplify_locals_scan_equivalent_node(
env : @validate.Env?,
func : @ir.HotFunc,
node_id : Int32,
equivalences : SimplifyLocalsEquivalentState,
get_counts : Array[Int32],
tee_defined_locals : Array[Bool],
protect_tee_defined_locals : Bool,
force_eval~ : Bool = false,
) -> Bool {
if !force_eval && equivalences.captures.contains(node_id) {
return false
}
let capture_depth = equivalences.captures.touched.length()
let changed = simplify_locals_scan_equivalent_node_uncaptured(
env, func, node_id, equivalences, get_counts, tee_defined_locals, protect_tee_defined_locals,
)
equivalences.captures.rewind(capture_depth)
changed
}
///|
fn simplify_locals_scan_equivalent_node_uncaptured(
env : @validate.Env?,
func : @ir.HotFunc,
node_id : Int32,
equivalences : SimplifyLocalsEquivalentState,
get_counts : Array[Int32],
tee_defined_locals : Array[Bool],
protect_tee_defined_locals : Bool,
) -> Bool {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
return false
}
let mut changed = false
let node = @ir.hot_node_get(func, node_id)
let mut previous_input = -1
let mut lane = 0
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop | @ir.HotOp::If =>
for index = 0
index < pass_value_operand_count(func, node_id)
index = index + 1 {
let slot = pass_value_operand_slot(node, index)
let input = @ir.hot_child_get(func, node_id, slot)
let arity = @ir.hot_node_result_arity(func, input)
lane = if previous_input == input { lane + 1 } else { 0 }
let force_eval = arity == 0 || (lane > 0 && lane % arity == 0)
previous_input = input
if simplify_locals_scan_equivalent_node(
env,
func,
input,
equivalences,
get_counts,
tee_defined_locals,
false,
force_eval~,
) {
changed = true
}
if !(node.op is @ir.HotOp::If && slot == 0) {
equivalences.captures.mark(input)
}
}
_ => ()
}
match node.op {
@ir.HotOp::Block => {
if simplify_locals_scan_equivalent_region(
env,
func,
@ir.hot_region_body_ref(func, node_id),
equivalences,
get_counts,
tee_defined_locals,
) {
changed = true
}
// Facts learned on the textual fallthrough path do not necessarily
// hold at a branch target. Keep scanning branch-free blocks precisely,
// but discard path-specific aliases when control can leave the region.
if simplify_locals_region_has_any_branch(
func,
@ir.hot_region_body_ref(func, node_id),
) {
simplify_locals_clear_equivalences(equivalences)
}
return changed
}
@ir.HotOp::If => {
let then_equivalences = simplify_locals_clone_equivalences(equivalences)
if simplify_locals_scan_equivalent_region(
env,
func,
@ir.hot_region_then_ref(func, node_id),
then_equivalences,
get_counts,
tee_defined_locals,
) {
changed = true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) => {
let else_equivalences = simplify_locals_clone_equivalences(
equivalences,
)
if simplify_locals_scan_equivalent_region(
env, func, else_ref, else_equivalences, get_counts, tee_defined_locals,
) {
changed = true
}
}
None => ()
}
simplify_locals_clear_equivalences(equivalences)
return changed
}
@ir.HotOp::Loop => {
let loop_equivalences = simplify_locals_new_equivalences(
func,
captures=Some(equivalences.captures),
read_versions=Some(equivalences.read_versions),
next_version=Some(equivalences.next_version),
)
if simplify_locals_scan_equivalent_region(
env,
func,
@ir.hot_region_body_ref(func, node_id),
loop_equivalences,
get_counts,
tee_defined_locals,
) {
changed = true
}
simplify_locals_clear_equivalences(equivalences)
return changed
}
@ir.HotOp::Try => {
let body_equivalences = simplify_locals_new_equivalences(
func,
captures=Some(equivalences.captures),
read_versions=Some(equivalences.read_versions),
next_version=Some(equivalences.next_version),
)
if simplify_locals_scan_equivalent_region(
env,
func,
@ir.hot_region_body_ref(func, node_id),
body_equivalences,
get_counts,
tee_defined_locals,
) {
changed = true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) => {
let catch_equivalences = simplify_locals_new_equivalences(
func,
captures=Some(equivalences.captures),
read_versions=Some(equivalences.read_versions),
next_version=Some(equivalences.next_version),
)
if simplify_locals_scan_equivalent_region(
env, func, catch_ref, catch_equivalences, get_counts, tee_defined_locals,
) {
changed = true
}
}
None => ()
}
simplify_locals_clear_equivalences(equivalences)
return changed
}
@ir.HotOp::TryTable => {
let body_equivalences = simplify_locals_new_equivalences(
func,
captures=Some(equivalences.captures),
read_versions=Some(equivalences.read_versions),
next_version=Some(equivalences.next_version),
)
if simplify_locals_scan_equivalent_region(
env,
func,
@ir.hot_region_body_ref(func, node_id),
body_equivalences,
get_counts,
tee_defined_locals,
) {
changed = true
}
let catch_equivalences = simplify_locals_new_equivalences(
func,
captures=Some(equivalences.captures),
read_versions=Some(equivalences.read_versions),
next_version=Some(equivalences.next_version),
)
if simplify_locals_scan_equivalent_region(
env,
func,
@ir.hot_region_catch_list_ref(func, node_id),
catch_equivalences,
get_counts,
tee_defined_locals,
) {
changed = true
}
simplify_locals_clear_equivalences(equivalences)
return changed
}
_ =>
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
}
if simplify_locals_scan_equivalent_node(
env,
func,
child_id,
equivalences,
get_counts,
tee_defined_locals,
@ir.hot_node_kind_is_call(func, node_id),
) {
changed = true
}
}
}
if simplify_locals_scan_equivalent_current(
env, func, node_id, equivalences, get_counts, tee_defined_locals, protect_tee_defined_locals,
) {
changed = true
}
changed
}
///|
fn simplify_locals_has_deferred_multi_use_local_set(
func : @ir.HotFunc,
get_counts : Array[Int32],
deferred_sets : Array[Int32],
) -> Bool {
for node_id in deferred_sets {
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet => {
let local_id = node.imm0
if local_id >= 0 &&
local_id < get_counts.length() &&
get_counts[local_id] > 1 {
return true
}
}
_ => ()
}
}
false
}
///|
fn simplify_locals_run_main_cycle(
ctx : HotPassContext,
func : @ir.HotFunc,
first_cycle : Bool,
detached : Array[Int32],
policy : SimplifyLocalsPolicy,
protect_control_embedded_local_tee : Bool,
get_count_cache~ : SimplifyLocalsGetCounts? = None,
get_count_scans~ : Array[Int32]? = None,
) -> (Bool, Bool) {
let sinkables = simplify_locals_new_sinkables(func, module_ctx=ctx.module_ctx)
let count_gets_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:count-local-gets",
)
let get_counts = simplify_locals_counts_for_cycle(
func,
get_count_cache,
full_scans=get_count_scans,
)
perf_stop_timer_accumulate_opt(
ctx.perf,
"detail:simplify-locals:count-local-gets",
count_gets_start_us,
)
let deferred_sets : Array[Int32] = []
let scan_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:scan-root-region",
)
let changed = simplify_locals_scan_region(
func,
@ir.HotRegionRef::root(),
sinkables,
first_cycle,
get_counts,
detached,
policy,
protect_control_embedded_local_tee,
false,
deferred_sets,
)
perf_stop_timer_accumulate_opt(
ctx.perf,
"detail:simplify-locals:scan-root-region",
scan_start_us,
)
let has_deferred = first_cycle &&
simplify_locals_has_deferred_multi_use_local_set(
func, get_counts, deferred_sets,
)
(changed, has_deferred)
}
///|
fn simplify_locals_collect_equivalent_tee_defined_locals(
func : @ir.HotFunc,
) -> Array[Bool] {
let tee_defined = 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) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalTee => {
let value_id = @ir.hot_child_get(func, node_id, 0)
match simplify_locals_equivalent_local_get_value(func, value_id) {
Some(_) =>
if node.imm0 >= 0 && node.imm0 < tee_defined.length() {
tee_defined[node.imm0] = true
}
None => ()
}
}
_ => ()
}
}
tee_defined
}
///|
fn simplify_locals_run_equivalent_cleanup(
ctx : HotPassContext,
func : @ir.HotFunc,
) -> Bool {
let env = simplify_locals_type_env(ctx)
let tee_defined_locals = simplify_locals_collect_equivalent_tee_defined_locals(
func,
)
simplify_locals_scan_equivalent_region(
env,
func,
@ir.HotRegionRef::root(),
simplify_locals_new_equivalences(func),
simplify_locals_count_local_gets(func),
tee_defined_locals,
)
}
///|
fn simplify_locals_equivalent_candidate_count(
func : @ir.HotFunc,
stop_after~ : Int32 = -1,
) -> Int32 {
let mut 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
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => {
if @ir.hot_child_count(func, node_id) == 0 {
continue
}
let value_id = @ir.hot_child_get(func, node_id, 0)
match simplify_locals_equivalent_local_get_value(func, value_id) {
Some(_) => {
count = count + 1
if stop_after >= 0 && count > stop_after {
return count
}
}
None => ()
}
}
_ => ()
}
}
count
}
///|
fn simplify_locals_fold_dead_set_copy_region(
ctx : HotPassContext,
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
get_counts : Array[Int32],
detached : Array[Int32],
nop_scan_visits~ : Array[Int32]? = None,
) -> Bool {
let mut changed = false
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
match @ir.hot_node_get(func, node_id).op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
@ir.hot_region_body_ref(func, node_id),
get_counts,
detached,
nop_scan_visits~,
) ||
changed
@ir.HotOp::If => {
changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
@ir.hot_region_then_ref(func, node_id),
get_counts,
detached,
nop_scan_visits~,
) ||
changed
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
else_ref,
get_counts,
detached,
nop_scan_visits~,
) ||
changed
None => ()
}
}
@ir.HotOp::Try => {
changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
@ir.hot_region_body_ref(func, node_id),
get_counts,
detached,
nop_scan_visits~,
) ||
changed
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
catch_ref,
get_counts,
detached,
nop_scan_visits~,
) ||
changed
None => ()
}
}
@ir.HotOp::TryTable => {
changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
@ir.hot_region_body_ref(func, node_id),
get_counts,
detached,
nop_scan_visits~,
) ||
changed
changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
@ir.hot_region_catch_list_ref(func, node_id),
get_counts,
detached,
nop_scan_visits~,
) ||
changed
}
_ => ()
}
}
let mut idx = 0
while idx + 3 < @ir.hot_region_root_count(func, region_ref) {
let value_id = @ir.hot_region_get(func, region_ref, idx)
if value_id >= 0 && @ir.hot_node_is_live(func, value_id) {
match @ir.hot_node_get(func, value_id).op {
@ir.HotOp::Nop => {
match nop_scan_visits {
Some(visits) => visits[0] += 1
None => ()
}
idx += 1
while idx < @ir.hot_region_root_count(func, region_ref) {
let run_id = @ir.hot_region_get(func, region_ref, idx)
if run_id < 0 || !@ir.hot_node_is_live(func, run_id) {
break
}
match @ir.hot_node_get(func, run_id).op {
@ir.HotOp::Nop => {
match nop_scan_visits {
Some(visits) => visits[0] += 1
None => ()
}
idx += 1
}
_ => break
}
}
continue
}
_ => ()
}
}
let spill_set_id = @ir.hot_region_get(func, region_ref, idx + 1)
let mut reload_idx = idx + 2
while reload_idx < @ir.hot_region_root_count(func, region_ref) {
let maybe_nop_id = @ir.hot_region_get(func, region_ref, reload_idx)
if maybe_nop_id < 0 || !@ir.hot_node_is_live(func, maybe_nop_id) {
break
}
match @ir.hot_node_get(func, maybe_nop_id).op {
@ir.HotOp::Nop => {
match nop_scan_visits {
Some(visits) => visits[0] += 1
None => ()
}
reload_idx += 1
}
_ => break
}
}
let target_idx = reload_idx + 1
if target_idx >= @ir.hot_region_root_count(func, region_ref) {
idx += 1
continue
}
let reload_id = @ir.hot_region_get(func, region_ref, reload_idx)
let target_set_id = @ir.hot_region_get(func, region_ref, target_idx)
if value_id >= 0 &&
spill_set_id >= 0 &&
reload_id >= 0 &&
target_set_id >= 0 &&
@ir.hot_node_is_live(func, value_id) &&
@ir.hot_node_is_live(func, spill_set_id) &&
@ir.hot_node_is_live(func, reload_id) &&
@ir.hot_node_is_live(func, target_set_id) {
let spill_set = @ir.hot_node_get(func, spill_set_id)
let reload = @ir.hot_node_get(func, reload_id)
let target_set = @ir.hot_node_get(func, target_set_id)
match (spill_set.op, reload.op, target_set.op) {
(@ir.HotOp::LocalSet, @ir.HotOp::LocalGet, @ir.HotOp::LocalSet) => {
let scratch = spill_set.imm0
if @ir.hot_node_result_arity(func, value_id) == 1 &&
spill_set.child_count == 0 &&
reload.child_count == 0 &&
target_set.child_count == 0 &&
scratch >= 0 &&
scratch < get_counts.length() &&
scratch != target_set.imm0 &&
reload.imm0 == scratch &&
get_counts[scratch] == 1 {
let replacement_set = @ir.hot_build_local_set_from_value(
func,
target_set.imm0,
value_id,
)
let removed = @ir.hot_region_splice(
func,
region_ref,
idx,
target_idx - idx + 1,
[replacement_set],
)
for removed_id in removed {
if removed_id != value_id {
detached.push(removed_id)
}
}
changed = true
idx += 1
continue
}
}
_ => ()
}
}
idx += 1
}
idx = 0
while idx + 1 < @ir.hot_region_root_count(func, region_ref) {
let first_id = @ir.hot_region_get(func, region_ref, idx)
if first_id >= 0 && @ir.hot_node_is_live(func, first_id) {
match @ir.hot_node_get(func, first_id).op {
@ir.HotOp::Nop => {
match nop_scan_visits {
Some(visits) => visits[0] += 1
None => ()
}
idx += 1
while idx < @ir.hot_region_root_count(func, region_ref) {
let run_id = @ir.hot_region_get(func, region_ref, idx)
if run_id < 0 || !@ir.hot_node_is_live(func, run_id) {
break
}
match @ir.hot_node_get(func, run_id).op {
@ir.HotOp::Nop => {
match nop_scan_visits {
Some(visits) => visits[0] += 1
None => ()
}
idx += 1
}
_ => break
}
}
continue
}
_ => ()
}
}
let mut second_idx = idx + 1
while second_idx < @ir.hot_region_root_count(func, region_ref) {
let maybe_nop_id = @ir.hot_region_get(func, region_ref, second_idx)
if maybe_nop_id < 0 || !@ir.hot_node_is_live(func, maybe_nop_id) {
break
}
match @ir.hot_node_get(func, maybe_nop_id).op {
@ir.HotOp::Nop => {
match nop_scan_visits {
Some(visits) => visits[0] += 1
None => ()
}
second_idx += 1
}
_ => break
}
}
if second_idx >= @ir.hot_region_root_count(func, region_ref) {
idx += 1
continue
}
let second_id = @ir.hot_region_get(func, region_ref, second_idx)
if first_id < 0 ||
second_id < 0 ||
!@ir.hot_node_is_live(func, first_id) ||
!@ir.hot_node_is_live(func, second_id) {
idx += 1
continue
}
let first = @ir.hot_node_get(func, first_id)
let second = @ir.hot_node_get(func, second_id)
match first.op {
@ir.HotOp::LocalSet => ()
_ => {
idx += 1
continue
}
}
match second.op {
@ir.HotOp::LocalSet => ()
_ => {
idx += 1
continue
}
}
if first.child_count != 1 || second.child_count != 1 {
idx += 1
continue
}
let scratch = first.imm0
if scratch < 0 ||
scratch >= get_counts.length() ||
scratch == second.imm0 ||
get_counts[scratch] != 1 {
idx += 1
continue
}
let get_id = @ir.hot_child_get(func, second_id, 0)
if get_id < 0 || !@ir.hot_node_is_live(func, get_id) {
idx += 1
continue
}
let get_node = @ir.hot_node_get(func, get_id)
match get_node.op {
@ir.HotOp::LocalGet => ()
_ => {
idx += 1
continue
}
}
if get_node.imm0 != scratch {
idx += 1
continue
}
if pass_node_use_count(ctx, func, get_id) != 1 {
idx += 1
continue
}
@ir.hot_replace_node(
func,
first_id,
simplify_locals_make_local_set(
func,
second.imm0,
@ir.hot_child_get(func, first_id, 0),
),
)
@ir.hot_replace_node(func, second_id, simplify_locals_make_nop(func))
detached.push(get_id)
changed = true
idx = second_idx + 1
}
changed
}
///|
fn simplify_locals_mark_one_armed_if_tail_sets(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
preserved : Array[Bool],
) -> Unit {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::If => {
let then_ref = @ir.hot_region_then_ref(func, node_id)
match @ir.hot_region_else_ref(func, node_id) {
None =>
match simplify_locals_region_tail_local_set(func, then_ref) {
Some(tail) =>
if tail.node_id >= 0 && tail.node_id < preserved.length() {
preserved[tail.node_id] = true
}
None => ()
}
Some(else_ref) =>
simplify_locals_mark_one_armed_if_tail_sets(
func, else_ref, preserved,
)
}
simplify_locals_mark_one_armed_if_tail_sets(func, then_ref, preserved)
}
@ir.HotOp::Block | @ir.HotOp::Loop =>
simplify_locals_mark_one_armed_if_tail_sets(
func,
@ir.hot_region_body_ref(func, node_id),
preserved,
)
@ir.HotOp::Try => {
simplify_locals_mark_one_armed_if_tail_sets(
func,
@ir.hot_region_body_ref(func, node_id),
preserved,
)
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
simplify_locals_mark_one_armed_if_tail_sets(
func, catch_ref, preserved,
)
None => ()
}
}
@ir.HotOp::TryTable => {
simplify_locals_mark_one_armed_if_tail_sets(
func,
@ir.hot_region_body_ref(func, node_id),
preserved,
)
simplify_locals_mark_one_armed_if_tail_sets(
func,
@ir.hot_region_catch_list_ref(func, node_id),
preserved,
)
}
_ => ()
}
}
}
///|
fn simplify_locals_one_armed_if_tail_sets(func : @ir.HotFunc) -> Array[Bool] {
let preserved = Array::make(@ir.hot_node_count(func), false)
simplify_locals_mark_one_armed_if_tail_sets(
func,
@ir.HotRegionRef::root(),
preserved,
)
preserved
}
///|
fn simplify_locals_run_dead_cleanup(
ctx : HotPassContext,
func : @ir.HotFunc,
detached : Array[Int32],
rehomed_owners : Array[SimplifyLocalsDetachedControlOwner],
discount_unreachable_tail_gets : Bool,
preserve_self_copy_reads~ : Bool = false,
get_count_cache~ : SimplifyLocalsGetCounts? = None,
get_count_scans~ : Array[Int32]? = None,
) -> Bool {
let get_counts = simplify_locals_counts_for_cycle(
func,
get_count_cache,
full_scans=get_count_scans,
)
if discount_unreachable_tail_gets {
simplify_locals_discount_unreachable_root_tail_gets(func, get_counts)
}
let one_armed_if_tail_sets = simplify_locals_one_armed_if_tail_sets(func)
let mut changed = simplify_locals_fold_dead_set_copy_region(
ctx,
func,
@ir.HotRegionRef::root(),
get_counts,
detached,
)
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::LocalSet => {
let value_id = @ir.hot_child_get(func, node_id, 0)
if value_id >= 0 && @ir.hot_node_is_live(func, value_id) {
let value = @ir.hot_node_get(func, value_id)
match value.op {
@ir.HotOp::LocalTee if value.imm0 == node.imm0 => {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_local_set(
func,
node.imm0,
@ir.hot_child_get(func, value_id, 0),
),
)
detached.push(value_id)
changed = true
continue
}
@ir.HotOp::LocalGet if value.imm0 == node.imm0 => {
if preserve_self_copy_reads {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_drop(func, value_id),
)
} else {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_nop(func),
)
detached.push(value_id)
}
changed = true
continue
}
_ => ()
}
}
if get_counts[node.imm0] != 0 {
continue
}
if node_id >= 0 &&
node_id < one_armed_if_tail_sets.length() &&
one_armed_if_tail_sets[node_id] {
continue
}
if simplify_locals_value_is_pure(func, value_id) {
@ir.hot_replace_node(func, node_id, simplify_locals_make_nop(func))
detached.push(value_id)
} else {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_drop(func, value_id),
)
}
changed = true
}
@ir.HotOp::LocalTee => {
let value_id = @ir.hot_child_get(func, node_id, 0)
if value_id >= 0 && @ir.hot_node_is_live(func, value_id) {
let value = @ir.hot_node_get(func, value_id)
match value.op {
@ir.HotOp::LocalGet if value.imm0 == node.imm0 => {
@ir.hot_replace_node(
func,
node_id,
@ir.hot_node_get(func, value_id),
)
detached.push(value_id)
changed = true
continue
}
_ => ()
}
}
if get_counts[node.imm0] != 0 {
continue
}
if value_id >= 0 &&
@ir.hot_node_is_live(func, value_id) &&
@ir.hot_node_kind_is_control(func, value_id) {
rehomed_owners.push({
replacement_id: node_id,
detached_id: value_id,
})
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, value_id))
detached.push(value_id)
changed = true
continue
}
if simplify_locals_value_can_clone_node(func, value_id) {
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, value_id))
detached.push(value_id)
changed = true
}
}
_ => ()
}
}
changed
}
///|
fn simplify_locals_try_remove_nostructure_transparent_copy_chain(
func : @ir.HotFunc,
detached : Array[Int32],
) -> Bool {
// Binaryen's tee-enabled no-structure cycles delete this exact transparent
// copy component before its final dropped read. Keep the precleanup narrow:
// two distinct scratch locals, one inner branch-free block, exact aggregate
// use counts, matching defaultable types, and no effectful value producers.
let region_ref = @ir.HotRegionRef::root()
let get_counts = simplify_locals_count_local_gets(func)
for first_idx = 0
first_idx < @ir.hot_region_root_count(func, region_ref)
first_idx = first_idx + 1 {
let first_id = @ir.hot_region_get(func, region_ref, first_idx)
if first_id < 0 || !@ir.hot_node_is_live(func, first_id) {
continue
}
let first = @ir.hot_node_get(func, first_id)
match first.op {
@ir.HotOp::LocalSet => ()
_ => continue
}
if first.child_count != 1 {
continue
}
let first_get_id = @ir.hot_child_get(func, first_id, 0)
if first_get_id < 0 || !@ir.hot_node_is_live(func, first_get_id) {
continue
}
let first_get = @ir.hot_node_get(func, first_get_id)
match first_get.op {
@ir.HotOp::LocalGet => ()
_ => continue
}
if first_get.child_count != 0 {
continue
}
let source_local = first_get.imm0
let target_local = first.imm0
let block_idx = match
simplify_locals_region_next_live_non_nop_root_idx(
func, region_ref, first_idx,
) {
Some(idx) => idx
None => continue
}
let block_id = @ir.hot_region_get(func, region_ref, block_idx)
if block_id < 0 || !@ir.hot_node_is_live(func, block_id) {
continue
}
match @ir.hot_node_get(func, block_id).op {
@ir.HotOp::Block => ()
_ => continue
}
let block_body = @ir.hot_region_body_ref(func, block_id)
let inner_idx = match
simplify_locals_region_last_live_non_nop_root_idx(func, block_body) {
Some(idx) => idx
None => continue
}
let mut live_inner_count = 0
for idx = 0
idx < @ir.hot_region_root_count(func, block_body)
idx = idx + 1 {
let inner_root = @ir.hot_region_get(func, block_body, idx)
if inner_root >= 0 && @ir.hot_node_is_live(func, inner_root) {
match @ir.hot_node_get(func, inner_root).op {
@ir.HotOp::Nop => ()
_ => live_inner_count = live_inner_count + 1
}
}
}
if live_inner_count != 1 {
continue
}
let inner_set_id = @ir.hot_region_get(func, block_body, inner_idx)
let inner_set = @ir.hot_node_get(func, inner_set_id)
match inner_set.op {
@ir.HotOp::LocalSet => ()
_ => continue
}
if inner_set.child_count != 1 {
continue
}
let inner_get_id = @ir.hot_child_get(func, inner_set_id, 0)
if inner_get_id < 0 || !@ir.hot_node_is_live(func, inner_get_id) {
continue
}
let inner_get = @ir.hot_node_get(func, inner_get_id)
match inner_get.op {
@ir.HotOp::LocalGet => ()
_ => continue
}
let scratch_local = inner_set.imm0
if inner_get.imm0 != target_local ||
scratch_local == target_local ||
scratch_local == source_local {
continue
}
let copy_back_idx = match
simplify_locals_region_next_live_non_nop_root_idx(
func, region_ref, block_idx,
) {
Some(idx) => idx
None => continue
}
let copy_back_id = @ir.hot_region_get(func, region_ref, copy_back_idx)
let copy_back = @ir.hot_node_get(func, copy_back_id)
match copy_back.op {
@ir.HotOp::LocalSet => ()
_ => continue
}
if copy_back.imm0 != target_local || copy_back.child_count != 1 {
continue
}
let scratch_get_id = @ir.hot_child_get(func, copy_back_id, 0)
if scratch_get_id < 0 || !@ir.hot_node_is_live(func, scratch_get_id) {
continue
}
let scratch_get = @ir.hot_node_get(func, scratch_get_id)
match scratch_get.op {
@ir.HotOp::LocalGet => ()
_ => continue
}
if scratch_get.imm0 != scratch_local {
continue
}
let drop_idx = match
simplify_locals_region_next_live_non_nop_root_idx(
func, region_ref, copy_back_idx,
) {
Some(idx) => idx
None => continue
}
let drop_id = @ir.hot_region_get(func, region_ref, drop_idx)
let drop_node = @ir.hot_node_get(func, drop_id)
match drop_node.op {
@ir.HotOp::Drop => ()
_ => continue
}
if drop_node.child_count != 1 {
continue
}
let target_get_id = @ir.hot_child_get(func, drop_id, 0)
if target_get_id < 0 || !@ir.hot_node_is_live(func, target_get_id) {
continue
}
let target_get = @ir.hot_node_get(func, target_get_id)
match target_get.op {
@ir.HotOp::LocalGet => ()
_ => continue
}
if target_get.imm0 != target_local ||
target_local < 0 ||
target_local >= get_counts.length() ||
scratch_local < 0 ||
scratch_local >= get_counts.length() ||
get_counts[target_local] != 2 ||
get_counts[scratch_local] != 1 ||
@ir.hot_local_type(func, source_local) !=
@ir.hot_local_type(func, target_local) ||
@ir.hot_local_type(func, target_local) !=
@ir.hot_local_type(func, scratch_local) ||
!simplify_locals_local_type_is_defaultable(func, target_local) {
continue
}
for node_id in [first_id, inner_set_id, copy_back_id, drop_id] {
@ir.hot_replace_node(func, node_id, simplify_locals_make_nop(func))
}
for node_id in [first_get_id, inner_get_id, scratch_get_id, target_get_id] {
detached.push(node_id)
}
return true
}
false
}
///|
fn simplify_locals_run_with_options(
ctx : HotPassContext,
func : @ir.HotFunc,
policy : SimplifyLocalsPolicy,
reuse_get_counts~ : Bool = true,
get_count_scans~ : Array[Int32]? = None,
) -> HotPassResult {
let get_count_cache = if reuse_get_counts {
simplify_locals_new_get_count_cache(func)
} else {
None
}
let precleanup_detached : Array[Int32] = []
let precleanup_changed = !policy.allow_structure_rewrites &&
policy.allow_sink_tee &&
policy.allow_nesting &&
simplify_locals_try_remove_nostructure_transparent_copy_chain(
func, precleanup_detached,
)
simplify_locals_delete_detached_nodes(func, precleanup_detached)
let mut first_cycle = true
let mut deferred_follow_up_cycle = false
let mut changed = precleanup_changed
while true {
let current_is_deferred_follow_up = deferred_follow_up_cycle
deferred_follow_up_cycle = false
let main_cycle_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:main-cycle",
)
let main_detached : Array[Int32] = []
let (main_changed, has_deferred_main_cycle) = simplify_locals_run_main_cycle(
ctx,
func,
first_cycle,
main_detached,
policy,
false,
get_count_cache~,
get_count_scans~,
)
perf_stop_timer_opt(
ctx.perf,
"detail:simplify-locals:main-cycle",
main_cycle_start_us,
)
let main_delete_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:delete-main-detached",
)
simplify_locals_delete_detached_nodes(func, main_detached)
perf_stop_timer_accumulate_opt(
ctx.perf,
"detail:simplify-locals:delete-main-detached",
main_delete_start_us,
)
let cleanup_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:dead-cleanup",
)
let cleanup_detached : Array[Int32] = []
let cleanup_rehomed_owners : Array[SimplifyLocalsDetachedControlOwner] = []
let cleanup_changed = simplify_locals_run_dead_cleanup(
ctx,
func,
cleanup_detached,
cleanup_rehomed_owners,
policy.allow_nesting,
preserve_self_copy_reads=policy.allow_structure_rewrites &&
!policy.allow_sink_tee,
get_count_cache~,
get_count_scans~,
)
perf_stop_timer_accumulate_opt(
ctx.perf,
"detail:simplify-locals:dead-cleanup",
cleanup_start_us,
)
let cleanup_delete_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:delete-dead-detached",
)
simplify_locals_rehome_detached_control_owners(func, cleanup_rehomed_owners)
simplify_locals_delete_detached_nodes(func, cleanup_detached)
perf_stop_timer_accumulate_opt(
ctx.perf,
"detail:simplify-locals:delete-dead-detached",
cleanup_delete_start_us,
)
if main_changed || cleanup_changed {
changed = true
}
if first_cycle {
first_cycle = false
// Binaryen's initial follow-up main cycle is needed for deferred multi-use
// local.set work that may become tee-shaped on the later cycle. No-tee
// variants also need one follow-up after a main or dead-cleanup rewrite:
// copy retargeting can expose a computed set/get only after the cycle's
// get facts were collected, and leaving that carrier for the next pass is
// non-idempotent.
if has_deferred_main_cycle ||
(policy.allow_structure_rewrites && main_changed) ||
(!policy.allow_sink_tee && (main_changed || cleanup_changed)) {
deferred_follow_up_cycle = true
continue
}
} else if main_changed || cleanup_changed {
// The deferred follow-up is the deliberate second main/dead pass for
// multi-use tee/drop cleanup. Its changes do not require a third no-op
// confirmation scan before the late no-structure cleanup gates below.
if !current_is_deferred_follow_up {
continue
}
}
if policy.allow_structure_rewrites {
let structure_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:structure",
)
let structure_detached : Array[Int32] = []
let structure_changed = simplify_locals_run_structure_rewrites(
func,
structure_detached,
allow_multi_use=policy.allow_sink_tee,
)
perf_stop_timer_opt(
ctx.perf,
"detail:simplify-locals:structure",
structure_start_us,
)
let structure_delete_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:delete-structure-detached",
)
simplify_locals_delete_detached_nodes(func, structure_detached)
perf_stop_timer_opt(
ctx.perf,
"detail:simplify-locals:delete-structure-detached",
structure_delete_start_us,
)
if structure_changed {
changed = true
continue
}
}
let local_count = @ir.hot_local_count(func)
if local_count >= 96 {
break
}
let sparse_equivalent_limit = if local_count >= 56 {
local_count / 6
} else {
-1
}
let equivalent_candidate_count = simplify_locals_equivalent_candidate_count(
func,
stop_after=sparse_equivalent_limit,
)
if equivalent_candidate_count == 0 ||
(
sparse_equivalent_limit >= 0 &&
equivalent_candidate_count <= sparse_equivalent_limit
) {
break
}
let equivalent_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:equivalent-cleanup",
)
let late_changed = simplify_locals_run_equivalent_cleanup(ctx, func)
perf_stop_timer_opt(
ctx.perf,
"detail:simplify-locals:equivalent-cleanup",
equivalent_start_us,
)
let late_cleanup_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:late-dead-cleanup",
)
let late_cleanup_detached : Array[Int32] = []
let late_cleanup_rehomed_owners : Array[SimplifyLocalsDetachedControlOwner] = []
let late_cleanup_changed = simplify_locals_run_dead_cleanup(
ctx,
func,
late_cleanup_detached,
late_cleanup_rehomed_owners,
policy.allow_nesting,
preserve_self_copy_reads=policy.allow_structure_rewrites &&
!policy.allow_sink_tee,
get_count_cache~,
get_count_scans~,
)
perf_stop_timer_opt(
ctx.perf,
"detail:simplify-locals:late-dead-cleanup",
late_cleanup_start_us,
)
let late_delete_start_us = perf_start_timer_opt(
ctx.perf,
"detail:simplify-locals:delete-late-detached",
)
simplify_locals_rehome_detached_control_owners(
func, late_cleanup_rehomed_owners,
)
simplify_locals_delete_detached_nodes(func, late_cleanup_detached)
perf_stop_timer_opt(
ctx.perf,
"detail:simplify-locals:delete-late-detached",
late_delete_start_us,
)
if late_changed || late_cleanup_changed {
changed = true
continue
}
break
}
if !changed {
return HotPassResult::unchanged()
}
ignore(@ir.hot_repair_nondefaultable_local_scopes(func))
pass_mark_mutated(ctx, func)
HotPassResult::changed()
}
///|
fn simplify_locals_is_region_child_slot(
func : @ir.HotFunc,
node_id : Int32,
slot : Int32,
) -> Bool {
if @ir.hot_node_get(func, node_id).op is @ir.HotOp::If {
slot == 1 || slot == 2
} else {
slot >= pass_value_operand_count(func, node_id)
}
}
///|
fn simplify_locals_region_contains_node_before(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
before_node_id : Int32,
seen : Array[Bool],
) -> Bool {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
if simplify_locals_node_contains_value_before(
func,
@ir.hot_region_get(func, region_ref, idx),
before_node_id,
seen,
) {
return true
}
}
false
}
///|
fn simplify_locals_node_contains_value_before(
func : @ir.HotFunc,
node_id : Int32,
before_node_id : Int32,
seen : Array[Bool],
) -> Bool {
if node_id < 0 ||
node_id >= seen.length() ||
seen[node_id] ||
!@ir.hot_node_is_live(func, node_id) {
return false
}
seen[node_id] = true
if node_id < before_node_id && @ir.hot_node_result_arity(func, node_id) > 0 {
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 {
if !simplify_locals_is_region_child_slot(func, node_id, slot) &&
simplify_locals_node_contains_value_before(
func,
@ir.hot_child_get(func, node_id, slot),
before_node_id,
seen,
) {
return true
}
}
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
simplify_locals_region_contains_node_before(
func,
@ir.hot_region_body_ref(func, node_id),
before_node_id,
seen,
)
@ir.HotOp::If => {
if simplify_locals_region_contains_node_before(
func,
@ir.hot_region_then_ref(func, node_id),
before_node_id,
seen,
) {
return true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
simplify_locals_region_contains_node_before(
func, else_ref, before_node_id, seen,
)
None => false
}
}
@ir.HotOp::Try => {
if simplify_locals_region_contains_node_before(
func,
@ir.hot_region_body_ref(func, node_id),
before_node_id,
seen,
) {
return true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
simplify_locals_region_contains_node_before(
func, catch_ref, before_node_id, seen,
)
None => false
}
}
@ir.HotOp::TryTable =>
simplify_locals_region_contains_node_before(
func,
@ir.hot_region_body_ref(func, node_id),
before_node_id,
seen,
) ||
simplify_locals_region_contains_node_before(
func,
@ir.hot_region_catch_list_ref(func, node_id),
before_node_id,
seen,
)
_ => false
}
}
///|
fn simplify_locals_set_first_use_crosses_intervening_local_write(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
root_idx : Int32,
local_id : Int32,
minimums~ : SimplifyLocalsValueOrderMinimums? = None,
later_seen_rows~ : Array[Int32]? = None,
) -> Bool {
if root_idx < 0 {
return false
}
let local_count = @ir.hot_local_count(func)
let intervening_reads : Array[Int32] = []
let intervening_writes : Array[Int32] = []
let intervening_read_stamps = Array::make(local_count, -1)
let intervening_write_stamps = Array::make(local_count, -1)
for idx = root_idx + 1
idx < @ir.hot_region_root_count(func, region_ref)
idx = idx + 1 {
let root_id = @ir.hot_region_get(func, region_ref, idx)
if root_id < 0 || !@ir.hot_node_is_live(func, root_id) {
continue
}
if simplify_locals_node_reads_local(func, root_id, local_id) {
let use_reads : Array[Int32] = []
let use_writes : Array[Int32] = []
simplify_locals_collect_subtree_local_effects(
func,
root_id,
use_reads,
use_writes,
Array::make(local_count, -1),
Array::make(local_count, -1),
0,
)
for read_local in use_reads {
if intervening_writes.contains(read_local) {
return true
}
}
let use_root_writes_local = match @ir.hot_node_get(func, root_id).op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee => true
_ => false
}
if use_root_writes_local && !use_writes.is_empty() {
for later_idx = idx + 1
later_idx < @ir.hot_region_root_count(func, region_ref)
later_idx = later_idx + 1 {
let later_root = @ir.hot_region_get(func, region_ref, later_idx)
if later_root < 0 || !@ir.hot_node_is_live(func, later_root) {
continue
}
if simplify_locals_contains_value_before_with_minimums(
func,
later_root,
root_id,
minimums,
seen_rows=later_seen_rows,
) {
let later_reads : Array[Int32] = []
let later_writes : Array[Int32] = []
simplify_locals_collect_subtree_local_effects(
func,
later_root,
later_reads,
later_writes,
Array::make(local_count, -1),
Array::make(local_count, -1),
0,
)
for written_local in use_writes {
if later_reads.contains(written_local) {
return true
}
}
}
}
}
return false
}
simplify_locals_collect_subtree_local_effects(
func, root_id, intervening_reads, intervening_writes, intervening_read_stamps,
intervening_write_stamps, 0,
)
let root = @ir.hot_node_get(func, root_id)
match root.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee if root.imm0 == local_id =>
return false
_ => ()
}
}
false
}
///|
fn simplify_locals_set_crosses_older_stack_value(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
root_idx : Int32,
set_node_id : Int32,
local_id : Int32,
minimums~ : SimplifyLocalsValueOrderMinimums? = None,
) -> Bool {
if root_idx < 0 {
return false
}
for idx = root_idx + 1
idx < @ir.hot_region_root_count(func, region_ref)
idx = idx + 1 {
let root_id = @ir.hot_region_get(func, region_ref, idx)
if root_id < 0 || !@ir.hot_node_is_live(func, root_id) {
continue
}
if simplify_locals_contains_value_before_with_minimums(
func, root_id, set_node_id, minimums,
) {
return true
}
if simplify_locals_node_reads_local(func, root_id, local_id) {
return false
}
let root = @ir.hot_node_get(func, root_id)
match root.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee if root.imm0 == local_id =>
return false
_ => ()
}
}
false
}
///|
fn simplify_locals_node_has_stack_effect_hazard(
func : @ir.HotFunc,
node_id : @ir.NodeId,
) -> 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::Call
| @ir.HotOp::CallIndirect
| @ir.HotOp::CallRef
| @ir.HotOp::MemoryCopy
| @ir.HotOp::MemoryFill => return true
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if simplify_locals_is_region_child_slot(func, node_id, slot) {
continue
}
if simplify_locals_node_has_stack_effect_hazard(
func,
@ir.hot_child_get(func, node_id, slot),
) {
return true
}
}
false
}
///|
fn simplify_locals_region_reads_local(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
local_id : Int32,
) -> Bool {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
if simplify_locals_node_reads_local(
func,
@ir.hot_region_get(func, region_ref, idx),
local_id,
) {
return true
}
}
false
}
///|
fn simplify_locals_node_reads_local(
func : @ir.HotFunc,
node_id : @ir.NodeId,
local_id : Int32,
) -> 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::LocalGet if node.imm0 == local_id => return true
@ir.HotOp::Block | @ir.HotOp::Loop =>
if simplify_locals_region_reads_local(
func,
@ir.hot_region_body_ref(func, node_id),
local_id,
) {
return true
}
@ir.HotOp::If => {
if simplify_locals_region_reads_local(
func,
@ir.hot_region_then_ref(func, node_id),
local_id,
) {
return true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
if simplify_locals_region_reads_local(func, else_ref, local_id) {
return true
}
None => ()
}
}
@ir.HotOp::Try => {
if simplify_locals_region_reads_local(
func,
@ir.hot_region_body_ref(func, node_id),
local_id,
) {
return true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
if simplify_locals_region_reads_local(func, catch_ref, local_id) {
return true
}
None => ()
}
}
@ir.HotOp::TryTable =>
if simplify_locals_region_reads_local(
func,
@ir.hot_region_body_ref(func, node_id),
local_id,
) ||
simplify_locals_region_reads_local(
func,
@ir.hot_region_catch_list_ref(func, node_id),
local_id,
) {
return true
}
_ => ()
}
let operand_node = @ir.hot_node_get(func, node_id)
let operand_count = pass_value_operand_count(func, node_id)
for slot = 0; slot < operand_count; slot = slot + 1 {
if simplify_locals_node_reads_local(
func,
@ir.hot_child_get(
func,
node_id,
pass_value_operand_slot(operand_node, slot),
),
local_id,
) {
return true
}
}
false
}
///|
priv struct SimplifyLocalsRootHazardScanState {
pending_writers : Array[Bool]
seen_stamps : Array[Int32]
mut next_stamp : Int32
mut pending_count : Int32
mut visited_nodes : Int32
}
///|
fn SimplifyLocalsRootHazardScanState::new(
func : @ir.HotFunc,
) -> SimplifyLocalsRootHazardScanState {
{
pending_writers: Array::make(@ir.hot_local_count(func), false),
seen_stamps: Array::make(@ir.hot_node_count(func), 0),
next_stamp: 0,
pending_count: 0,
visited_nodes: 0,
}
}
///|
fn simplify_locals_region_reads_pending_root_hazard_local(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
state : SimplifyLocalsRootHazardScanState,
) -> Bool {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
if simplify_locals_node_reads_pending_root_hazard_local(
func,
@ir.hot_region_get(func, region_ref, idx),
state,
) {
return true
}
}
false
}
///|
fn simplify_locals_node_reads_pending_root_hazard_local(
func : @ir.HotFunc,
node_id : @ir.NodeId,
state : SimplifyLocalsRootHazardScanState,
) -> Bool {
if node_id < 0 ||
node_id >= state.seen_stamps.length() ||
!@ir.hot_node_is_live(func, node_id) ||
state.seen_stamps[node_id] == state.next_stamp {
return false
}
state.seen_stamps[node_id] = state.next_stamp
state.visited_nodes += 1
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalGet if node.imm0 >= 0 &&
node.imm0 < state.pending_writers.length() &&
state.pending_writers[node.imm0] => return true
@ir.HotOp::Block | @ir.HotOp::Loop =>
if simplify_locals_region_reads_pending_root_hazard_local(
func,
@ir.hot_region_body_ref(func, node_id),
state,
) {
return true
}
@ir.HotOp::If => {
if simplify_locals_region_reads_pending_root_hazard_local(
func,
@ir.hot_region_then_ref(func, node_id),
state,
) {
return true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
if simplify_locals_region_reads_pending_root_hazard_local(
func, else_ref, state,
) {
return true
}
None => ()
}
}
@ir.HotOp::Try => {
if simplify_locals_region_reads_pending_root_hazard_local(
func,
@ir.hot_region_body_ref(func, node_id),
state,
) {
return true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
if simplify_locals_region_reads_pending_root_hazard_local(
func, catch_ref, state,
) {
return true
}
None => ()
}
}
@ir.HotOp::TryTable =>
if simplify_locals_region_reads_pending_root_hazard_local(
func,
@ir.hot_region_body_ref(func, node_id),
state,
) ||
simplify_locals_region_reads_pending_root_hazard_local(
func,
@ir.hot_region_catch_list_ref(func, node_id),
state,
) {
return true
}
_ => ()
}
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if simplify_locals_is_region_child_slot(func, node_id, slot) {
continue
}
if simplify_locals_node_reads_pending_root_hazard_local(
func,
@ir.hot_child_get(func, node_id, slot),
state,
) {
return true
}
}
false
}
///|
fn simplify_locals_root_local_set_stack_hazard_scan(
func : @ir.HotFunc,
) -> (Bool, Int32) {
let root_ref = @ir.HotRegionRef::root()
if @ir.hot_region_root_count(func, root_ref) <= 1 {
return (false, 0)
}
let state = SimplifyLocalsRootHazardScanState::new(func)
for idx = 0; idx < @ir.hot_region_root_count(func, root_ref); idx = idx + 1 {
let root_id = @ir.hot_region_get(func, root_ref, idx)
if root_id < 0 || !@ir.hot_node_is_live(func, root_id) {
continue
}
let has_stack_hazard = simplify_locals_node_has_stack_effect_hazard(
func, root_id,
)
if has_stack_hazard && state.pending_count > 0 {
state.next_stamp += 1
if simplify_locals_node_reads_pending_root_hazard_local(
func, root_id, state,
) {
return (true, state.visited_nodes)
}
}
let root = @ir.hot_node_get(func, root_id)
match root.op {
@ir.HotOp::LocalSet | @ir.HotOp::LocalTee if root.imm0 >= 0 &&
root.imm0 < state.pending_writers.length() => {
if state.pending_writers[root.imm0] {
state.pending_count -= 1
}
state.pending_writers[root.imm0] = has_stack_hazard
if has_stack_hazard {
state.pending_count += 1
}
}
_ => ()
}
}
(false, state.visited_nodes)
}
///|
fn simplify_locals_should_skip_root_local_set_stack_hazard(
func : @ir.HotFunc,
) -> Bool {
simplify_locals_root_local_set_stack_hazard_scan(func).0
}
///|
fn simplify_locals_should_skip_root_local_set_stack_global_hazard(
func : @ir.HotFunc,
) -> Bool {
let root_ref = @ir.HotRegionRef::root()
if @ir.hot_region_root_count(func, root_ref) <= 1 {
return false
}
let mut has_local_set = false
let mut has_stack_effect = false
let mut has_global_state = false
for idx = 0; idx < @ir.hot_region_root_count(func, root_ref); idx = idx + 1 {
let root_id = @ir.hot_region_get(func, root_ref, idx)
if root_id < 0 || !@ir.hot_node_is_live(func, root_id) {
continue
}
match @ir.hot_node_get(func, root_id).op {
@ir.HotOp::LocalSet => has_local_set = true
_ => ()
}
if simplify_locals_node_has_stack_effect_hazard(func, root_id) {
has_stack_effect = true
}
if simplify_locals_effects_has(
simplify_locals_effects_mask_for_subtree(func, root_id),
@ir.EFFECT_MASK_GLOBAL_STATE,
) {
has_global_state = true
}
}
has_local_set && has_stack_effect && has_global_state
}
///|
fn simplify_locals_memory_write_scan_push_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
pending : Array[Int32],
) -> Unit {
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
pending.push(@ir.hot_region_get(func, region_ref, idx))
}
}
///|
fn simplify_locals_root_memory_write_scan(func : @ir.HotFunc) -> (Bool, Int32) {
let pending : Array[Int32] = []
simplify_locals_memory_write_scan_push_region(
func,
@ir.HotRegionRef::root(),
pending,
)
let seen = Array::make(@ir.hot_node_count(func), false)
let mut visited_nodes = 0
while !pending.is_empty() {
let node_id = pending.pop().unwrap()
if node_id < 0 ||
node_id >= seen.length() ||
seen[node_id] ||
!@ir.hot_node_is_live(func, node_id) {
continue
}
seen[node_id] = true
visited_nodes += 1
if simplify_locals_effects_has(
simplify_locals_effects_mask_for_exact_node(func, node_id),
@ir.EFFECT_MASK_MEMORY_WRITE,
) {
return (true, visited_nodes)
}
let node = @ir.hot_node_get(func, node_id)
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if !simplify_locals_is_region_child_slot(func, node_id, slot) {
pending.push(@ir.hot_child_get(func, node_id, slot))
}
}
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
simplify_locals_memory_write_scan_push_region(
func,
@ir.hot_region_body_ref(func, node_id),
pending,
)
@ir.HotOp::If => {
simplify_locals_memory_write_scan_push_region(
func,
@ir.hot_region_then_ref(func, node_id),
pending,
)
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
simplify_locals_memory_write_scan_push_region(
func, else_ref, pending,
)
None => ()
}
}
@ir.HotOp::Try => {
simplify_locals_memory_write_scan_push_region(
func,
@ir.hot_region_body_ref(func, node_id),
pending,
)
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
simplify_locals_memory_write_scan_push_region(
func, catch_ref, pending,
)
None => ()
}
}
@ir.HotOp::TryTable => {
simplify_locals_memory_write_scan_push_region(
func,
@ir.hot_region_body_ref(func, node_id),
pending,
)
simplify_locals_memory_write_scan_push_region(
func,
@ir.hot_region_catch_list_ref(func, node_id),
pending,
)
}
_ => ()
}
}
(false, visited_nodes)
}
///|
fn simplify_locals_large_local_tee_memory_write_hazard_scan(
func : @ir.HotFunc,
) -> (Bool, Int32) {
if @ir.hot_local_count(func) < 16 || !simplify_locals_has_local_tee(func) {
return (false, 0)
}
simplify_locals_root_memory_write_scan(func)
}
///|
fn simplify_locals_should_skip_large_local_tee_memory_write_hazard(
func : @ir.HotFunc,
) -> Bool {
simplify_locals_large_local_tee_memory_write_hazard_scan(func).0
}
///|
fn simplify_locals_run(
ctx : HotPassContext,
func : @ir.HotFunc,
) -> HotPassResult {
if simplify_locals_should_skip_large_local_tee_memory_write_hazard(func) {
pipeline_trace_hook(
ctx.trace,
ctx.descriptor.name,
"skip-hot",
payload="reason=large-local-tee-memory-write-hazard-noop",
)
return HotPassResult::unchanged()
}
simplify_locals_run_with_options(
ctx,
func,
SimplifyLocalsPolicy::new(true, true, true),
)
}
///|
fn simplify_locals_notee_run(
ctx : HotPassContext,
func : @ir.HotFunc,
) -> HotPassResult {
if simplify_locals_should_skip_large_local_tee_memory_write_hazard(func) {
pipeline_trace_hook(
ctx.trace,
ctx.descriptor.name,
"skip-hot",
payload="reason=large-local-tee-memory-write-hazard-noop",
)
return HotPassResult::unchanged()
}
if (@ir.hot_local_count(func) >= 64 || @ir.hot_node_count(func) >= 512) &&
simplify_locals_should_skip_root_local_set_stack_global_hazard(func) {
pipeline_trace_hook(
ctx.trace,
ctx.descriptor.name,
"skip-hot",
payload="reason=root-local-set-stack-global-hazard-noop",
)
return HotPassResult::unchanged()
}
simplify_locals_run_with_options(
ctx,
func,
SimplifyLocalsPolicy::new(true, false, true),
)
}
///|
fn simplify_locals_has_local_tee(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::LocalTee => return true
_ => ()
}
}
false
}
///|
fn simplify_locals_region_has_control_embedded_local_tee(
func : @ir.HotFunc,
region : @ir.HotRegionRef,
inside_control : Bool,
) -> Bool {
for idx = 0; idx < @ir.hot_region_root_count(func, region); idx = idx + 1 {
if simplify_locals_node_has_control_embedded_local_tee(
func,
@ir.hot_region_get(func, region, idx),
inside_control,
) {
return true
}
}
false
}
///|
fn simplify_locals_node_has_control_embedded_local_tee(
func : @ir.HotFunc,
node_id : Int32,
inside_control : Bool,
) -> 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 => return inside_control
_ => ()
}
let child_inside_control = inside_control ||
(match node.op {
@ir.HotOp::Block
| @ir.HotOp::Loop
| @ir.HotOp::If
| @ir.HotOp::Try
| @ir.HotOp::TryTable => true
_ => false
})
for slot = 0; slot < @ir.hot_child_count(func, node_id); slot = slot + 1 {
if simplify_locals_is_region_child_slot(func, node_id, slot) {
continue
}
if simplify_locals_node_has_control_embedded_local_tee(
func,
@ir.hot_child_get(func, node_id, slot),
child_inside_control,
) {
return true
}
}
match node.op {
@ir.HotOp::Block | @ir.HotOp::Loop =>
simplify_locals_region_has_control_embedded_local_tee(
func,
@ir.hot_region_body_ref(func, node_id),
true,
)
@ir.HotOp::If => {
if simplify_locals_region_has_control_embedded_local_tee(
func,
@ir.hot_region_then_ref(func, node_id),
true,
) {
return true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_region) =>
simplify_locals_region_has_control_embedded_local_tee(
func, else_region, true,
)
None => false
}
}
@ir.HotOp::Try => {
if simplify_locals_region_has_control_embedded_local_tee(
func,
@ir.hot_region_body_ref(func, node_id),
true,
) {
return true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_region) =>
simplify_locals_region_has_control_embedded_local_tee(
func, catch_region, true,
)
None => false
}
}
@ir.HotOp::TryTable =>
simplify_locals_region_has_control_embedded_local_tee(
func,
@ir.hot_region_body_ref(func, node_id),
true,
) ||
simplify_locals_region_has_control_embedded_local_tee(
func,
@ir.hot_region_catch_list_ref(func, node_id),
true,
)
_ => false
}
}
///|
fn simplify_locals_has_control_embedded_local_tee(func : @ir.HotFunc) -> Bool {
simplify_locals_region_has_control_embedded_local_tee(
func,
@ir.HotRegionRef::root(),
false,
)
}
///|
fn simplify_locals_lower_dropped_tees_in_protected_region(
func : @ir.HotFunc,
region_ref : @ir.HotRegionRef,
inside_control : Bool,
get_counts : Array[Int32],
one_armed_if_tail_sets : Array[Bool],
detached : Array[Int32],
) -> Bool {
let mut changed = false
for idx = 0; idx < @ir.hot_region_root_count(func, region_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, region_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet if inside_control => {
let value_id = @ir.hot_child_get(func, node_id, 0)
if node.imm0 >= 0 &&
node.imm0 < get_counts.length() &&
get_counts[node.imm0] == 0 {
if value_id >= 0 && @ir.hot_node_is_live(func, value_id) {
match @ir.hot_node_get(func, value_id).op {
@ir.HotOp::If => continue
_ => ()
}
}
if value_id < 0 {
if idx > 0 {
let previous_id = @ir.hot_region_get(func, region_ref, idx - 1)
if previous_id >= 0 &&
@ir.hot_node_is_live(func, previous_id) &&
simplify_locals_value_is_pure(func, previous_id) {
@ir.hot_replace_node(
func,
previous_id,
simplify_locals_make_nop(func),
)
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_nop(func),
)
changed = true
}
}
} else if simplify_locals_value_is_pure(func, value_id) {
@ir.hot_replace_node(func, node_id, simplify_locals_make_nop(func))
detached.push(value_id)
changed = true
} else {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_drop(func, value_id),
)
changed = true
}
}
}
@ir.HotOp::Drop if inside_control => {
let child_id = @ir.hot_child_get(func, node_id, 0)
if child_id >= 0 && @ir.hot_node_is_live(func, child_id) {
let child = @ir.hot_node_get(func, child_id)
match child.op {
@ir.HotOp::LocalTee => {
let value_id = @ir.hot_child_get(func, child_id, 0)
if value_id >= 0 &&
@ir.hot_node_is_live(func, value_id) &&
simplify_locals_two_child_uses_are_unique(
func, child_id, value_id,
) {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_local_set(func, child.imm0, value_id),
)
detached.push(child_id)
changed = true
}
}
@ir.HotOp::Block | @ir.HotOp::Loop =>
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_body_ref(func, child_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
@ir.HotOp::If => {
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_then_ref(func, child_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
match @ir.hot_region_else_ref(func, child_id) {
Some(else_ref) =>
if simplify_locals_lower_dropped_tees_in_protected_region(
func, else_ref, true, get_counts, one_armed_if_tail_sets, detached,
) {
changed = true
}
None => ()
}
}
@ir.HotOp::Try => {
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_body_ref(func, child_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
match @ir.hot_region_catch_ref(func, child_id) {
Some(catch_ref) =>
if simplify_locals_lower_dropped_tees_in_protected_region(
func, catch_ref, true, get_counts, one_armed_if_tail_sets,
detached,
) {
changed = true
}
None => ()
}
}
@ir.HotOp::TryTable => {
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_body_ref(func, child_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_catch_list_ref(func, child_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
}
_ => ()
}
}
}
@ir.HotOp::Block | @ir.HotOp::Loop =>
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_body_ref(func, node_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
@ir.HotOp::If => {
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_then_ref(func, node_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
match @ir.hot_region_else_ref(func, node_id) {
Some(else_ref) =>
if simplify_locals_lower_dropped_tees_in_protected_region(
func, else_ref, true, get_counts, one_armed_if_tail_sets, detached,
) {
changed = true
}
None => ()
}
}
@ir.HotOp::Try => {
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_body_ref(func, node_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
match @ir.hot_region_catch_ref(func, node_id) {
Some(catch_ref) =>
if simplify_locals_lower_dropped_tees_in_protected_region(
func, catch_ref, true, get_counts, one_armed_if_tail_sets, detached,
) {
changed = true
}
None => ()
}
}
@ir.HotOp::TryTable => {
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_body_ref(func, node_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
if simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.hot_region_catch_list_ref(func, node_id),
true,
get_counts,
one_armed_if_tail_sets,
detached,
) {
changed = true
}
}
_ => ()
}
}
changed
}
///|
fn simplify_locals_run_root_dead_cleanup(
func : @ir.HotFunc,
detached : Array[Int32],
get_count_cache~ : SimplifyLocalsGetCounts? = None,
get_count_scans~ : Array[Int32]? = None,
) -> Bool {
let get_counts = simplify_locals_counts_for_cycle(
func,
get_count_cache,
full_scans=get_count_scans,
)
let one_armed_if_tail_sets = simplify_locals_one_armed_if_tail_sets(func)
let mut changed = false
let root_ref = @ir.HotRegionRef::root()
for idx = 0; idx < @ir.hot_region_root_count(func, root_ref); idx = idx + 1 {
let node_id = @ir.hot_region_get(func, root_ref, idx)
if node_id < 0 || !@ir.hot_node_is_live(func, node_id) {
continue
}
let node = @ir.hot_node_get(func, node_id)
match node.op {
@ir.HotOp::LocalSet => {
let value_id = @ir.hot_child_get(func, node_id, 0)
if value_id >= 0 && @ir.hot_node_is_live(func, value_id) {
let value = @ir.hot_node_get(func, value_id)
match value.op {
@ir.HotOp::LocalTee if value.imm0 == node.imm0 => {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_local_set(
func,
node.imm0,
@ir.hot_child_get(func, value_id, 0),
),
)
detached.push(value_id)
changed = true
continue
}
@ir.HotOp::LocalGet if value.imm0 == node.imm0 => {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_nop(func),
)
detached.push(value_id)
changed = true
continue
}
_ => ()
}
}
if get_counts[node.imm0] != 0 {
continue
}
if node_id >= 0 &&
node_id < one_armed_if_tail_sets.length() &&
one_armed_if_tail_sets[node_id] {
continue
}
if value_id >= 0 && @ir.hot_node_is_live(func, value_id) {
match @ir.hot_node_get(func, value_id).op {
@ir.HotOp::If => continue
_ => ()
}
}
if simplify_locals_value_is_pure(func, value_id) {
@ir.hot_replace_node(func, node_id, simplify_locals_make_nop(func))
detached.push(value_id)
} else {
@ir.hot_replace_node(
func,
node_id,
simplify_locals_make_drop(func, value_id),
)
}
changed = true
}
@ir.HotOp::LocalTee => {
let value_id = @ir.hot_child_get(func, node_id, 0)
if value_id >= 0 && @ir.hot_node_is_live(func, value_id) {
let value = @ir.hot_node_get(func, value_id)
match value.op {
@ir.HotOp::LocalGet if value.imm0 == node.imm0 => {
@ir.hot_replace_node(
func,
node_id,
@ir.hot_node_get(func, value_id),
)
detached.push(value_id)
changed = true
continue
}
_ => ()
}
}
if get_counts[node.imm0] != 0 {
continue
}
if simplify_locals_value_can_clone_node(func, value_id) {
@ir.hot_replace_node(func, node_id, @ir.hot_node_get(func, value_id))
detached.push(value_id)
changed = true
}
}
_ => ()
}
}
changed
}
///|
fn simplify_locals_run_control_embedded_tee_root_only(
ctx : HotPassContext,
func : @ir.HotFunc,
policy : SimplifyLocalsPolicy,
reuse_get_counts~ : Bool = true,
get_count_scans~ : Array[Int32]? = None,
) -> HotPassResult {
let get_count_cache = if reuse_get_counts {
simplify_locals_new_get_count_cache(func)
} else {
None
}
pipeline_trace_hook(
ctx.trace,
ctx.descriptor.name,
"protect-hot",
payload="reason=control-local-tee-hazard-root-only",
)
let mut first_cycle = true
let mut changed = false
while true {
let main_detached : Array[Int32] = []
let (main_changed, has_deferred_main_cycle) = simplify_locals_run_main_cycle(
ctx,
func,
first_cycle,
main_detached,
policy,
true,
get_count_cache~,
get_count_scans~,
)
simplify_locals_delete_detached_nodes(func, main_detached)
let cleanup_detached : Array[Int32] = []
let cleanup_changed = simplify_locals_run_root_dead_cleanup(
func,
cleanup_detached,
get_count_cache~,
get_count_scans~,
)
simplify_locals_delete_detached_nodes(func, cleanup_detached)
let protected_detached : Array[Int32] = []
let protected_get_counts = simplify_locals_counts_for_cycle(
func,
get_count_cache,
full_scans=get_count_scans,
)
let protected_one_armed_if_tail_sets = simplify_locals_one_armed_if_tail_sets(
func,
)
let protected_changed = simplify_locals_lower_dropped_tees_in_protected_region(
func,
@ir.HotRegionRef::root(),
true,
protected_get_counts,
protected_one_armed_if_tail_sets,
protected_detached,
)
simplify_locals_delete_detached_nodes(func, protected_detached)
if main_changed || cleanup_changed || protected_changed {
changed = true
}
if first_cycle {
first_cycle = false
if has_deferred_main_cycle || protected_changed {
continue
}
} else if main_changed || cleanup_changed || protected_changed {
continue
}
break
}
if !changed {
return HotPassResult::unchanged()
}
ignore(@ir.hot_repair_nondefaultable_local_scopes(func))
pass_mark_mutated(ctx, func)
HotPassResult::changed()
}
///|
fn simplify_locals_nostructure_run(
ctx : HotPassContext,
func : @ir.HotFunc,
) -> HotPassResult {
if simplify_locals_has_control_embedded_local_tee(func) {
return simplify_locals_run_control_embedded_tee_root_only(
ctx,
func,
SimplifyLocalsPolicy::new(false, true, true),
)
}
simplify_locals_run_with_options(
ctx,
func,
SimplifyLocalsPolicy::new(false, true, true),
)
}
///|
fn simplify_locals_nonesting_run(
ctx : HotPassContext,
func : @ir.HotFunc,
) -> HotPassResult {
let policy = SimplifyLocalsPolicy::new(false, false, false)
if simplify_locals_should_skip_root_local_set_stack_hazard(func) {
pipeline_trace_hook(
ctx.trace,
ctx.descriptor.name,
"skip-hot",
payload="reason=root-local-set-stack-hazard-noop",
)
return HotPassResult::unchanged()
}
if simplify_locals_has_control_embedded_local_tee(func) {
return simplify_locals_run_control_embedded_tee_root_only(ctx, func, policy)
}
simplify_locals_run_with_options(ctx, func, policy)
}
///|
fn simplify_locals_notee_nostructure_run(
ctx : HotPassContext,
func : @ir.HotFunc,
) -> HotPassResult {
// The no-tee spelling disables creating new tees, but Binaryen still cleans
// existing dropped tees and optimizes unrelated locals. When a local.tee is
// embedded under structured control, keep that child protected while allowing
// unrelated root-level cleanup.
if simplify_locals_has_control_embedded_local_tee(func) {
return simplify_locals_run_control_embedded_tee_root_only(
ctx,
func,
SimplifyLocalsPolicy::new(false, false, true),
)
}
simplify_locals_run_with_options(
ctx,
func,
SimplifyLocalsPolicy::new(false, false, true),
)
}
///|
fn simplify_locals_grouping_descriptor_is_admitted(name : String) -> Bool {
name == "simplify-locals" ||
name == "simplify-locals-nostructure" ||
name == "simplify-locals-no-structure"
}
///|
// Group only changed, selected scalar bodies, after writeback repair has settled
// their local indices. The cheap run scan keeps ordinary/no-change functions
// out of the shared encoded-cost and validation selector.
fn simplify_locals_group_numeric_locals_if_smaller(
before : @lib.Module,
mod_ : @lib.Module,
touched~ : Array[Bool]? = None,
) -> @lib.Module {
if physical_equal(before, mod_) {
return mod_
}
let original = match before.code_sec {
Some(@lib.CodeSec(funcs)) => funcs
None => return mod_
}
let funcs = match mod_.code_sec {
Some(@lib.CodeSec(funcs)) => funcs
None => return mod_
}
let mut selected : Array[Bool]? = None
for index in 0..= original.length() ||
physical_equal(original[index], funcs[index]) {
continue
}
if touched is Some(mask) && (index >= mask.length() || !mask[index]) {
continue
}
let @lib.Func(locals, _) = funcs[index]
if locals.run_count() <= 1 {
continue
}
let mut seen = 0
let mut repeated = false
let mut numeric = true
for run in locals.runs() {
let bit = match run.vt {
@lib.NumTypeValType(@lib.I32NumType) => 1
@lib.NumTypeValType(@lib.I64NumType) => 2
@lib.NumTypeValType(@lib.F32NumType) => 4
@lib.NumTypeValType(@lib.F64NumType) => 8
_ => {
numeric = false
break
}
}
repeated = repeated || (seen & bit) != 0
seen = seen | bit
}
if numeric && repeated {
if selected is None {
selected = Some(Array::make(funcs.length(), false))
}
selected.unwrap()[index] = true
}
}
match selected {
Some(mask) =>
oi_group_numeric_locals_if_smaller(
mod_,
touched=Some(mask),
allow_frequency=false,
)
None => mod_
}
}
///|
// Finalize module-wide type indices only after all HOT bodies are written back.
// Body-local grouping stays selected; a type remap also updates unselected
// typed instructions so their module references remain valid.
fn simplify_locals_cleanup_changed_module_encoding(
before : @lib.Module,
mod_ : @lib.Module,
allow_grouping~ : Bool = true,
touched~ : Array[Bool]? = None,
) -> @lib.Module {
let cleaned = pass_cleanup_control_and_type_encoding(mod_, touched~)
let grouped = if allow_grouping {
simplify_locals_group_numeric_locals_if_smaller(before, cleaned, touched~)
} else {
cleaned
}
if physical_equal(before, grouped) ||
!grouped.custom_secs.is_empty() ||
pass_module_has_offset_sensitive_metadata(grouped) {
return grouped
}
dfe_cleanup_simple_type_encoding(grouped)
}