// 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.

///|
pub fn string_gathering_summary() -> String {
  "Hoist wasm string constants into shared immutable string globals and rewrite uses to global.get."
}

///|
priv struct SgStringSet {
  strings : Array[Bytes]
  mut indices : Map[Bytes, Int32]
}

///|
priv struct SgRewritePlan {
  strings : Array[Bytes]
  string_indices : Map[Bytes, Int32]
  string_global_abs : Array[Int32]
  old_defined_to_abs : Array[Int32]
  old_preserve_type : Array[Bool]
  entry_is_fresh : Array[Bool]
  entry_value : Array[Int32]
}

///|
fn SgStringSet::new() -> SgStringSet {
  { strings: [], indices: Map([]), }
}

///|
fn sg_string_index(set : SgStringSet, bytes : Bytes) -> Int32? {
  set.indices.get(bytes)
}

///|
fn sg_intern_string(set : SgStringSet, bytes : Bytes) -> Int32 {
  match sg_string_index(set, bytes) {
    Some(idx) => idx
    None => {
      let idx = set.strings.length()
      set.strings.push(bytes)
      set.indices[bytes] = idx
      idx
    }
  }
}

///|
fn sg_collect_instr(instr : @lib.Instruction, set : SgStringSet) -> Unit {
  match instr {
    @lib.StringConst(bytes) => ignore(sg_intern_string(set, bytes))
    @lib.Block(_, body) | @lib.Loop(_, body) => sg_collect_expr(body, set)
    @lib.If(_, then_body, else_body) => {
      sg_collect_instrs(then_body, set)
      match else_body {
        Some(body) => sg_collect_instrs(body, set)
        None => ()
      }
    }
    @lib.TryTable(_, _, body) => sg_collect_expr(body, set)
    _ => ()
  }
}

///|
fn sg_collect_instrs(
  instrs : Array[@lib.Instruction],
  set : SgStringSet,
) -> Unit {
  for instr in instrs {
    sg_collect_instr(instr, set)
  }
}

///|
fn sg_collect_expr(expr : @lib.Expr, set : SgStringSet) -> Unit {
  let @lib.Expr(instrs) = expr
  sg_collect_instrs(instrs, set)
}

///|
fn sg_collect_code_sec(code_sec : @lib.CodeSec?, set : SgStringSet) -> Unit {
  match code_sec {
    Some(@lib.CodeSec(funcs)) =>
      for func in funcs {
        let @lib.Func(_, body) = func
        sg_collect_expr(body, set)
      }
    None => ()
  }
}

///|
fn sg_collect_global_sec(
  global_sec : @lib.GlobalSec?,
  set : SgStringSet,
) -> Unit {
  match global_sec {
    Some(@lib.GlobalSec(globals)) =>
      for global in globals {
        let @lib.Global(_, init) = global
        sg_collect_expr(init, set)
      }
    None => ()
  }
}

///|
fn sg_collect_elem_mode(mode : @lib.ElemMode, set : SgStringSet) -> Unit {
  match mode {
    @lib.Active(_, expr) => sg_collect_expr(expr, set)
    _ => ()
  }
}

///|
fn sg_collect_elem_kind(kind : @lib.ElemKind, set : SgStringSet) -> Unit {
  match kind {
    @lib.FuncExprsElemKind(exprs) | @lib.TypedExprsElemKind(_, exprs) =>
      for expr in exprs {
        sg_collect_expr(expr, set)
      }
    _ => ()
  }
}

///|
fn sg_collect_elem_sec(elem_sec : @lib.ElemSec?, set : SgStringSet) -> Unit {
  match elem_sec {
    Some(@lib.ElemSec(elems)) =>
      for elem in elems {
        let @lib.Elem(mode, kind) = elem
        sg_collect_elem_mode(mode, set)
        sg_collect_elem_kind(kind, set)
      }
    None => ()
  }
}

///|
fn sg_collect_data_sec(data_sec : @lib.DataSec?, set : SgStringSet) -> Unit {
  match data_sec {
    Some(@lib.DataSec(datas)) =>
      for data in datas {
        let @lib.Data(mode, _) = data
        match mode {
          @lib.Active(_, expr) => sg_collect_expr(expr, set)
          _ => ()
        }
      }
    None => ()
  }
}

