// Licensing scope: see ../../LICENSE and this directory's LICENSE and NOTICE.
// Copyright 2023 WebAssembly Community Group participants.
// Retained historical upstream notice; current derivation remains unconfirmed.
// Conservative attribution from pinned Binaryen version_130 counterpart:
// Copyright 2021 WebAssembly Community Group participants
// Counterpart: src/passes/GlobalRefining.cpp (5d704ad5); origin remains unconfirmed.
// 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.

///|
pub fn global_refining_summary() -> String {
  "Refine defined global reference types from their initializer and observed writes, while preserving mutable export boundaries."
}

///|
fn gr_imported_func_count(mod_ : @lib.Module) -> Int32 {
  let mut count = 0
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) =>
      for import_ in imports {
        match import_ {
          @lib.Import(_, _, @lib.FuncExternType(_)) => count += 1
          _ => ()
        }
      }
    None => ()
  }
  count
}

///|
fn gr_imported_global_count(mod_ : @lib.Module) -> Int32 {
  let mut count = 0
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) =>
      for import_ in imports {
        match import_ {
          @lib.Import(_, _, @lib.GlobalExternType(_)) => count += 1
          _ => ()
        }
      }
    None => ()
  }
  count
}

///|
fn gr_slot_for_abs_idx(slots : Array[Int32], abs_idx : UInt32) -> Int32 {
  let slot = abs_idx.reinterpret_as_int()
  if slot < 0 || slot >= slots.length() {
    -1
  } else {
    slots[slot]
  }
}

///|
fn gr_global_ref_type(global_type : @lib.GlobalType) -> @lib.RefType? {
  match global_type {
    @lib.GlobalType(@lib.RefTypeValType(ref_type), _) => Some(ref_type)
    _ => None
  }
}

///|
fn gr_null_bottom_type_for_comp(comp_type : @lib.CompType) -> @lib.AbsHeapType {
  match comp_type {
    @lib.FuncCompType(_, _) => @lib.AbsHeapType::no_func()
    @lib.ContCompType(_) => @lib.AbsHeapType::no_cont()
    @lib.StructCompType(_) | @lib.ArrayCompType(_) => @lib.AbsHeapType::none()
  }
}

///|
fn gr_ref_func_result_type(
  env : @validate.Env,
  func_idx : @lib.FuncIdx,
) -> @lib.RefType? {
  match env.get_functypeidx_by_funcidx(func_idx) {
    Some(type_idx) =>
      Some(@lib.RefType::new(false, @lib.HeapType::new(type_idx)))
    None => None
  }
}

///|
fn gr_ref_null_result_type(
  ref_type : @lib.RefType,
  env : @validate.Env,
) -> @lib.RefType {
  match ref_type.get_heap_type() {
    @lib.AbsHeapTypeHeapType(abs) => {
      let bottom = match abs {
        @lib.FuncAbsHeapType => @lib.AbsHeapType::no_func()
        @lib.NoFuncAbsHeapType => @lib.AbsHeapType::no_func()
        @lib.WaitqueueAbsHeapType | @lib.NoWaitqueueAbsHeapType =>
          @lib.AbsHeapType::no_waitqueue()
        @lib.ContAbsHeapType => @lib.AbsHeapType::no_cont()
        @lib.NoContAbsHeapType => @lib.AbsHeapType::no_cont()
        @lib.ExternAbsHeapType => @lib.AbsHeapType::no_extern()
        @lib.NoExternAbsHeapType => @lib.AbsHeapType::no_extern()
        @lib.ExnAbsHeapType => @lib.AbsHeapType::no_exn()
        @lib.NoExnAbsHeapType => @lib.AbsHeapType::no_exn()
        @lib.AnyAbsHeapType
        | @lib.EqAbsHeapType
        | @lib.I31AbsHeapType
        | @lib.StructAbsHeapType
        | @lib.ArrayAbsHeapType
        | @lib.StringAbsHeapType
        | @lib.NoneAbsHeapType => @lib.AbsHeapType::none()
      }
      @lib.RefType::new(true, @lib.HeapType::abs(bottom))
    }
    heap_type =>
      match env.resolve_heaptype_subtype(heap_type) {
        Some(subtype) =>
          @lib.RefType::new(
            true,
            @lib.HeapType::abs(
              gr_null_bottom_type_for_comp(subtype.get_comptype()),
            ),
          )
        None => ref_type.make_nullable()
      }
  }
}

///|
fn gr_expr_typechecked_result_type(
  env : @validate.Env,
  expr : @lib.Expr,
) -> @lib.ValType? {
  match @validate.Typecheck::typecheck(expr, @validate.make_state(env, [])) {
    Ok(state) =>
      if state.reachable && state.stack.length() == 1 {
        Some(state.stack[0])
      } else {
        None
      }
    Err(_) => None
  }
}

