// Resolving a bare name, and knowing whether a local holds a value yet.
//
// Ported from wax/src/lib-wax/typing.ml.

///|
/// What a bare name turned out to be.
pub(all) enum ResolvedVar {
  Local(@infer.InferredValType?, @basic.Location)
  Global(Bool, @infer.InferredValType?)
  FuncRef(@type_store.Id, String, Bool)
  /// A function whose signature failed to resolve. It is BOUND -- already
  /// reported at its definition -- so it reads as an error with nothing further
  /// said, rather than as an unbound name.
  Poisoned
  Unbound
}

///|
/// Resolve a bare name.
///
/// The order is the language's: a local shadows everything. Globals, functions,
/// memories and tables share one namespace, so a local is the only thing that
/// can collide with them -- which is why every receiver test below has to defer
/// to a local in the same way.
pub fn resolve_variable(
  ctx : @typing_env.ModuleContext,
  idx : @ast.Ident,
) -> ResolvedVar {
  match ctx.locals.get(idx.name) {
    Some((ty, def)) => {
      @typing_env.record_reference(
        ctx.resolve_links,
        idx.loc,
        [def],
        hover=ty.map(v => @typing_env.HoverTarget::ValueType(v)),
      )
      Local(ty, def)
    }
    None =>
      match ctx.globals.resolve(idx.name, idx.loc) {
        Some((mut_, ty)) => Global(mut_, ty)
        None =>
          match ctx.functions.resolve(idx.name, idx.loc) {
            Some(Some((ty, name, exact))) => FuncRef(ty, name, exact)
            Some(None) => Poisoned
            None => Unbound
          }
      }
  }
}

///|
/// Is this name a memory usable as a receiver -- `mem.load(..)`?
///
/// A local of the same name shadows it, exactly as it shadows a `Get`.
pub fn memory_receiver(
  ctx : @typing_env.ModuleContext,
  name : @ast.Ident,
) -> Bool {
  !ctx.locals.contains(name.name) && !ctx.memories.visible(name.name).is_empty()
}

///|
/// Likewise for a table used as `tab[..]` or `tab.size()`.
pub fn table_receiver(
  ctx : @typing_env.ModuleContext,
  name : @ast.Ident,
) -> Bool {
  !ctx.locals.contains(name.name) && !ctx.tables.visible(name.name).is_empty()
}

///|
/// Likewise for a data or element segment named by `seg.drop()`.
pub fn segment_receiver(
  ctx : @typing_env.ModuleContext,
  name : @ast.Ident,
) -> Bool {
  !ctx.locals.contains(name.name) &&
  (
    !ctx.datas.visible(name.name).is_empty() ||
    !ctx.elems.visible(name.name).is_empty()
  )
}

// ============================================================
// Initialization
// ============================================================

///|
/// Does this type have a zero value?
///
/// Everything but a non-nullable reference does. A local of such a type has
/// nothing to start as, so it must be assigned before it is read -- which is
/// the whole reason the checker tracks initialization at all.
pub fn is_defaultable(ty : @wasm_types.ValType[@ast.Ident]) -> Bool {
  match ty {
    Ref(r) => r.nullable
    _ => true
  }
}

///|
pub fn mark_initialized(ctx : @typing_env.ModuleContext, name : String) -> Unit {
  let one : Map[String, Unit] = Map([])
  one[name] = ()
  ctx.merge_initialized(one)
}

///|
/// Report a read of a local that does not hold a value yet -- or, while a
/// trailing operand is being checked out of emission order, defer it.
///
/// Both the ordinary read and a deferred read's re-check come through here, so
/// a re-check that still fails under an OUTER deferral re-defers rather than
/// reporting.
pub fn report_uninitialized(
  ctx : @typing_env.ModuleContext,
  idx : @ast.Ident,
) -> Unit {
  match ctx.current_deferral() {
    Some(collector) => collector.push(idx)
    None => uninitialized_local(ctx.diagnostics, idx.loc, idx.name)
  }
}

