// 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.
///|
priv struct Dae2TypeWorkspace {
mut types : @lib.TypeSec
mut flat_count : Int32
}
///|
fn Dae2TypeWorkspace::new(types : @lib.TypeSec) -> Dae2TypeWorkspace {
let @lib.TypeSec(groups) = types
// Own the outer group vector before appending. Original groups and their
// immutable definitions retain their identities until final family rewriting.
{
types: @lib.TypeSec::new(groups.copy()),
flat_count: dae_type_sec_flattened_length(types),
}
}
///|
fn Dae2TypeWorkspace::append_signature(
self : Dae2TypeWorkspace,
params : Array[@lib.ValType],
results : Array[@lib.ValType],
metadata : @lib.TypeMetadata,
) -> @lib.TypeIdx {
let index = @lib.TypeIdx::new(self.flat_count.to_uint())
let @lib.TypeSec(groups) = self.types
groups.push(
@lib.RecType::new(
@lib.SubType::comp_type(@lib.CompType::func(params, results), metadata~),
),
)
self.flat_count += 1
index
}
///|
fn Dae2TypeWorkspace::adopt_lower_types(
self : Dae2TypeWorkspace,
state : HotPipelineLowerState,
) -> Unit {
if state.type_sec_changed {
match state.next_type_sec {
Some(types) => {
self.types = types
self.flat_count = dae_type_sec_flattened_length(types)
}
None => abort("DAE2 lowering removed the type section")
}
}
}
///|
// Type locations use the same graph as function parameters/results. All
// members of a declared subtype family share liveness, while private direct
// functions keep independent locations and receive replacement signatures.
fn dae2_exposure_module(mod_ : @lib.Module) -> @lib.Module {
match mod_.elem_sec {
Some(@lib.ElemSec(elems)) => {
let retained = elems.filter(fn(elem) {
let @lib.Elem(mode, kind) = elem
if !(mode is @lib.Declarative) {
return true
}
// An untyped declaration authorizes ref.func validation but does not
// create a runtime reference. Real body/global/table references remain
// visible to the ordinary exposure collector. Concrete typed element
// declarations retain their signature until their type is remapped too.
match kind {
@lib.FuncsElemKind(_) | @lib.FuncExprsElemKind(_) => false
@lib.TypedExprsElemKind(ref_type, _) =>
ref_type.get_heap_type() !=
@lib.HeapType::abs(@lib.AbsHeapType::func())
}
})
mod_.with_elem_sec(@lib.ElemSec::new(retained))
}
None => mod_
}
}
///|
fn dae2_call_info(
infos : Array[Dae2Function],
type_infos : Array[Int32],
func : @ir.HotFunc,
node : Int32,
) -> Dae2Function {
infos[dae2_call_info_index(type_infos, func, node)]
}
///|
fn dae2_call_info_index(
type_infos : Array[Int32],
func : @ir.HotFunc,
node : Int32,
) -> Int32 {
let data = @ir.hot_node_get(func, node)
dae2_call_info_index_from_data(type_infos, func, data)
}
///|
fn dae2_call_info_index_from_data(
type_infos : Array[Int32],
func : @ir.HotFunc,
data : @ir.HotNode,
) -> Int32 {
match data.op {
@ir.Call | @ir.ReturnCall => data.imm0
_ => {
let sig = @ir.hot_call_sig_get(func, data.imm0)
type_infos[dae2_type_slot(sig.type_idx, 0)]
}
}
}
///|
fn dae2_type_slot(index : @lib.TypeIdx, base : Int32) -> Int32 {
match index {
@lib.TypeIdx(raw) => raw.reinterpret_as_int()
@lib.RecIdx(raw) => base + raw.reinterpret_as_int()
}
}
///|
fn dae2_table_slot(index : @lib.TableIdx) -> Int32 {
match index {
@lib.TableIdx(raw) => raw.reinterpret_as_int()
}
}
///|
fn dae2_is_abstract_funcref_table(table_type : @lib.TableType) -> Bool {
let @lib.TableType(ref_type, _) = table_type
dae2_is_abstract_funcref_ref_type(ref_type)
}
///|
fn dae2_is_abstract_funcref_ref_type(ref_type : @lib.RefType) -> Bool {
match ref_type.get_heap_type() {
@lib.AbsHeapTypeHeapType(@lib.FuncAbsHeapType)
| @lib.SharedAbsHeapTypeHeapType(@lib.FuncAbsHeapType) => true
_ => false
}
}
///|
fn dae2_is_abstract_funcref_global(global_type : @lib.GlobalType) -> Bool {
match global_type {
@lib.GlobalType(@lib.RefTypeValType(ref_type), _) =>
dae2_is_abstract_funcref_ref_type(ref_type)
_ => false
}
}
///|
fn dae2_has_host_abstract_funcref_global(
mod_ : @lib.Module,
context : @ir.HotModuleContext,
) -> Bool {
let mut global_import = 0
match mod_.import_sec {
Some(@lib.ImportSec(imports)) =>
for import_ in imports {
match import_ {
@lib.Import(_, _, @lib.GlobalExternType(global_type)) => {
if dae2_is_abstract_funcref_global(global_type) {
return true
}
global_import += 1
}
_ => ()
}
}
None => ()
}
match mod_.export_sec {
Some(@lib.ExportSec(exports)) =>
for export_ in exports {
match export_ {
@lib.Export(_, @lib.GlobalExternIdx(@lib.GlobalIdx(raw))) => {
let slot = raw.reinterpret_as_int()
if slot >= global_import &&
slot < context.globals.length() &&
dae2_is_abstract_funcref_global(context.globals[slot]) {
return true
}
}
_ => ()
}
}
None => ()
}
false
}
///|
fn dae2_host_abstract_funcref_tables(
mod_ : @lib.Module,
context : @ir.HotModuleContext,
) -> Array[Bool] {
let visible = Array::make(context.tables.length(), false)
let mut table_import = 0
match mod_.import_sec {
Some(@lib.ImportSec(imports)) =>
for import_ in imports {
match import_ {
@lib.Import(_, _, @lib.TableExternType(table_type)) => {
if table_import < visible.length() &&
dae2_is_abstract_funcref_table(table_type) {
visible[table_import] = true
}
table_import += 1
}
_ => ()
}
}
None => ()
}
match mod_.export_sec {
Some(@lib.ExportSec(exports)) =>
for export_ in exports {
match export_ {
@lib.Export(_, @lib.TableExternIdx(table_idx)) => {
let slot = dae2_table_slot(table_idx)
if slot >= 0 &&
slot < visible.length() &&
dae2_is_abstract_funcref_table(context.tables[slot]) {
visible[slot] = true
}
}
_ => ()
}
}
None => ()
}
match mod_.code_sec {
Some(@lib.CodeSec(funcs)) =>
for func in funcs {
let @lib.Func(_, @lib.Expr(body)) = func
dae2_mark_runtime_funcref_table_ingress(body, context.tables, visible)
}
None => ()
}
visible
}
///|
fn dae2_mark_runtime_funcref_table_ingress(
instrs : Array[@lib.Instruction],
tables : Array[@lib.TableType],
visible : Array[Bool],
) -> Unit {
for instr in instrs {
match instr {
@lib.Block(_, @lib.Expr(body))
| @lib.Loop(_, @lib.Expr(body))
| @lib.TryTable(_, _, @lib.Expr(body)) =>
dae2_mark_runtime_funcref_table_ingress(body, tables, visible)
@lib.Try(_, @lib.Expr(body), catches, _) => {
dae2_mark_runtime_funcref_table_ingress(body, tables, visible)
for catch_ in catches {
let @lib.Expr(catch_body) = match catch_ {
@lib.LegacyCatch(_, catch_body) | @lib.LegacyCatchAll(catch_body) =>
catch_body
}
dae2_mark_runtime_funcref_table_ingress(catch_body, tables, visible)
}
}
@lib.If(_, then_body, else_body) => {
dae2_mark_runtime_funcref_table_ingress(then_body, tables, visible)
match else_body {
Some(body) =>
dae2_mark_runtime_funcref_table_ingress(body, tables, visible)
None => ()
}
}
@lib.TableSet(table_idx)
| @lib.TableGrow(table_idx)
| @lib.TableFill(table_idx)
| @lib.TableCopy(table_idx, _) => {
let slot = dae2_table_slot(table_idx)
if slot >= 0 &&
slot < tables.length() &&
dae2_is_abstract_funcref_table(tables[slot]) {
visible[slot] = true
}
}
_ => ()
}
}
}
///|
fn dae2_function_reference_may_reach_table(
exposure : DaeFunctionExposure,
) -> Bool {
exposure.ref_func_body ||
exposure.ref_func_module ||
exposure.active_element ||
exposure.passive_element
}
///|
fn dae2_mark_host_table_call_types(
instrs : Array[@lib.Instruction],
host_tables : Array[Bool],
exposed : Array[Bool],
) -> Unit {
for instr in instrs {
match instr {
@lib.Block(_, @lib.Expr(body))
| @lib.Loop(_, @lib.Expr(body))
| @lib.TryTable(_, _, @lib.Expr(body)) =>
dae2_mark_host_table_call_types(body, host_tables, exposed)
@lib.Try(_, @lib.Expr(body), catches, _) => {
dae2_mark_host_table_call_types(body, host_tables, exposed)
for catch_ in catches {
let @lib.Expr(catch_body) = match catch_ {
@lib.LegacyCatch(_, catch_body) | @lib.LegacyCatchAll(catch_body) =>
catch_body
}
dae2_mark_host_table_call_types(catch_body, host_tables, exposed)
}
}
@lib.If(_, then_body, else_body) => {
dae2_mark_host_table_call_types(then_body, host_tables, exposed)
match else_body {
Some(body) =>
dae2_mark_host_table_call_types(body, host_tables, exposed)
None => ()
}
}
@lib.CallIndirect(type_idx, table_idx)
| @lib.ReturnCallIndirect(type_idx, table_idx) => {
let slot = dae2_table_slot(table_idx)
if slot >= 0 && slot < host_tables.length() && host_tables[slot] {
dae_mark_used_type_idx(type_idx, exposed)
}
}
_ => ()
}
}
}
///|
fn dae2_type_ref_edges(
graph : Dae2Graph,
owner : Int32,
value : @lib.ValType,
base : Int32,
embedded_types_root : Array[Int32],
) -> Unit {
match value {
@lib.RefTypeValType(ref_type) =>
match ref_type.get_heap_type() {
@lib.HeapType(index) => graph.edge(owner, dae2_type_slot(index, base))
@lib.DefTypeHeapType(_) => {
// Embedded definitions have no stable module index. Keep the visible
// closure conservative instead of treating a missing identity as
// empty. Share one fanout node across all embedded references so E
// references over T types require E + T edges, not E * T edges.
if embedded_types_root[0] < 0 {
let type_count = graph.used.length()
let root = graph.location()
embedded_types_root[0] = root
for index = 0; index < type_count; index = index + 1 {
graph.edge(root, index)
}
}
graph.edge(owner, embedded_types_root[0])
}
_ => ()
}
_ => ()
}
}
///|
fn dae2_type_field_edges(
graph : Dae2Graph,
owner : Int32,
field : @lib.FieldType,
base : Int32,
embedded_types_root : Array[Int32],
) -> Unit {
let @lib.FieldType(storage, _) = field
match storage {
@lib.ValTypeStorageType(value) =>
dae2_type_ref_edges(graph, owner, value, base, embedded_types_root)
_ => ()
}
}
///|
fn dae2_prepare_type_locations(
mod_ : @lib.Module,
context : @ir.HotModuleContext,
graph : Dae2Graph,
infos : Array[Dae2Function],
exposure : Array[DaeFunctionExposure],
closed_world : Bool,
private_elements~ : Array[Bool]? = None,
) -> Array[Int32] {
let total = context.subtypes.length()
let bases = Array::make(total, 0)
match mod_.type_sec {
Some(@lib.TypeSec(groups)) => {
let mut offset = 0
for group in groups {
let size = match group {
@lib.SingleRecType(_) => 1
@lib.GroupRecType(items) => items.length()
}
for member_idx = 0; member_idx < size; member_idx = member_idx + 1 {
bases[offset + member_idx] = offset
}
offset += size
}
}
None => ()
}
let roots = Array::makei(total, fn(index) { index })
// Validated Wasm supertypes precede their subtypes. Resolve each family root
// without depending on hash-map insertion order or destination mutations.
for index = 0; index < total; index = index + 1 {
for super_type in context.subtypes[index].super_types() {
let parent = dae2_type_slot(super_type, bases[index])
roots[index] = roots[parent]
}
}
let type_infos = Array::make(total, -1)
for index = 0; index < total; index = index + 1 {
match context.subtypes[index].get_comptype() {
@lib.FuncCompType(params, results) => {
let root = roots[index]
if type_infos[root] < 0 {
type_infos[root] = infos.length()
infos.push(Dae2Function::new(graph, params, results))
}
type_infos[index] = type_infos[root]
}
_ => ()
}
}
let public_types = Dae2Graph::new()
for index = 0; index < total; index = index + 1 {
ignore(public_types.location())
}
let embedded_types_root = [-1]
// Exposing any recursive-group member exposes the identity of the whole
// group, including siblings with no explicit reference edge between them.
// A star through the base keeps this closure linear in the group size.
for index = 0; index < total; index = index + 1 {
if bases[index] != index {
public_types.edge(index, bases[index])
public_types.edge(bases[index], index)
}
}
for index = 0; index < total; index = index + 1 {
let subtype = context.subtypes[index]
let base = bases[index]
for parent in subtype.super_types() {
public_types.edge(index, dae2_type_slot(parent, base))
}
for metadata_type in [subtype.describes_type(), subtype.descriptor_type()] {
match metadata_type {
Some(target) => public_types.edge(index, dae2_type_slot(target, base))
None => ()
}
}
match subtype.get_comptype() {
@lib.FuncCompType(params, results) =>
for value in params + results {
dae2_type_ref_edges(
public_types, index, value, base, embedded_types_root,
)
}
@lib.StructCompType(fields) =>
for field in fields {
dae2_type_field_edges(
public_types, index, field, base, embedded_types_root,
)
}
@lib.ArrayCompType(field) =>
dae2_type_field_edges(
public_types, index, field, base, embedded_types_root,
)
@lib.ContCompType(target) => {
let target = dae2_type_slot(target, base)
public_types.edge(index, target)
// Stack switching signatures require separate operand/handler rewrites.
public_types.observe(target)
}
}
}
let host_abstract_tables = dae2_host_abstract_funcref_tables(mod_, context)
let has_host_abstract_table = host_abstract_tables.any(fn(visible) { visible })
let has_host_abstract_global = dae2_has_host_abstract_funcref_global(
mod_, context,
)
let has_host_callable_parameter = dae2_has_host_callable_parameter(context)
for index = 0; index < exposure.length(); index = index + 1 {
let e = exposure[index]
let type_index = dae2_type_slot(context.func_type_idxs[index], 0)
if dae_function_exposure_blocks_boundary(e) {
if !closed_world ||
e.imported ||
e.exported ||
(
(
has_host_abstract_table ||
has_host_abstract_global ||
has_host_callable_parameter
) &&
dae2_function_reference_may_reach_table(e)
) {
public_types.observe(type_index)
}
}
}
// Imported/exported tables and globals expose their concrete reference types.
let exposed = Array::make(total, false)
match mod_.import_sec {
Some(@lib.ImportSec(imports)) =>
for import_ in imports {
let @lib.Import(_, _, kind) = import_
match kind {
@lib.TableExternType(tt) => dae_mark_used_table_type(tt, exposed)
@lib.GlobalExternType(gt) => dae_mark_used_global_type(gt, exposed)
@lib.TagExternType(@lib.TagType(tt)) =>
dae_mark_used_type_idx(tt, exposed)
_ => ()
}
}
None => ()
}
match mod_.export_sec {
Some(@lib.ExportSec(exports)) =>
for export_ in exports {
let @lib.Export(_, kind) = export_
match kind {
@lib.TableExternIdx(@lib.TableIdx(raw)) =>
dae_mark_used_table_type(
context.tables[raw.reinterpret_as_int()],
exposed,
)
@lib.GlobalExternIdx(@lib.GlobalIdx(raw)) =>
dae_mark_used_global_type(
context.globals[raw.reinterpret_as_int()],
exposed,
)
_ => ()
}
}
None => ()
}
for tag in context.tags {
let @lib.TagType(index) = tag
dae_mark_used_type_idx(index, exposed)
}
if has_host_abstract_table {
match mod_.code_sec {
Some(@lib.CodeSec(funcs)) =>
for func in funcs {
let @lib.Func(_, @lib.Expr(body)) = func
dae2_mark_host_table_call_types(body, host_abstract_tables, exposed)
}
None => ()
}
}
for index = 0; index < total; index = index + 1 {
if exposed[index] || !closed_world {
public_types.observe(index)
}
}
public_types.solve()
// Build boundary edges after the public closure. A private scalar element
// target can use a new group identity while its original type stays fixed.
for index = 0; index < exposure.length(); index = index + 1 {
let e = exposure[index]
let type_index = dae2_type_slot(context.func_type_idxs[index], 0)
let type_info = infos[type_infos[type_index]]
let private_element = match private_elements {
Some(mask) => {
mask[index] = mask[index] && !public_types.used[type_index]
mask[index]
}
None => false
}
if private_element {
for location in type_info.param_locations {
graph.observe(location)
}
graph.observe(type_info.result_location)
// This is the pass-local boundary view. Keep the separate mask so final
// signature allocation preserves the runtime reference identity.
e.active_element = false
e.passive_element = false
} else if dae_function_exposure_blocks_boundary(e) {
let info = infos[index]
for param = 0; param < info.param_locations.length(); param = param + 1 {
graph.edge(
info.param_locations[param],
type_info.param_locations[param],
)
graph.edge(
type_info.param_locations[param],
info.param_locations[param],
)
}
graph.edge(info.result_location, type_info.result_location)
graph.edge(type_info.result_location, info.result_location)
}
}
for index = 0; index < total; index = index + 1 {
if public_types.used[index] && type_infos[index] >= 0 {
let info = infos[type_infos[index]]
for location in info.param_locations {
graph.observe(location)
}
graph.observe(info.result_location)
}
}
type_infos
}
///|
fn dae2_owned_signature_params(
params : Array[@lib.ValType],
source : Array[@lib.ValType],
) -> Array[@lib.ValType] {
// Unchanged plans borrow source arrays. A newly materialized signature
// owns its parameters, as it did before lazy plan construction.
if physical_equal(params, source) {
params.copy()
} else {
params
}
}
///|
fn dae2_rewrite_signature_type(
subtype : @lib.SubType,
params : Array[@lib.ValType],
results : Array[@lib.ValType],
) -> @lib.SubType {
let params = match subtype.get_comptype() {
@lib.FuncCompType(source, _) => dae2_owned_signature_params(params, source)
_ => params
}
let comp = @lib.CompType::func(params, results)
match subtype {
@lib.SubType(final_, supers, metadata, _) =>
@lib.SubType::new(final_, supers, comp, metadata~)
@lib.CompTypeSubType(metadata, _) =>
@lib.SubType::comp_type(comp, metadata~)
}
}
///|
fn dae2_rewrite_type_section(
types : @lib.TypeSec,
graph : Dae2Graph,
infos : Array[Dae2Function],
type_infos : Array[Int32],
signature_changes~ : Array[Bool]? = None,
) -> @lib.TypeSec {
let @lib.TypeSec(groups) = types
let mut index = 0
fn rewrite(subtype : @lib.SubType, current : Int32) -> @lib.SubType {
if current >= type_infos.length() || type_infos[current] < 0 {
return subtype
}
let info_index = type_infos[current]
let info = infos[info_index]
let changed = match signature_changes {
Some(changes) => changes[info_index]
// Identity stabilization can observe locations and solve again. Its
// callers must inspect the current graph rather than a final cache.
None => dae2_signature_changes(graph, info)
}
if !changed {
return subtype
}
match subtype.get_comptype() {
@lib.FuncCompType(params, results) => {
let (kept, _) = dae2_parameter_plan(graph, info, params=Some(params))
dae2_rewrite_signature_type(
subtype,
kept,
if graph.used[info.result_location] {
results
} else {
[]
},
)
}
_ => subtype
}
}
let mut rewritten : Array[@lib.RecType]? = None
for group_index = 0
group_index < groups.length()
group_index = group_index + 1 {
let group = groups[group_index]
let next_group = match group {
@lib.SingleRecType(subtype) => {
let next = rewrite(subtype, index)
index += 1
if physical_equal(next, subtype) {
group
} else {
@lib.RecType::new(next)
}
}
@lib.GroupRecType(members) => {
let mut next_members : Array[@lib.SubType]? = None
for slot = 0; slot < members.length(); slot = slot + 1 {
let next = rewrite(members[slot], index)
index += 1
if !physical_equal(next, members[slot]) {
let rows = match next_members {
Some(rows) => rows
None => {
let rows = members.copy()
next_members = Some(rows)
rows
}
}
rows[slot] = next
}
}
match next_members {
Some(members) => @lib.RecType::group(members)
None => group
}
}
}
if !physical_equal(next_group, group) {
let rows = match rewritten {
Some(rows) => rows
None => {
let rows = groups.copy()
rewritten = Some(rows)
rows
}
}
rows[group_index] = next_group
}
}
match rewritten {
Some(groups) => @lib.TypeSec::new(groups)
None => types
}
}
///|
fn dae2_preserve_control_types(
func : @ir.HotFunc,
context : @ir.HotModuleContext,
graph : Dae2Graph,
infos : Array[Dae2Function],
type_infos : Array[Int32],
lower_state : HotPipelineLowerState,
replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> Unit {
for node = 0; node < @ir.hot_node_count(func); node = node + 1 {
if !@ir.hot_node_is_live(func, node) || !@ir.hot_is_control_node(func, node) {
continue
}
let block_type = match
@ir.hot_type_get(func, @ir.hot_node_type(func, node)) {
@ir.BlockResult(bt) | @ir.BlockResultResolved(bt, _, _) => bt
_ => continue
}
let old_index = match block_type {
@lib.TypeIdxBlockType(index) => dae2_type_slot(index, 0)
_ => continue
}
if old_index >= type_infos.length() || type_infos[old_index] < 0 {
continue
}
let info = infos[type_infos[old_index]]
if info.param_locations.all(fn(location) { graph.used[location] }) &&
(info.results.is_empty() || graph.used[info.result_location]) {
continue
}
let @lib.FuncType(params, results) = match
@ir.hot_module_resolve_type_func_type(
context,
@lib.TypeIdx::new(old_index.reinterpret_as_uint()),
) {
Some(sig) => sig
None => continue
}
let replacement = match replacements.get(old_index) {
Some(index) => index
None => {
let types = match lower_state.next_type_sec {
Some(types) => types
None => continue
}
let (types, index) = dae_append_new_func_type(types, params, results)
lower_state.next_type_sec = Some(types)
lower_state.type_sec_changed = true
replacements.set(old_index, index)
index
}
}
@ir.hot_control_result_type_set(
func,
node,
@ir.hot_type_intern_resolved_block_result(
func,
@lib.BlockType::type_idx(replacement),
params,
results,
),
)
}
}
///|
// A successful raw replay keeps complete control signatures. Preserve their
// original types independently of the private families being pruned, as HOT
// replay does. This scan runs only after admission and owns no source arrays.
fn dae2_preserve_raw_control_types(
body : Array[@lib.Instruction],
context : @ir.HotModuleContext,
graph : Dae2Graph,
infos : Array[Dae2Function],
type_infos : Array[Int32],
workspace : Dae2TypeWorkspace,
replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> Unit {
fn preserve(block : @lib.BlockType) -> Unit {
guard block is @lib.TypeIdxBlockType(index) else { return }
let old_index = dae2_type_slot(index, 0)
if old_index < 0 ||
old_index >= type_infos.length() ||
type_infos[old_index] < 0 ||
replacements.get(old_index) is Some(_) {
return
}
let info = infos[type_infos[old_index]]
if !dae2_signature_changes(graph, info) {
return
}
let @lib.FuncType(params, results) = match
@ir.hot_module_resolve_type_func_type(context, index) {
Some(signature) => signature
None => return
}
replacements.set(
old_index,
workspace.append_signature(params, results, @lib.TypeMetadata::new()),
)
}
fn visit(instructions : Array[@lib.Instruction]) -> Unit {
for instruction in instructions {
match instruction {
@lib.Block(block, @lib.Expr(child))
| @lib.Loop(block, @lib.Expr(child)) => {
// Source preorder matches the control-node allocation order in HOT.
preserve(block)
visit(child)
}
@lib.If(block, yes, no) => {
preserve(block)
visit(yes)
if no is Some(child) {
visit(child)
}
}
_ => ()
}
}
}
visit(body)
}
///|
fn dae2_original_block_type(
block : @lib.BlockType,
replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> @lib.BlockType {
if block is @lib.TypeIdxBlockType(index) {
if replacements.get(dae2_type_slot(index, 0)) is Some(replacement) {
return @lib.BlockType::type_idx(replacement)
}
}
block
}
///|
fn dae2_original_control_types(
body : Array[@lib.Instruction],
replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> Array[@lib.Instruction] {
body.map(fn(instruction) {
match instruction {
@lib.Block(block, @lib.Expr(body)) =>
@lib.Instruction::block(
dae2_original_block_type(block, replacements),
@lib.Expr::new(dae2_original_control_types(body, replacements)),
)
@lib.Loop(block, @lib.Expr(body)) =>
@lib.Instruction::loop_(
dae2_original_block_type(block, replacements),
@lib.Expr::new(dae2_original_control_types(body, replacements)),
)
@lib.If(block, yes, no) =>
@lib.Instruction::if_(
dae2_original_block_type(block, replacements),
dae2_original_control_types(yes, replacements),
no.map(fn(body) { dae2_original_control_types(body, replacements) }),
)
@lib.Try(block, @lib.Expr(body), catches, delegate) =>
@lib.Instruction::try_(
dae2_original_block_type(block, replacements),
@lib.Expr::new(dae2_original_control_types(body, replacements)),
catches.map(fn(catch_) {
match catch_ {
@lib.LegacyCatch(tag, @lib.Expr(body)) =>
@lib.LegacyCatch::tagged(
tag,
@lib.Expr::new(
dae2_original_control_types(body, replacements),
),
)
@lib.LegacyCatchAll(@lib.Expr(body)) =>
@lib.LegacyCatch::all(
@lib.Expr::new(
dae2_original_control_types(body, replacements),
),
)
}
}),
delegate~,
)
@lib.TryTable(block, catches, @lib.Expr(body)) =>
@lib.Instruction::try_table(
dae2_original_block_type(block, replacements),
catches,
@lib.Expr::new(dae2_original_control_types(body, replacements)),
)
_ => instruction
}
})
}
///|
fn dae2_instrs_have_indexed_control(instrs : Array[@lib.Instruction]) -> Bool {
instrs.any(fn(instr) {
match instr {
@lib.Block(bt, @lib.Expr(body))
| @lib.Loop(bt, @lib.Expr(body))
| @lib.TryTable(bt, _, @lib.Expr(body)) =>
bt is @lib.TypeIdxBlockType(_) || dae2_instrs_have_indexed_control(body)
@lib.If(bt, then_body, else_body) =>
bt is @lib.TypeIdxBlockType(_) ||
dae2_instrs_have_indexed_control(then_body) ||
(match else_body {
Some(body) => dae2_instrs_have_indexed_control(body)
None => false
})
// Legacy adaptation retains its complete preparation path.
@lib.Try(_, _, _, _) => true
_ => false
}
})
}
///|
fn dae2_function_needs_rewrite(
graph : Dae2Graph,
infos : Array[Dae2Function],
info : Dae2Function,
) -> Bool {
dae2_signature_changes(graph, info) ||
info.call_infos.any(fn(callee) {
dae2_signature_changes(graph, infos[callee])
})
}
///|
fn dae2_intrinsic_import_mask(context : @ir.HotModuleContext) -> Array[Bool]? {
let module_name = @lib.Name::from_string("binaryen-intrinsics")
let field_name = @lib.Name::from_string("call.without.effects")
let mut result : Array[Bool]? = None
for index in 0.. {
let mask = match result {
Some(mask) => mask
None => {
let mask = Array::make(context.func_imports.length(), false)
result = Some(mask)
mask
}
}
mask[index] = true
}
_ => ()
}
}
result
}
///|
// Only a concrete forwarding wrapper triggers the broader block cleanup.
// Most final artifacts have none, so preserve the existing lightweight walk.
fn dae2_cleanup_forwarding(
instrs : Array[@lib.Instruction],
module_ctx : @ir.HotModuleContext,
) -> (Array[@lib.Instruction], Bool, Bool) {
let out : Array[@lib.Instruction] = []
let mut changed = false
let mut scalar = false
let visit_body = fn(body) {
let (body, nested_changed, nested_scalar) = dae2_cleanup_forwarding(
body, module_ctx,
)
changed = changed || nested_changed
scalar = scalar || nested_scalar
body
}
for instr in instrs {
let instr = pass_lower_map_children(instr, visit_body)
match instr {
@lib.Block(block_type, @lib.Expr(body)) if !body.is_empty() &&
body.all(fn(instr) { instr is @lib.LocalGet(_) }) => {
let results = match block_type {
@lib.ValTypeBlockType(_) => 1
@lib.TypeIdxBlockType(index) =>
match @ir.hot_module_resolve_type_func_type(module_ctx, index) {
Some(@lib.FuncType([], results)) => results.length()
_ => -1
}
_ => -1
}
if results == body.length() {
out.append(body)
changed = true
scalar = scalar || results == 1
continue
}
}
_ => ()
}
out.push(instr)
}
(out, changed, scalar)
}
///|
fn dae2_cleanup_has_forwarding_candidate(
instrs : Array[@lib.Instruction],
module_ctx : @ir.HotModuleContext,
) -> Bool {
for instr in instrs {
match instr {
@lib.Block(block_type, @lib.Expr(body)) => {
if !body.is_empty() && body.all(fn(item) { item is @lib.LocalGet(_) }) {
let results = match block_type {
@lib.ValTypeBlockType(_) => 1
@lib.TypeIdxBlockType(index) =>
match @ir.hot_module_resolve_type_func_type(module_ctx, index) {
Some(@lib.FuncType([], results)) => results.length()
_ => -1
}
_ => -1
}
if results == body.length() {
return true
}
}
if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
return true
}
}
@lib.Loop(_, @lib.Expr(body)) | @lib.TryTable(_, _, @lib.Expr(body)) =>
if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
return true
}
@lib.If(_, yes, no) => {
if dae2_cleanup_has_forwarding_candidate(yes, module_ctx) {
return true
}
match no {
Some(body) =>
if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
return true
}
None => ()
}
}
@lib.Try(_, @lib.Expr(body), catches, _) => {
if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
return true
}
for catch_ in catches {
let catch_body = match catch_ {
@lib.LegacyCatch(_, @lib.Expr(body))
| @lib.LegacyCatchAll(@lib.Expr(body)) => body
}
if dae2_cleanup_has_forwarding_candidate(catch_body, module_ctx) {
return true
}
}
}
_ => ()
}
}
false
}
///|
// Count-free admissions use constant enum cases, without per-function records.
priv enum Dae2CleanupAdmission {
NoChange
Forwarding
Normalize
Counts(Array[Int32], Array[Int32])
}
///|
fn Dae2CleanupAdmission::may_change(self : Dae2CleanupAdmission) -> Bool {
!(self is NoChange)
}
///|
fn Dae2CleanupAdmission::has_forwarding(self : Dae2CleanupAdmission) -> Bool {
self is Forwarding
}
///|
fn Dae2CleanupAdmission::local_counts(
self : Dae2CleanupAdmission,
) -> (Array[Int32], Array[Int32])? {
match self {
Counts(gets, writes) => Some((gets, writes))
_ => None
}
}
///|
fn dae2_cleanup_admit_func(
func : @lib.Func,
params : Array[@lib.ValType],
module_ctx : @ir.HotModuleContext,
) -> Dae2CleanupAdmission {
let @lib.Func(locals, @lib.Expr(body)) = func
if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
return Dae2CleanupAdmission::Forwarding
}
let runs = locals.runs
for index in 0.. 0 && runs[index - 1].vt == runs[index].vt) {
return Dae2CleanupAdmission::Normalize
}
}
if locals.length() == 0 {
return Dae2CleanupAdmission::NoChange
}
let count = params.length() + locals.length()
let start = params.length()
let gets = Array::make(count, 0)
let writes = Array::make(count, 0)
pass_lower_count_locals(body, gets, writes)
let mut has_single_write = false
for local_id in start.. Bool {
dae2_cleanup_admit_func(func, params, module_ctx).may_change()
}
///|
// Match the forwarding walk's body-container ownership when compaction has
// no body edits. Leaf payload arrays keep the existing early-return contract.
fn dae2_cleanup_copy_body_containers(
body : Array[@lib.Instruction],
) -> Array[@lib.Instruction] {
body.map(fn(instr) {
pass_lower_map_children(instr, dae2_cleanup_copy_body_containers)
})
}
///|
// A retained stack capture stays below every intervening operand. Reject
// control transfers and structured regions; ordinary calls and traps keep
// their original execution point. Resolved calls use the shared type snapshot.
fn dae2_capture_stack_effect(
instr : @lib.Instruction,
mod_ : @lib.Module,
module_ctx : @ir.HotModuleContext,
) -> (Int32, Int32)? {
if instr is @lib.Unreachable ||
run_hot_pipeline_raw_simplify_locals_instr_is_structured(instr) ||
dae_instr_has_branch(instr) {
return None
}
match instr {
@lib.Call(index) =>
match @ir.hot_module_resolve_func_type(module_ctx, index) {
Some(@lib.FuncType(params, results)) =>
Some((params.length(), results.length()))
None => None
}
@lib.CallIndirect(index, _) | @lib.CallRef(index) =>
match @ir.hot_module_resolve_type_func_type(module_ctx, index) {
Some(@lib.FuncType(params, results)) =>
Some((params.length() + 1, results.length()))
None => None
}
_ => dae_instr_stack_effect(instr, mod_, [])
}
}
///|
// The optimizing variant removes redundant capture pairs after its cleanup
// pipeline as well: SimplifyLocals may itself lift and materialize a stack
// value. Compact source and generated locals together to make reruns bounded.
fn dae2_cleanup_captures(mod_ : @lib.Module) -> @lib.Module {
let funcs = match mod_.code_sec {
Some(@lib.CodeSec(funcs)) => funcs
None => return mod_
}
if funcs.is_empty() {
return mod_
}
let params = rl_defined_func_param_cache(mod_)
let module_ctx = @ir.hot_module_context_from_module(mod_)
let mut module_env : @validate.Env? = None
let stack_effects = Some(fn(instr) {
dae2_capture_stack_effect(instr, mod_, module_ctx)
})
let mut output : (Array[@lib.Func], Array[Array[Int32]?], Array[Bool])? = None
let imported = rl_imported_func_count(mod_)
for index = 0; index < funcs.length(); index = index + 1 {
// Classify and consume one body at a time. The local census must not be
// retained across functions or across a body/signature change.
let admission = dae2_cleanup_admit_func(
funcs[index],
params[index],
module_ctx,
)
if !admission.may_change() {
continue
}
let (bodies, remaps, changed_labels) = match output {
Some(output) => output
None => {
let next = (
funcs.copy(),
Array::make(funcs.length(), None),
Array::make(funcs.length(), false),
)
output = Some(next)
next
}
}
let @lib.Func(locals, @lib.Expr(instrs)) = funcs[index]
let (input, forwarding_changed, scalar) = if admission.has_forwarding() {
let (instrs, changed, scalar) = dae2_cleanup_forwarding(
instrs, module_ctx,
)
(@lib.Func::new(locals, @lib.Expr::new(instrs)), changed, scalar)
} else {
(funcs[index], false, false)
}
let (input, labels_changed) = if scalar {
let env = match module_env {
Some(env) => env
None => {
let env = @validate.Env::new().with_module(mod_)
module_env = Some(env)
env
}
}
let (input, blocks_changed) = pass_lower_cleanup_branchless_blocks(
input,
mod_,
module_env=Some(env),
)
(input, forwarding_changed || blocks_changed)
} else {
(input, forwarding_changed)
}
changed_labels[index] = labels_changed
// Forwarding wrappers preserve every local access. The broader scalar
// cleanup can change accesses, so it must invalidate any earlier census.
let local_counts = if scalar { None } else { admission.local_counts() }
let (body, mapping) = pass_lower_compact_captures_with_map(
input,
@lib.Func::new(@lib.Locals::empty(), @lib.Expr::new([])),
params[index],
stack_effects~,
propagate_parameter_aliases=true,
reorder_body_locals=true,
local_name_sec=mod_.name_sec,
function_abs=imported + index,
local_counts~,
)
let @lib.Func(body_locals, @lib.Expr(compacted)) = body
let body = if physical_equal(compacted, instrs) {
// Normalization can return the borrowed source body. Clone only then;
// capture and alias remapping already produce independent containers.
@lib.Func::new(
body_locals,
@lib.Expr::new(dae2_cleanup_copy_body_containers(compacted)),
)
} else {
body
}
bodies[index] = body
remaps[index] = Some(mapping)
}
let (bodies, remaps, changed_labels) = match output {
Some(output) => output
None => return mod_
}
let next = mod_.with_code_sec(@lib.CodeSec::new(bodies))
match rl_rewrite_name_sec(mod_.name_sec, imported, remaps) {
Some(names) => {
// Removing a control label changes the ordinal of later labels. Keep
// metadata for unchanged functions; changed functions have no label map.
let label_names = match names.label_names {
Some(@lib.IndirectNameMap(entries)) => {
let retained = entries.filter(fn(entry) {
let @lib.IndirectNameAssoc(index, _) = entry
let defined = index.to_int() - imported
defined < 0 ||
defined >= changed_labels.length() ||
!changed_labels[defined]
})
if retained.is_empty() {
None
} else {
Some(@lib.IndirectNameMap::new(retained))
}
}
None => None
}
next.with_name_sec(
@lib.NameSec::new(
module_name=names.module_name,
func_names=names.func_names,
local_names=names.local_names,
label_names~,
type_names=names.type_names,
table_names=names.table_names,
memory_names=names.memory_names,
global_names=names.global_names,
elem_names=names.elem_names,
data_names=names.data_names,
field_names=names.field_names,
tag_names=names.tag_names,
),
)
}
None => next.without_name_sec()
}
}
///|
// Capture original indexed control types before lifting normalizes block types.
// The summary belongs to this body/signature snapshot and contains no HOT arena.
fn dae2_collect_control_type_infos(
body : Array[@lib.Instruction],
type_infos : Array[Int32],
out : Array[Int32],
call_infos~ : Array[Int32]? = None,
suffix_state~ : Dae2StackSuffixState? = None,
function_index~ : Int32 = 0,
) -> Bool {
let mut has_calls = false
for instruction in body {
let block_type = match instruction {
@lib.Block(block_type, @lib.Expr(child))
| @lib.Loop(block_type, @lib.Expr(child))
| @lib.TryTable(block_type, _, @lib.Expr(child)) => {
has_calls = dae2_collect_control_type_infos(
child,
type_infos,
out,
call_infos~,
suffix_state~,
function_index~,
) ||
has_calls
Some(block_type)
}
@lib.If(block_type, yes, no) => {
has_calls = dae2_collect_control_type_infos(
yes,
type_infos,
out,
call_infos~,
suffix_state~,
function_index~,
) ||
has_calls
if no is Some(child) {
has_calls = dae2_collect_control_type_infos(
child,
type_infos,
out,
call_infos~,
suffix_state~,
function_index~,
) ||
has_calls
}
Some(block_type)
}
@lib.Try(_, @lib.Expr(child), catches, _) => {
match suffix_state {
Some(state) =>
state.admissions[function_index] = state.admissions[function_index] |
3
None => ()
}
out.push(-1)
has_calls = dae2_collect_control_type_infos(
child,
type_infos,
out,
call_infos~,
suffix_state~,
function_index~,
) ||
has_calls
for catch_ in catches {
let @lib.Expr(child) = match catch_ {
@lib.LegacyCatch(_, body) | @lib.LegacyCatchAll(body) => body
}
has_calls = dae2_collect_control_type_infos(
child,
type_infos,
out,
call_infos~,
suffix_state~,
function_index~,
) ||
has_calls
}
None
}
@lib.Call(@lib.FuncIdx(raw)) | @lib.ReturnCall(@lib.FuncIdx(raw)) => {
has_calls = true
match call_infos {
Some(callees) => callees.push(raw.reinterpret_as_int())
None => ()
}
None
}
@lib.CallIndirect(index, _)
| @lib.ReturnCallIndirect(index, _)
| @lib.CallRef(index)
| @lib.ReturnCallRef(index) => {
match suffix_state {
Some(state) =>
state.admissions[function_index] = state.admissions[function_index] |
2
None => ()
}
has_calls = true
match call_infos {
Some(callees) => {
let slot = dae2_type_slot(index, 0)
callees.push(
if slot >= 0 && slot < type_infos.length() {
type_infos[slot]
} else {
-1
},
)
}
None => ()
}
None
}
@lib.Return => {
match suffix_state {
Some(state) =>
state.admissions[function_index] = state.admissions[function_index] |
2
None => ()
}
None
}
_ => None
}
match (suffix_state, block_type) {
(Some(state), Some(block)) => {
// Bit 1 records structured control; bit 2 excludes static shapes
// requiring general HOT normalization. Reuse this mandatory scan.
let unsupported = match block {
@lib.TypeIdxBlockType(index) =>
!(instruction is @lib.Loop(_, _) &&
dae2_stack_suffix_indexed_loop_has_no_results(state.context, index))
_ => false
}
state.admissions[function_index] = state.admissions[function_index] |
(if unsupported { 3 } else { 1 })
}
_ => ()
}
if block_type is Some(@lib.TypeIdxBlockType(index)) {
let slot = dae2_type_slot(index, 0)
out.push(
if slot >= 0 && slot < type_infos.length() {
type_infos[slot]
} else {
-1
},
)
}
}
has_calls
}
///|
test "DAE2 cleanup normalizes declarations without dropping accessed locals" {
let input = pass_test_module_from_wat(
"(module (func (result i32) (local i32 i32) local.get 0 local.get 0 i32.add local.get 1 i32.add local.get 1 i32.add))",
)
let @lib.Func(_, expr) = input.code_sec.unwrap().0[0]
let locals = @lib.Locals::from_decl_runs_preserving_groups([
@lib.LocalRun::new(0U, @lib.ValType::i64()),
@lib.LocalRun::new(1U, @lib.ValType::i32()),
@lib.LocalRun::new(1U, @lib.ValType::i32()),
])
let func = @lib.Func::new(locals, expr)
let input = input.with_code_sec(@lib.CodeSec::new([func]))
let admission = dae2_cleanup_admit_func(
func,
[],
@ir.hot_module_context_from_module(input),
)
assert_true(admission.may_change())
assert_false(admission.has_forwarding())
assert_true(admission.local_counts() is None)
let saved = @binary.encode_module(input).unwrap()
let output = dae2_cleanup_captures(input)
let @lib.Func(locals, next_expr) = output.code_sec.unwrap().0[0]
assert_eq(locals.runs, [@lib.LocalRun::new(2U, @lib.ValType::i32())])
assert_eq(next_expr, expr)
@validate.validate_module(output).unwrap()
let @lib.Expr(body) = next_expr
body.push(@lib.Instruction::nop())
assert_eq(@binary.encode_module(input).unwrap(), saved)
}