// Licensing scope: see ../../LICENSE and this directory's LICENSE and NOTICE.
// Potentially incorporated Binaryen expression retains Apache-2.0 terms.
// Original Starshine contributions use the scoped MIT grant in ../../LICENSE.
// Modified for Starshine through 2026: implementation and validation use
// Starshine's representations and APIs; this is not an unchanged upstream file.
// Conservative notice retention does not establish copying or independence.
///|
fn pass_instrs_have_descriptor_branches(
instrs : Array[@lib.Instruction],
) -> Bool {
for instr in instrs {
match instr {
@lib.BrOnCastDescEq(_, _, _) | @lib.BrOnCastDescEqFail(_, _, _) =>
return true
@lib.Block(_, @lib.Expr(body))
| @lib.Loop(_, @lib.Expr(body))
| @lib.TryTable(_, _, @lib.Expr(body)) =>
if pass_instrs_have_descriptor_branches(body) {
return true
}
@lib.If(_, yes, no) => {
if pass_instrs_have_descriptor_branches(yes) {
return true
}
if no is Some(body) && pass_instrs_have_descriptor_branches(body) {
return true
}
}
@lib.Try(_, @lib.Expr(body), catches, _) => {
if pass_instrs_have_descriptor_branches(body) {
return true
}
for catch_ in catches {
let body = match catch_ {
@lib.LegacyCatch(_, @lib.Expr(body))
| @lib.LegacyCatchAll(@lib.Expr(body)) => body
}
if pass_instrs_have_descriptor_branches(body) {
return true
}
}
}
_ => ()
}
}
false
}
///|
fn pass_refinalize_descriptor_schedule_children(
func : @ir.HotFunc,
node : Int32,
work : Array[(Int32, Bool)],
) -> Unit {
// Scheduling only needs child IDs; the ready phase materializes the one span
// it uses for the node's summaries.
let child_count = @ir.hot_child_count(func, node)
for slot = 0; slot < child_count; slot = slot + 1 {
work.push((@ir.hot_child_get(func, node, slot), false))
}
}
///|
// A null descriptor traps before its branch can be taken. Give the replacement
// an uninhabitable reference result, preserving every operand evaluation. This
// is shared result recomputation: local refinement must never replace that
// bottom result with the nullable source annotation (#8975).
fn pass_refinalize_descriptor_bottoms(
func : @ir.HotFunc,
module_ctx : @ir.HotModuleContext,
) -> Bool {
let count = @ir.hot_node_count(func)
let seen = Array::make(count, false)
let nulls = Array::make(count, false)
let never = Array::make(count, false)
let branches = Array::make(count, false)
let work : Array[(Int32, Bool)] = []
for root in @ir.hot_root_span(func) {
work.push((root, false))
}
let mut changed = false
while !work.is_empty() {
let (node, ready) = work.pop().unwrap()
if node < 0 || !@ir.hot_node_is_live(func, node) {
continue
}
if !ready {
if seen[node] {
continue
}
seen[node] = true
work.push((node, true))
pass_refinalize_descriptor_schedule_children(func, node, work)
continue
}
let data = @ir.hot_node_get(func, node)
let children = @ir.hot_child_span(func, node)
branches[node] = (@ir.hot_node_flags(func, node) & @ir.HOT_FLAG_BRANCH) !=
0U ||
data.op is @ir.HotOp::Try ||
data.op is @ir.HotOp::TryTable ||
data.op is @ir.HotOp::Continuation ||
children.any(fn(child) { child >= 0 && branches[child] })
nulls[node] = data.op is @ir.HotOp::RefNull
never[node] = match data.op {
@ir.HotOp::Unreachable
| @ir.HotOp::Return
| @ir.HotOp::ReturnCall
| @ir.HotOp::ReturnCallIndirect
| @ir.HotOp::ReturnCallRef
| @ir.HotOp::Throw
| @ir.HotOp::ThrowRef => true
@ir.HotOp::Block | @ir.HotOp::Loop =>
!branches[node] &&
children.any(fn(child) { child >= 0 && never[child] })
@ir.HotOp::If =>
(children[0] >= 0 && never[children[0]]) ||
(
!branches[node] &&
children.length() == 3 &&
children[1] >= 0 &&
children[2] >= 0 &&
never[children[1]] &&
never[children[2]]
)
@ir.HotOp::Try | @ir.HotOp::TryTable => false
_ => children.any(fn(child) { child >= 0 && never[child] })
}
if @ir.hot_node_exact_instr(func, node) is Some(@lib.RefAsNonNull) &&
children.length() == 1 &&
children[0] >= 0 &&
nulls[children[0]] {
never[node] = true
}
match @ir.hot_type_results(func, data.ty) {
[@lib.RefTypeValType(ref_type)] =>
match ref_type.get_heap_type() {
@lib.AbsHeapTypeHeapType(@lib.NoneAbsHeapType)
| @lib.SharedAbsHeapTypeHeapType(@lib.NoneAbsHeapType) =>
if ref_type.is_nullable() {
nulls[node] = true
} else {
never[node] = true
}
_ => ()
}
_ => ()
}
if never[node] &&
!branches[node] &&
(data.op is @ir.HotOp::Block || data.op is @ir.HotOp::Loop) {
let declared = match @ir.hot_type_get(func, data.ty) {
@ir.BlockResult(@lib.ValTypeBlockType(@lib.RefTypeValType(ref_type)))
| @ir.BlockResultResolved(
@lib.ValTypeBlockType(@lib.RefTypeValType(ref_type)),
[],
_
) => Some(ref_type)
_ => None
}
if declared is Some(ref_type) {
let gc_shared = match ref_type.get_heap_type() {
@lib.HeapType(index) =>
match @ir.hot_module_resolve_subtype(module_ctx, index) {
Some(subtype) =>
match subtype.get_comptype() {
@lib.StructCompType(_) | @lib.ArrayCompType(_) =>
Some(subtype.metadata().is_shared())
_ => None
}
None => None
}
@lib.AbsHeapTypeHeapType(@lib.NoneAbsHeapType) => Some(false)
@lib.SharedAbsHeapTypeHeapType(@lib.NoneAbsHeapType) => Some(true)
_ => None
}
if gc_shared is Some(shared) {
let bottom = @lib.RefType::new(
false,
if shared {
@lib.HeapType::shared_abs(@lib.AbsHeapType::none())
} else {
@lib.HeapType::abs(@lib.AbsHeapType::none())
},
)
if bottom != ref_type {
@ir.hot_control_result_type_set(
func,
node,
@ir.hot_type_intern_block_result(
func,
@lib.BlockType::val_type(@lib.ValType::ref_type(bottom)),
),
)
changed = true
}
}
}
}
let target = match @ir.hot_node_exact_instr(func, node) {
Some(@lib.BrOnCastDescEq(_, _, target))
| Some(@lib.BrOnCastDescEqFail(_, _, target)) => target
_ => continue
}
if children.length() < 2 {
continue
}
let descriptor = children[children.length() - 1]
let source = children[children.length() - 2]
if descriptor < 0 || source < 0 {
continue
}
if !nulls[descriptor] && !never[descriptor] && !never[source] {
continue
}
let shared = match target.get_heap_type() {
@lib.HeapType(type_idx) =>
match @ir.hot_module_resolve_subtype(module_ctx, type_idx) {
Some(subtype) => subtype.metadata().is_shared()
None => continue
}
heap => heap.is_shared()
}
let bottom = @lib.ValType::ref_type(
@lib.RefType::new(
false,
if shared {
@lib.HeapType::shared_abs(@lib.AbsHeapType::none())
} else {
@lib.HeapType::abs(@lib.AbsHeapType::none())
},
),
)
// Earlier label payloads are borrowed from the enclosing operand stack.
// Only the source and descriptor are consumed by this instruction.
let body = [
@ir.hot_build_drop(func, source),
@ir.hot_build_drop(func, descriptor),
]
body.push(@ir.hot_build_unreachable(func))
@ir.hot_node_promote_to_block(
func,
node,
@lib.BlockType::val_type(bottom),
body,
)
never[node] = true
branches[node] = children.any(fn(child) { branches[child] })
changed = true
}
changed
}
///|
// Refining a control result to non-null bottom also requires discarding the
// linear tail after a terminator. Dead explicit values still participate in
// Wasm stack validation, even when their producer cannot execute.
fn pass_refinalize_descriptor_trim_dead_tail(
instrs : Array[@lib.Instruction],
) -> Array[@lib.Instruction] {
let out : Array[@lib.Instruction] = []
for instr in instrs {
out.push(
pass_lower_map_children(instr, pass_refinalize_descriptor_trim_dead_tail),
)
if run_hot_pipeline_raw_simplify_locals_instr_is_unconditional_terminator(
instr,
) {
break
}
}
out
}
///|
fn pass_refinalize_descriptor_func(
mod_ : @lib.Module,
func_idx : @lib.FuncIdx,
original : @lib.Func,
module_ctx : @ir.HotModuleContext,
) -> (@lib.Func, Bool) {
let @lib.Func(_, @lib.Expr(body)) = original
if !pass_instrs_have_descriptor_branches(body) {
return (original, false)
}
let hot = match
@ir.hot_lift_func_with_context_result(mod_, func_idx, module_ctx) {
Ok(hot) => hot
Err(_) => return (original, false)
}
if pass_refinalize_descriptor_bottoms(hot, module_ctx) {
let @lib.Func(locals, @lib.Expr(body)) = @ir.hot_lower_func(hot)
(
@lib.Func::new(
locals,
@lib.Expr::new(pass_refinalize_descriptor_trim_dead_tail(body)),
),
true,
)
} else {
(original, false)
}
}