///|
fn sg_collect_strings(mod_ : @lib.Module) -> SgStringSet {
  let set = SgStringSet::new()
  // Binaryen's StringGathering visits function bodies before module-level
  // constant expressions, then inserts gathered globals before existing defined
  // globals. Keep that order for normalized-WAT parity.
  sg_collect_code_sec(mod_.code_sec, set)
  sg_collect_global_sec(mod_.global_sec, set)
  sg_collect_elem_sec(mod_.elem_sec, set)
  sg_collect_data_sec(mod_.data_sec, set)
  set
}

///|
fn sg_instrs_have_string_const(instrs : Array[@lib.Instruction]) -> Bool {
  for instr in instrs {
    match instr {
      @lib.StringConst(_) => return true
      @lib.Block(_, body) | @lib.Loop(_, body) | @lib.TryTable(_, _, body) =>
        if sg_expr_has_string_const(body) {
          return true
        }
      @lib.If(_, then_body, else_body) => {
        if sg_instrs_have_string_const(then_body) {
          return true
        }
        match else_body {
          Some(body) => if sg_instrs_have_string_const(body) { return true }
          None => ()
        }
      }
      _ => ()
    }
  }
  false
}

///|
fn sg_expr_has_string_const(expr : @lib.Expr) -> Bool {
  let @lib.Expr(instrs) = expr
  sg_instrs_have_string_const(instrs)
}

///|
fn sg_module_has_string_const(mod_ : @lib.Module) -> Bool {
  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) =>
      for func in funcs {
        let @lib.Func(_, body) = func
        if sg_expr_has_string_const(body) {
          return true
        }
      }
    None => ()
  }
  match mod_.global_sec {
    Some(@lib.GlobalSec(globals)) =>
      for global in globals {
        let @lib.Global(_, init) = global
        if sg_expr_has_string_const(init) {
          return true
        }
      }
    None => ()
  }
  match mod_.elem_sec {
    Some(@lib.ElemSec(elems)) =>
      for elem in elems {
        let @lib.Elem(mode, kind) = elem
        match mode {
          @lib.Active(_, offset) =>
            if sg_expr_has_string_const(offset) {
              return true
            }
          _ => ()
        }
        match kind {
          @lib.FuncExprsElemKind(exprs) | @lib.TypedExprsElemKind(_, exprs) =>
            for expr in exprs {
              if sg_expr_has_string_const(expr) {
                return true
              }
            }
          _ => ()
        }
      }
    None => ()
  }
  match mod_.data_sec {
    Some(@lib.DataSec(datas)) =>
      for data in datas {
        let @lib.Data(mode, _) = data
        match mode {
          @lib.Active(_, offset) =>
            if sg_expr_has_string_const(offset) {
              return true
            }
          _ => ()
        }
      }
    None => ()
  }
  false
}

///|
fn sg_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 sg_global_idx_raw(idx : @lib.GlobalIdx) -> Int32 {
  let @lib.GlobalIdx(raw) = idx
  raw.reinterpret_as_int()
}

///|
fn sg_global_idx(raw : Int32) -> @lib.GlobalIdx {
  @lib.GlobalIdx::new(raw.reinterpret_as_uint())
}

///|
fn sg_remap_existing_global_idx(
  idx : @lib.GlobalIdx,
  imported_globals : Int32,
  plan : SgRewritePlan,
) -> @lib.GlobalIdx {
  let raw = sg_global_idx_raw(idx)
  if raw < imported_globals {
    return idx
  }
  let defined_idx = raw - imported_globals
  if defined_idx < 0 || defined_idx >= plan.old_defined_to_abs.length() {
    return idx
  }
  let next = plan.old_defined_to_abs[defined_idx]
  if next < 0 {
    idx
  } else {
    sg_global_idx(next)
  }
}

///|
fn sg_string_global_idx(plan : SgRewritePlan, bytes : Bytes) -> @lib.GlobalIdx {
  let idx = match plan.string_indices.get(bytes) {
    Some(idx) => idx
    None => abort("string-gathering rewrite saw uncollected string constant")
  }
  let raw = plan.string_global_abs[idx]
  if raw < 0 {
    abort("string-gathering rewrite saw unplanned string constant")
  }
  sg_global_idx(raw)
}

///|
fn sg_direct_string_init(global : @lib.Global) -> Bytes? {
  match global {
    @lib.Global(
      @lib.GlobalType(@lib.RefTypeValType(ref_type), false),
      @lib.Expr([@lib.Instruction::StringConst(bytes)])
    ) if ref_type.get_heap_type() ==
      @lib.HeapType::abs(@lib.AbsHeapType::string_()) => Some(bytes)
    _ => None
  }
}