///|
fn gr_expr_result_type(env : @validate.Env, expr : @lib.Expr) -> @lib.ValType? {
  match expr {
    @lib.Expr([@lib.RefNull(ref_type)]) =>
      Some(@lib.ValType::ref_type(gr_ref_null_result_type(ref_type, env)))
    @lib.Expr([@lib.RefFunc(func_idx)]) =>
      match gr_ref_func_result_type(env, func_idx) {
        Some(ref_type) => Some(@lib.ValType::ref_type(ref_type))
        None => None
      }
    _ => gr_expr_typechecked_result_type(env, expr)
  }
}

///|
priv struct GrPublicTypeScratch {
  states : Array[Int32]
  visited : Array[Int32]
}

///|
fn GrPublicTypeScratch::new(env : @validate.Env) -> GrPublicTypeScratch {
  { states: Array::make(env.global_types.length(), 0), visited: [], }
}

///|
fn gr_valtype_is_public(
  val_type : @lib.ValType,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  match val_type {
    @lib.RefTypeValType(ref_type) =>
      gr_reftype_is_public(ref_type, env, scratch)
    _ => true
  }
}

///|
fn gr_storage_type_is_public(
  storage_type : @lib.StorageType,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  match storage_type {
    @lib.ValTypeStorageType(val_type) =>
      gr_valtype_is_public(val_type, env, scratch)
    @lib.PackTypeStorageType(_) => true
  }
}

///|
fn gr_field_type_is_public(
  field_type : @lib.FieldType,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  gr_storage_type_is_public(field_type.get_storage_type(), env, scratch)
}

///|
fn gr_comptype_is_public(
  comp_type : @lib.CompType,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  match comp_type {
    @lib.FuncCompType(params, results) => {
      for param in params {
        if !gr_valtype_is_public(param, env, scratch) {
          return false
        }
      }
      for result in results {
        if !gr_valtype_is_public(result, env, scratch) {
          return false
        }
      }
      true
    }
    @lib.StructCompType(fields) => {
      for field in fields {
        if !gr_field_type_is_public(field, env, scratch) {
          return false
        }
      }
      true
    }
    @lib.ArrayCompType(field) => gr_field_type_is_public(field, env, scratch)
    @lib.ContCompType(func_type) =>
      gr_typeidx_is_public(func_type, env, scratch)
  }
}

///|
fn gr_typeidx_is_public(
  type_idx : @lib.TypeIdx,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  match type_idx {
    @lib.TypeIdx(raw) => {
      let idx = raw.reinterpret_as_int()
      if idx < 0 || idx >= scratch.states.length() {
        return true
      }
      match scratch.states[idx] {
        2 => return true
        3 => return false
        1 => return true
        _ => ()
      }
      scratch.states[idx] = 1
      scratch.visited.push(idx)
      let public = match env.resolve_subtype(type_idx) {
        Some(subtype) =>
          gr_comptype_is_public(subtype.get_comptype(), env, scratch)
        None => true
      }
      scratch.states[idx] = if public { 2 } else { 3 }
      public
    }
    @lib.RecIdx(_) =>
      match env.resolve_subtype(type_idx) {
        Some(subtype) =>
          gr_comptype_is_public(subtype.get_comptype(), env, scratch)
        None => true
      }
  }
}

///|
fn gr_heaptype_is_public(
  heap_type : @lib.HeapType,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  match heap_type {
    @lib.AbsHeapTypeHeapType(_) | @lib.SharedAbsHeapTypeHeapType(_) => true
    @lib.HeapType(type_idx) => gr_typeidx_is_public(type_idx, env, scratch)
    @lib.DefTypeHeapType(def_type) =>
      match def_type.project() {
        Some(subtype) =>
          gr_comptype_is_public(subtype.get_comptype(), env, scratch)
        None => true
      }
  }
}

///|
fn gr_reftype_is_public(
  ref_type : @lib.RefType,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  if ref_type.is_exact() {
    return false
  }
  gr_heaptype_is_public(ref_type.get_heap_type(), env, scratch)
}

///|
fn gr_custom_descriptors_enabled_locally(env : @validate.Env) -> Bool {
  env.infer_exact_references
}

///|
fn gr_refined_export_type_is_public(
  ref_type : @lib.RefType,
  env : @validate.Env,
  scratch : GrPublicTypeScratch,
) -> Bool {
  if gr_custom_descriptors_enabled_locally(env) {
    return true
  }
  let public = gr_reftype_is_public(ref_type, env, scratch)
  // Reset after each root query: a cycle may have been provisionally public
  // while another path in the same query established a private type.
  for idx in scratch.visited {
    scratch.states[idx] = 0
  }
  scratch.visited.clear()
  public
}

///|
priv struct GrNominalAncestryStats {
  mut declared_subtype_queries : Int32
  mut visited_type_nodes : Int32
  mut candidate_collections : Int32
  mut declared_ancestor_collections : Int32
  mut candidate_nodes_visited : Int32
  temporary_arrays_allocated : Int32
}

