// 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 dae2_local_index_width(index : Int32) -> Int32 {
  if index < 128 {
    1
  } else if index < 16384 {
    2
  } else if index < 2097152 {
    3
  } else if index < 268435456 {
    4
  } else {
    5
  }
}

///|
// Mark possible copy writers and the Get sources of removable Set packets.
// A held tee needs another tee within the same array's preceding 64 slots.
// Set retirement can also remove a root's last reads through unused copies.
fn dae2_mark_possible_alias_writers(
  instrs : Array[@lib.Instruction],
  possible : Array[Bool],
) -> Unit {
  let mut previous_get = false
  let mut previous_tee = -65
  for position in 0.. {
        let producer = if previous_get {
          position - 1
        } else {
          dae2_alias_packet_get_position(instrs, position)
        }
        if producer >= 0 {
          match instrs[producer] {
            @lib.LocalGet(@lib.LocalIdx(source)) => {
              possible[target.to_int()] = true
              possible[source.to_int()] = true
            }
            _ => ()
          }
        }
      }
      @lib.LocalTee(@lib.LocalIdx(target)) => {
        if previous_get || previous_tee >= position - 64 {
          possible[target.to_int()] = true
        }
        previous_tee = position
      }
      @lib.Block(_, @lib.Expr(body))
      | @lib.Loop(_, @lib.Expr(body))
      | @lib.TryTable(_, _, @lib.Expr(body)) =>
        dae2_mark_possible_alias_writers(body, possible)
      @lib.If(_, yes, no) => {
        dae2_mark_possible_alias_writers(yes, possible)
        match no {
          Some(body) => dae2_mark_possible_alias_writers(body, possible)
          None => ()
        }
      }
      @lib.Try(_, @lib.Expr(body), catches, _) => {
        dae2_mark_possible_alias_writers(body, possible)
        for catch_ in catches {
          let body = match catch_ {
            @lib.LegacyCatch(_, @lib.Expr(body))
            | @lib.LegacyCatchAll(@lib.Expr(body)) => body
          }
          dae2_mark_possible_alias_writers(body, possible)
        }
      }
      _ => ()
    }
    previous_get = instr is @lib.LocalGet(_)
  }
}

///|
fn Dae2ParameterAliases::source_is_nonwidening(
  self : Dae2ParameterAliases,
  source : Int32,
  target : Int32,
) -> Bool {
  if dae2_alias_source_is_nonwidening(source, target) {
    return true
  }
  if source == target {
    return false
  }
  let bounds = match self.index_bounds {
    Some(bounds) => bounds
    None => {
      let body = match self.width_body {
        Some(body) => body
        // The legacy benchmark control deliberately omits final-index facts.
        None => return false
      }
      let possible = Array::make(self.reads.length(), false)
      dae2_mark_possible_alias_writers(body, possible)
      let mut upper = 0
      let mut lower = 0
      let bounds = Array::makei(self.reads.length(), fn(i) {
        let packed = (upper.to_int64() << 32) | lower.to_int64()
        // Discovery redirects reads but does not retire writes. Alias targets
        // still have their writer; roots already had a producer read or write.
        // Thus the initially retained set remains an upper bound until the
        // retirement pass, even if these facts are built during discovery.
        if i < self.start || self.reads[i] > 0 || self.writes[i] > 0 {
          upper += 1
        }
        // Unread writes can retire. A possible Set feeder can also lose its
        // last reads after unused copy retirement. Multiwriters with reads
        // cannot be alias sources or targets and retain those reads.
        if i < self.start ||
          (
            self.reads[i] > 0 &&
            (self.writes[i] > 1 || (self.writes[i] == 1 && !possible[i]))
          ) {
          lower += 1
        }
        packed
      })
      self.width_body = None
      self.index_bounds = Some(bounds)
      bounds
    }
  }
  let source_upper = (bounds[source] >> 32).to_int()
  let target_lower = (bounds[target] & 0xffffffffL).to_int()
  dae2_local_index_width(source_upper) <= dae2_local_index_width(target_lower)
}

///|
fn dae2_alias_copy_pays_for_widening(
  source_upper : Int32,
  target_lower : Int32,
  reads : Int32,
  tee : Bool,
) -> Bool {
  // One syntactic read can be forwarded or remain before/outside the alias.
  // In the latter case there is no widening and the writer remains. Multiple
  // reads are deliberately outside this new admission contract.
  if reads < 0 || reads > 1 {
    return false
  }
  let growth = dae2_local_index_width(source_upper) -
    dae2_local_index_width(target_lower)
  let removed = if tee { 2 } else { 4 }
  // A set removes get+set (>=4 bytes); a tee removes only tee (>=2).
  // Strict inequality requires a byte win even at the worst index bounds.
  reads * growth < removed
}

///|
fn Dae2ParameterAliases::source_is_profitable(
  self : Dae2ParameterAliases,
  source : Int32,
  target : Int32,
  tee : Bool,
) -> Bool {
  if self.source_is_nonwidening(source, target) {
    return true
  }
  if source == target || self.reads[target] > 1 {
    return false
  }
  let bounds = match self.index_bounds {
    Some(bounds) => bounds
    // Preserve the legacy control with no body/final-index proof.
    None => return false
  }
  dae2_alias_copy_pays_for_widening(
    (bounds[source] >> 32).to_int(),
    (bounds[target] & 0xffffffffL).to_int(),
    self.reads[target],
    tee,
  )
}