///|
fn sg_defined_globals(mod_ : @lib.Module) -> Array[@lib.Global] {
  match mod_.global_sec {
    Some(@lib.GlobalSec(globals)) => globals
    None => []
  }
}

///|
fn sg_bytes_less_than(lhs : Bytes, rhs : Bytes) -> Bool {
  let min_len = if lhs.length() < rhs.length() {
    lhs.length()
  } else {
    rhs.length()
  }
  for i = 0; i < min_len; i = i + 1 {
    if lhs[i] < rhs[i] {
      return true
    }
    if lhs[i] > rhs[i] {
      return false
    }
  }
  lhs.length() < rhs.length()
}

///|
fn sg_sort_strings(strings : Array[Bytes]) -> Unit {
  strings.sort_by(fn(lhs, rhs) {
    if sg_bytes_less_than(lhs, rhs) {
      -1
    } else if sg_bytes_less_than(rhs, lhs) {
      1
    } else {
      0
    }
  })
}

///|
fn sg_reindex_strings(set : SgStringSet) -> Unit {
  let indices : Map[Bytes, Int32] = Map([])
  for idx = 0; idx < set.strings.length(); idx = idx + 1 {
    indices[set.strings[idx]] = idx
  }
  set.indices = indices
}

///|
fn sg_make_rewrite_plan(
  mod_ : @lib.Module,
  set : SgStringSet,
  imported_globals : Int32,
) -> SgRewritePlan {
  sg_sort_strings(set.strings)
  sg_reindex_strings(set)
  let globals = sg_defined_globals(mod_)
  let exported_globals : Map[UInt32, Unit] = Map([])
  match mod_.export_sec {
    Some(@lib.ExportSec(exports)) =>
      for export_ in exports {
        match export_ {
          @lib.Export(_, @lib.GlobalExternIdx(@lib.GlobalIdx(idx))) =>
            exported_globals[idx] = ()
          _ => ()
        }
      }
    None => ()
  }
  let old_preserve_type = Array::make(globals.length(), false)
  for old_idx = 0; old_idx < globals.length(); old_idx = old_idx + 1 {
    let nullable = match globals[old_idx] {
      @lib.Global(@lib.GlobalType(@lib.RefTypeValType(ref_type), _), _) =>
        ref_type.is_nullable()
      _ => false
    }
    old_preserve_type[old_idx] = nullable &&
      exported_globals.contains(
        (imported_globals + old_idx).reinterpret_as_uint(),
      )
  }
  let canonical_old = Array::make(set.strings.length(), -1)
  for old_idx = 0; old_idx < globals.length(); old_idx = old_idx + 1 {
    match sg_direct_string_init(globals[old_idx]) {
      Some(bytes) =>
        match sg_string_index(set, bytes) {
          Some(string_idx) if canonical_old[string_idx] < 0 &&
            !old_preserve_type[old_idx] => canonical_old[string_idx] = old_idx
          _ => ()
        }
      None => ()
    }
  }

  let string_global_abs = Array::make(set.strings.length(), -1)
  let old_defined_to_abs = Array::make(globals.length(), -1)
  let entry_is_fresh : Array[Bool] = []
  let entry_value : Array[Int32] = []

  fn push_old(old_idx : Int32) -> Unit {
    old_defined_to_abs[old_idx] = imported_globals + entry_value.length()
    entry_is_fresh.push(false)
    entry_value.push(old_idx)
  }

  fn push_fresh(string_idx : Int32) -> Unit {
    string_global_abs[string_idx] = imported_globals + entry_value.length()
    entry_is_fresh.push(true)
    entry_value.push(string_idx)
  }

  for old_idx = 0; old_idx < globals.length(); old_idx = old_idx + 1 {
    match sg_direct_string_init(globals[old_idx]) {
      Some(bytes) =>
        match sg_string_index(set, bytes) {
          Some(string_idx) if canonical_old[string_idx] == old_idx => {
            string_global_abs[string_idx] = imported_globals +
              entry_value.length()
            push_old(old_idx)
          }
          _ => ()
        }
      None => ()
    }
  }
  for string_idx = 0
      string_idx < set.strings.length()
      string_idx = string_idx + 1 {
    if canonical_old[string_idx] < 0 {
      push_fresh(string_idx)
    }
  }
  for old_idx = 0; old_idx < globals.length(); old_idx = old_idx + 1 {
    if old_defined_to_abs[old_idx] < 0 {
      push_old(old_idx)
    }
  }

  SgRewritePlan::{
    strings: set.strings,
    string_indices: set.indices,
    string_global_abs,
    old_defined_to_abs,
    old_preserve_type,
    entry_is_fresh,
    entry_value,
  }
}

