// 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 Dae2PrivateGroup {
signatures : Array[@lib.SubType]
slots : Array[Int32]
}
///|
fn Dae2PrivateGroup::new() -> Dae2PrivateGroup {
{ signatures: [], slots: [], }
}
///|
fn Dae2PrivateGroup::append(
self : Dae2PrivateGroup,
workspace : Dae2TypeWorkspace,
new_indices : Array[@lib.TypeIdx],
) -> Bool {
if self.signatures.is_empty() {
return false
}
if self.signatures.length() == 1 {
// Source admission requires final singletons. An open singleton keeps
// this one changed private target distinct without a witness member.
let base = workspace.flat_count
let @lib.TypeSec(groups) = workspace.types
groups.push(
@lib.RecType::new(
@lib.SubType::new(false, [], self.signatures[0].get_comptype()),
),
)
workspace.flat_count += 1
new_indices[self.slots[0]] = @lib.TypeIdx::new(base.to_uint())
return true
}
let base = workspace.flat_count
let @lib.TypeSec(groups) = workspace.types
groups.push(@lib.RecType::group(self.signatures))
workspace.flat_count += self.signatures.length()
for member = 0; member < self.slots.length(); member = member + 1 {
new_indices[self.slots[member]] = @lib.TypeIdx::new(
(base + member).to_uint(),
)
}
true
}
///|
// Imported code can invoke a function reference received as an argument. The
// signature stays observable even if the reference travels through a local.
fn dae2_has_host_callable_parameter(context : @ir.HotModuleContext) -> Bool {
for index = 0; index < context.func_imports.length(); index = index + 1 {
if context.func_imports[index] is None {
break
}
let @lib.FuncType(params, _) = context.func_types[index]
for param in params {
match param {
@lib.RefTypeValType(ref_type) =>
match ref_type.get_heap_type() {
@lib.AbsHeapTypeHeapType(@lib.FuncAbsHeapType)
| @lib.SharedAbsHeapTypeHeapType(@lib.FuncAbsHeapType) =>
return true
@lib.HeapType(@lib.RecIdx(_)) => return true
@lib.HeapType(type_idx) =>
match @ir.hot_module_resolve_subtype(context, type_idx) {
Some(subtype) =>
if subtype.get_comptype() is @lib.FuncCompType(_, _) {
return true
}
None => ()
}
@lib.DefTypeHeapType(def_type) =>
match def_type.project() {
Some(subtype) =>
if subtype.get_comptype() is @lib.FuncCompType(_, _) {
return true
}
None => ()
}
_ => ()
}
_ => ()
}
}
}
false
}
///|
fn dae2_private_element_candidate(e : DaeFunctionExposure) -> Bool {
(e.active_element || e.passive_element) &&
!e.imported &&
!e.exported &&
!e.start &&
!e.ref_func_body &&
!e.ref_func_module &&
!e.declarative_element
}
///|
fn dae2_private_element_scalar(value : @lib.ValType) -> Bool {
value is @lib.NumTypeValType(_) || value is @lib.VecTypeValType
}
///|
fn dae2_private_element_subtype(subtype : @lib.SubType) -> Bool {
let metadata = subtype.metadata()
if metadata.shared ||
metadata.describes is Some(_) ||
metadata.descriptor is Some(_) {
return false
}
match subtype {
@lib.SubType(final_, supers, _, _) =>
if !final_ || !supers.is_empty() {
return false
}
_ => ()
}
match subtype.get_comptype() {
@lib.FuncCompType(params, results) =>
params.all(dae2_private_element_scalar) &&
results.all(dae2_private_element_scalar)
_ => false
}
}
///|
// Admit flat scalar bodies only. In particular, indirect calls, reference
// reads/tests, GC, handlers, and indexed control keep the existing family path.
// Reuse the numeric inventory without creating a second opcode table.
fn dae2_private_element_body(body : Array[@lib.Instruction]) -> Bool {
for instr in body {
match instr {
@lib.StringConst(_)
| @lib.RefNull(_)
| @lib.RefFunc(_)
| @lib.RefIsNull
| @lib.RefTest(_, _, _)
| @lib.TableSet(_)
| @lib.StructSet(_, _)
| @lib.ArraySet(_) => return false
@lib.Select(Some(values)) =>
if !values.all(dae2_private_element_scalar) {
return false
}
@lib.Nop
| @lib.Drop
| @lib.Unreachable
| @lib.Return
| @lib.Br(_)
| @lib.BrIf(_)
| @lib.BrTable(_, _)
| @lib.LocalSet(_)
| @lib.LocalTee(_)
| @lib.Call(_)
| @lib.ReturnCall(_) => ()
_ =>
if dae2_raw_pure_arity(instr) < 0 && dae2_raw_observer_arity(instr) < 0 {
return false
}
}
}
true
}
///|
fn dae2_private_element_mask(
mod_ : @lib.Module,
context : @ir.HotModuleContext,
exposure : Array[DaeFunctionExposure],
closed_world : Bool,
) -> Array[Bool]? {
if !closed_world || !exposure.any(dae2_private_element_candidate) {
return None
}
let @lib.TypeSec(groups) = mod_.type_sec.unwrap()
for group in groups {
match group {
@lib.SingleRecType(subtype) =>
if !dae2_private_element_subtype(subtype) {
return None
}
@lib.GroupRecType(_) => return None
}
}
// Concrete table/global/local references can observe the old contract.
for table_type in context.tables {
if !dae2_is_abstract_funcref_table(table_type) {
return None
}
}
for global_type in context.globals {
let @lib.GlobalType(value, _) = global_type
if !dae2_private_element_scalar(value) {
return None
}
}
match mod_.table_sec {
Some(@lib.TableSec(tables)) =>
for table in tables {
let @lib.Table(_, init) = table
if init is Some(_) {
return None
}
}
None => ()
}
match mod_.elem_sec {
Some(@lib.ElemSec(elems)) =>
for elem in elems {
let @lib.Elem(mode, kind) = elem
if !(kind is @lib.FuncsElemKind(_)) {
return None
}
match mode {
@lib.Active(_, @lib.Expr(offset)) =>
if !dae2_private_element_body(offset) {
return None
}
_ => ()
}
}
None => ()
}
match mod_.global_sec {
Some(@lib.GlobalSec(globals)) =>
for global in globals {
let @lib.Global(_, @lib.Expr(init)) = global
if !dae2_private_element_body(init) {
return None
}
}
None => ()
}
match mod_.data_sec {
Some(@lib.DataSec(datas)) =>
for data in datas {
let @lib.Data(mode, _) = data
match mode {
@lib.Active(_, @lib.Expr(offset)) =>
if !dae2_private_element_body(offset) {
return None
}
_ => ()
}
}
None => ()
}
match mod_.code_sec {
Some(@lib.CodeSec(funcs)) =>
for func in funcs {
let @lib.Func(locals, @lib.Expr(body)) = func
for run in locals.runs() {
if !dae2_private_element_scalar(run.val_type()) {
return None
}
}
if !dae2_private_element_body(body) {
return None
}
}
None => ()
}
// A unique singleton type owned by one function cannot split an equivalent
// source identity. Count shared keys without cloning their operand arrays.
let counts : @hashmap.HashMap[@lib.CompType, Int32] = @hashmap.new()
for subtype in context.subtypes {
let key = subtype.get_comptype()
counts.set(key, counts.get(key).unwrap_or(0) + 1)
}
for type_idx in context.func_type_idxs {
let key = context.subtypes[dae2_type_slot(type_idx, 0)].get_comptype()
counts.set(key, counts.get(key).unwrap_or(0) + 1)
}
let mask = Array::make(exposure.length(), false)
let mut any = false
for index = 0; index < exposure.length(); index = index + 1 {
if dae2_private_element_candidate(exposure[index]) {
let signature = context.subtypes[dae2_type_slot(
context.func_type_idxs[index],
0,
)].get_comptype()
if counts.get(signature) == Some(2) {
mask[index] = true
any = true
}
}
}
if any {
Some(mask)
} else {
None
}
}
///|
// Private group members are numeric and index-free. Scan roots once, keep
// every member of a live group, then remap the module and its type name map.
// This removes obsolete source singletons without splitting group identities.
fn dae2_prune_private_element_source_types(mod_ : @lib.Module) -> @lib.Module {
let @lib.TypeSec(groups) = mod_.type_sec.unwrap()
for group in groups {
match group {
@lib.SingleRecType(subtype) =>
if !dae2_private_element_subtype(subtype) {
return mod_
}
@lib.GroupRecType(members) =>
if !members.all(dae2_private_element_subtype) {
return mod_
}
}
}
let refs : DfeTypeRefs = {
live: Array::make(
dae_type_sec_flattened_length(mod_.type_sec.unwrap()),
false,
),
pending: [],
}
dfe_type_refs_module_roots(mod_, refs)
let kept : Array[@lib.RecType] = []
let remap : Map[UInt32, UInt32] = Map([])
let mut source = 0
let mut destination = 0
for group in groups {
let count = match group {
@lib.SingleRecType(_) => 1
@lib.GroupRecType(members) => members.length()
}
let mut live = false
for offset = 0; offset < count; offset = offset + 1 {
live = live || refs.live[source + offset]
}
if live {
kept.push(group)
for offset = 0; offset < count; offset = offset + 1 {
remap[(source + offset).to_uint()] = (destination + offset).to_uint()
}
destination += count
}
source += count
}
if source == destination {
return mod_
}
let rewritten = dfe_rewrite_module_type_idxs(
mod_.with_type_sec(@lib.TypeSec::new(kept)),
remap,
)
match rewritten.name_sec {
Some(names) =>
rewritten.with_name_sec(dfe_rewrite_type_name_sec(names, remap))
None => rewritten
}
}