///|
/// Check an operand that is written before the values it follows but EMITTED
/// after them, and return a thunk to run at its real slot.
///
/// The initialized-locals analysis threads in emission order, so checking such
/// an operand early sees a state that is a SUBSET of the true one. Three things
/// follow, and each is what makes the analysis sound rather than merely
/// plausible:
///
///   * A read that succeeds now would also succeed later, so it is fine.
///   * A read that FAILS now might succeed later, once an earlier operand has
///     assigned the local -- so it is deferred, not reported.
///   * The operand's own writes are withheld, because an earlier operand runs
///     first and must not see them.
///
/// The returned thunk re-checks the deferred reads against the state that
/// actually holds at the emission slot, then applies the withheld writes.
pub fn[A] type_trailing_operand(
  ctx : @typing_env.ModuleContext,
  run : () -> A,
) -> (A, () -> Unit) {
  let saved = ctx.snapshot_initialized()
  let collector = ctx.push_deferral()
  let result = run()
  ctx.pop_deferral()
  // What this operand initialized, which is withheld until its real slot.
  let delta = ctx.snapshot_initialized()
  ctx.restore_initialized(saved)
  let replay = () => {
    for idx in collector {
      if !ctx.initialized_locals.contains(idx.name) {
        report_uninitialized(ctx, idx)
      }
    }
    ctx.merge_initialized(delta)
  }
  (result, replay)
}

///|
/// Names close enough to be worth suggesting for an unbound READ.
///
/// Everything a bare name could have meant: locals, globals, functions.
pub fn get_suggestions(
  ctx : @typing_env.ModuleContext,
  name : String,
) -> Array[String] {
  let candidates : Array[String] = []
  for k, _ in ctx.locals {
    candidates.push(k)
  }
  for k in ctx.globals.names() {
    candidates.push(k)
  }
  for k in ctx.functions.names() {
    candidates.push(k)
  }
  @spell.suggest(candidates.iter(), name)
}

///|
/// Names worth suggesting for an unbound ASSIGNMENT.
///
/// A narrower set than for a read: only a MUTABLE global can be assigned, so
/// suggesting an immutable one would send the reader somewhere that cannot
/// work.
pub fn set_suggestions(
  ctx : @typing_env.ModuleContext,
  name : String,
) -> Array[String] {
  let candidates : Array[String] = []
  for k, _ in ctx.locals {
    candidates.push(k)
  }
  for k in ctx.globals.names() {
    for g in ctx.globals.visible(k) {
      if g.0 {
        candidates.push(k)
        break
      }
    }
  }
  @spell.suggest(candidates.iter(), name)
}

///|
/// The composite type an anonymous reference names.
///
/// A synthesized reference type -- a string's byte array, an inline function
/// type used as a cast target -- has a generated name beginning with `<`. The
/// name keeps lookups working, but a diagnostic renders the structure instead,
/// because the name means nothing to the reader.
fn inline_comptype(
  ctx : @typing_env.ModuleContext,
  name : @ast.Ident,
) -> @ast.CompType? {
  if name.name.has_prefix("<") {
    ctx.type_context.types.resolve(name.name, name.loc).map(r => r.1.typ)
  } else {
    None
  }
}

///|
/// Type a read of a bare name.
///
/// Every failure recovers as an error value rather than stopping, and each does
/// so for a slightly different reason: an unbound name has just been reported,
/// a poisoned function was reported at its definition, and a poison local's
/// initializer was reported where it failed. In all three the point is the
/// same -- one mistake should produce one diagnostic.
pub fn type_get(
  ctx : @typing_env.ModuleContext,
  idx : @ast.Ident,
) -> @infer.Cell[@infer.InferredType] {
  match resolve_variable(ctx, idx) {
    Local(ty, def) => {
      // Keyed by the BINDING's offset rather than the name, so a shadowing
      // inner local does not mask an outer one as read.
      ctx.read_locals.push(def.start.cnum)
      if !ctx.initialized_locals.contains(idx.name) {
        report_uninitialized(ctx, idx)
      }
      cell_of(ty)
    }
    Global(_, ty) => cell_of(ty)
    FuncRef(id, type_name, exact) => {
      let name : @ast.Ident = { name: type_name, loc: @basic.dummy_loc }
      let typ : @wasm_types.HeapType[@ast.Ident] = if exact {
        Exact(name)
      } else {
        Type(name)
      }
      let internal : @wasm_types.HeapType[@type_store.Id] = if exact {
        Exact(id)
      } else {
        Type(id)
      }
      @infer.Cell::make(
        Valtype({
          typ: Ref({ nullable: false, typ }),
          internal: Ref({ nullable: false, typ: internal }),
          anon_comptype: inline_comptype(ctx, name),
        }),
      )
    }
    // Already reported at the definition; the poison keeps the use quiet.
    Poisoned => @infer.Cell::make(Error)
    Unbound => {
      unbound_name(
        ctx.diagnostics,
        idx.loc,
        "variable",
        idx.name,
        suggestions=get_suggestions(ctx, idx.name),
      )
      @infer.Cell::make(Error)
    }
  }
}