///|
fn sg_rewrite_instr(
  instr : @lib.Instruction,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.Instruction {
  match instr {
    @lib.StringConst(bytes) =>
      @lib.Instruction::global_get(sg_string_global_idx(plan, bytes))
    @lib.GlobalGet(idx) =>
      @lib.Instruction::global_get(
        sg_remap_existing_global_idx(idx, imported_globals, plan),
      )
    @lib.GlobalSet(idx) =>
      @lib.Instruction::global_set(
        sg_remap_existing_global_idx(idx, imported_globals, plan),
      )
    @lib.Block(block_type, body) =>
      @lib.Instruction::block(
        block_type,
        sg_rewrite_expr(body, plan, imported_globals),
      )
    @lib.Loop(block_type, body) =>
      @lib.Instruction::loop_(
        block_type,
        sg_rewrite_expr(body, plan, imported_globals),
      )
    @lib.If(block_type, then_body, else_body) => {
      let next_then = sg_rewrite_instrs(then_body, plan, imported_globals)
      let next_else = match else_body {
        Some(body) => Some(sg_rewrite_instrs(body, plan, imported_globals))
        None => None
      }
      @lib.Instruction::if_(block_type, next_then, next_else)
    }
    @lib.TryTable(block_type, catches, body) =>
      @lib.Instruction::try_table(
        block_type,
        catches,
        sg_rewrite_expr(body, plan, imported_globals),
      )
    _ => instr
  }
}

///|
fn sg_rewrite_instrs(
  instrs : Array[@lib.Instruction],
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> Array[@lib.Instruction] {
  if !sg_instrs_need_rewrite(instrs, plan, imported_globals) {
    return instrs
  }
  sg_rewrite_instrs_unchecked(instrs, plan, imported_globals)
}

///|
fn sg_rewrite_instrs_unchecked(
  instrs : Array[@lib.Instruction],
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> Array[@lib.Instruction] {
  let out : Array[@lib.Instruction] = []
  for instr in instrs {
    out.push(sg_rewrite_instr(instr, plan, imported_globals))
  }
  out
}

///|
fn sg_instrs_need_rewrite(
  instrs : Array[@lib.Instruction],
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> Bool {
  for instr in instrs {
    match instr {
      @lib.StringConst(_) => return true
      @lib.GlobalGet(idx) | @lib.GlobalSet(idx) =>
        if sg_remap_existing_global_idx(idx, imported_globals, plan) != idx {
          return true
        }
      @lib.Block(_, body) | @lib.Loop(_, body) =>
        if sg_expr_needs_rewrite(body, plan, imported_globals) {
          return true
        }
      @lib.If(_, then_body, else_body) => {
        if sg_instrs_need_rewrite(then_body, plan, imported_globals) {
          return true
        }
        match else_body {
          Some(body) =>
            if sg_instrs_need_rewrite(body, plan, imported_globals) {
              return true
            }
          None => ()
        }
      }
      @lib.TryTable(_, _, body) =>
        if sg_expr_needs_rewrite(body, plan, imported_globals) {
          return true
        }
      _ => ()
    }
  }
  false
}

///|
fn sg_expr_needs_rewrite(
  expr : @lib.Expr,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> Bool {
  sg_instrs_need_rewrite(expr.0, plan, imported_globals)
}

///|
fn sg_rewrite_expr(
  expr : @lib.Expr,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.Expr {
  if !sg_expr_needs_rewrite(expr, plan, imported_globals) {
    return expr
  }
  let @lib.Expr(instrs) = expr
  @lib.Expr::new(sg_rewrite_instrs_unchecked(instrs, plan, imported_globals))
}

///|
fn sg_rewrite_global_sec(
  global_sec : @lib.GlobalSec?,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.GlobalSec {
  let globals = match global_sec {
    Some(@lib.GlobalSec(globals)) => globals
    None => []
  }
  let out : Array[@lib.Global] = []
  let string_global_type = @lib.GlobalType::new(
    @lib.ValType::ref_type(
      @lib.RefType::new(false, @lib.HeapType::abs(@lib.AbsHeapType::string_())),
    ),
    false,
  )
  for entry_idx = 0
      entry_idx < plan.entry_value.length()
      entry_idx = entry_idx + 1 {
    let value = plan.entry_value[entry_idx]
    if plan.entry_is_fresh[entry_idx] {
      out.push(
        @lib.Global::new(
          string_global_type,
          @lib.Expr::new([@lib.Instruction::string_const(plan.strings[value])]),
        ),
      )
    } else {
      let @lib.Global(global_type, init) = globals[value]
      let rewritten_init = match sg_direct_string_init(globals[value]) {
        Some(bytes) =>
          match plan.string_indices.get(bytes) {
            Some(string_idx) =>
              if plan.string_global_abs[string_idx] ==
                imported_globals + entry_idx {
                init
              } else {
                sg_rewrite_expr(init, plan, imported_globals)
              }
            _ => sg_rewrite_expr(init, plan, imported_globals)
          }
        None => sg_rewrite_expr(init, plan, imported_globals)
      }
      // A direct immutable string literal is non-null, including when its
      // original declaration used a nullable shorthand. Reused globals must
      // expose that proof to every rewritten literal consumer.
      let rewritten_type = if sg_direct_string_init(globals[value]) is Some(_) {
        if plan.old_preserve_type[value] {
          global_type
        } else {
          string_global_type
        }
      } else {
        global_type
      }
      out.push(@lib.Global::new(rewritten_type, rewritten_init))
    }
  }
  @lib.GlobalSec::new(out)
}

///|
fn sg_rewrite_export_sec(
  export_sec : @lib.ExportSec?,
  imported_globals : Int32,
  plan : SgRewritePlan,
) -> @lib.ExportSec? {
  match export_sec {
    Some(@lib.ExportSec(exports)) => {
      let out : Array[@lib.Export] = []
      for export_ in exports {
        match export_ {
          @lib.Export(name, @lib.GlobalExternIdx(idx)) =>
            out.push(
              @lib.Export::new(
                name,
                @lib.ExternIdx::global(
                  sg_remap_existing_global_idx(idx, imported_globals, plan),
                ),
              ),
            )
          _ => out.push(export_)
        }
      }
      Some(@lib.ExportSec::new(out))
    }
    None => None
  }
}

///|
fn sg_rewrite_elem_mode(
  mode : @lib.ElemMode,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.ElemMode {
  match mode {
    @lib.Active(table_idx, expr) =>
      @lib.ElemMode::active(
        table_idx,
        sg_rewrite_expr(expr, plan, imported_globals),
      )
    _ => mode
  }
}

///|
fn sg_rewrite_elem_kind(
  kind : @lib.ElemKind,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.ElemKind {
  match kind {
    @lib.FuncExprsElemKind(exprs) => {
      let out : Array[@lib.Expr] = []
      for expr in exprs {
        out.push(sg_rewrite_expr(expr, plan, imported_globals))
      }
      @lib.ElemKind::func_exprs(out)
    }
    @lib.TypedExprsElemKind(ref_type, exprs) => {
      let out : Array[@lib.Expr] = []
      for expr in exprs {
        out.push(sg_rewrite_expr(expr, plan, imported_globals))
      }
      @lib.ElemKind::typed_exprs(ref_type, out)
    }
    _ => kind
  }
}

///|
fn sg_rewrite_elem_sec(
  elem_sec : @lib.ElemSec?,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.ElemSec? {
  match elem_sec {
    Some(@lib.ElemSec(elems)) => {
      let out : Array[@lib.Elem] = []
      for elem in elems {
        let @lib.Elem(mode, kind) = elem
        out.push(
          @lib.Elem::new(
            sg_rewrite_elem_mode(mode, plan, imported_globals),
            sg_rewrite_elem_kind(kind, plan, imported_globals),
          ),
        )
      }
      Some(@lib.ElemSec::new(out))
    }
    None => None
  }
}

///|
fn sg_rewrite_data_sec(
  data_sec : @lib.DataSec?,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.DataSec? {
  match data_sec {
    Some(@lib.DataSec(datas)) => {
      let out : Array[@lib.Data] = []
      for data in datas {
        let @lib.Data(mode, bytes) = data
        let next_mode = match mode {
          @lib.Active(mem_idx, expr) =>
            @lib.DataMode::active(
              mem_idx,
              sg_rewrite_expr(expr, plan, imported_globals),
            )
          _ => mode
        }
        out.push(@lib.Data::new(next_mode, bytes))
      }
      Some(@lib.DataSec::new(out))
    }
    None => None
  }
}

///|
fn sg_rewrite_global_name_map(
  names : @lib.NameMap?,
  imported_globals : Int32,
  plan : SgRewritePlan,
) -> @lib.NameMap? {
  match names {
    Some(@lib.NameMap(entries)) => {
      let out : Array[@lib.NameAssoc] = []
      for entry in entries {
        let @lib.NameAssoc(idx, name) = entry
        let raw = idx.reinterpret_as_int()
        if raw >= imported_globals {
          let defined_idx = raw - imported_globals
          if defined_idx >= 0 && defined_idx < plan.old_defined_to_abs.length() {
            out.push(
              @lib.NameAssoc::new(
                plan.old_defined_to_abs[defined_idx].reinterpret_as_uint(),
                name,
              ),
            )
          }
        } else {
          out.push(entry)
        }
      }
      out.sort_by(fn(left, right) {
        let @lib.NameAssoc(left_idx, _) = left
        let @lib.NameAssoc(right_idx, _) = right
        left_idx.compare(right_idx)
      })
      Some(@lib.NameMap::new(out))
    }
    None => None
  }
}

///|
fn sg_rewrite_name_sec(
  name_sec : @lib.NameSec?,
  imported_globals : Int32,
  plan : SgRewritePlan,
) -> @lib.NameSec? {
  match name_sec {
    Some(sec) =>
      Some(
        @lib.NameSec::new(
          module_name=sec.module_name,
          func_names=sec.func_names,
          local_names=sec.local_names,
          label_names=sec.label_names,
          type_names=sec.type_names,
          table_names=sec.table_names,
          memory_names=sec.memory_names,
          global_names=sg_rewrite_global_name_map(
            sec.global_names,
            imported_globals,
            plan,
          ),
          elem_names=sec.elem_names,
          data_names=sec.data_names,
          field_names=sec.field_names,
          tag_names=sec.tag_names,
        ),
      )
    None => None
  }
}

///|
fn sg_rewrite_code_sec(
  code_sec : @lib.CodeSec?,
  plan : SgRewritePlan,
  imported_globals : Int32,
) -> @lib.CodeSec? {
  match code_sec {
    Some(@lib.CodeSec(funcs)) => {
      let mut out : Array[@lib.Func]? = None
      for index, func in funcs {
        let @lib.Func(locals, body) = func
        let next_body = sg_rewrite_expr(body, plan, imported_globals)
        if next_body != body {
          if out is None {
            out = Some(funcs.copy())
          }
          out.unwrap()[index] = @lib.Func::new(locals, next_body)
        }
      }
      match out {
        Some(rewritten) => Some(@lib.CodeSec::new(rewritten))
        None => code_sec
      }
    }
    None => None
  }
}

///|
pub fn string_gathering_run_module_pass(mod_ : @lib.Module) -> @lib.Module {
  if pass_module_has_legacy_try(mod_) ||
    pass_module_has_offset_sensitive_metadata(mod_) {
    return mod_
  }
  let set = sg_collect_strings(mod_)
  if set.strings.is_empty() {
    return mod_
  }
  let imported_globals = sg_imported_global_count(mod_)
  let plan = sg_make_rewrite_plan(mod_, set, imported_globals)
  @lib.Module::new(
    custom_secs=mod_.custom_secs,
    type_sec=mod_.type_sec,
    import_sec=mod_.import_sec,
    func_annotation_sec=mod_.func_annotation_sec,
    func_sec=mod_.func_sec,
    table_sec=mod_.table_sec,
    mem_sec=mod_.mem_sec,
    tag_sec=mod_.tag_sec,
    stringrefs_sec=mod_.stringrefs_sec,
    global_sec=Some(
      sg_rewrite_global_sec(mod_.global_sec, plan, imported_globals),
    ),
    export_sec=sg_rewrite_export_sec(mod_.export_sec, imported_globals, plan),
    start_sec=mod_.start_sec,
    elem_sec=sg_rewrite_elem_sec(mod_.elem_sec, plan, imported_globals),
    data_cnt_sec=mod_.data_cnt_sec,
    code_sec=sg_rewrite_code_sec(mod_.code_sec, plan, imported_globals),
    data_sec=sg_rewrite_data_sec(mod_.data_sec, plan, imported_globals),
    name_sec=sg_rewrite_name_sec(mod_.name_sec, imported_globals, plan),
    raw_name_sec_payload=None,
  )
}