///|
priv struct GrNominalAncestryScratch {
  mut generation : Int32
  marks : Array[Int32]
  type_work : Array[@lib.TypeIdx]
  non_absolute_visited : Array[@lib.TypeIdx]
  heap_work : Array[@lib.HeapType]
  candidates : Array[@lib.HeapType]
  abstract_fallbacks : Array[@lib.HeapType]
  failed_ref_slots : @hashmap.HashMap[Int32, Bool]
  stats : GrNominalAncestryStats
}

///|
fn GrNominalAncestryScratch::new(
  env : @validate.Env,
) -> GrNominalAncestryScratch {
  {
    generation: 0,
    marks: Array::make(env.global_types.length(), 0),
    type_work: [],
    non_absolute_visited: [],
    heap_work: [],
    candidates: [],
    abstract_fallbacks: [],
    failed_ref_slots: @hashmap.new(),
    stats: {
      declared_subtype_queries: 0,
      visited_type_nodes: 0,
      candidate_collections: 0,
      declared_ancestor_collections: 0,
      candidate_nodes_visited: 0,
      temporary_arrays_allocated: 6,
    },
  }
}

///|
fn GrNominalAncestryScratch::next_generation(
  self : GrNominalAncestryScratch,
) -> Int32 {
  if self.generation >= 0x7FFFFFFF {
    for idx = 0; idx < self.marks.length(); idx = idx + 1 {
      self.marks[idx] = 0
    }
    self.generation = 1
  } else {
    self.generation += 1
  }
  self.generation
}

///|
fn GrNominalAncestryScratch::observed_work(
  self : GrNominalAncestryScratch,
) -> Int32 {
  self.stats.declared_subtype_queries +
  self.stats.visited_type_nodes +
  self.stats.candidate_collections +
  self.stats.declared_ancestor_collections +
  self.stats.candidate_nodes_visited +
  self.stats.temporary_arrays_allocated
}

///|
fn gr_absolute_type_slot(type_idx : @lib.TypeIdx, type_count : Int32) -> Int32 {
  match type_idx {
    @lib.TypeIdx(raw) => {
      let slot = raw.reinterpret_as_int()
      if slot >= 0 && slot < type_count {
        slot
      } else {
        -1
      }
    }
    @lib.RecIdx(_) => -1
  }
}

///|
fn gr_append_unique_heaptype(
  out : Array[@lib.HeapType],
  heap_type : @lib.HeapType,
) -> Unit {
  for existing in out {
    if existing == heap_type {
      return
    }
  }
  out.push(heap_type)
}

///|
fn gr_append_abs_heap_supertypes(
  out : Array[@lib.HeapType],
  abs : @lib.AbsHeapType,
  shared~ : Bool = false,
) -> Unit {
  let make_heap = fn(abs) {
    if shared {
      @lib.HeapType::shared_abs(abs)
    } else {
      @lib.HeapType::abs(abs)
    }
  }
  match abs {
    @lib.I31AbsHeapType | @lib.StructAbsHeapType | @lib.ArrayAbsHeapType => {
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::eq()))
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::any()))
    }
    @lib.EqAbsHeapType =>
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::any()))
    _ => ()
  }
}

///|
fn gr_append_shape_heap_supertypes(
  out : Array[@lib.HeapType],
  comp_type : @lib.CompType,
  shared~ : Bool = false,
) -> Unit {
  let make_heap = fn(abs) {
    if shared {
      @lib.HeapType::shared_abs(abs)
    } else {
      @lib.HeapType::abs(abs)
    }
  }
  match comp_type {
    @lib.StructCompType(_) => {
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::struct_()))
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::eq()))
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::any()))
    }
    @lib.ArrayCompType(_) => {
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::array()))
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::eq()))
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::any()))
    }
    @lib.FuncCompType(_, _) =>
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::func()))
    @lib.ContCompType(_) =>
      gr_append_unique_heaptype(out, make_heap(@lib.AbsHeapType::cont()))
  }
}