///|
/// A resolved type, or the poison of one that failed to resolve.
fn cell_of(ty : @infer.InferredValType?) -> @infer.Cell[@infer.InferredType] {
  match ty {
    Some(v) => @infer.Cell::make(Valtype(v))
    None => @infer.Cell::make(Error)
  }
}

///|
/// Names worth suggesting for an unbound TEE target.
///
/// Narrower again than an assignment's: only a local can be tee'd, because the
/// value has to stay on the stack afterwards and only a local supports that.
fn local_suggestions(
  ctx : @typing_env.ModuleContext,
  name : String,
) -> Array[String] {
  let candidates : Array[String] = []
  for k, _ in ctx.locals {
    candidates.push(k)
  }
  @spell.suggest(candidates.iter(), name)
}

///|
/// Record and report what an assignment target turned out to be.
///
/// Called AFTER the value has been checked, which matters: the local is marked
/// initialized only then, so `x = x + 1` still sees `x`'s pre-assignment state
/// while its right-hand side is being checked.
///
/// `compound` says the source wrote `x op= e`. That form desugars to `x = x op
/// e`, and the desugared READ already reported an unbound name at this very
/// span -- so reporting the write as well would say the same thing twice.
pub fn assign_target(
  ctx : @typing_env.ModuleContext,
  idx : @ast.Ident,
  resolved : ResolvedVar,
  compound? : Bool = false,
) -> Unit {
  match resolved {
    Local(_, _) => mark_initialized(ctx, idx.name)
    Global(mut_, _) =>
      if !mut_ {
        immutable(ctx.diagnostics, idx.loc, "global")
      } else {
        // The only place a global is written, so also the only place a mutable
        // one can be recorded as actually assigned -- which is what the
        // unnecessary-mut warning reads.
        ctx.assigned_globals[idx.name] = ()
      }
    FuncRef(_, _, _) => not_assignable(ctx.diagnostics, idx.loc, idx.name)
    // Already reported at the definition.
    Poisoned => ()
    Unbound =>
      if !compound {
        unbound_name(
          ctx.diagnostics,
          idx.loc,
          "variable",
          idx.name,
          suggestions=set_suggestions(ctx, idx.name),
        )
      }
  }
}

///|
/// The type a tee delivers, and the reporting that goes with it.
///
/// Only a local is tee-able. Everything else recovers with the OPERAND's own
/// type rather than an unknown, because an unknown cannot be checked against
/// anything and would turn one error into a second one downstream.
pub fn tee_target(
  ctx : @typing_env.ModuleContext,
  idx : @ast.Ident,
  resolved : ResolvedVar,
  operand : @infer.Cell[@infer.InferredType],
) -> @infer.Cell[@infer.InferredType] {
  match resolved {
    Local(Some(ity), _) => {
      mark_initialized(ctx, idx.name)
      @infer.valtype_cell(ity)
    }
    // A poison local: no check to make, but it does now hold a value.
    Local(None, _) => {
      mark_initialized(ctx, idx.name)
      operand
    }
    Global(_, _) | FuncRef(_, _, _) => {
      not_assignable(ctx.diagnostics, idx.loc, idx.name)
      operand
    }
    Poisoned => operand
    Unbound => {
      unbound_name(
        ctx.diagnostics,
        idx.loc,
        "variable",
        idx.name,
        suggestions=local_suggestions(ctx, idx.name),
      )
      operand
    }
  }
}

///|
/// Introduce a local with a known type.
///
/// Used for a binding that has an initializer: the value has just been checked
/// against the type, so the local holds one.
pub fn bind_local(
  ctx : @typing_env.ModuleContext,
  name : @ast.Ident,
  ty : @infer.InferredValType?,
) -> Unit {
  ctx.locals[name.name] = (ty, name.loc)
  ctx.local_decls.push(name)
  mark_initialized(ctx, name.name)
}

///|
/// Introduce a local declared without an initializer -- `let x: t;`.
///
/// Whether it starts holding a value is decided by the type, and this is the
/// only place that decision is made: a defaultable type has a zero value to
/// start at, and a non-nullable reference does not, so it stays uninitialized
/// until something assigns it. That is what makes a later read of it an error
/// rather than a read of nothing.
///
/// An unannotated name has no type to take and declares nothing at all.
pub fn declare_local(
  ctx : @typing_env.ModuleContext,
  name : @ast.Ident,
  typ : @wasm_types.ValType[@ast.Ident],
) -> Unit {
  guard internalize_valtype(ctx.type_context, ctx.diagnostics, typ) is Some(ity) else {
    return
  }
  ctx.locals[name.name] = (Some(ity), name.loc)
  ctx.local_decls.push(name)
  if is_defaultable(typ) {
    mark_initialized(ctx, name.name)
  }
}

