// 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.
///|
#valtype
priv struct SlConstantStoreSink {
store : Int32
read : Int32
}
///|
fn SlConstantStoreSink::new(store : Int32, read : Int32) -> SlConstantStoreSink {
{ store, read, }
}
///|
fn sl_sinkable_numeric_constant(instr : @lib.Instruction) -> Bool {
match instr {
@lib.I32Const(_) | @lib.I64Const(_) | @lib.F32Const(_) | @lib.F64Const(_) =>
true
_ => false
}
}
///|
fn sl_constant_store_prefix_is_nontrapping(instr : @lib.Instruction) -> Bool {
match instr {
@lib.Nop | @lib.Drop => true
_ =>
if sl_sinkable_numeric_constant(instr) {
true
} else {
// Reuse the existing nontrapping operand classifier. Exclude other
// zero-input leaves; this lane only crosses arithmetic/selection here.
match run_hot_pipeline_raw_vacuum_pure_instr_operand_count(instr) {
Some(inputs) => inputs > 0
None => false
}
}
}
}
///|
fn sl_constant_pending_get(
dense : Array[Int32]?,
sparse : Map[Int32, Int32]?,
local : Int32,
) -> Int32 {
match dense {
Some(row) =>
if local >= 0 && local < row.length() {
row[local]
} else {
-1
}
None => sparse.unwrap().get(local).unwrap_or(-1)
}
}
///|
fn sl_constant_pending_set(
dense : Array[Int32]?,
sparse : Map[Int32, Int32]?,
local : Int32,
store : Int32,
) -> Unit {
match dense {
Some(row) => row[local] = store
None => sparse.unwrap()[local] = store
}
}
///|
// Track all pending constant assignments in one forward scan after a cheap
// candidate-only census. Dense storage is capped at 4096 Int slots and at eight
// slots per candidate; sparse/high-index bodies retain the map. The choice never
// limits rewrites. First reads consume entries; writes and trapping/throwing/
// control instructions invalidate them. Retain each assignment as a tee for
// later reads, including enclosing catches and structured releases.
fn sl_sink_constant_stores_flat(
instrs : Array[@lib.Instruction],
scan_visits~ : Array[Int32]? = None,
) -> (Array[@lib.Instruction], Int32) {
let mut candidates = 0
let mut max_local = -1
let mut nonnegative = true
for idx in 0.. visits[0] += 1
None => ()
}
if instrs[idx] is @lib.LocalSet(@lib.LocalIdx(raw)) &&
idx > 0 &&
sl_sinkable_numeric_constant(instrs[idx - 1]) {
candidates += 1
let local = raw.reinterpret_as_int()
nonnegative = nonnegative && local >= 0
max_local = Int32::max(max_local, local)
}
}
if candidates == 0 {
return (instrs, 0)
}
let use_dense = nonnegative &&
max_local < 4096 &&
(max_local + 8) / 8 <= candidates
let dense = if use_dense {
Some(Array::make(max_local + 1, -1))
} else {
None
}
let sparse : Map[Int32, Int32]? = if use_dense { None } else { Some(Map([])) }
let mut sinks : Array[SlConstantStoreSink]? = None
let mut last_barrier = -1
let mut active = 0
for idx in 0.. visits[0] += 1
None => ()
}
match instrs[idx] {
@lib.LocalGet(@lib.LocalIdx(raw)) => {
if active == 0 {
continue
}
let local = raw.reinterpret_as_int()
let store = sl_constant_pending_get(dense, sparse, local)
if store > last_barrier {
let records = match sinks {
Some(records) => records
None => {
let records : Array[SlConstantStoreSink] = []
sinks = Some(records)
records
}
}
records.push(SlConstantStoreSink::new(store, idx))
sl_constant_pending_set(dense, sparse, local, -1)
active -= 1
}
}
@lib.LocalSet(@lib.LocalIdx(raw)) => {
let local = raw.reinterpret_as_int()
if active > 0 &&
sl_constant_pending_get(dense, sparse, local) > last_barrier {
sl_constant_pending_set(dense, sparse, local, -1)
active -= 1
}
if idx > 0 && sl_sinkable_numeric_constant(instrs[idx - 1]) {
sl_constant_pending_set(dense, sparse, local, idx)
active += 1
}
}
@lib.LocalTee(@lib.LocalIdx(raw)) => {
if active == 0 {
continue
}
let local = raw.reinterpret_as_int()
if sl_constant_pending_get(dense, sparse, local) > last_barrier {
sl_constant_pending_set(dense, sparse, local, -1)
active -= 1
}
}
instr =>
if active > 0 && !sl_constant_store_prefix_is_nontrapping(instr) {
// Store positions are epochs: invalidate the whole prefix in O(1).
last_barrier = idx
active = 0
}
}
}
let sinks = match sinks {
Some(records) => records
None => return (instrs, 0)
}
// One compact splice row: -1 keeps the original instruction, -2 deletes a
// producer/store, and a nonnegative value selects the moved constant.
let changes = Array::make(instrs.length(), -1)
for sink in sinks {
changes[sink.store - 1] = -2
changes[sink.store] = -2
changes[sink.read] = sink.store - 1
}
let rewritten : Array[@lib.Instruction] = Array(
capacity=instrs.length() - sinks.length(),
)
for idx in 0.. visits[0] += 1
None => ()
}
if changes[idx] >= 0 {
rewritten.push(instrs[changes[idx]])
let local = match instrs[idx] {
@lib.LocalGet(local) => local
_ => abort("constant sink requires a first local read")
}
rewritten.push(@lib.Instruction::local_tee(local))
} else if changes[idx] != -2 {
rewritten.push(instrs[idx])
}
}
(rewritten, sinks.length())
}
///|
fn sl_sink_constant_stores(
instrs : Array[@lib.Instruction],
) -> (Array[@lib.Instruction], Int32) {
let mut normalized = instrs
let mut nested_count = 0
for idx in 0.. {
let (next, count) = sl_sink_constant_stores(body)
if count == 0 {
continue
}
(@lib.Instruction::block(bt, @lib.Expr::new(next)), count)
}
@lib.Loop(bt, @lib.Expr(body)) => {
let (next, count) = sl_sink_constant_stores(body)
if count == 0 {
continue
}
(@lib.Instruction::loop_(bt, @lib.Expr::new(next)), count)
}
@lib.TryTable(bt, catches, @lib.Expr(body)) => {
let (next, count) = sl_sink_constant_stores(body)
if count == 0 {
continue
}
(@lib.Instruction::try_table(bt, catches, @lib.Expr::new(next)), count)
}
@lib.If(bt, yes, no) => {
let (next_yes, yes_count) = sl_sink_constant_stores(yes)
let (next_no, no_count) = match no {
Some(body) => {
let (next, count) = sl_sink_constant_stores(body)
(Some(next), count)
}
None => (None, 0)
}
let count = yes_count + no_count
if count == 0 {
continue
}
(@lib.Instruction::if_(bt, next_yes, next_no), count)
}
// Legacy handlers stay opaque here; their read/write facts remain in the
// surrounding cleanup. Do not move an outer assignment across a try.
_ => continue
}
if count > 0 {
if nested_count == 0 {
normalized = instrs.copy()
}
normalized[idx] = replacement
nested_count += count
}
}
let (rewritten, flat_count) = sl_sink_constant_stores_flat(normalized)
(rewritten, nested_count + flat_count)
}