///|
fn gr_collect_heaptype_candidates(
  heap_type : @lib.HeapType,
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> Array[@lib.HeapType] {
  scratch.stats.candidate_collections += 1
  let generation = scratch.next_generation()
  scratch.heap_work.clear()
  scratch.candidates.clear()
  scratch.abstract_fallbacks.clear()
  scratch.non_absolute_visited.clear()
  scratch.heap_work.push(heap_type)
  while scratch.heap_work.length() > 0 {
    let current = scratch.heap_work.pop().unwrap()
    let duplicate = match current {
      @lib.HeapType(type_idx) => {
        let slot = gr_absolute_type_slot(type_idx, scratch.marks.length())
        if slot >= 0 {
          if scratch.marks[slot] == generation {
            true
          } else {
            scratch.marks[slot] = generation
            false
          }
        } else if scratch.non_absolute_visited.contains(type_idx) {
          true
        } else {
          scratch.non_absolute_visited.push(type_idx)
          false
        }
      }
      @lib.AbsHeapTypeHeapType(_)
      | @lib.SharedAbsHeapTypeHeapType(_)
      | @lib.DefTypeHeapType(_) => scratch.candidates.contains(current)
    }
    if duplicate {
      continue
    }
    scratch.stats.candidate_nodes_visited += 1
    scratch.candidates.push(current)
    match current {
      @lib.AbsHeapTypeHeapType(abs) =>
        gr_append_abs_heap_supertypes(scratch.abstract_fallbacks, abs)
      @lib.SharedAbsHeapTypeHeapType(abs) =>
        gr_append_abs_heap_supertypes(
          scratch.abstract_fallbacks,
          abs,
          shared=true,
        )
      @lib.HeapType(type_idx) =>
        match env.resolve_subtype(type_idx) {
          Some(subtype) => {
            for super_type in subtype.super_types().rev_iter() {
              scratch.heap_work.push(@lib.HeapType::new(super_type))
            }
            gr_append_shape_heap_supertypes(
              scratch.abstract_fallbacks,
              subtype.get_comptype(),
              shared=subtype.metadata().is_shared(),
            )
          }
          None => ()
        }
      @lib.DefTypeHeapType(def_type) =>
        match def_type.project() {
          Some(subtype) => {
            for super_type in subtype.super_types().rev_iter() {
              scratch.heap_work.push(@lib.HeapType::new(super_type))
            }
            gr_append_shape_heap_supertypes(
              scratch.abstract_fallbacks,
              subtype.get_comptype(),
              shared=subtype.metadata().is_shared(),
            )
          }
          None => ()
        }
    }
  }
  for fallback in scratch.abstract_fallbacks {
    gr_append_unique_heaptype(scratch.candidates, fallback)
  }
  scratch.candidates
}

///|
fn gr_typeidx_is_declared_subtype(
  source : @lib.TypeIdx,
  target : @lib.TypeIdx,
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> Bool {
  scratch.stats.declared_subtype_queries += 1
  let generation = scratch.next_generation()
  scratch.type_work.clear()
  scratch.non_absolute_visited.clear()
  scratch.type_work.push(source)
  while scratch.type_work.length() > 0 {
    let current = scratch.type_work.pop().unwrap()
    let slot = gr_absolute_type_slot(current, scratch.marks.length())
    if slot >= 0 {
      if scratch.marks[slot] == generation {
        continue
      }
      scratch.marks[slot] = generation
    } else if scratch.non_absolute_visited.contains(current) {
      continue
    } else {
      scratch.non_absolute_visited.push(current)
    }
    scratch.stats.visited_type_nodes += 1
    if current == target {
      return true
    }
    match env.resolve_subtype(current) {
      Some(subtype) =>
        for super_type in subtype.super_types() {
          scratch.type_work.push(super_type)
        }
      None => ()
    }
  }
  false
}

///|
fn gr_mark_declared_ancestors(
  source : @lib.TypeIdx,
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> Int32 {
  scratch.stats.declared_ancestor_collections += 1
  let generation = scratch.next_generation()
  scratch.type_work.clear()
  scratch.non_absolute_visited.clear()
  scratch.type_work.push(source)
  while scratch.type_work.length() > 0 {
    let current = scratch.type_work.pop().unwrap()
    let slot = gr_absolute_type_slot(current, scratch.marks.length())
    if slot >= 0 {
      if scratch.marks[slot] == generation {
        continue
      }
      scratch.marks[slot] = generation
    } else if scratch.non_absolute_visited.contains(current) {
      continue
    } else {
      scratch.non_absolute_visited.push(current)
    }
    scratch.stats.visited_type_nodes += 1
    match env.resolve_subtype(current) {
      Some(subtype) =>
        for super_type in subtype.super_types() {
          scratch.type_work.push(super_type)
        }
      None => ()
    }
  }
  generation
}

///|
fn gr_declared_ancestor_was_marked(
  candidate : @lib.TypeIdx,
  generation : Int32,
  scratch : GrNominalAncestryScratch,
) -> Bool {
  let slot = gr_absolute_type_slot(candidate, scratch.marks.length())
  if slot >= 0 {
    scratch.marks[slot] == generation
  } else {
    scratch.non_absolute_visited.contains(candidate)
  }
}

///|
fn gr_heaptype_matches_join_candidate(
  source : @lib.HeapType,
  candidate : @lib.HeapType,
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> Bool {
  match (source, candidate) {
    (@lib.HeapType(source), @lib.HeapType(candidate)) =>
      gr_typeidx_is_declared_subtype(source, candidate, env, scratch)
    _ => @validate.Match::matches(source, candidate, env)
  }
}

///|
fn gr_join_heaptypes(
  left : @lib.HeapType,
  right : @lib.HeapType,
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> @lib.HeapType? {
  if gr_heaptype_matches_join_candidate(left, right, env, scratch) {
    return Some(right)
  }
  if gr_heaptype_matches_join_candidate(right, left, env, scratch) {
    return Some(left)
  }
  let candidates = gr_collect_heaptype_candidates(left, env, scratch)
  match right {
    @lib.HeapType(right_type_idx) => {
      let generation = gr_mark_declared_ancestors(right_type_idx, env, scratch)
      for candidate in candidates {
        match candidate {
          @lib.HeapType(candidate_type_idx) =>
            if gr_declared_ancestor_was_marked(
                candidate_type_idx, generation, scratch,
              ) {
              return Some(candidate)
            }
          _ =>
            if @validate.Match::matches(right, candidate, env) {
              return Some(candidate)
            }
        }
      }
    }
    _ =>
      for candidate in candidates {
        // Every collected candidate is already a supertype of `left`, so only
        // the right side requires a compatibility query here.
        if gr_heaptype_matches_join_candidate(right, candidate, env, scratch) {
          return Some(candidate)
        }
      }
  }
  None
}

///|
fn gr_heaptype_supports_exact(heap_type : @lib.HeapType) -> Bool {
  match heap_type {
    @lib.HeapType(_) | @lib.DefTypeHeapType(_) => true
    @lib.AbsHeapTypeHeapType(_) | @lib.SharedAbsHeapTypeHeapType(_) => false
  }
}

///|
fn gr_ref_type_is_nullable_bottom_for_heap(
  ref_type : @lib.RefType,
  heap_type : @lib.HeapType,
  env : @validate.Env,
) -> Bool {
  if !ref_type.is_nullable() || ref_type.is_exact() {
    return false
  }
  let bottom = match ref_type.get_heap_type() {
    @lib.AbsHeapTypeHeapType(abs) => abs
    _ => return false
  }
  match env.resolve_heaptype_subtype(heap_type) {
    Some(subtype) =>
      match subtype.get_comptype() {
        @lib.FuncCompType(_, _) => bottom == @lib.AbsHeapType::no_func()
        @lib.ContCompType(_) => bottom == @lib.AbsHeapType::no_cont()
        @lib.StructCompType(_) | @lib.ArrayCompType(_) =>
          bottom == @lib.AbsHeapType::none()
      }
    None => false
  }
}

///|
fn gr_join_reftypes_preserves_exact(
  left : @lib.RefType,
  right : @lib.RefType,
  heap_type : @lib.HeapType,
  env : @validate.Env,
) -> Bool {
  if !gr_heaptype_supports_exact(heap_type) {
    return false
  }
  if left.is_exact() &&
    right.is_exact() &&
    left.get_heap_type() == heap_type &&
    right.get_heap_type() == heap_type {
    return true
  }
  if left.is_exact() &&
    left.get_heap_type() == heap_type &&
    gr_ref_type_is_nullable_bottom_for_heap(right, heap_type, env) {
    return true
  }
  if right.is_exact() &&
    right.get_heap_type() == heap_type &&
    gr_ref_type_is_nullable_bottom_for_heap(left, heap_type, env) {
    return true
  }
  false
}

///|
fn gr_join_reftypes(
  left : @lib.RefType,
  right : @lib.RefType,
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> @lib.RefType? {
  let heap_type = match
    gr_join_heaptypes(left.get_heap_type(), right.get_heap_type(), env, scratch) {
    Some(heap_type) => heap_type
    None => return None
  }
  let exact = gr_join_reftypes_preserves_exact(left, right, heap_type, env)
  let joined = @lib.RefType::new(
    left.is_nullable() || right.is_nullable(),
    heap_type,
    exact~,
  )
  if @validate.Match::matches(left, joined, env) &&
    @validate.Match::matches(right, joined, env) {
    Some(joined)
  } else {
    None
  }
}

///|
fn gr_note_ref_type(
  noted : Array[@lib.RefType?],
  slot : Int32,
  next : @lib.RefType,
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> Unit {
  if slot < 0 ||
    slot >= noted.length() ||
    scratch.failed_ref_slots.contains(slot) {
    return
  }
  match noted[slot] {
    None => noted[slot] = Some(next)
    Some(current) =>
      match gr_join_reftypes(current, next, env, scratch) {
        Some(joined) => noted[slot] = Some(joined)
        None => {
          noted[slot] = None
          scratch.failed_ref_slots.set(slot, true)
        }
      }
  }
}

///|
fn gr_scan_candidate_sets(
  instrs : Array[@lib.Instruction],
  candidate_slots : Array[Int32],
) -> Bool {
  for instr in instrs {
    match instr {
      @lib.GlobalSet(@lib.GlobalIdx(global_idx)) =>
        if gr_slot_for_abs_idx(candidate_slots, global_idx) >= 0 {
          return true
        }
      @lib.Block(_, @lib.Expr(body)) | @lib.Loop(_, @lib.Expr(body)) =>
        if gr_scan_candidate_sets(body, candidate_slots) {
          return true
        }
      @lib.If(_, then_body, else_body) => {
        if gr_scan_candidate_sets(then_body, candidate_slots) {
          return true
        }
        match else_body {
          Some(else_body) =>
            if gr_scan_candidate_sets(else_body, candidate_slots) {
              return true
            }
          None => ()
        }
      }
      @lib.TryTable(_, _, @lib.Expr(body)) =>
        if gr_scan_candidate_sets(body, candidate_slots) {
          return true
        }
      _ => ()
    }
  }
  false
}

///|
fn gr_collect_func_global_sets(
  hot_func : @ir.HotFunc,
  candidate_slots : Array[Int32],
  noted : Array[@lib.RefType?],
  env : @validate.Env,
  scratch : GrNominalAncestryScratch,
) -> Unit {
  let stack : Array[Int32] = []
  for root_idx = @ir.hot_root_count(hot_func) - 1
      root_idx >= 0
      root_idx = root_idx - 1 {
    stack.push(@ir.hot_root_get(hot_func, root_idx))
  }
  while stack.length() > 0 {
    let node_id = stack.pop().unwrap()
    let node = @ir.hot_node_get(hot_func, node_id)
    match node.op {
      @ir.HotOp::GlobalSet => {
        let global_idx = node.imm0
        if global_idx >= 0 && global_idx < candidate_slots.length() {
          let slot = candidate_slots[global_idx]
          if slot >= 0 && @ir.hot_child_count(hot_func, node_id) > 0 {
            let value_id = @ir.hot_child_get(hot_func, node_id, 0)
            if value_id >= 0 {
              let value_node = @ir.hot_node_get(hot_func, value_id)
              match value_node.op {
                @ir.HotOp::RefFunc =>
                  if value_node.imm0 >= 0 {
                    match
                      gr_ref_func_result_type(
                        env,
                        @lib.FuncIdx::new(value_node.imm0.reinterpret_as_uint()),
                      ) {
                      Some(ref_type) =>
                        gr_note_ref_type(noted, slot, ref_type, env, scratch)
                      None => ()
                    }
                  }
                @ir.HotOp::RefNull =>
                  if value_node.imm0 >= 0 {
                    match @ir.hot_const_get(hot_func, value_node.imm0) {
                      @ir.HotConstPayload::RefNullConst(ref_type) => {
                        let noted_ref_type = match ref_type.get_heap_type() {
                          @lib.AbsHeapTypeHeapType(_) =>
                            gr_ref_null_result_type(ref_type, env)
                          _ => ref_type
                        }
                        gr_note_ref_type(
                          noted, slot, noted_ref_type, env, scratch,
                        )
                      }
                      _ => ()
                    }
                  }
                _ =>
                  match
                    @ir.hot_type_results(
                      hot_func,
                      @ir.hot_node_type(hot_func, value_id),
                    ) {
                    [@lib.RefTypeValType(ref_type)] =>
                      gr_note_ref_type(noted, slot, ref_type, env, scratch)
                    _ => ()
                  }
              }
            }
          }
        }
      }
      _ => ()
    }
    for child_idx = 0
        child_idx < @ir.hot_child_count(hot_func, node_id)
        child_idx = child_idx + 1 {
      let child_id = @ir.hot_child_get(hot_func, node_id, child_idx)
      if child_id >= 0 && @ir.hot_node_is_live(hot_func, child_id) {
        stack.push(child_id)
      }
    }
  }
}

///|
fn gr_rewrite_ref_null_set_instrs(
  instrs : Array[@lib.Instruction],
  refined_globals : Array[@lib.RefType?],
  mod_ : @lib.Module,
  func_type_indices : Array[@lib.TypeIdx],
) -> (Array[@lib.Instruction], Bool) {
  let mut next = instrs
  let mut changed = false
  let null_producers = gr_null_write_producers(instrs, mod_, func_type_indices)
  for idx = 0; idx < next.length(); idx = idx + 1 {
    match next[idx] {
      @lib.GlobalSet(@lib.GlobalIdx(global_idx)) => {
        let slot = global_idx.reinterpret_as_int()
        if idx > 0 && slot >= 0 && slot < refined_globals.length() {
          match refined_globals[slot] {
            Some(refined_ref_type) =>
              if null_producers[idx] >= 0 {
                let producer = null_producers[idx]
                if !changed {
                  next = instrs.copy()
                }
                next[producer] = @lib.Instruction::ref_null_type(
                  refined_ref_type,
                )
                changed = true
              }
            None => ()
          }
        }
      }
      @lib.Block(block_type, @lib.Expr(body)) => {
        let (next_body, body_changed) = gr_rewrite_ref_null_set_instrs(
          body, refined_globals, mod_, func_type_indices,
        )
        if body_changed {
          if !changed {
            next = instrs.copy()
          }
          next[idx] = @lib.Instruction::block(
            block_type,
            @lib.Expr::new(next_body),
          )
          changed = true
        }
      }
      @lib.Loop(block_type, @lib.Expr(body)) => {
        let (next_body, body_changed) = gr_rewrite_ref_null_set_instrs(
          body, refined_globals, mod_, func_type_indices,
        )
        if body_changed {
          if !changed {
            next = instrs.copy()
          }
          next[idx] = @lib.Instruction::loop_(
            block_type,
            @lib.Expr::new(next_body),
          )
          changed = true
        }
      }
      @lib.If(block_type, then_body, else_body) => {
        let (next_then, then_changed) = gr_rewrite_ref_null_set_instrs(
          then_body, refined_globals, mod_, func_type_indices,
        )
        let (next_else, else_changed) = match else_body {
          Some(else_body) => {
            let (rewritten_else, changed) = gr_rewrite_ref_null_set_instrs(
              else_body, refined_globals, mod_, func_type_indices,
            )
            (Some(rewritten_else), changed)
          }
          None => (None, false)
        }
        if then_changed || else_changed {
          if !changed {
            next = instrs.copy()
          }
          next[idx] = @lib.Instruction::if_(block_type, next_then, next_else)
          changed = true
        }
      }
      @lib.TryTable(block_type, catches, @lib.Expr(body)) => {
        let (next_body, body_changed) = gr_rewrite_ref_null_set_instrs(
          body, refined_globals, mod_, func_type_indices,
        )
        if body_changed {
          if !changed {
            next = instrs.copy()
          }
          next[idx] = @lib.Instruction::try_table(
            block_type,
            catches,
            @lib.Expr::new(next_body),
          )
          changed = true
        }
      }
      _ => ()
    }
  }
  (next, changed)
}

///|
fn gr_null_write_producers(
  instrs : Array[@lib.Instruction],
  mod_ : @lib.Module,
  func_type_indices : Array[@lib.TypeIdx],
) -> Array[Int32] {
  let producers = Array::make(instrs.length(), -1)
  // Track the known suffix of the operand stack. An unsupported or
  // polymorphic instruction clears that suffix, but later independent leaves
  // can immediately begin a new one.
  let stack : Array[Int32] = []
  for idx = 0; idx < instrs.length(); idx = idx + 1 {
    if instrs[idx] is @lib.GlobalSet(_) && !stack.is_empty() {
      producers[idx] = stack[stack.length() - 1]
    }
    let (pops, pushes) = match
      dae_instr_stack_effect(instrs[idx], mod_, func_type_indices) {
      Some(effect) => effect
      None => {
        stack.clear()
        continue
      }
    }
    let known_inputs = pops <= stack.length()
    if known_inputs {
      for pop_idx = 0; pop_idx < pops; pop_idx = pop_idx + 1 {
        ignore(pop_idx)
        ignore(stack.pop())
      }
    } else {
      stack.clear()
    }
    let producer = if known_inputs &&
      pops == 0 &&
      pushes == 1 &&
      instrs[idx] is @lib.RefNull(_) {
      idx
    } else {
      -1
    }
    for push_idx = 0; push_idx < pushes; push_idx = push_idx + 1 {
      ignore(push_idx)
      stack.push(producer)
    }
  }
  producers
}

///|
fn gr_rewrite_ref_null_sets_in_code(
  mod_ : @lib.Module,
  refined_globals : Array[@lib.RefType?],
) -> (@lib.Module, Bool) {
  let funcs = match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => funcs
    None => return (mod_, false)
  }
  let func_type_indices = dae_collect_func_type_indices(mod_)
  let next_funcs = funcs.copy()
  let mut changed = false
  for idx = 0; idx < funcs.length(); idx = idx + 1 {
    let @lib.Func(locals, @lib.Expr(body)) = funcs[idx]
    let (next_body, body_changed) = gr_rewrite_ref_null_set_instrs(
      body, refined_globals, mod_, func_type_indices,
    )
    if body_changed {
      next_funcs[idx] = @lib.Func::new(locals, @lib.Expr::new(next_body))
      changed = true
    }
  }
  if changed {
    (mod_.with_code_sec(@lib.CodeSec::new(next_funcs)), true)
  } else {
    (mod_, false)
  }
}

///|
fn gr_refine_module(
  mod_ : @lib.Module,
  closed_world~ : Bool = false,
) -> @lib.Module {
  if pass_module_has_legacy_try(mod_) {
    return mod_
  }
  let env = @validate.Env::new().with_module(mod_)
  let imported_global_count = gr_imported_global_count(mod_)
  let total_globals = env.globals.length()
  if imported_global_count >= total_globals {
    return mod_
  }
  let globals = match mod_.global_sec {
    Some(@lib.GlobalSec(globals)) => globals
    None => return mod_
  }

  let exported_globals = Array::make(total_globals, false)
  match mod_.export_sec {
    Some(@lib.ExportSec(exports)) =>
      for export_ in exports {
        match export_ {
          @lib.Export(_, @lib.GlobalExternIdx(@lib.GlobalIdx(global_idx))) => {
            let slot = global_idx.reinterpret_as_int()
            if slot >= 0 && slot < exported_globals.length() {
              exported_globals[slot] = true
            }
          }
          _ => ()
        }
      }
    None => ()
  }

  let candidate_slots = Array::make(total_globals, -1)
  let candidate_abs_globals : Array[Int32] = []
  for defined_idx = 0
      defined_idx < globals.length()
      defined_idx = defined_idx + 1 {
    let abs_idx = imported_global_count + defined_idx
    let is_mutable = match env.globals[abs_idx] {
      @lib.GlobalType(_, mutable_) => mutable_
    }
    if exported_globals[abs_idx] && (closed_world || is_mutable) {
      continue
    }
    match gr_global_ref_type(env.globals[abs_idx]) {
      Some(_) => {
        candidate_slots[abs_idx] = candidate_abs_globals.length()
        candidate_abs_globals.push(abs_idx)
      }
      None => ()
    }
  }
  if candidate_abs_globals.length() == 0 {
    return mod_
  }

  let scratch = GrNominalAncestryScratch::new(env)
  let noted : Array[@lib.RefType?] = []
  for _ in candidate_abs_globals {
    noted.push(None)
  }

  for defined_idx = 0
      defined_idx < globals.length()
      defined_idx = defined_idx + 1 {
    let abs_idx = imported_global_count + defined_idx
    let slot = candidate_slots[abs_idx]
    if slot < 0 {
      continue
    }
    let @lib.Global(_, init) = globals[defined_idx]
    match gr_expr_result_type(env, init) {
      Some(@lib.RefTypeValType(ref_type)) =>
        gr_note_ref_type(noted, slot, ref_type, env, scratch)
      _ => ()
    }
  }

  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => {
      let module_ctx = @ir.hot_module_context_from_module(mod_)
      let imported_func_count = gr_imported_func_count(mod_)
      for defined_idx = 0
          defined_idx < funcs.length()
          defined_idx = defined_idx + 1 {
        let @lib.Func(_, @lib.Expr(instrs)) = funcs[defined_idx]
        if !gr_scan_candidate_sets(instrs, candidate_slots) {
          continue
        }
        let abs_idx = imported_func_count + defined_idx
        let hot_func = @ir.hot_lift_func_with_context(
          mod_,
          @lib.FuncIdx::new(abs_idx.reinterpret_as_uint()),
          module_ctx,
        )
        gr_collect_func_global_sets(
          hot_func, candidate_slots, noted, env, scratch,
        )
      }
    }
    None => ()
  }

  ignore(scratch.observed_work())
  let next_globals = globals.copy()
  let refined_globals : Array[@lib.RefType?] = Array::make(total_globals, None)
  let mut public_type_scratch : GrPublicTypeScratch? = None
  let mut changed = false
  for defined_idx = 0
      defined_idx < globals.length()
      defined_idx = defined_idx + 1 {
    let abs_idx = imported_global_count + defined_idx
    let slot = candidate_slots[abs_idx]
    if slot < 0 {
      continue
    }
    let old_global_type = env.globals[abs_idx]
    let old_ref_type = match gr_global_ref_type(old_global_type) {
      Some(ref_type) => ref_type
      None => continue
    }
    let inferred_ref_type = match noted[slot] {
      Some(ref_type) => ref_type
      None => continue
    }
    let refined_ref_type = if inferred_ref_type.is_exact() &&
      !old_ref_type.is_exact() {
      @lib.RefType::new(
        inferred_ref_type.is_nullable(),
        inferred_ref_type.get_heap_type(),
        exact=false,
      )
    } else {
      inferred_ref_type
    }
    if refined_ref_type == old_ref_type ||
      !@validate.Match::matches(refined_ref_type, old_ref_type, env) {
      continue
    }
    if exported_globals[abs_idx] {
      let scratch = match public_type_scratch {
        Some(existing) => existing
        None => {
          let created = GrPublicTypeScratch::new(env)
          public_type_scratch = Some(created)
          created
        }
      }
      if !gr_refined_export_type_is_public(refined_ref_type, env, scratch) {
        continue
      }
    }
    let @lib.Global(@lib.GlobalType(_, mutable_), init) = next_globals[defined_idx]
    let refined_init = match init {
      @lib.Expr([@lib.RefNull(_)]) =>
        @lib.Expr::new([@lib.Instruction::ref_null_type(refined_ref_type)])
      _ => init
    }
    next_globals[defined_idx] = @lib.Global::new(
      @lib.GlobalType::new(@lib.ValType::ref_type(refined_ref_type), mutable_),
      refined_init,
    )
    refined_globals[abs_idx] = Some(refined_ref_type)
    changed = true
  }
  if !changed {
    return mod_
  }
  let rewritten = mod_.with_global_sec(@lib.GlobalSec::new(next_globals))
  let (rewritten, _) = gr_rewrite_ref_null_sets_in_code(
    rewritten, refined_globals,
  )
  rewritten
}

///|
pub fn global_refining_run_module_pass(
  mod_ : @lib.Module,
  closed_world~ : Bool = false,
) -> @lib.Module {
  if pass_module_has_offset_sensitive_metadata(mod_) {
    return mod_
  }
  let refined = gr_refine_module(mod_, closed_world~)
  // A module without candidate globals can still carry duplicate signatures
  // and fragmented numeric local runs. Reuse the guarded encoding cleanup.
  if pass_module_has_legacy_try(refined) {
    return refined
  }
  dfe_cleanup_raw_encoding(pass_cleanup_unused_type_groups(refined))
}