///|
/// The type a local takes from an initializer with no annotation.
///
/// An `Error` initializer has no determinable type, so the local is recorded as
/// POISON (`None`) rather than defaulting to i32 -- a wrong type here would
/// cascade into every use of the local. An `Unknown` one is additionally
/// reported: unlike `Error` it has not been complained about yet, and a binding
/// whose type nobody can name is worth saying out loud.
fn bound_value_type(
  ctx : @typing_env.ModuleContext,
  location : @basic.Location,
  result_ty : @infer.Cell[@infer.InferredType],
) -> @infer.InferredValType? {
  match result_ty.get() {
    Error => None
    Unknown => {
      unknown_operand_type(ctx.diagnostics, location)
      None
    }
    _ => resolve_omitted_valtype(ctx.type_context, ctx.diagnostics, result_ty)
  }
}

///|
/// Whether a method receiver names a value whose type is a REFERENCE.
///
/// This decides between two readings of the same spelling. `x.min(y)` is the
/// scalar intrinsic when `x` is a number, and an indirect call through a
/// function-pointer field when `x` is a struct reference -- so the receiver's
/// type disambiguates, not the argument count.
///
/// Pure: it reads the name's type out of the locals and globals and records
/// nothing, because it runs as a GUARD, before anything has been typed.
fn receiver_is_ref(
  ctx : @typing_env.ModuleContext,
  recv : @ast.Instr[@basic.Location],
) -> Bool {
  fn is_ref(t : @infer.InferredValType?) -> Bool {
    t is Some({ typ: Ref(_), .. })
  }

  guard recv.desc is Get(name) else { return false }
  match ctx.locals.get(name.name) {
    Some((ity, _)) => is_ref(ity)
    None =>
      match ctx.globals.visible(name.name) {
        [(_, ity), ..] => is_ref(ity)
        [] => false
      }
  }
}

///|
/// Whether a method receiver names a value whose type is a reference to an ARRAY
/// type.
///
/// Pure like `receiver_is_ref`, and for the same reason: it gates a bulk-method
/// call before anything has been typed, so a struct with a field named `fill` or
/// `copy` is left to the indirect-call path.
fn receiver_is_array_ref(
  ctx : @typing_env.ModuleContext,
  recv : @ast.Instr[@basic.Location],
) -> Bool {
  fn ref_name(t : @infer.InferredValType?) -> @ast.Ident? {
    match t {
      Some({ typ: Ref({ typ: Type(n) | Exact(n), .. }), .. }) => Some(n)
      _ => None
    }
  }

  guard recv.desc is Get(name) else { return false }
  let named = match ctx.locals.get(name.name) {
    Some((ity, _)) => ref_name(ity)
    None =>
      match ctx.globals.visible(name.name) {
        [(_, ity), ..] => ref_name(ity)
        [] => None
      }
  }
  guard named is Some(n) else { return false }
  match ctx.types.find_no_mark(n.name) {
    Some((_, sub)) => sub.typ is Array(_)
    None => false
  }
}

///|
/// Record that an instruction NAMED this memory, table or segment.
///
/// These are resolved with `find_no_mark`, because the lookup is a shape
/// question the guard already answered and reporting it again would say the same
/// thing twice. But naming a memory in `m.load(..)` really is using it, and the
/// unused-declaration lint has no other way to know -- so the use is recorded
/// here, separately from the resolution.
fn[A] note_use(
  ctx : @typing_env.ModuleContext,
  tbl : @typing_env.Tbl[A],
  name : @ast.Ident,
) -> Unit {
  tbl.mark_reference(name.name, ctx.origin.val)
}

///|
/// The width of an atomic NARROW load, when that is what this expression is.
///
/// `W32` is not narrow enough to matter here: an `i64` reading 32 bits is the
/// only case, and it has the same absent sign-extending form, but the reference
/// asks only about the 8- and 16-bit ones.
fn atomic_narrow_load_width(
  ctx : @typing_env.ModuleContext,
  e : @ast.Instr[@basic.Location],
) -> @atomics.Width? {
  guard e.desc is Call(callee, _) else { return None }
  guard callee.desc is StructGet(recv, meth) else { return None }
  guard recv.desc is Get(memname) else { return None }
  guard memory_receiver(ctx, memname) else { return None }
  match @atomics.of_method_name(meth.name) {
    Some(Load(W8)) => Some(W8)
    Some(Load(W16)) => Some(W16)
    _ => None
  }
}